Input/Output the definitive guide
The basics
The vast majority of common IO work is done by the IO::Path type. If you want to read from or write to a file in some form or shape, this is the class you want. It abstracts away the details of filehandles (or "file descriptors") and so you mostly don't even have to think about them.
Behind the scenes, IO::Path works with IO::Handle, a class which you can use directly if you need a bit more control than what IO::Path provides. When working with other processes, e.g. via Proc or Proc::Async types, you'll also be dealing with a subclass of IO::Handle: the IO::Pipe.
Lastly, you have the IO::CatHandle, as well as IO::Spec and its subclasses, that you'll rarely, if ever, use directly. These classes give you advanced features, such as operating on multiple files as one handle, or low-level path manipulations.
Along with all these classes, Raku provides several subroutines that let you indirectly work with these classes. These come in handy if you like functional programming style or in Raku one liners.
While IO::Socket and its subclasses also have to do with Input and Output, this guide does not cover them.
Navigating paths
What's an IO::Path anyway?
To represent paths as either files or directories, use IO::Path type. The simplest way to obtain an object of that type is to coerce a Str by calling the .IO method on it:
say 'my-file.txt'.IO; # OUTPUT: Ā«"my-file.txt".IOā¤Ā»
It may seem like something is missing hereāthere is no volume or absolute path involvedābut that information is actually present in the object. You can see it by using .raku method:
say 'my-file.txt'.IO.raku;
# OUTPUT: Ā«IO::Path.new("my-file.txt", :SPEC(IO::Spec::Unix), :CWD("/home/camelia"))ā¤Ā»
The two extra attributesāSPEC
and CWD
āspecify what type of operating
system semantics the path should use as well as the "current working directory"
for the path, i.e. if it's a relative path, then it's relative to that
directory.
This means that regardless of how you made one, an IO::Path object technically always refers to an absolute path. This is why its .absolute and .relative methods return Str objects and they are the correct way to stringify a path.
However, don't be in a rush to stringify anything. Pass paths around as IO::Path objects. All the routines that operate on paths can handle them, so there's no need to convert them.
Path parts
Given a local file name, it's very easy to get its components. For example, we have a file, "financial.data", in some directory, "/usr/local/data". Use Raku to analyze its path:
my $fname = "financial.data";
# Stringify the full path name
my $f = $fname.IO.absolute;
say $f;
# OUTPUT: Ā«/usr/local/data/financial.dataā¤Ā»
# Stringify the path's parts:
say $f.IO.dirname; # OUTPUT: Ā«/usr/local/dataā¤Ā»
say $f.IO.basename; # OUTPUT: Ā«financial.dataā¤Ā»
# And the basename's parts:
# Use a method for the extension:
say $f.IO.extension; # OUTPUT: Ā«dataā¤Ā»
# Remove the extension by redefining it:
say ($f.IO.extension("")).IO.basename; # OUTPUT: Ā«financialā¤Ā»
Working with files
Writing into files
Writing new content
Let's make some files and write and read data from them! The spurt and slurp routines write and read the data in one chunk respectively. Unless you're working with very large files that are difficult to store entirely in memory all at the same time, these two routines are for you.
"my-file.txt".IO.spurt: "I ā„ Raku!";
The code above creates a file named my-file.txt
in the current directory
and then writes text I ā„ Raku!
into it. If Raku is your first language,
celebrate your accomplishment! Try to open the file you created with a
text editor to verify what you wrote with your program. If you already know
some other language, you may be wondering if this guide missed anything like
handling encoding or error conditions.
However, that is all the code you need. The string will be encoded in utf-8
encoding by default and the errors are handled via the Failure
mechanism: these are exceptions you can handle using regular conditionals. In
this case, we're letting all potential Failures get sunk
after the call and so any Exceptions they contain will be
thrown.
Appending content
If you wanted to add more content to the file we created in the previous
section, you could note the spurt documentation mentions
:append
as one of its argument options. However, for finer control, let's
get ourselves an IO::Handle to work with:
my $fh = 'my-file.txt'.IO.open: :a;
$fh.print: "I count: ";
$fh.print: "$_ " for ^10;
$fh.close;
The .open method call opens our IO::Path
and returns an IO::Handle. We passed :a
as argument, to
indicate we want to open the file for writing in append mode.
In the next two lines of code, we use the usual .print
method on that IO::Handle to print a line with 11 pieces
of text (the 'I count: '
string and 10 numbers). Note that, once again,
Failure mechanism takes care of all the error checking for us.
If the .open fails, it returns a Failure,
which will throw when we attempt to call method the .print
on it.
Finally, we close the IO::Handle by calling the .close method on it. It is important that you do it, especially in large programs or ones that deal with a lot of files, as many systems have limits to how many files a program can have open at the same time. If you don't close your handles, eventually you'll reach that limit and the .open call will fail. Note that unlike some other languages, Raku does not use reference counting, so the filehandles are NOT closed when the scope they're defined in is left. They will be closed only when they're garbage collected and failing to close the handles may cause your program to reach the file limit before the open handles get a chance to get garbage collected.
Reading from files
Using IO::Path
We've seen in previous sections that writing stuff to files is a single-line of code in Raku. Reading from them, is similarly easy:
say 'my-file.txt'.IO.slurp; # OUTPUT: Ā«I ā„ Raku!ā¤Ā»
say 'my-file.txt'.IO.slurp: :bin; # OUTPUT: Ā«Buf[uint8]:0x<49 20 E2 99 A5 20 52 61 6B 75 21>ā¤Ā»
The .slurp method reads entire contents of the file
and returns them as a single Str object, or as a Buf
object, if binary mode was requested, by specifying :bin
named argument.
Since slurping loads the entire file into memory, it's not ideal for working with huge files.
The IO::Path type offers two other handy methods: .words and .lines that lazily read the file in smaller chunks and return Seq objects that (by default) don't keep already-consumed values around.
Here's an example that finds lines in a text file that mention Raku and prints them out. Despite the file itself being too large to fit into available RAM, the program will not have any issues running, as the contents are processed in small chunks:
.say for '500-PetaByte-File.txt'.IO.lines.grep: *.contains: 'Raku';
Here's another example that prints the first 100 words from a file, without loading it entirely:
.say for '500-PetaByte-File.txt'.IO.words: 100
Note that we did this by passing a limit argument to .words instead of, say, using a list indexing operation. The reason for that is there's still a filehandle in use under the hood, and until you fully consume the returned Seq, the handle will remain open. If nothing references the Seq, eventually the handle will get closed, during a garbage collection run, but in large programs that work with a lot of files, it's best to ensure all the handles get closed right away. So, you should always ensure the Seq from IO::Path's .words and .lines methods is fully reified; and the limit argument is there to help you with that.
Using IO::Handle
You can read from files using the IO::Handle type; this gives you a finer control over the process.
given 'some-file.txt'.IO.open {
say .readchars: 8; # OUTPUT: Ā«I ā„ Rakuā¤Ā»
.seek: 1, SeekFromCurrent;
say .readchars: 15; # OUTPUT: Ā«I ā„ Programmingā¤Ā»
.close
}
The IO::Handle gives you .read, .readchars, .get, .getc, .words, .lines, .slurp, .comb, .split, and .Supply methods to read data from it. Plenty of options; and the catch is you need to close the handle when you're done with it.
Unlike some languages, the handle won't get automatically closed when the
scope it's defined in is left. Instead, it'll remain open until it's garbage
collected. To make the closing business easier, some of the methods let you
specify a :close
argument, you can also use the
will leave trait, or the
does auto-close
trait provided by the
Trait::IO module.
The wrong way to do things
This section describes how NOT to do Raku IO.
Leave $*SPEC alone
You may have heard of $*SPEC and seen some code or books show its usage for splitting and joining path fragments. Some of the routine names it provides may even look familiar to what you've used in other languages.
However, unless you're writing your own IO framework, you almost never need to use $*SPEC directly. $*SPEC provides low-level stuff and its use will not only make your code tough to read, you'll likely introduce security issues (e.g. null characters)!
The IO::Path type is the workhorse of Raku world. It caters to all the path manipulation needs as well as provides shortcut routines that let you avoid dealing with filehandles. Use that instead of the $*SPEC stuff.
Tip: you can join path parts with /
and feed them to
IO::Path's routines; they'll still do The Right Thingā¢
with them, regardless of the operating system.
# WRONG!! TOO MUCH WORK!
my $fh = open $*SPEC.catpath: '', 'foo/bar', $file;
my $data = $fh.slurp;
$fh.close;
# RIGHT! Use IO::Path to do all the dirty work
my $data = 'foo/bar'.IO.add($file).slurp;
However, it's fine to use it for things not otherwise provided by IO::Path. For example, the .devnull method:
{
temp $*OUT = open :w, $*SPEC.devnull;
say "In space no one can hear you scream!";
}
say "Hello";
Stringifying IO::Path
Don't use the .Str
method to stringify IO::Path objects,
unless you just want to display them somewhere for information purposes or
something. The .Str
method returns whatever basic path string the
IO::Path was instantiated with. It doesn't consider the
value of the $.CWD attribute. For example,
this code is broken:
my $path = 'foo'.IO;
chdir 'bar';
# WRONG!! .Str DOES NOT USE $.CWD!
run <tar -cvvf archive.tar>, $path.Str;
The chdir call changed the value of the current directory,
but the $path
we created is relative to the directory before that change.
However, the IO::Path object does know what directory it's relative to. We just need to use .absolute or .relative to stringify the object. Both routines return a Str object; they only differ in whether the result is an absolute or relative path. So, we can fix our code like this:
my $path = 'foo'.IO;
chdir 'bar';
# RIGHT!! .absolute does consider the value of $.CWD!
run <tar -cvvf archive.tar>, $path.absolute;
# Also good:
run <tar -cvvf archive.tar>, $path.relative;
Be mindful of $*CWD
While usually out of view, every IO::Path object, by default, uses the current value of $*CWD to set its $.CWD attribute. This means there are two things to pay attention to.
temp the $*CWD
This code is a mistake:
# WRONG!!
my $*CWD = "foo".IO;
The my $*CWD
made $*CWD
undefined. The .IO coercer
then goes ahead and sets the $.CWD attribute
of the path it's creating to the stringified version of the undefined $*CWD
;
an empty string.
The correct way to perform this operation is use
temp instead of my
. It'll localize the effect of changes
to $*CWD, just like my
would,
but it won't make it undefined, so the .IO coercer will still
get the correct old value:
temp $*CWD = "foo".IO;
Better yet, if you want to perform some code in a localized $*CWD, use the indir routine for that purpose.