Perl file tests
last modified October 20, 2026
File test operators are unary operators that examine a file or a directory. Each
one is a dash and a single letter, such as -e or -d,
followed by a path or a filehandle. They return true or false, or a value such
as the size or the age of the file. File tests are the quick way to check a path
before opening it.
Existence and type: -e, -f, -d, -l
The test -e checks that a path exists, -f that it is a
plain file, -d that it is a directory and -l that it
is a symbolic link.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
my $file = "$dir/data.txt";
my $link = "$dir/data.lnk";
open(my $fh, '>', $file) or die "Cannot open $file: $!";
print $fh "hello\n";
close $fh;
symlink($file, $link) or die "Cannot link: $!";
for my $path ($file, $link, $dir, "$dir/missing") {
my $name = $path =~ s/^\Q$dir\E\/?//r || 'tempdir';
say sprintf "%-9s -e:%d -f:%d -d:%d -l:%d", $name,
(-e $path ? 1 : 0), (-f $path ? 1 : 0),
(-d $path ? 1 : 0), (-l $path ? 1 : 0);
}
The script creates a file, a symbolic link to it, and looks at the file, the
link, the directory and a missing path. The tests that follow symbolic links
(-e, -f, -d) report on the target, so the
link counts as a plain file. Only -l looks at the link itself. A
missing path fails every test. The temporary directory is removed when the
program ends.
$ perl exists.pl data.txt -e:1 -f:1 -d:0 -l:0 data.lnk -e:1 -f:1 -d:0 -l:1 tempdir -e:1 -f:0 -d:1 -l:0 missing -e:0 -f:0 -d:0 -l:0
File size: -s and -z
The test -s returns the size of a file in bytes and is false when
the file is empty. The test -z is true when the file has zero size.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
sub create {
my ($name, $content) = @_;
open(my $fh, '>', "$dir/$name") or die $!;
print $fh $content;
close $fh;
return "$dir/$name";
}
my $empty = create('empty.txt', '');
my $full = create('full.txt', "0123456789\n");
for my $path ($empty, $full) {
my $size = -s $path;
say sprintf "%-10s zero: %s, size: %s", (split m{/}, $path)[-1],
(-z $path ? 'yes' : 'no'),
$size // 'empty';
}
The create helper writes a file and returns its path.
-s returns the byte count, 11 for the text with its newline, so it
can be used both as a condition and as a value. For the empty file
-z is true. Both tests return undef for a path that does not exist,
which makes -z unsuitable as a check for existence.
$ perl size.pl empty.txt zero: yes, size: 0 full.txt zero: no, size: 11
Permissions: -r, -w, -x
The tests -r, -w and -x tell whether the
effective user can read, write or execute the file. Their uppercase variants
-R, -W and -X use the real user id
instead.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
my $file = "$dir/script.sh";
open(my $fh, '>', $file) or die $!;
print $fh "#!/bin/sh\necho hi\n";
close $fh;
for my $mode (0644, 0444, 0755) {
chmod $mode, $file;
say sprintf "%04o readable: %s writable: %s executable: %s", $mode,
(-r $file ? 'yes' : 'no'),
(-w $file ? 'yes' : 'no'),
(-x $file ? 'yes' : 'no');
}
The loop changes the mode with chmod and repeats the tests. With
mode 0444 the file is no longer writable and with 0755 it becomes executable.
The result depends on the user who runs the script: the superuser can read and
write any file, so the outcome shown here is for a regular user.
$ perl perms.pl 0644 readable: yes writable: yes executable: no 0444 readable: yes writable: no executable: no 0755 readable: yes writable: yes executable: yes
File age: -M, -A, -C
The tests -M, -A and -C return the age of
the file in days, relative to the time the program started ($^T).
They report the modification time, the access time and the inode change time.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
my $file = "$dir/old.log";
open(my $fh, '>', $file) or die $!;
close $fh;
my $two_days_ago = time - 2 * 24 * 3600;
utime($two_days_ago, $two_days_ago, $file) or die $!;
say sprintf "modified %.1f days ago", -M $file;
say sprintf "accessed %.1f days ago", -A $file;
if (-M $file > 1) {
say "file is older than one day";
}
The utime function sets the access and modification times to two
days ago, so both tests return about 2. The results are fractional numbers,
which is why the example formats them with sprintf. The age is
measured from the start of the program, not from the current moment, so a
long-running script should set $^T if it needs fresh values. Files
modified in the future have a negative age.
$ perl age.pl modified 2.0 days ago accessed 2.0 days ago file is older than one day
Stacked tests and the underscore filehandle
Since Perl 5.10 file tests can be stacked: -f -r $file is
equivalent to -r $file && -f $file. Each test looks at the
file, so Perl normally calls stat repeatedly. The special
filehandle _ reuses the result of the last test or
stat.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
my $file = "$dir/report.txt";
open(my $fh, '>', $file) or die $!;
print $fh "some text\n";
close $fh;
chmod 0644, $file;
say "regular and readable" if -f -r $file;
say "regular and executable" if -f -x $file;
say "regular, readable, writable" if -f -r -w $file;
if (-e $file) {
say "size: ", -s _, " bytes, mode: ", sprintf("%04o", (stat _)[2] & 07777);
say "is a file: ", (-f _ ? 'yes' : 'no');
}
The three stacked checks show that the file is regular, readable and writable,
but not executable. After -e $file has run, the later tests use
_ instead of the path. This saves system calls and avoids a race
where the file changes between two tests. The stacked form is evaluated right to
left, so the test nearest the path runs first.
$ perl stacked.pl regular and readable regular, readable, writable size: 10 bytes, mode: 0644 is a file: yes
Text and binary files, filehandles: -T, -B, -t
The tests -T and -B guess whether a file is text or
binary by examining its first block. File tests also accept an open filehandle,
and -t tells whether a handle is connected to a terminal.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Temp qw(tempdir);
my $dir = tempdir(CLEANUP => 1);
open(my $t, '>', "$dir/text.txt") or die $!;
print $t "plain text\nsecond line\n";
close $t;
open(my $b, '>:raw', "$dir/data.bin") or die $!;
print $b pack('C*', 0, 1, 2, 255, 254, 0, 128, 7);
close $b;
for my $name (qw(text.txt data.bin)) {
my $path = "$dir/$name";
say sprintf "%-9s text: %s binary: %s", $name,
(-T $path ? 'yes' : 'no'), (-B $path ? 'yes' : 'no');
}
open(my $in, '<', "$dir/text.txt") or die $!;
say "open handle is a file: ", (-f $in ? 'yes' : 'no');
say "handle size: ", -s $in;
close $in;
say "STDIN is a terminal: ", (-t STDIN ? 'yes' : 'no');
The text file passes -T, while the file with null bytes and high
bytes passes -B. The test is a heuristic, so an empty file is both
text and binary. Applying -f and -s to a lexical
filehandle works the same as with a path. Here the script was run with its input
redirected, so -t STDIN is false; it is true when run
interactively.
$ perl text.pl text.txt text: yes binary: no data.bin text: no binary: yes open handle is a file: yes handle size: 23 STDIN is a terminal: no
Best Practices
- Test before use, but handle open errors anyway: a file can disappear between the test and
open, so always check the result ofopentoo. - Use -e only when existence is the question:
-fand-dalso confirm the type. - Reuse the stat buffer: use the
_filehandle for several tests on one file. - Remember that tests follow symbolic links: use
-lwhen the link itself matters. - Compare ages with care:
-Mis relative to$^T, the program start time.
In this article we have covered the Perl file test operators: existence and
type, size, permissions, age, stacked tests, the _ filehandle, and
the text and binary checks.
Source
Author
List all Perl tutorials.