Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
take-apart.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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.csv

Three 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

Azvya Education Pvt. Ltd.VLSI Mentor
build-up.pl
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);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 | timing

Look 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.

  • catfile joins components into a path ending in a file.
  • catdir does the same for a path ending in a directory.
  • splitdir is the reverse: it breaks a path into its components, which is how you ask questions like "is this under run2" 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

Azvya Education Pvt. Ltd.VLSI Mentor
relative-absolute.pl
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");
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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  : no

Four 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 rel2abs it immediately. Then nothing later depends on the working directory.
  • Build every path with catfile or catdir. Never with . and a slash.
  • Use abs2rel only for display, at the point of printing.
  • canonpath before comparing or keying; Cwd::abs_path when you need the real resolved location.

4. What To Remember

  • basename gives the filename, dirname the directory with no trailing slash, fileparse all three pieces including the extension.
  • fileparse is the one that separates the extension — the job for changing .rpt to .csv. Give it a pattern; it does not guess.
  • fileparse's directory ends in a slash, dirname's does not.
  • Build paths with catfile and catdir. Measured: joining a directory that already ends in a slash gave proj/runs//setup.rpt by hand and the correct path from catfile.
  • 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.
  • splitdir breaks a path into components.
  • File::Spec is a class, so call it with an arrow.
  • abs2rel for readable output, rel2abs on the way in, so nothing later depends on the working directory.
  • canonpath tidies a path as a string — it does not resolve .., symlinks, or check existence. Cwd::abs_path does.
  • file_name_is_absolute saves 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.