Perl Path::Tiny
last modified November 7, 2026
Working with files in core Perl means combining several modules:
File::Spec and File::Basename for paths,
File::Path to create and remove directories,
File::Copy, File::Temp, and the built-in functions
open, opendir, and the file test operators.
Path::Tiny wraps all of this in one small object. A path object
knows how to read and write itself, list its children, create its parents, and
report errors with clear exceptions. The module is widely used; the
Parallel::ForkManager article, for example,
relies on it to write its result files.
The module is installed with cpanm Path::Tiny. The examples were
run with Perl 5.44 and Path::Tiny 0.150 on Linux.
Building and inspecting paths
The path function creates a path object. The object stringifies to
the path, so it can be printed and passed to functions that expect a file name.
Most path methods are pure string operations and do not touch the file
system.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Path::Tiny;
my $file = path('/var/log/nginx/access.log.1');
say $file;
say $file->basename;
say $file->basename(qr/\.\d+$/);
say $file->parent;
say $file->parent(2);
say $file->sibling('error.log');
say path('/var/log')->child('nginx', 'error.log');
say $file->is_absolute ? 'absolute' : 'relative';
my $rel = path('docs/../src/./main.pl');
say $rel;
say $rel->absolute('/srv/app');
say path('/srv/app/src/main.pl')->relative('/srv/app/docs');
say path('/srv/app')->subsumes('/srv/app/src/main.pl') ? 'inside' : 'outside';
say path('~');
The basename method optionally strips a suffix given as a regular
expression. parent moves up one level, or several with an argument,
sibling replaces the last component, and child appends
components. A relative path is cleaned of . components but keeps
.., because collapsing it could be wrong when symbolic links are
involved. absolute resolves a relative path against a base, the
current directory by default, and relative computes the path from
one directory to another. subsumes tests whether a path lies inside
another, which is useful for rejecting paths that escape a directory. A leading
tilde expands to the home directory.
$ perl paths.pl /var/log/nginx/access.log.1 access.log.1 access.log /var/log/nginx /var/log /var/log/nginx/error.log /var/log/nginx/error.log absolute docs/../src/main.pl /srv/app/docs/../src/main.pl ../src/main.pl inside /home/jano
Reading and writing files
A file can be read or written in a single call. Each method has a variant with
the _utf8 suffix that encodes or decodes UTF-8, and a
_raw variant for binary data.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use utf8;
use open qw(:std :encoding(UTF-8));
use Path::Tiny;
my $file = path('notes.txt');
$file->spew_utf8("Úvod\nfirst line\n");
$file->append_utf8("second line\n", "third line\n");
print $file->slurp_utf8;
my @lines = $file->lines_utf8({ chomp => 1 });
say scalar(@lines), ' lines, last: ', $lines[-1];
my @head = $file->lines_utf8({ count => 2, chomp => 1 });
say "head: @head";
$file->edit_lines_utf8(sub { s/line/LINE/ });
print $file->slurp_utf8;
say $file->size, ' bytes, ', length $file->slurp_utf8, ' characters';
$file->remove;
say $file->exists ? 'still there' : 'removed';
The spew_utf8 method writes the file and append_utf8
adds to it. spew is atomic: it writes to a temporary file and
renames it over the target, so a reader never sees a half-written file.
slurp_utf8 returns the whole content, and lines_utf8
returns a list of lines; the chomp option removes the newlines and
count limits the number of lines. edit_lines_utf8
runs a block on each line with the line in $_ and writes the file
back, a safe in-place edit. The size method returns 40 bytes for 39
characters, because the letter Ú takes two bytes in UTF-8. Finally,
remove deletes the file.
$ perl readwrite.pl Úvod first line second line third line 4 lines, last: third line head: Úvod first line Úvod first LINE second LINE third LINE 40 bytes, 39 characters removed
File handles
For large files or line-by-line processing, Path::Tiny opens ordinary file
handles with openr, openw, and opena,
each with _utf8 and _raw variants.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Path::Tiny;
my $log = path('app.log');
$log->spew(map { "2026-11-07 10:0$_ " . ($_ % 3 ? 'INFO' : 'ERROR') . " event $_\n" } 1 .. 9);
my $fh = $log->openr;
my $errors = 0;
while (my $line = <$fh>) {
$errors++ if $line =~ /ERROR/;
}
close $fh;
say "errors: $errors";
my $out = path('errors.log')->openw;
print {$out} grep { /ERROR/ } $log->lines;
close $out;
print path('errors.log')->slurp;
path($_)->remove for 'app.log', 'errors.log';
The program creates a log file from a list of lines; spew
accepts a list as well as a string. It then counts the error lines while
reading through openr, which keeps only one line in memory, and
writes the error lines to a second file through openw. Unlike
open, these methods throw an exception when the file cannot be
opened, so there is no or die to forget.
$ perl lines.pl errors: 3 2026-11-07 10:03 ERROR event 3 2026-11-07 10:06 ERROR event 6 2026-11-07 10:09 ERROR event 9
Iterating over directories
The children method lists the entries of a directory, and
iterator and visit walk a whole tree.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Path::Tiny;
my $root = Path::Tiny->tempdir;
$root->child($_)->touchpath for qw(
README.md
src/app.pl
src/lib/Util.pm
t/basic.t
t/data/sample.csv
);
$root->child('src/app.pl')->spew("say 'hi';\n" x 20);
say 'children of root:';
say ' ', $_->relative($root) for sort $root->children;
say 'Perl files in src:';
say ' ', $_->basename for $root->child('src')->children(qr/\.p[lm]$/);
say 'recursive iterator:';
my $next = $root->iterator({ recurse => 1 });
my @all;
while (my $p = $next->()) {
push @all, $p->relative($root) . ($p->is_dir ? '/' : '');
}
say " $_" for sort @all;
say 'sizes with visit:';
my $sizes = $root->visit(
sub {
my ($p, $state) = @_;
$state->{ $p->relative($root) } = $p->size if $p->is_file;
},
{ recurse => 1 },
);
printf " %-20s %4d\n", $_, $sizes->{$_} for sort keys %$sizes;
The program builds a small project in a temporary directory.
touchpath creates an empty file together with any missing parent
directories. children returns path objects for the entries,
without . and .., and accepts a regular expression
that filters the names. The iterator returns one path per call until the tree is
exhausted, which keeps memory use low for large trees; the order is that of the
file system, so we sort the result. The visit method calls a
function for each path and passes a state hash that it returns at the end,
which makes it convenient for collecting data, here the file sizes.
$ perl tree.pl children of root: README.md src t Perl files in src: app.pl recursive iterator: README.md src/ src/app.pl src/lib/ src/lib/Util.pm t/ t/basic.t t/data/ t/data/sample.csv sizes with visit: README.md 0 src/app.pl 200 src/lib/Util.pm 0 t/basic.t 0 t/data/sample.csv 0
Temporary files and directories
The tempdir and tempfile class methods create
temporary paths using File::Temp under the hood. They are removed
automatically when the object goes out of scope.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Path::Tiny;
my $dir_name;
{
my $tmp = Path::Tiny->tempdir('demo-XXXXXX');
$dir_name = "$tmp";
my $file = $tmp->child('data.txt');
$file->spew("temporary data\n");
say 'dir exists: ', $tmp->is_dir ? 'yes' : 'no';
say 'name: ', $tmp->basename;
say 'parent: ', $tmp->parent;
print 'content: ', $file->slurp;
}
say 'after scope: ', -e $dir_name ? 'still exists' : 'deleted';
my $tf = Path::Tiny->tempfile(SUFFIX => '.json');
$tf->spew('{"ok":1}');
say 'temp file: ', $tf->basename;
say 'temp file size: ', $tf->size;
The template demo-XXXXXX gives the directory a recognizable
prefix; the X characters are replaced with random ones. The directory is
created in the system temporary directory, /tmp here. When the
block ends and $tmp is destroyed, the directory is deleted together
with its content; we keep only its name as a plain string to verify that. The
SUFFIX option of tempfile sets the file extension.
Temporary paths are ideal for tests and for intermediate files that must not be
left behind.
$ perl temp.pl dir exists: yes name: demo-k6_FLA parent: /tmp content: temporary data after scope: deleted temp file: k_f9rKhh29.json temp file size: 8
Copying, moving, and checksums
Path::Tiny also covers the common file operations: creating directories, copying, moving, comparing, and computing digests.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Path::Tiny;
my $base = path('work');
$base->remove_tree;
my $src = $base->child('in/report.txt');
$src->touchpath->spew("quarterly report\n");
$base->child('out/archive/2026')->mkdir;
my $copy = $src->copy($base->child('out/report-copy.txt'));
say 'copied: ', $copy;
say 'same bytes: ', $src->has_same_bytes($copy) ? 'yes' : 'no';
my $moved = $copy->move($base->child('out/archive/2026/report.txt'));
say 'moved to: ', $moved;
say 'copy left: ', $copy->exists ? 'yes' : 'no';
say 'digest: ', substr $src->digest, 0, 16;
say 'md5: ', $src->digest('MD5');
my @tree;
$base->visit(sub { push @tree, $_[0]->relative($base) }, { recurse => 1 });
say "tree:";
say " $_" for sort @tree;
The mkdir method creates a directory with all its parents, and
remove_tree deletes a directory recursively, doing nothing if it does
not exist. copy and move return the path of the new
file, so the result can be used directly. has_same_bytes compares
the content of two files. The digest method computes SHA-256 by
default, or any other algorithm supported by the Digest module,
such as MD5. The visit call collects the tree for printing.
$ perl ops.pl copied: work/out/report-copy.txt same bytes: yes moved to: work/out/archive/2026/report.txt copy left: no digest: 8a3c67892f82af22 md5: 6c88f0a21babcdba2ed8c37b1afe7707 tree: in in/report.txt out out/archive out/archive/2026 out/archive/2026/report.txt
Error handling
Path::Tiny methods die on failure with a Path::Tiny::Error
exception object, which carries the details of what went wrong.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Path::Tiny;
for my $file ('missing.txt', '/etc/shadow') {
eval { path($file)->slurp };
if (my $err = $@) {
if (ref $err && $err->isa('Path::Tiny::Error')) {
say "op=$err->{op} file=$err->{file} error=$err->{err}";
} else {
die $err;
}
}
}
eval { path('missing.txt')->slurp };
print "message: $@";
The exception is a hash-based object with the failed operation in
op, the file in file, and the system error message in
err. The program tells a missing file from a file without read
permission and re-throws anything that is not a Path::Tiny error. Printed as a
string, the exception produces a complete message with the file and line of the
call.
$ perl errors.pl op=open (<) file=missing.txt error=No such file or directory op=open (<) file=/etc/shadow error=Permission denied message: Error open (<) on 'missing.txt': No such file or directory at errors.pl line 20.
Comparison with core modules
The last example performs the same task twice, first with core modules and then with Path::Tiny: create a nested directory in a temporary location, write a file, copy it, list the text files, and read the lines back.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use File::Basename qw(basename dirname);
use File::Spec::Functions qw(catfile);
use File::Path qw(make_path remove_tree);
use File::Copy qw(copy);
use File::Temp qw(tempdir);
use Path::Tiny;
{
my $dir = tempdir(CLEANUP => 1);
my $sub = catfile($dir, 'data', 'raw');
make_path($sub);
my $file = catfile($sub, 'a.txt');
open my $fh, '>', $file or die "$file: $!";
print $fh "one\ntwo\n";
close $fh or die "$file: $!";
copy($file, catfile($sub, 'b.txt')) or die "copy: $!";
opendir my $dh, $sub or die "$sub: $!";
my @txt = sort grep { /\.txt$/ } readdir $dh;
closedir $dh;
open $fh, '<', $file or die "$file: $!";
chomp(my @lines = <$fh>);
close $fh;
say 'core: ', basename(dirname($file)), " | @txt | @lines";
}
{
my $dir = Path::Tiny->tempdir;
my $sub = $dir->child('data', 'raw');
$sub->mkdir;
my $file = $sub->child('a.txt');
$file->spew("one\ntwo\n");
$file->copy($sub->child('b.txt'));
my @txt = sort map { $_->basename } $sub->children(qr/\.txt$/);
my @lines = $file->lines({ chomp => 1 });
say 'Path::Tiny: ', $file->parent->basename, " | @txt | @lines";
}
The core version imports functions from five modules and needs explicit error
checks after open, close, copy, and
opendir. The Path::Tiny version expresses each step as a method
call and gets the error handling for free, at the cost of one CPAN dependency.
The core modules remain a good choice for scripts that must run on a bare Perl
installation, and for File::Find-style traversals with complex
pruning rules.
$ perl core.pl core: raw | a.txt b.txt | one two Path::Tiny: raw | a.txt b.txt | one two
Best practices
- Pick an explicit encoding: use the
_utf8methods for text and the_rawmethods for binary data. - Rely on exceptions: catch
Path::Tiny::Errorwhere a failure can be handled, and let it propagate otherwise. - Stream large files: use
lineswithcount, oropenr, instead of slurping files that may be large. - Use spew for safe updates: its atomic write never leaves a truncated file behind.
- Clean up with tempdir: temporary directories that delete themselves keep tests and scripts tidy.
- Check containment: use
subsumesorrealpathbefore working with paths supplied by users.
In this article we have built and inspected paths with Path::Tiny, read and
written files with encodings, used file handles, iterated over directory trees,
created temporary files and directories, copied, moved, and checksummed files,
handled errors, and compared the module with the core File::
modules.
Source
Author
List all Perl tutorials.