File I/O and System Interaction¶
Reading, Writing, and Talking to the OS¶
Version: 1.1 Year: 2026
Copyright Notice¶
Copyright (c) 2025-2026 Ryan Thomas Robson / Robworks Software LLC. Licensed under CC BY-NC-ND 4.0. You may share this material for non-commercial purposes with attribution, but you may not distribute modified versions.
Perl was built for text processing on Unix systems. Its file I/O is not an afterthought bolted onto a language - it is woven into the core. Reading files, writing output, testing file attributes, running external commands, and managing processes all use simple, consistent syntax that maps directly to the underlying operating system calls.
This guide covers everything from opening your first file to forking child processes. By the end, you will be able to read and write files safely, navigate directories, run system commands, and build the kind of file-processing scripts that Perl is famous for.
Opening Files¶
The open function connects a filehandle to a file. Modern Perl uses the three-argument form:
Three things happen here:
my $fhdeclares a lexical filehandle - a variable that holds the connection to the file.'<'is the mode - read-only in this case.'data.txt'is the filename, completely separate from the mode.
The or die idiom terminates the program with an error message if open fails. $! contains the operating system's error message (like "No such file or directory" or "Permission denied").
File Modes¶
| Mode | Meaning | Creates file? | Truncates? |
|---|---|---|---|
< |
Read only | No | No |
> |
Write (create/truncate) | Yes | Yes |
>> |
Append | Yes | No |
+< |
Read and write | No | No |
+> |
Read and write (truncate) | Yes | Yes |
+>> |
Read and append | Yes | No |
The most common modes by far are < (read), > (write), and >> (append). Read/write modes (+<, +>, +>>) are rare in practice - most programs read from one file and write to another.
Why Three-Argument open?¶
Perl also supports a two-argument form:
This is dangerous because the mode is parsed from the filename string itself. If $filename comes from user input and contains "> /etc/passwd", Perl will happily open that file for writing. The three-argument form keeps mode and filename separate, preventing injection attacks:
# Safe: mode and filename are separate arguments
open my $fh, '<', $filename or die "Cannot open $filename: $!";
Lexical vs. Bareword Filehandles¶
Older Perl code uses bareword (uppercase) filehandles:
# Old style - bareword filehandle (global)
open(INFILE, '<', 'data.txt') or die "Cannot open: $!";
print INFILE "data"; # Oops: INFILE is open for reading, not writing
close INFILE;
Bareword filehandles are global, meaning they can collide across subroutines and modules. Lexical filehandles (stored in my variables) are scoped to the block where they are declared and automatically close when they go out of scope:
# Modern style - lexical filehandle (scoped)
{
open my $fh, '<', 'data.txt' or die "Cannot open: $!";
# ... use $fh ...
} # $fh goes out of scope, file is automatically closed
Always use lexical filehandles. The only bareword filehandles you should use are the built-in ones: STDIN, STDOUT, and STDERR.
Closing Files¶
Call close when you are done:
Checking the return value of close matters for write handles - it is the point where buffered output is flushed to disk. A full disk will cause close to fail even if all print calls succeeded. Lexical filehandles auto-close when they go out of scope, but explicit close makes intent clear and catches errors.
Reading Files¶
Line by Line¶
The most common pattern reads a file one line at a time using the diamond operator <$fh> inside a while loop:
open my $fh, '<', 'server.log' or die "Cannot open: $!";
while (my $line = <$fh>) {
chomp $line; # Remove trailing newline
print ">> $line\n";
}
close $fh;
chomp removes the trailing newline from a string. Without it, every line ends with \n, which causes double-spacing when you print with your own newline.
Why while and not for?
while (<$fh>) reads one line at a time, keeping memory usage constant regardless of file size. for my $line (<$fh>) reads the entire file into memory first, then iterates. For a 10 GB log file, while uses a few kilobytes; for needs 10 GB of RAM.
The Default Variable: $_¶
When you omit the variable in the while loop, Perl reads into $_:
while (<$fh>) {
chomp; # chomp operates on $_ by default
print if /ERROR/; # print and regex match also use $_
}
This is idiomatic Perl. The $_ variable is the default for dozens of built-in functions - chomp, print, split, lc, uc, and most regex operations.
Reading into an Array¶
To load all lines at once:
open my $fh, '<', 'names.txt' or die "Cannot open: $!";
my @lines = <$fh>;
close $fh;
chomp @lines; # chomp works on arrays too - removes newline from each element
print "Read ", scalar @lines, " lines\n";
Each element in @lines includes the trailing newline until you chomp the array.
Slurping an Entire File¶
Sometimes you need the whole file as a single string - for regex matching across lines, for instance:
# Method 1: local $/
my $content;
{
open my $fh, '<', 'config.txt' or die "Cannot open: $!";
local $/; # Undefine the input record separator
$content = <$fh>; # Read everything as one string
close $fh;
}
# Method 2: File::Slurper (recommended for production)
use File::Slurper 'read_text';
my $content = read_text('config.txt');
$/ is the input record separator - by default, it is "\n", which is why <$fh> reads one line at a time. Setting it to undef makes <$fh> read the entire remaining file. The local keyword ensures $/ reverts to its original value when the block exits.
The Diamond Operator: <>¶
The bare diamond operator <> (with no filehandle) reads from files named on the command line, or from STDIN if no files are given:
# Reads from file1.txt and file2.txt:
perl script.pl file1.txt file2.txt
# Reads from standard input:
echo "hello" | perl script.pl
$. holds the current line number. It resets to 1 at the start of each file when using <>.
The diamond operator looks at @ARGV, which contains the command-line arguments. It opens each filename in @ARGV in sequence. If @ARGV is empty, it reads from STDIN. This is exactly how Unix utilities like cat, grep, and sed work - and Perl borrowed the pattern directly.
Writing Files¶
print and say¶
Write to a filehandle with print:
open my $fh, '>', 'output.txt' or die "Cannot open for writing: $!";
print $fh "First line\n";
print $fh "Second line\n";
close $fh or die "Error closing: $!";
No Comma After the Filehandle
print $fh "text" has no comma between $fh and "text". Writing print $fh, "text" is a common mistake - Perl interprets it as printing both $fh and "text" to STDOUT.
The say function (requires use feature 'say' or use v5.10) is identical to print but adds a newline automatically:
use feature 'say';
open my $fh, '>', 'output.txt' or die "Cannot open: $!";
say $fh "First line"; # Equivalent to: print $fh "First line\n";
say $fh "Second line";
close $fh;
Formatted Output with printf¶
printf works like C's printf - format string with placeholders:
Common format specifiers:
| Specifier | Meaning | Example |
|---|---|---|
%s |
String | "hello" |
%d |
Integer | 42 |
%f |
Float | 3.14 |
%e |
Scientific | 3.14e+00 |
%x |
Hexadecimal | 2a |
%% |
Literal % |
% |
Width and precision: %10s (right-align in 10 chars), %-10s (left-align), %.2f (2 decimal places), %08d (zero-pad to 8 digits).
sprintf returns the formatted string instead of printing it:
Binary Mode¶
By default, Perl may translate line endings on some platforms. For binary files (images, compressed data, executables), use binmode:
open my $fh, '<', 'image.png' or die "Cannot open: $!";
binmode $fh;
# Read raw bytes
my $bytes_read = read $fh, my $buffer, 1024;
close $fh;
For UTF-8 text files, use the encoding layer:
open my $fh, '<:encoding(UTF-8)', 'unicode.txt' or die $!;
# or:
open my $fh, '<', 'unicode.txt' or die $!;
binmode $fh, ':encoding(UTF-8)';
File Tests¶
Perl provides a full set of file test operators - single-character flags prefixed with a hyphen that check properties of files and directories.
Common File Tests¶
my $file = '/etc/passwd';
if (-e $file) { print "Exists\n" }
if (-f $file) { print "Is a regular file\n" }
if (-d $file) { print "Is a directory\n" }
if (-r $file) { print "Is readable\n" }
if (-w $file) { print "Is writable\n" }
if (-x $file) { print "Is executable\n" }
| Operator | Tests for |
|---|---|
-e |
File exists |
-f |
Regular file (not directory or device) |
-d |
Directory |
-l |
Symbolic link |
-r |
Readable by effective uid |
-w |
Writable by effective uid |
-x |
Executable by effective uid |
-s |
File size in bytes (returns size, false if zero) |
-z |
File has zero size |
-T |
File looks like a text file |
-B |
File looks like a binary file |
Timestamps¶
Three operators return file age in days (fractional) since the script started:
| Operator | Measures |
|---|---|
-M |
Time since last modification |
-A |
Time since last access |
-C |
Time since inode change |
my $age = -M '/var/log/syslog';
printf "Log file last modified %.1f days ago\n", $age;
# Find files modified in the last 24 hours
if (-M $file < 1) {
print "$file was modified today\n";
}
The _ Cache Filehandle¶
Each file test performs a stat system call. When you chain multiple tests on the same file, use the special _ filehandle to reuse the cached stat data:
The _ uses the result from the most recent file test or stat call, avoiding redundant system calls. Without _, the three tests above would stat the file three times.
Stacking Tests (Perl 5.10+)¶
Perl 5.10 and later allow stacking file tests:
# These are equivalent:
if (-f $file && -r $file && -w $file) { ... }
if (-f -r -w $file) { ... } # Stacked - reads right to left
Stacked tests read right to left: -f -r -w $file tests writable first, then readable, then regular file. They also use the _ cache automatically.
stat and File Information¶
The stat function returns a 13-element list with detailed information about a file:
| Index | Field | Description |
|---|---|---|
| 0 | dev |
Device number |
| 1 | ino |
Inode number |
| 2 | mode |
File mode (permissions and type) |
| 3 | nlink |
Number of hard links |
| 4 | uid |
User ID of owner |
| 5 | gid |
Group ID of owner |
| 6 | rdev |
Device identifier (special files) |
| 7 | size |
File size in bytes |
| 8 | atime |
Last access time (epoch seconds) |
| 9 | mtime |
Last modification time (epoch seconds) |
| 10 | ctime |
Inode change time (epoch seconds) |
| 11 | blksize |
Preferred block size for I/O |
| 12 | blocks |
Number of blocks allocated |
Remembering indices is painful. The File::stat module provides named access:
use File::stat;
my $st = stat('data.txt') or die "Cannot stat: $!";
printf "Size: %d bytes\n", $st->size;
printf "Owner UID: %d\n", $st->uid;
printf "Modified: %s\n", scalar localtime($st->mtime);
printf "Permissions: %04o\n", $st->mode & 07777;
lstat for Symbolic Links¶
lstat is identical to stat but returns information about the symlink itself rather than the file it points to:
if (-l '/usr/local/bin/python') {
my @link_info = lstat('/usr/local/bin/python');
my @target_info = stat('/usr/local/bin/python');
printf "Link size: %d, Target size: %d\n", $link_info[7], $target_info[7];
}
Timestamps and Formatting¶
Convert epoch timestamps to readable dates with localtime or the POSIX::strftime function:
use POSIX 'strftime';
my $mtime = (stat 'data.txt')[9];
my $formatted = strftime "%Y-%m-%d %H:%M:%S", localtime($mtime);
print "Last modified: $formatted\n";
Directory Operations¶
opendir/readdir/closedir¶
The opendir/readdir/closedir trio works like open/<$fh>/close but for directories:
opendir my $dh, '/var/log' or die "Cannot open directory: $!";
my @entries = readdir $dh;
closedir $dh;
# readdir returns ALL entries, including . and ..
my @files = grep { $_ ne '.' && $_ ne '..' } @entries;
print "$_\n" for sort @files;
readdir Returns Names, Not Paths
readdir returns bare filenames, not full paths. To use the results with open, stat, or file tests, prepend the directory path:
Filtering Directory Contents¶
Combine readdir with grep and file tests:
opendir my $dh, $dir or die $!;
# Only regular files
my @files = grep { -f "$dir/$_" } readdir $dh;
# Rewind to read again
rewinddir $dh;
# Only directories (excluding . and ..)
my @subdirs = grep { -d "$dir/$_" && $_ !~ /^\./ } readdir $dh;
closedir $dh;
glob and Filename Expansion¶
The glob function expands shell-style wildcards and returns full paths:
# Find all .txt files in current directory
my @txt_files = glob('*.txt');
# Find all .log files in /var/log
my @logs = glob('/var/log/*.log');
# Multiple patterns
my @code = glob('*.pl *.pm *.t');
# Angle bracket syntax (same as glob)
my @configs = <~/.config/*.conf>;
glob returns full paths (or relative to the current directory), unlike readdir which returns bare names. For simple wildcard matching, glob is more convenient than opendir/readdir.
Recursive Traversal with File::Find¶
For walking directory trees, use File::Find:
use File::Find;
find(sub {
return unless -f; # Skip non-files
return unless /\.pm$/; # Only .pm files
print "$File::Find::name\n"; # Full path
}, '/usr/lib/perl5');
File::Find calls your subroutine once for each file and directory found. Inside the callback:
$_is the bare filename$File::Find::nameis the full path$File::Find::diris the current directory
Creating and Removing Directories¶
mkdir 'output' or die "Cannot create directory: $!";
mkdir 'output', 0755 or die "Cannot create directory: $!"; # with permissions
rmdir 'output' or die "Cannot remove directory: $!"; # must be empty
rmdir only removes empty directories. For recursive removal, use File::Path:
use File::Path qw(make_path remove_tree);
make_path('a/b/c/d'); # Like mkdir -p
remove_tree('a'); # Like rm -rf
Changing Directory¶
chdir changes the current working directory:
Avoid chdir in Libraries
chdir affects the entire process. In subroutines and modules, use full paths instead of changing directories. If you must chdir, save and restore the original directory:
File I/O Decision Tree¶
When choosing how to interact with files and commands, this decision tree covers the most common paths:
flowchart TD
A[Need to work with a file?] --> B{Read or Write?}
B -->|Read| C{Whole file or line by line?}
C -->|Line by line| D["while (<$fh>) { }"]
C -->|Whole file| E["my @lines = <$fh>\nor slurp"]
B -->|Write| F{New or append?}
F -->|New/overwrite| G["open $fh, '>', $file"]
F -->|Append| H["open $fh, '>>', $file"]
B -->|Both| I["open $fh, '+<', $file"]
A --> J{External command?}
J -->|Run, ignore output| K["system 'cmd'"]
J -->|Capture output| L["my $out = `cmd`"]
J -->|Stream output| M["open $fh, '-|', 'cmd'"]
Running External Commands¶
Perl gives you several ways to run external programs. Each one has a different purpose, and choosing the right one matters.
system()¶
system runs a command and waits for it to finish. It returns the exit status, not the output:
my $status = system('ls', '-la', '/tmp');
if ($status == 0) {
print "Command succeeded\n";
} else {
warn "Command failed with status: ", $status >> 8, "\n";
}
The return value is the raw wait status. To get the actual exit code, shift right by 8: $status >> 8. Or check $? after the call:
system('make', 'install');
if ($? == -1) {
die "Failed to execute: $!";
} elsif ($? & 127) {
die "Killed by signal ", $? & 127;
} else {
printf "Exited with value %d\n", $? >> 8;
}
Shell Injection
system with a single string argument passes it through the shell:
Always use the list form to bypass the shell:
Backticks and qx()¶
Backticks and qx() capture the command's standard output as a string:
my $output = `ls -la /tmp`;
# or equivalently:
my $output = qx(ls -la /tmp);
# In list context, returns one element per line
my @lines = `ls -1 /tmp`;
chomp @lines;
The exit status is available in $? after the backtick expression.
open with Pipes¶
For streaming data to or from a command, use open with pipe modes:
# Read from a command (like backticks but streaming)
open my $reader, '-|', 'find', '/var/log', '-name', '*.log'
or die "Cannot run find: $!";
while (<$reader>) {
chomp;
print "Found: $_\n";
}
close $reader;
# Write to a command
open my $writer, '|-', 'mail', '-s', 'Report', 'admin@example.com'
or die "Cannot run mail: $!";
print $writer "Today's report:\n";
print $writer "All systems operational.\n";
close $writer;
The -| mode opens a pipe for reading from the command. The |- mode opens a pipe for writing to the command's STDIN. The three-argument form with list arguments avoids shell interpretation.
exec()¶
exec replaces the current Perl process entirely with the new command:
exec('vim', $filename) or die "Cannot exec vim: $!";
# This line NEVER executes - exec replaces the process
print "You will never see this\n";
exec is typically used after fork (covered in the next section) to run a command in the child process. If you just want to run a command and continue your Perl script, use system instead.
Comparison Table¶
| Method | Captures output? | Returns to Perl? | Shell involved? |
|---|---|---|---|
system('cmd') |
No (goes to terminal) | Yes (returns exit status) | Yes (single string) |
system('cmd', @args) |
No | Yes | No (list form) |
`cmd` / qx(cmd) |
Yes (returns stdout) | Yes | Yes |
open $fh, '-\|', 'cmd' |
Yes (via filehandle) | Yes | No (list form) |
exec('cmd') |
N/A | No (replaces process) | Yes (single string) |
exec('cmd', @args) |
N/A | No | No (list form) |
IPC::Open3 for Full Control
When you need separate access to a command's STDIN, STDOUT, and STDERR, the IPC::Open3 module gives you filehandles for all three streams:
Process Control¶
fork()¶
fork creates a copy of the current process. The parent gets the child's PID; the child gets 0:
my $pid = fork();
die "Fork failed: $!" unless defined $pid;
if ($pid) {
# Parent process
print "Parent ($$): spawned child $pid\n";
waitpid($pid, 0); # Wait for child to finish
print "Child exited with status: ", $? >> 8, "\n";
} else {
# Child process
print "Child ($$): doing work...\n";
sleep 2;
exit 0; # Child exits - DO NOT continue into parent's code
}
Always exit or exec in the Child
After fork, the child process has a full copy of the parent's code. If you forget to exit or exec, the child will fall through and execute the parent's remaining code - leading to duplicate output, double database connections, and other chaos.
wait and waitpid¶
wait blocks until any child process exits. waitpid waits for a specific child:
# Wait for any child
my $finished_pid = wait();
# Wait for a specific child
waitpid($pid, 0); # Block until $pid exits
# Non-blocking check
use POSIX ':sys_wait_h';
my $result = waitpid($pid, WNOHANG);
if ($result == 0) {
print "Child still running\n";
} elsif ($result > 0) {
print "Child finished\n";
}
The fork-exec Pattern¶
The classic Unix pattern for running a command in a subprocess:
my $pid = fork();
die "Fork failed: $!" unless defined $pid;
if ($pid == 0) {
# Child: replace with the desired command
exec('sort', '-u', 'data.txt') or die "Cannot exec: $!";
}
# Parent: wait for the child
waitpid($pid, 0);
my $exit_code = $? >> 8;
Signals with kill¶
Send signals to processes with kill:
kill 'TERM', $pid; # Polite termination request
kill 'KILL', $pid; # Forceful kill (cannot be caught)
kill 'HUP', $pid; # Hangup (often means "reload config")
kill 0, $pid; # Check if process exists (signal 0)
Signal 0 is a useful trick - it does not actually send a signal but returns true if the process exists and you have permission to signal it:
Capturing Child Exit Status¶
The $? variable holds the status of the last child process. It packs three pieces of information:
system('some_command');
my $exit_code = $? >> 8; # Bits 8-15: exit code (0-255)
my $signal = $? & 127; # Bits 0-6: signal that killed it
my $core_dump = $? & 128; # Bit 7: core dump flag
if ($? == -1) {
print "Failed to execute: $!\n";
} elsif ($signal) {
printf "Died from signal %d%s\n", $signal, $core_dump ? " (core dumped)" : "";
} else {
printf "Exited with code %d\n", $exit_code;
}
Environment and Filesystem¶
%ENV¶
The %ENV hash gives you direct access to environment variables:
# Read environment variables
my $home = $ENV{HOME};
my $path = $ENV{PATH};
my $user = $ENV{USER} // 'unknown';
# Set environment variables (affects child processes)
$ENV{DEBUG} = 1;
$ENV{DATABASE_URL} = 'postgres://localhost/mydb';
# Remove an environment variable
delete $ENV{DEBUG};
# Print all environment variables
for my $key (sort keys %ENV) {
print "$key=$ENV{$key}\n";
}
Changes to %ENV affect any child processes spawned by system, backticks, or fork/exec. They do not affect the parent shell that launched your Perl script.
Program Identity¶
print "Script: $0\n"; # Program name/path
print "PID: $$\n"; # Process ID
print "Effective UID: $>\n"; # Effective user ID
print "Effective GID: $)\n"; # Effective group ID
$0 contains the script name as it was invoked. You can assign to $0 to change what shows up in ps output - useful for long-running daemons.
Portable Path Handling¶
Hardcoding / as the directory separator breaks on Windows. Use File::Spec for portable path construction:
use File::Spec;
my $path = File::Spec->catfile('usr', 'local', 'bin', 'perl');
# Unix: usr/local/bin/perl
# Windows: usr\local\bin\perl
my ($volume, $dir, $file) = File::Spec->splitpath('/usr/local/bin/perl');
# $volume = '' $dir = '/usr/local/bin/' $file = 'perl'
my $abs = File::Spec->rel2abs('lib/My/Module.pm');
# Converts relative path to absolute
File::Basename¶
File::Basename extracts filename components:
use File::Basename;
my $path = '/home/user/documents/report.pdf';
my $name = basename($path); # report.pdf
my $dir = dirname($path); # /home/user/documents
my ($base, $dirpart, $ext) = fileparse($path, qr/\.[^.]*/);
# $base = 'report' $dirpart = '/home/user/documents/' $ext = '.pdf'
Essential File Utilities¶
| Module | Purpose |
|---|---|
Cwd |
Get current working directory (getcwd, abs_path) |
File::Copy |
Copy and move files (copy, move) |
File::Path |
Create/remove directory trees (make_path, remove_tree) |
File::Temp |
Create temporary files and directories |
File::Spec |
Portable path manipulation |
File::Basename |
Extract path components |
use Cwd 'getcwd';
use File::Copy qw(copy move);
use File::Temp 'tempfile';
# Current directory
my $cwd = getcwd();
# Copy a file
copy('source.txt', 'backup.txt') or die "Copy failed: $!";
# Move (rename) a file
move('old.txt', 'new.txt') or die "Move failed: $!";
# Create a temporary file (auto-deleted when $fh goes out of scope)
my ($fh, $tempname) = tempfile(UNLINK => 1);
print $fh "Temporary data\n";
print "Temp file: $tempname\n";
Practical Patterns¶
In-Place Editing¶
Perl can modify files directly with the -i flag - the same capability sed -i provides:
# Replace all occurrences of "foo" with "bar" in file.txt
perl -i -pe 's/foo/bar/g' file.txt
# Same, but keep a backup with .bak extension
perl -i.bak -pe 's/foo/bar/g' file.txt
From within a script, use $^I and @ARGV:
local $^I = '.bak'; # Set backup extension (empty string = no backup)
local @ARGV = ('file.txt'); # Files to process
while (<>) {
s/foo/bar/g;
print; # Prints to the modified file, not STDOUT
}
File Locking with flock¶
When multiple processes access the same file, use flock to prevent corruption:
use Fcntl ':flock'; # Import LOCK_SH, LOCK_EX, LOCK_NB, LOCK_UN
open my $fh, '>>', 'counter.txt' or die "Cannot open: $!";
# Exclusive lock - blocks until acquired
flock($fh, LOCK_EX) or die "Cannot lock: $!";
# Now safe to write
print $fh "Entry at " . localtime() . "\n";
# Lock is released when filehandle is closed
close $fh;
Lock types:
| Constant | Value | Meaning |
|---|---|---|
LOCK_SH |
1 | Shared lock (multiple readers) |
LOCK_EX |
2 | Exclusive lock (single writer) |
LOCK_NB |
4 | Non-blocking (combine with \|) |
LOCK_UN |
8 | Unlock |
# Non-blocking lock attempt
if (flock($fh, LOCK_EX | LOCK_NB)) {
print "Got the lock\n";
# ... do work ...
} else {
print "File is locked by another process\n";
}
flock Caveats
flock is advisory - it only works if all processes accessing the file use flock. A process that opens the file without locking will ignore locks entirely. Also, flock does not work over NFS on many systems.
Atomic Writes¶
Writing directly to a file risks corruption if the script crashes mid-write. The safe pattern is write-to-temp-then-rename:
use File::Temp 'tempfile';
use File::Copy 'move';
my $target = 'config.json';
# Write to a temporary file in the same directory
my ($tmp_fh, $tmp_name) = tempfile(DIR => '.', UNLINK => 0);
print $tmp_fh '{"setting": "new_value"}';
close $tmp_fh or die "Error closing temp file: $!";
# Atomic rename (on the same filesystem)
move($tmp_name, $target) or die "Cannot rename: $!";
The rename (or move on the same filesystem) operation is atomic on Unix - the file either has the old content or the new content, never a partial write. Creating the temp file in the same directory as the target ensures they are on the same filesystem.
Log Rotation¶
A practical pattern combining file tests, directory operations, and file manipulation:
use strict;
use warnings;
use File::Copy 'move';
sub rotate_log {
my ($logfile, $max_rotations) = @_;
$max_rotations //= 5;
# Delete the oldest
unlink "$logfile.$max_rotations" if -f "$logfile.$max_rotations";
# Shift existing rotated files up
for my $n (reverse 1 .. $max_rotations - 1) {
my $src = "$logfile.$n";
my $dst = "$logfile." . ($n + 1);
move($src, $dst) if -f $src;
}
# Rotate the current log
if (-f $logfile) {
move($logfile, "$logfile.1") or warn "Cannot rotate $logfile: $!";
}
# Create fresh log
open my $fh, '>', $logfile or die "Cannot create $logfile: $!";
close $fh;
}
# Rotate if log exceeds 10 MB
if (-s '/var/log/app.log' > 10 * 1024 * 1024) {
rotate_log('/var/log/app.log', 7);
}
Quick Reference¶
File Open Modes¶
| Mode | Meaning |
|---|---|
open $fh, '<', $f |
Read |
open $fh, '>', $f |
Write (truncate) |
open $fh, '>>', $f |
Append |
open $fh, '+<', $f |
Read/write |
open $fh, '-\|', @cmd |
Read from command |
open $fh, '\|-', @cmd |
Write to command |
File Test Operators¶
| Test | Meaning |
|---|---|
-e |
Exists |
-f |
Regular file |
-d |
Directory |
-r/-w/-x |
Readable/writable/executable |
-s |
Size in bytes |
-z |
Zero size |
-l |
Symlink |
-M/-A/-C |
Modification/access/change age (days) |
External Commands¶
| Syntax | Purpose |
|---|---|
system(@cmd) |
Run command, get exit status |
`cmd` |
Run command, capture output |
open $fh, '-\|', @cmd |
Stream output from command |
open $fh, '\|-', @cmd |
Stream input to command |
exec(@cmd) |
Replace current process |
fork() |
Create child process |
Essential Modules¶
| Module | Purpose |
|---|---|
File::Copy |
copy, move |
File::Path |
make_path, remove_tree |
File::Temp |
Temporary files/directories |
File::Find |
Recursive directory traversal |
File::Spec |
Portable path building |
File::Basename |
basename, dirname, fileparse |
File::Slurper |
Read/write entire files |
Fcntl |
File locking constants |
Cwd |
Current working directory |
IPC::Open3 |
Full subprocess I/O control |
Further Reading¶
- perlopentut - Perl open tutorial with examples for every mode
- perlio - PerlIO layers (encoding, buffering, compression)
- perlfunc - complete built-in function reference
- perlvar - special variables (
$!,$?,$/,$.,$_) - perlipc - interprocess communication (signals, pipes, sockets, fork)
- File::Find documentation - recursive directory traversal
- Learning Perl, Chapter 11: Filehandles and File Tests - the "Llama Book" covers file I/O fundamentals
- Intermediate Perl, Chapter 14: Process Management - fork, exec, signals, and IPC
Previous: Subroutines and References | Next: Modules and CPAN | Back to Index