Perl Part I · Module 13 · File Handling
Paths With File::Basename and File::Spec
Every chapter in this module has quietly used these two modules, and this is where they get explained. One takes a path apart — give it a full path and get back the filename, the directory, or the extension on its own. The other puts a path together from pieces. Joining with a slash by hand usually works, which is exactly why the case where it does not is worth seeing: section two produces a path with a doubled separator from perfectly reasonable inputs. Neither module is complicated and both are already installed. The reason to use them is that they handle the awkward inputs you will eventually be handed, and they make the intent of the line obvious to whoever reads it next.
Foundation9 min readPerlPart IFile Handling
Part I · Module 13 · Chapter 13.11 · Paths With File::Basename and File::Spec
1. Taking a Path Apart
use strict;
use warnings;
use File::Basename qw(basename dirname fileparse);
my $path = "/proj/runs/run2/timing/setup.rpt";
printf "path : %s\n", $path;
printf "basename : %s\n", basename($path);
printf "dirname : %s\n", dirname($path);
my ($stem, $dir, $ext) = fileparse($path, qr/\.[^.]*/);
printf "fileparse : stem=[%s] dir=[%s] ext=[%s]\n", $stem, $dir, $ext;
# The common job: change the extension.
printf "as .csv : %s%s.csv\n", $dir, $stem;path : /proj/runs/run2/timing/setup.rpt
basename : setup.rpt
dirname : /proj/runs/run2/timing
fileparse : stem=[setup] dir=[/proj/runs/run2/timing/] ext=[.rpt]
as .csv : /proj/runs/run2/timing/setup.csvThree functions, three different jobs:
basename— just the filename,setup.rpt. This is what you put in a message or use as a label.dirname— everything before it, with no trailing slash.fileparse— all three pieces at once, and it is the only one that separates the extension.
The last line is the job fileparse exists for: turning setup.rpt into setup.csv. You need the stem without the extension to do that, and neither basename nor dirname gives you one.
Two details that matter in practice.
fileparse needs to be told what an extension looks like. The qr/\.[^.]*/ says "a dot and everything after it that is not a dot". Without a pattern it returns the whole filename as the stem and an empty extension — it does not guess. Given v1.2.rpt, that pattern leaves the stem as v1.2, which is the sensible answer.
fileparse's directory ends in a slash; dirname's does not. That is why "$dir$stem.csv" in the script works with no separator added. If you use dirname instead you have to add one — or better, use catfile, which is the next section.
2. Putting a Path Together
use strict;
use warnings;
use File::Spec;
# catfile / catdir join without worrying about separators
printf "catdir : %s\n", File::Spec->catdir("proj", "runs", "run2");
printf "catfile : %s\n", File::Spec->catfile("proj", "runs", "setup.rpt");
# a doubled separator is the bug hand-written joining actually has
my $dir = "proj/runs/";
printf "by hand : %s\n", $dir . "/" . "setup.rpt";
printf "catfile : %s\n", File::Spec->catfile($dir, "setup.rpt");
# splitdir takes a path apart into its components
my @parts = File::Spec->splitdir("proj/runs/run2/timing");
printf "splitdir : %d parts: %s\n", scalar @parts, join(" | ", @parts);catdir : proj/runs/run2
catfile : proj/runs/setup.rpt
by hand : proj/runs//setup.rpt
catfile : proj/runs/setup.rpt
splitdir : 4 parts: proj | runs | run2 | timingLook at the two middle lines. The directory already ends in a slash — which it will, if it came from fileparse, from a configuration file, or from a user who typed it that way. Joining by hand gives:
proj/runs//setup.rpt
catfile gives proj/runs/setup.rpt.
catfilejoins components into a path ending in a file.catdirdoes the same for a path ending in a directory.splitdiris the reverse: it breaks a path into its components, which is how you ask questions like "is this underrun2" or "how deep is this path".
Note these are called with an arrow — File::Spec->catfile(...) — because File::Spec is a class rather than a set of exported functions. That is Chapter 12.12's method call syntax; there is no object here, just a package name on the left.
3. Relative and Absolute
use strict;
use warnings;
use File::Spec;
my $base = "/proj/runs";
my $full = "/proj/runs/run2/timing/setup.rpt";
printf "abs2rel : %s\n", File::Spec->abs2rel($full, $base);
printf "rel2abs : %s\n", File::Spec->rel2abs("run2/timing/setup.rpt", $base);
printf "canonpath : %s\n", File::Spec->canonpath("/proj//runs/./run2/");
printf "is absolute /proj/x : %s\n", (File::Spec->file_name_is_absolute("/proj/x") ? "yes" : "no");
printf "is absolute run2/x : %s\n", (File::Spec->file_name_is_absolute("run2/x") ? "yes" : "no");abs2rel : run2/timing/setup.rpt
rel2abs : /proj/runs/run2/timing/setup.rpt
canonpath : /proj/runs/run2
is absolute /proj/x : yes
is absolute run2/x : noFour more functions, each solving a problem you will meet:
abs2rel turns a full path into one relative to a base. This is what makes output readable: a report listing forty files as /proj/runs/run2/timing/setup.rpt is much harder to scan than one listing them as run2/timing/setup.rpt. Chapter 13.10's traversal used it for exactly that.
rel2abs goes the other way. Use it early, on anything a user gave you, so that the rest of your script is working with absolute paths — which matters because Chapter 13.10 measured File::Find changing your working directory, and Chapter 12.4 measured use lib resolving against it.
canonpath tidies a path: it collapsed /proj//runs/./run2/ into /proj/runs/run2. Use it before comparing two paths or using one as a hash key. Note what it does not do — it is a string operation, so it does not resolve .. or symbolic links, and it does not check that the path exists. For that you want Cwd::abs_path, which does touch the filesystem.
file_name_is_absolute answers the question without you writing a pattern for it.
A short decision list for real scripts:
- Take anything from a user or a config file and
rel2absit immediately. Then nothing later depends on the working directory. - Build every path with
catfileorcatdir. Never with.and a slash. - Use
abs2relonly for display, at the point of printing. canonpathbefore comparing or keying;Cwd::abs_pathwhen you need the real resolved location.
4. What To Remember
basenamegives the filename,dirnamethe directory with no trailing slash,fileparseall three pieces including the extension.fileparseis the one that separates the extension — the job for changing.rptto.csv. Give it a pattern; it does not guess.fileparse's directory ends in a slash,dirname's does not.- Build paths with
catfileandcatdir. Measured: joining a directory that already ends in a slash gaveproj/runs//setup.rptby hand and the correct path fromcatfile. - A doubled separator usually still opens the file — and breaks every string comparison and hash key for it. That is the real reason, not Windows.
splitdirbreaks a path into components.File::Specis a class, so call it with an arrow.abs2relfor readable output,rel2abson the way in, so nothing later depends on the working directory.canonpathtidies a path as a string — it does not resolve.., symlinks, or check existence.Cwd::abs_pathdoes.file_name_is_absolutesaves you writing a pattern.
Chapter 13.12 covers the three cases where the plain read loop is not enough: a file with non-English text in it, a file that is not text at all, and a file too large to treat a line at a time.
Where this fits
Part of the Perl curriculum.
