I/O and File Operations
2026-03-17
Every practical program needs to read and write files. Raku makes file operations clean and intuitive with its .IO method, which turns any string into an IO::Path object with a full suite of file operations. In this tutorial, we will cover reading files, writing files, testing file properties, working with directories, and handling standard I/O.
The .IO Method
In Raku, any string can become a file path by calling .IO on it:
my $path = "/tmp/example.txt".IO;
say $path.WHAT;
say $path.basename;
say $path.dirname;
say $path.extension;
.IO is the gateway to all file operations. You will see it everywhere.
Reading Files
slurp: Read the Entire File
The simplest way to read a file is .slurp, which returns the entire contents as a string:
my $content = "/etc/hostname".IO.slurp;
say $content;
Or using the slurp function directly:
my $content = slurp "/etc/hostname";
say $content;
lines: Read Line by Line
.lines returns a lazy sequence of lines, which is memory-efficient for large files:
for "myfile.txt".IO.lines -> $line {
say $line;
}
You can also get all lines as a list:
my @lines = "myfile.txt".IO.lines;
say "File has { @lines.elems } lines";
Since .lines is lazy, you can process huge files without loading everything into memory:
my $errors = "/var/log/syslog".IO.lines.grep(/error/).elems;
say "Found $errors error lines";
words: Read Word by Word
my @words = "document.txt".IO.words;
say "Word count: { @words.elems }";
Writing Files
spurt: Write the Entire File
.spurt writes a string to a file, creating it if it does not exist and overwriting it if it does:
"output.txt".IO.spurt("Hello, World!\n");
To append instead of overwriting, use the :append adverb:
"log.txt".IO.spurt("New log entry\n", :append);
Writing Multiple Lines
my @data = "Line one", "Line two", "Line three";
"output.txt".IO.spurt(@data.join("\n") ~ "\n");
Or build up content and write it all at once:
my $report = "";
$report ~= "=== Report ===\n";
$report ~= "Date: { DateTime.now }\n";
$report ~= "Status: OK\n";
"report.txt".IO.spurt($report);
Using File Handles
For more control, open a file handle:
my $fh = open "output.txt", :w;
$fh.say("First line");
$fh.say("Second line");
$fh.print("No newline here");
$fh.close;
File handle modes:
| Mode |
Meaning |
:r |
Read (default) |
:w |
Write (truncate existing) |
:a |
Append |
:rw |
Read and write |
my $fh = open "log.txt", :a;
$fh.say("{ DateTime.now } - Application started");
$fh.close;
Automatic Cleanup with will leave
You can ensure file handles are closed automatically:
{
my $fh = open "data.txt", :w;
LEAVE $fh.close;
$fh.say("This will be written");
$fh.say("So will this");
}
File Tests
Raku provides file test methods on IO::Path objects:
my $path = "somefile.txt".IO;
say $path.e;
say $path.f;
say $path.d;
say $path.l;
say $path.r;
say $path.w;
say $path.x;
say $path.s;
say $path.z;
Practical usage:
my $config = "app.conf".IO;
if $config.e && $config.f {
say "Loading config ({ $config.s } bytes)...";
my $content = $config.slurp;
}
else {
say "Config file not found, using defaults.";
}
File Metadata
my $file = "example.txt".IO;
if $file.e {
say "Size: { $file.s } bytes";
say "Modified: { $file.modified }";
say "Accessed: { $file.accessed }";
say "Changed: { $file.changed }";
}
Directory Operations
Listing Directory Contents
for "/tmp".IO.dir -> $entry {
say $entry.basename;
}
for "/tmp".IO.dir(test => /\.txt$/) -> $file {
say $file;
}
Checking and Creating Directories
my $dir = "output/reports".IO;
unless $dir.d {
$dir.mkdir;
say "Created directory: $dir";
}
.mkdir creates the full directory path (like mkdir -p):
"deep/nested/directory/structure".IO.mkdir;
Removing Directories
Working with Paths
IO::Path provides useful methods for path manipulation:
my $path = "/home/user/documents/report.pdf".IO;
say $path.basename;
say $path.dirname;
say $path.extension;
say $path.volume;
say $path.parent;
say $path.parent.parent;
my $base = "/home/user".IO;
say $base.add("documents").add("file.txt");
Resolving and Absolute Paths
my $rel = "../../etc/hosts".IO;
say $rel.resolve;
say $rel.absolute;
Standard I/O
Reading from STDIN
my $name = prompt("Enter your name: ");
say "Hello, $name!";
for $*IN.lines -> $line {
say "Got: $line";
}
When your script receives piped input:
my $count = 0;
for $*IN.lines -> $line {
$count++;
say "$count: $line";
}
say "Total: $count lines";
Writing to STDOUT and STDERR
say "This goes to STDOUT";
put "This also goes to STDOUT";
print "STDOUT no newline";
note "This goes to STDERR";
warn "This is a warning";
The .note method is particularly useful for debug output that should not interfere with normal output:
for 1..100 -> $n {
note "Processing $n..." if $n %% 10;
say $n * $n;
}
The Standard I/O Handles
$*OUT.say("Explicit STDOUT");
$*ERR.say("Explicit STDERR");
my $line = $*IN.get;
Practical Example: File Backup Script
sub backup-file(IO::Path $source --> Bool) {
unless $source.e {
note "Source does not exist: $source";
return False;
}
my $backup = ($source.Str ~ ".bak").IO;
if $backup.e {
note "Backup already exists: $backup";
return False;
}
$backup.spurt($source.slurp);
say "Backed up: $source -> $backup ({ $source.s } bytes)";
return True;
}
my @files = ".".IO.dir(test => /'.txt' $/);
if @files.elems == 0 {
say "No .txt files found.";
}
else {
my $count = 0;
for @files -> $file {
$count++ if backup-file($file);
}
say "\nBacked up $count of { @files.elems } files.";
}
Practical Example: Simple CSV Reader
sub read-csv(IO::Path $file --> Array) {
my @rows;
my @headers;
for $file.lines.kv -> $i, $line {
my @fields = $line.split(',').map(*.trim);
if $i == 0 {
@headers = @fields;
}
else {
my %row;
for @headers.kv -> $j, $header {
%row{$header} = @fields[$j] // "";
}
@rows.push(%row);
}
}
return @rows;
}
my $csv-file = "/tmp/sample.csv".IO;
$csv-file.spurt(q:to/END/);
name, age, city
Alice, 30, Toronto
Bob, 25, Vancouver
Charlie, 35, Montreal
END
my @data = read-csv($csv-file);
say "=== CSV Data ===";
for @data -> %row {
say "{ %row<name> } is { %row<age> } years old, lives in { %row<city> }";
}
$csv-file.unlink;
say "\nCleaned up temp file.";
Output:
=== CSV Data ===
Alice is 30 years old, lives in Toronto
Bob is 25 years old, lives in Vancouver
Charlie is 35 years old, lives in Montreal
Cleaned up temp file.
Quick Reference
| Operation |
Code |
| Read entire file |
"file".IO.slurp |
| Read lines |
"file".IO.lines |
| Write file |
"file".IO.spurt($content) |
| Append to file |
"file".IO.spurt($text, :append) |
| File exists? |
"file".IO.e |
| Is a file? |
"file".IO.f |
| Is a directory? |
"file".IO.d |
| File size |
"file".IO.s |
| Delete file |
"file".IO.unlink |
| Create directory |
"dir".IO.mkdir |
| List directory |
"dir".IO.dir |
| Basename |
"path/to/file.txt".IO.basename |
What is Next?
Congratulations! You have completed the beginner Raku tutorial series. You now have a solid foundation covering variables, operators, control flow, subroutines, strings, arrays, hashes, regexes, and file I/O. From here, you can explore intermediate topics like classes and objects, grammars, concurrency with promises and channels, and Raku's module ecosystem. The language has incredible depth, and you are well prepared to explore it. Happy hacking!