Perl directories
last modified October 9, 2026
Directories are the containers of a file system, and Perl offers both built-in
functions and core modules for working with them. The built-ins
opendir, readdir, closedir,
mkdir, rmdir, chdir, and
glob cover the everyday operations, while File::Path,
File::Find, File::Basename, File::Spec,
and Cwd add higher level tools on top of them.
A directory handle is not a filehandle: you open a directory with
opendir, read its entries with readdir, and release
it with closedir. Entries come back as bare names without any
path prefix, and the order in which a file system reports them is not
guaranteed, so sorting is usually the first thing a directory program does.
In this tutorial we list, filter, sort, create, remove, and walk directories
in Perl with practical examples. Every program is complete and runnable, and
each one builds its own scratch directory with
File::Temp::tempdir, so nothing is written outside of it. The
output of every example is fixed and does not depend on the machine it runs
on.
Listing a directory with opendir, readdir, and closedir
The opendir function opens a directory and returns a handle. The
readdir function then returns the entries of that directory one
at a time in scalar context, and closedir releases the handle.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
open my $fh, '>', "$dir/alpha.txt" or die $!;
close $fh;
mkdir "$dir/subdir" or die $!;
opendir my $dh, $dir or die "Cannot open directory: $!";
my @names;
while (my $entry = readdir $dh) {
push @names, $entry;
}
closedir $dh;
say for sort @names;
readdir returns names only, without any path prefix, and it
includes the special entries . and .., so a raw
listing is rarely what a program wants. The example sorts the names because
the order in which readdir reports entries is decided by the
file system and is not guaranteed to be alphabetical. A directory is never
read into memory as a whole: the handle walks it entry by entry, which keeps
memory use constant even for very large directories.
$ perl list_dir.pl . .. alpha.txt subdir
Filtering out . and .. and hidden entries
Almost every listing has to skip the . and ..
entries. A regular expression that tests for a leading dot removes them
together with the hidden files.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
open my $fh, '>', "$dir/report.txt" or die $!;
close $fh;
open my $hidden, '>', "$dir/.config" or die $!;
close $hidden;
mkdir "$dir/archive" or die $!;
opendir my $dh, $dir or die "Cannot open directory: $!";
my @all = readdir $dh;
closedir $dh;
my @visible = grep { !/^\./ } @all;
my @hidden = grep { /^\./ && $_ ne '.' && $_ ne '..' } @all;
say "visible: @{[ sort @visible ]}";
say "hidden: @{[ sort @hidden ]}";
grep { !/^\./ } drops the two navigation entries and every hidden
name. When hidden files matter, compare the names directly instead:
grep { $_ ne '.' && $_ ne '..' } keeps dotfiles such as
.config. Directory names never carry a trailing newline, so no
chomp is needed, and readdir returns
undef once the last entry has been read.
$ perl filter_entries.pl visible: archive report.txt hidden: .config
Sorting directory entries deterministically
Because readdir order is arbitrary, sorting is what makes a
listing reproducible. Perl's sort is a string sort by default,
and extra comparisons are easy to add.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
for my $name (qw(Beta.txt alpha.md Gamma.txt delta.txt)) {
open my $fh, '>', "$dir/$name" or die $!;
close $fh;
}
opendir my $dh, $dir or die "Cannot open directory: $!";
my @entries = grep { !/^\./ } readdir $dh;
closedir $dh;
say "default: @{[ sort @entries ]}";
say "case folding: @{[ sort { lc $a cmp lc $b or $a cmp $b } @entries ]}";
say "by length: @{[ sort { length $a <=> length $b or $a cmp $b } @entries ]}";
say "reverse: @{[ sort { $b cmp $a } @entries ]}";
cmp compares strings and <=> compares numbers.
The or in the middle two lines adds a tie-breaker, so names of
equal length still come out in a stable alphabetical order. The last line
reverses the default sort, and folding with lc makes the
comparison ignore the case of the names. The same block applies to any list
of names, including the output of glob.
$ perl sort_entries.pl default: Beta.txt Gamma.txt alpha.md delta.txt case folding: alpha.md Beta.txt delta.txt Gamma.txt by length: Beta.txt alpha.md Gamma.txt delta.txt reverse: delta.txt alpha.md Gamma.txt Beta.txt
Listing with glob
The glob function expands shell style wildcards such as
*, ?, and [a-z], and returns the
matching paths as a list.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
my @files = (
'index.txt', 'notes.md', 'script.pl',
'src/app.pl', 'src/util.pl', 'docs/readme.md'
);
for my $name (@files) {
my ($subdir) = $name =~ m{^(.+)/[^/]+$};
make_path("$dir/$subdir") if defined $subdir;
open my $fh, '>', "$dir/$name" or die $!;
close $fh;
}
my @flat = glob "$dir/*";
s!^\Q$dir/\E!! for @flat;
say "flat: @{[ sort @flat ]}";
my @nested = glob "$dir/*/*";
s!^\Q$dir/\E!! for @nested;
say "nested: @{[ sort @nested ]}";
my @text = glob "$dir/*.txt";
s!^\Q$dir/\E!! for @text;
say "text: @{[ sort @text ]}";
glob returns paths in file system order, does not remove
duplicates, never matches dotfiles unless the pattern starts with a dot, and
does not descend into subdirectories by itself. The angle bracket form
<*.pl> is an abbreviation for the same operation, but
<$fh> is a readline, so calling glob with an
explicit pattern string keeps the intent clear. The pairs {a,b}
are alternatives inside a pattern, and ** only recurses when the
File::Glob flags ask for it; for real tree walking use
File::Find, which is covered further down.
$ perl glob_dir.pl flat: docs index.txt notes.md script.pl src nested: docs/readme.md src/app.pl src/util.pl text: index.txt
File test operators
File test operators answer questions about a path. Each one is a single
letter behind a hyphen and returns true or false, so they are normally used
as conditions inside if statements or in a ternary expression.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
my $file = "$dir/sample.txt";
open my $fh, '>', $file or die $!;
print {$fh} "hello\n";
close $fh;
chmod 0644, $file;
my $empty = "$dir/empty.txt";
open my $efh, '>', $empty or die $!;
close $efh;
chmod 0644, $empty;
mkdir "$dir/folder" or die $!;
say "exists: " . (-e $file ? 1 : 0);
say "is file: " . (-f $file ? 1 : 0);
say "is dir: " . (-d $file ? 1 : 0);
say "non-empty: " . (-s $file ? 1 : 0);
say "empty file: " . (-z $empty ? 1 : 0);
say "readable: " . (-r $file ? 1 : 0);
say "writable: " . (-w $file ? 1 : 0);
say "executable: " . (-x $file ? 1 : 0);
say "missing path: " . (-e "$dir/nope.txt" ? 1 : 0);
-e tests existence, -f a plain file,
-d a directory, -s a non-empty file,
-z an empty file, and -r, -w, and
-x stand for readable, writable, and executable. The results are
cached: a test without an argument reuses the last stat, so
-d _ repeats the previous test without touching the disk.
Permission tests describe the current user rather than the mode bits of the
file, which is why the freshly written file is not executable.
$ perl file_tests.pl exists: 1 is file: 1 is dir: 0 non-empty: 1 empty file: 1 readable: 1 writable: 1 executable: 0 missing path: 0
Checking whether a path is a file or a directory
Classifying a path means combining several tests. The order matters:
-l has to be checked before -f and -d,
because those two follow symbolic links.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
open my $fh, '>', "$dir/sample.txt" or die $!;
close $fh;
mkdir "$dir/folder" or die $!;
symlink 'sample.txt', "$dir/shortcut.txt" or die $!;
sub describe {
my $path = shift;
return "missing" unless -e $path;
return "symlink" if -l $path;
return "directory" if -d $path;
return "file" if -f $path;
return "other";
}
for my $name (qw(sample.txt folder shortcut.txt absent.txt)) {
printf "%-13s -> %s\n", $name, describe("$dir/$name");
}
The example funnels every question through one subroutine, which keeps the
logic in a single place when the same classification is needed in several
branches of a program. A symbolic link that points to a file reports true for
both -l and -f, while a dangling link reports false
for -e but still true for -l.
$ perl path_type.pl sample.txt -> file folder -> directory shortcut.txt -> symlink absent.txt -> missing
Creating directories with mkdir and File::Path::make_path
mkdir creates a single directory. To create missing parents as
well, use File::Path::make_path, which builds every component of
the path in one call.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
mkdir "$dir/logs", 0755 or die "mkdir failed: $!";
chmod 0755, "$dir/logs";
make_path("$dir/build/tmp/cache");
say "logs exists: " . (-d "$dir/logs" ? 1 : 0);
say "build tree exists: " . (-d "$dir/build/tmp/cache" ? 1 : 0);
my @created = grep { -d "$dir/$_" } qw(logs build build/tmp build/tmp/cache);
s!^\Q$dir/\E!! for @created;
say "created: @created";
say "logs mode: " . sprintf("%04o", (stat "$dir/logs")[2] & 07777);
The mode passed to mkdir is masked by the umask of the process,
so the directory can end up with fewer permissions than requested; the example
calls chmod afterwards to pin the mode down. make_path
accepts more than one path at a time, takes an optional mode
option, and reports problems on STDERR while continuing, so its
return value should be checked when a failure has to stop the program. It
fails if the directory already exists.
$ perl make_dirs.pl logs exists: 1 build tree exists: 1 created: logs build build/tmp build/tmp/cache logs mode: 0755
Removing directories with rmdir and File::Path::remove_tree
rmdir removes an empty directory only. Removing an entire tree
takes File::Path::remove_tree, which descends into the
subdirectories and deletes their contents first.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Path qw(make_path remove_tree);
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
mkdir "$dir/scratch" or die "mkdir failed: $!";
rmdir "$dir/scratch" or die "rmdir failed: $!";
say "empty dir removed: " . (-d "$dir/scratch" ? 0 : 1);
make_path("$dir/tmp/a/b");
open my $deep, '>', "$dir/tmp/a/b/deep.txt" or die $!;
close $deep;
open my $top, '>', "$dir/tmp/root.txt" or die $!;
close $top;
rmdir "$dir/tmp" or say "rmdir on a non-empty dir failed as expected";
remove_tree("$dir/tmp");
say "tree removed: " . (-d "$dir/tmp" ? 0 : 1);
say "nested file removed: " . (-e "$dir/tmp/a/b/deep.txt" ? 0 : 1);
Passing a non-empty directory to rmdir fails, which is the safety
net that keeps a simple cleanup from taking a whole tree with it.
remove_tree has no such protection, so it should only be called
with a path that the program built itself, as the example does. Symbolic
links inside the tree are removed rather than followed, and after the call
the directory no longer exists, so no separate cleanup is required.
$ perl remove_dirs.pl empty dir removed: 1 rmdir on a non-empty dir failed as expected tree removed: 1 nested file removed: 1
Walking a tree recursively with File::Find
File::Find walks a directory tree recursively. It calls a
callback for every entry it encounters, starting with the directory that was
passed in.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Find;
use File::Path qw(make_path);
use File::Temp qw(tempdir);
my $root = tempdir(CLEANUP => 1);
make_path("$root/$_") for qw(src docs data);
for my $name (qw(index.txt src/main.pl src/helper.pl docs/readme.md data/values.csv)) {
open my $fh, '>', "$root/$name" or die $!;
close $fh;
}
my @found;
find({ wanted => sub { push @found, $File::Find::name }, no_chdir => 1 }, $root);
my @relative = sort map { s!^\Q$root/\E!!r } grep { $_ ne $root } @found;
say for @relative;
my $dirs = grep { -d "$root/$_" } @relative;
my $files = grep { -f "$root/$_" } @relative;
say "directories: $dirs";
say "files: $files";
By default find changes into each directory before visiting it,
so $File::Find::name becomes relative to that directory and the
process is left in an unexpected working directory. The no_chdir
option keeps the paths anchored at the starting point and is usually what a
modern program wants. Inside the callback $_ holds the bare name,
$File::Find::dir the current directory, and
$File::Find::name the full path. Entries arrive in file system
order, so the example collects them and sorts before printing. The related
function finddepth visits the contents of a directory before the
directory itself.
$ perl find_tree.pl data data/values.csv docs docs/readme.md index.txt src src/helper.pl src/main.pl directories: 3 files: 5
Splitting paths with File::Basename and building them with File::Spec
Paths should be taken apart with File::Basename and put together
with File::Spec instead of with ad hoc string operations.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Basename qw(basename dirname fileparse);
use File::Spec;
my $path = '/var/log/nginx/access.log';
say "basename: " . basename($path);
say "no suffix: " . basename($path, '.log');
say "dirname: " . dirname($path);
my ($name, $dirs, $suffix) = fileparse($path, qr/\.[^.]*/);
say "fileparse: $name | $dirs | $suffix";
say "catfile: " . File::Spec->catfile('data', 'logs', 'app.log');
say "catdir: " . File::Spec->catdir('data', 'logs');
say "splitpath: " . join ' | ', File::Spec->splitpath('data/app.log');
say "absolute: " . (File::Spec->file_name_is_absolute($path) ? 1 : 0);
basename returns the last component of a path and can strip a
known suffix, while dirname returns everything in front of it.
fileparse splits a path into name, directory, and suffix in one
step, and it returns a list, so the destination list is written first.
catfile and catdir join components using the
separator of the current system, and splitpath breaks a path into
volume, directory, and file parts. The leading vertical bar in the
splitpath output stands for the empty volume name used on Unix;
on a system with drive letters the same slot would hold the drive.
$ perl path_parts.pl basename: access.log no suffix: access dirname: /var/log/nginx fileparse: access | /var/log/nginx/ | .log catfile: data/logs/app.log catdir: data/logs splitpath: | data/ | app.log absolute: 1
Getting and changing the working directory
Cwd::getcwd returns the current working directory as an absolute
path and abs_path resolves a relative path against it, while
chdir changes the working directory of the process.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Cwd qw(getcwd abs_path);
use File::Spec;
use File::Temp qw(tempdir);
my $start = getcwd();
say "start absolute: " . (File::Spec->file_name_is_absolute($start) ? 1 : 0);
my $dir = tempdir(CLEANUP => 1);
mkdir "$dir/child" or die $!;
chdir $dir or die "chdir failed: $!";
chdir "$dir/child" or die "chdir failed: $!";
say "in child dir: " . (getcwd() eq abs_path("$dir/child") ? 1 : 0);
chdir $start or die "chdir failed: $!";
say "back to start: " . (getcwd() eq $start ? 1 : 0);
chdir affects the whole process, not just the current subroutine,
so a program that changes directory should save the original path and return
to it before it exits. getcwd reports the physical path, which
differs from the logical path when a symbolic link was used to enter the
directory. A failed chdir is silent unless the return value is
checked, and File::Temp::tempdir never changes the working
directory by itself, which is why the example calls chdir
explicitly.
$ perl working_dir.pl start absolute: 1 in child dir: 1 back to start: 1
Best Practices
- Check every call:
opendir,mkdir,rmdir, andchdirreturn false on failure, so test the result and report$!. - Never rely on readdir order: file systems may return entries in any order, so sort the names before printing or comparing them.
- Filter navigation entries explicitly:
.and..belong to every directory and must be removed before the names are processed. - Prefer core modules to shell commands:
File::PathandFile::Findare portable and safe, unlike stringing together external tools. - Build paths with File::Spec: gluing strings with a slash breaks on systems that use another separator and mishandles edge cases.
- Confine destructive operations: create scratch
directories with
File::Tempand handremove_treea path that the program itself constructed.
In this article we have covered directories in Perl, from opendir,
readdir, and glob to mkdir,
rmdir, the recursive helpers of File::Path and
File::Find, and the path utilities of File::Basename,
File::Spec, and Cwd.
Source
Perl built-in functions · File::Path · File::Find · File::Basename · File::Spec · Cwd · File::Temp
Author
List all Perl tutorials.