ZetCode

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.

paths.pl
#!/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.

readwrite.pl
#!/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.

lines.pl
#!/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.

tree.pl
#!/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.

temp.pl
#!/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.

ops.pl
#!/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.

errors.pl
#!/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.

core.pl
#!/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

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

Path::Tiny on MetaCPAN

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Perl tutorials.