- Item 51. Don't ignore the file test operators.
- Item 52. Always use the three-argument open.
- Item 53. Consider different ways of reading from a stream.
- Item 54. Open filehandles to and from strings.
- Item 55. Make flexible output.
- Item 56. Use File::Spec or Path::Class to work with paths.
- Item 57. Leave most of the data on disk to save memory.
Item 56. Use File::Spec or Path::Class to work with paths.
Perl runs on a couple hundred different platforms, and it's almost a law of software engineering that any useful program that you write will migrate from the system you most prefer to the system you least prefer. If you have to work with file paths, use one of the modules that handle all of the portability details for you. Not only is it safer, it's also easier.
Use File::Spec for portability
The File::Spec module comes with Perl, and the most convenient way to use it is through its function interface. It automatically imports several subroutines into the current namespace:
use File::Spec::Functions;
To construct a new path, you need the volume (maybe), the directory, and the filename. The volume and filename are easy:
my $volume = 'C:'; my $file = 'perl.exe';
You have to do a bit of work to create the directory from its parts, but that's not so bad. The rootdir function gets you started, and the catdir puts everything together according to the local system:
my $directory =
catdir
( rootdir(), qw(strawberry perl bin) );
If you are used to Windows or UNIX, you may not appreciate that some systems, such as VMS, format the directory portion of the path the same as the filename portion. If you use File::Spec, however, you don't have to worry too much about that.
Now that you have all three parts, you can put them together with catpath:
my $full_path =
catpath
( $volume, $directory, $file );
On UNIX-like filesystems, catpath ignores the argument for the volume, so if you don't care about that portion, you can use undef as a placeholder:
my $full_path =
catpath
( undef, $directory, $file );
This might seem like a silly way to do that if you think that your program will ever run only on your local system. If you don't want to handle the portable paths, just don't tell anyone about your useful program, so you'll never have to migrate it.
File::Spec has many other functions that deal with putting together and taking apart paths, as well as getting the local representations to common paths such as the parent directory, the temporary directory, the devnull device, and so on.
Use Path::Class if you can
The Path::Class module is a wrapper around File::Spec and provides convenience methods for things that are terribly annoying to work out yourself. To start, you construct a file or a directory object. On Windows, you just give file your Windows path, and it figures it out. The file function assumes that the path is for the local filesystem:
use Path::Class qw(file dir);
my $file =file
('C:/strawberry/perl/bin/perl.exe');
This path doesn't have to exist. The object in $file doesn't do anything to verify that the path is valid; it just deals with the rules for constructing paths on the local system.
If you aren't on Windows but still need to work with a Windows path, you use foreign_file instead:
my $file = foreign_file
( 'Win32',
'C:/strawberry/perl/bin/perl.exe' );
Now $file does everything correctly for a Windows path. If you need to go the other way and translate it into a path suitable for another system, you can use the as_foreign method:
# /strawberry/perl
my $unix_path = $file->as_foreign
('Unix');
Once you have the object, you call methods to interact with the file.
To get a filehandle for reading, call open with no arguments. It's really just a wrapper around IO::File, so it's just like calling IO::File->new:
my $read_fh = $file->open
or die "Could not open $file: $!";
If you want to create a new file, you start with a file object. That doesn't create the file, since the object simply deals with paths. When you call open and pass it the >, the file is created for you and you get back a write filehandle:
my $file = file('new_file');
my $fh = $file->open('>')
;
print $fh "Put this line in the file\n";
You can get the directory that contains the file, and then open a directory handle:
my $dir = $file->dir;
my $dh = $dir->open
or die "Could not open $dir: $!";
If you already have a directory object, it's easy to get its parent directory:
my $parent = $dir->parent;
You read from the directory handle with readdir, as normal, and get the name of the file. As with any readdir operation, you get only the file-name, so you have to add the directory portion yourself. That's not a problem when you use file to put it together for you:
while ( my $filename = readdir($dh) ) {
next if $filename =~ /^\.\.?$/;
my $file = file( $dir, $file )
;
print "Found $file\n";
}
Things to remember
- Don't hard-code file paths with operating system specific details.
- Use File::Spec or Path::Class to construct portable paths.