raku.gg / beginner

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; # (IO::Path) say $path.basename; # example.txt say $path.dirname; # /tmp say $path.extension; # txt
.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:
# Count lines matching a pattern in a large log file 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; # :w for write $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
# Append to an existing file 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; # runs when the block exits $fh.say("This will be written"); $fh.say("So will this"); # $fh.close happens automatically when we leave this block }

File Tests

Raku provides file test methods on IO::Path objects:
my $path = "somefile.txt".IO; say $path.e; # True if path exists say $path.f; # True if it is a regular file say $path.d; # True if it is a directory say $path.l; # True if it is a symbolic link say $path.r; # True if readable say $path.w; # True if writable say $path.x; # True if executable say $path.s; # File size in bytes say $path.z; # True if file is zero bytes
Practical usage:
my $config = "app.conf".IO; if $config.e &;& $config.f { say "Loading config ({ $config.s } bytes)..."; my $content = $config.slurp; # process config... } 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 }"; # Instant object say "Accessed: { $file.accessed }"; say "Changed: { $file.changed }"; }

Directory Operations

Listing Directory Contents

# List all entries in a directory for "/tmp".IO.dir -> $entry { say $entry.basename; } # Filter by pattern 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

"empty-dir".IO.rmdir; # only works on empty directories

Working with Paths

IO::Path provides useful methods for path manipulation:
my $path = "/home/user/documents/report.pdf".IO; say $path.basename; # report.pdf say $path.dirname; # /home/user/documents say $path.extension; # pdf say $path.volume; # (empty on Unix, drive letter on Windows) say $path.parent; # /home/user/documents say $path.parent.parent; # /home/user # Build paths with .add or / my $base = "/home/user".IO; say $base.add("documents").add("file.txt"); # /home/user/documents/file.txt

Resolving and Absolute Paths

my $rel = "../../etc/hosts".IO; say $rel.resolve; # resolves to absolute path say $rel.absolute; # converts to absolute without resolving symlinks

Standard I/O

Reading from STDIN

# Read a single line my $name = prompt("Enter your name: "); say "Hello, $name!"; # Read from STDIN line by line (useful for piped input) for $*IN.lines -> $line { say "Got: $line"; }
When your script receives piped input:
#!/usr/bin/env raku # pipe-example.raku # Usage: cat data.txt | raku pipe-example.raku 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"; # STDOUT with newline put "This also goes to STDOUT"; # STDOUT with newline print "STDOUT no newline"; # STDOUT without newline note "This goes to STDERR"; # STDERR with newline warn "This is a warning"; # STDERR with "Warning:" prefix
The .note method is particularly useful for debug output that should not interfere with normal output:
# In a script that produces data on STDOUT for 1..100 -> $n { note "Processing $n..." if $n %% 10; # progress to STDERR say $n * $n; # results to STDOUT }

The Standard I/O Handles

$*OUT.say("Explicit STDOUT"); $*ERR.say("Explicit STDERR"); my $line = $*IN.get; # read one line from STDIN

Practical Example: File Backup Script

#!/usr/bin/env raku 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; } # Backup all .txt files in the current directory 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

#!/usr/bin/env raku 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; } # Create a sample CSV 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 # Read and display it my @data = read-csv($csv-file); say "=== CSV Data ==="; for @data -> %row { say "{ %row<name> } is { %row<age> } years old, lives in { %row<city> }"; } # Clean up $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!