Correctly use Raku IO
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.
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.
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 = "financial.data";# Stringify the full path namemy = .IO.absolute;say ;# OUTPUT: «/usr/local/data/financial.data»# Stringify the path's parts:say .IO.dirname; # OUTPUT: «/usr/local/data»say .IO.basename; # OUTPUT: «financial.data»# And the basename's parts:# Use a method for the extension:say .IO.extension; # OUTPUT: «data»# Remove the extension by redefining it:say (.IO.extension("")).IO.basename; # OUTPUT: «financial»
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 Failure get sunk after the call and so any Exceptions they contain will be thrown.
If you wanted to add more content to the file we created in the previous section, you could note the spurt
mentions :append
as one of its argument options. However, for finer control, let's get ourselves an IO::Handle to work with:
my = 'my-file.txt'.IO.open: :a;.print: "I count: ";.print: "$_ " for ^10;.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.
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 slurp
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.
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
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.
This section describes how NOT to do Raku IO.
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 = open .catpath: '', 'foo/bar', ;my = .slurp;.close;
# RIGHT! Use IO::Path to do all the dirty workmy = 'foo/bar'.IO.add().slurp;
However, it's fine to use it for things not otherwise provided by IO::Path. For example, the .devnull
method:
say "Hello";
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 = 'foo'.IO;chdir 'bar';# WRONG!! .Str DOES NOT USE $.CWD!run <tar -cvvf archive.tar>, .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 = 'foo'.IO;chdir 'bar';# RIGHT!! .absolute does consider the value of $.CWD!run <tar -cvvf archive.tar>, .absolute;# Also good:run <tar -cvvf archive.tar>, .relative;
While usually out of view, every IO::Path object, by default, uses the current value of $*SPEC
to set its $.CWD
attribute. This means there are two things to pay attention to.
This code is a mistake:
# WRONG!!my = "foo".IO;
The my $*CWD
made $*SPEC
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 $*SPEC
, just like my
would, but it won't make it undefined, so the .IO
coercer will still get the correct old value:
temp = "foo".IO;
Better yet, if you want to perform some code in a localized $*SPEC
, use the indir
routine for that purpose.