Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
list-a-directory.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
readdir returned 6 entries:
  . .. archive hold.rpt notes.txt setup.rpt

note . and .. -- every directory contains them

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

Azvya Education Pvt. Ltd.VLSI Mentor
names-not-paths.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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

Azvya Education Pvt. Ltd.VLSI Mentor
mkdir-and-paths.pl
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");
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 for a/b/c and a does not exist. mkdir creates exactly one level and will not create the parents.
  • File exists — the directory is already there. mkdir treats 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_path for an output directory. One call, no parent problem, safe on a re-run.
  • Use mkdir with or die only when you genuinely want to fail if the directory already exists — for example when creating a per-run directory whose name must be unique, where File exists is a real error you want to hear about.
  • Do not write mkdir $d unless -d $d; — that is Chapter 13.6's race, and make_path already 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 / closedir list one directory, and opendir needs or die like open.
  • Every listing contains . and ... Measured: 6 entries for 4 things. Filter them: grep { $_ ne "." && $_ ne ".." }.
  • readdir returns bare names, not paths. Measured: -f said 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, while opendir is for every entry.
  • The order from readdir is not sorted and not stable. Sort it if a human will see it.
  • mkdir makes one level and fails with No such file or directory on a deep path.
  • mkdir on an existing directory fails with File exists.
  • make_path creates 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_path is 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.