Perl Part I · Module 13 · File Handling
Directories: opendir, readdir and mkdir
Reading a directory looks like reading a file and has three differences that each cause a bug the first time you meet them. A directory listing always contains two entries you did not create and almost never want. What you get back is a list of plain names, not paths, so a file test on one of them quietly asks about the wrong place — section two shows that saying no for files that plainly exist. And making a directory only makes one level, so the obvious call fails on a path that needs two. All three are measured here, with the exact error messages, because recognising the message is faster than reasoning about the cause.
Intermediate9 min readPerlPart IFile Handling
Part I · Module 13 · Chapter 13.9 · Directories: opendir, readdir and mkdir
1. Listing a Directory
use strict;
use warnings;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
for my $n (qw(setup.rpt hold.rpt notes.txt)) {
open my $w, ">", "$dir/$n" or die $!; print $w "x\n"; close $w;
}
mkdir "$dir/archive" or die $!;
opendir my $dh, $dir or die "cannot read dir $dir: $!";
my @raw = readdir $dh;
closedir $dh;
printf "readdir returned %d entries:\n", scalar @raw;
printf " %s\n", join(" ", sort @raw);
print "\nnote . and .. -- every directory contains them\n";readdir returned 6 entries:
. .. archive hold.rpt notes.txt setup.rpt
note . and .. -- every directory contains themThe script created three files and one subdirectory — four things. readdir returned six.
The two extras are . (this directory) and .. (the parent). Every directory on the filesystem has them, you did not make them, and you almost never want them. Forgetting to remove them is the classic first bug: a loop that recurses into .. goes up and then back down for ever.
The three calls mirror the file ones: opendir to open, readdir to read, closedir to finish. Note opendir needs or die for the same reason open does, and the message should name the directory.
The standard way to get a usable list is to filter as you read:
my @entries = grep { $_ ne "." && $_ ne ".." } readdir $dh;
You will also see grep { !/^\.\.?$/ } which does the same job with a pattern, and grep { !/^\./ } which additionally hides all dotfiles — reasonable if you are listing for a user, wrong if a .cshrc is something your script must process.
readdir returns entries in whatever order the filesystem gives them, which is not sorted and not stable between machines. If the order matters — and for anything a human reads it does — sort it yourself.
2. readdir Gives Names, Not Paths
This is the one that produces a genuinely confusing bug.
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
for my $n (qw(setup.rpt hold.rpt)) {
open my $w, ">", "$dir/$n" or die $!; print $w "data\n"; close $w;
}
mkdir "$dir/archive" or die $!;
opendir my $dh, $dir or die $!;
my @entries = grep { $_ ne "." && $_ ne ".." } readdir $dh;
closedir $dh;
# readdir gives BARE NAMES. A test on the bare name asks about the WRONG path.
for my $e (sort @entries) {
my $full = File::Spec->catfile($dir, $e);
printf "%-12s -f on bare name: %-4s -f on full path: %s\n",
$e, (-f $e ? "yes" : "no"), (-f $full ? "yes" : "no");
}
print "\nreaddir returns names, not paths -- join them to the directory first\n";archive -f on bare name: no -f on full path: no
hold.rpt -f on bare name: no -f on full path: yes
setup.rpt -f on bare name: no -f on full path: yes
readdir returns names, not paths -- join them to the directory first-f on the bare name said no for setup.rpt and hold.rpt — two files that definitely exist. On the full path it said yes.
The reason is simple once stated. readdir gives you setup.rpt, not /tmp/xyz/setup.rpt. A relative path is resolved against the current working directory, which is wherever your script happens to be — almost certainly not the directory you just listed. So -f "setup.rpt" asks about a different file, and correctly reports that it is not there.
Note also that the test on the archive directory says no in both columns, correctly — -f is false for a directory, as Chapter 13.6 measured. Use -d for that.
3. Making Directories
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Path qw(make_path);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
# mkdir makes ONE level and fails if the parent is missing.
my $deep = File::Spec->catdir($dir, "a", "b", "c");
my $one = mkdir $deep;
printf "mkdir on a deep path : %s (%s)\n", ($one ? "ok" : "failed"), ($one ? "" : $!);
# make_path creates every missing level.
make_path($deep);
printf "make_path : %s\n", (-d $deep ? "created" : "failed");
# mkdir on an EXISTING directory fails -- so test first, or use make_path.
my $again = mkdir $deep;
printf "mkdir on an existing dir : %s (%s)\n", ($again ? "ok" : "failed"), ($again ? "" : $!);
make_path($deep);
printf "make_path again : %s (idempotent)\n", (-d $deep ? "still there" : "gone");mkdir on a deep path : failed (No such file or directory)
make_path : created
mkdir on an existing dir : failed (File exists)
make_path again : still there (idempotent)Two different mkdir failures, with two different messages:
No such file or directory— you asked fora/b/candadoes not exist.mkdircreates exactly one level and will not create the parents.File exists— the directory is already there.mkdirtreats that as a failure.
make_path from File::Path handles both: it creates every missing level, and it succeeds quietly if the directory already exists. That second property — being idempotent — is what makes it the right call in a script that may be re-run, which in this field is every script.
So the practical guidance:
- Use
make_pathfor an output directory. One call, no parent problem, safe on a re-run. - Use
mkdirwithor dieonly when you genuinely want to fail if the directory already exists — for example when creating a per-run directory whose name must be unique, whereFile existsis a real error you want to hear about. - Do not write
mkdir $d unless -d $d;— that is Chapter 13.6's race, andmake_pathalready does it correctly.
remove_tree from the same module is the counterpart, and it deletes a whole subtree. Treat it with the suspicion that deserves: build the path with catdir from variables you control, and never from a string a user supplied.
4. What To Remember
opendir/readdir/closedirlist one directory, andopendirneedsor dielikeopen.- Every listing contains
.and... Measured: 6 entries for 4 things. Filter them:grep { $_ ne "." && $_ ne ".." }. readdirreturns bare names, not paths. Measured:-fsaid no for two files that exist, because a bare name resolves against your current directory.- Join the name to the directory first with
File::Spec->catfile($dir, $entry). glob "$dir/*.rpt"returns paths, so it avoids that trap — good for a simple pattern, whileopendiris for every entry.- The order from
readdiris not sorted and not stable. Sort it if a human will see it. mkdirmakes one level and fails withNo such file or directoryon a deep path.mkdiron an existing directory fails withFile exists.make_pathcreates every level and is idempotent — the right call for an output directory in a script that may be re-run.- Do not write
mkdir unless -d— that is a race;make_pathis correct.
Chapter 13.10 takes the next step: instead of listing one directory, walking a whole tree of them with File::Find, including the three variables its callback gives you and the one that lets you skip a subtree you do not want to enter.
Where this fits
Part of the Perl curriculum.
