Perl’s IO::Handle

Perl traditionally does IO using filehandles, but these have a number of problems, least of all that they can not be treaded like normal variables and passing them to functions can be difficult. Fortunately, this being perl, there is an alternative and that is IO::Handle objects.

Replacing Filehandles with IO::Handles

Normally when you open a file for reading you would do somethig similar to:

open FILE, "<$filename or die "could not open $filename: $!\n";
while (my $line = <FILE>) {
   print $line;
}
close FILE;

We can replace that with IO::File:

use IO::File;
my $fh = new IO::File($filename, "r") or die "could not open $filename: $!\n";
while (my $line = $fh->getline()) {
   print $line;
}
$fh->close(); 

This simple example doesn’t really show you why you’d want to use IO::Handle based IO.

Normal Object

The biggest win over normal file handles is that IO::Handles are normal perl objects and can be passed to functions like you’d pass any ordinary object. Doing the same with file handles is cumbersome at best and a nightmare the rest of the time.

sub get_contents {
   my $in = shift;
   my $ret = ""
   while (<$in>) {
      $ret.= $_;
   }
   return $ret;
}

open FILE, "<$filename";
my $contents = get_contents(*FILE{IO});

Using several tricks we can get the above code, which is nearly usual perl, but notice the odd parameter passed to the function. This is because you can’t pass file handles, so you have to pass a reference to every thing called FILE, hence the *. The {IO} then restricts this to just the file handle. More information is available from stonehenge.com.

It turns out that thanks to the fact that you can use IO::Handles between <>brackets, we don’t need to rewrite the get_contents function. We can just write:

my $fh = new IO::File($filename, "r");
my $contents = get_contents($fh);

I don’t know about you, but I find this is much nicer.

Handle Agnostic

All handles inherit from IO::Handle and will implement all the functions that it defines. This means you can use the same code regardless of whether you have a socket, a file or a pipe.

As an example, lets write a program which outputs 100 random letters to a socket, a pipe or a file depending on the arguments. We start with the same 3 lines all programs should start with.

#!/usr/bin/perl

use strict;
use warnings;

Next we want to import the IO modules we will use

use IO::File;
use IO::Socket::INET;
use IO::Pipe;

Next, let’s write a function which output’s 100 random characters to a file handle that is passed as a parameter.

sub output_random_letters {
   my $out = shift;
   
   my $line = "";
   for (0..100) {
      $line .= chr(int(rand(26))+65);
   }
   $out->print($line);
   $out->flush();
}

So now, lets create our some file handles:

my $fh;

if ($ARGV[0] eq "file") {
   $fh = new IO::File($ARGV[1], "w");
} elsif ($ARGV[0] eq "pipe") {
   $fh = new IO::Pipe;
   $fh->writer($ARGV[1]);
} elsif ($ARGV[0] eq "socket") {
   $fh = IO::Socket::INET->new(PeerAddr => $ARGV[1],
                               PeerPort => $ARGV[2],
                               Proto => 'tcp');
} else {
   die "Unknown filehandle type: $ARGV[0]\n";
}

Now we can call our function and clean up.

output_random_bytes($fh);
$fh->close();
exit 0;

Now, lets test this.

beebo david% perl /tmp/io.pl file /tmp/output && cat /tmp/output
KWDQNVNDRNTPIJPGYRLQMIBWBKYAREJHENUDUOHGQUHBTXOVJSFWTREJAYYSRZEARAMWQVHFIUHGXDQGNIKOBNLKTBBJNQCMCDFRC
beebo david% perl /tmp/io.pl pipe cat
YVYLVUEGJQACDIKCGBWDESIEYJYAPGYMVDSXTDAECZQBAZRWENGVSTNTZMQGXPYERLRXOUTGBFDWBRRTHPBUQLUGKHUKTHJUUOPZZ
beebo david% nc -l -p 12345 &
[1] 12201
beebo david% perl /tmp/io.pl socket localhost 12345
YSQKJKIAADOPXHHDRZZFNFYGESBEVHRNGWQSNDMZQGEBJMVRQTMCPKJCMAODAQARXUYVSJMZEEUUUEBVCOREWFRQOFIHXSHYVAAHT
[1]+  Done                    nc -l -p 12345

Useful IO modules

IO::AtomicFile

This class is like IO::File, except it doesn’t overwrite the file you are writing to until you close the handle. This gives you atomic file writes so you don’t have to worry about your program dying in the middle of writing the file. It does this by actually writing to a temporary file and then calling rename() when you close the file handle. You can throw away your updates by calling $filehandle->delete().

my $passwd = IO::File("/etc/passwd", "w");
my $passwd = IO::AtomicFile("/etc/passwd", "w");

IO::Digest

This module hooks into the perlio layer to allow you to compute hash digests of your file as you read and write it, saving you from having to reread the file to calculate this. This can be particularly useful when you pass your handle to a Module to parse and you don’t want to or can’t reread the data (say when you are reading from the network). Basically, you create an IO::Digest object, register a IO::Handle object with it and then use tht handle. When you are done you can call IO::Digest->hexdigest() function to get the hash.

use IO::Digest;

my $fh = new IO::file($filename, "r");
my $iod = new IO::Digest($fh, ’MD5’);

read_and_parse($fh);

print $iod->hexdigest

IO::Zlib

This module gives you a IO::File api to read and write gzipped files. You create your handle and then use it as if it was a normal file.

$fh = new IO::Zlib("file.gz", "rb") or die "Could not open file.gz: $!\n";
while my $line = $fh->getline()) {
   print $line;
}
$fh->close;

You may want to investigate IO::Uncompress::AnyUncompress too.