Perl Filehandles That Do Not Lose Data or Encoding
In about 15 minutes, you will have a small set of Perl filehandle patterns for reading UTF-8 text, appending without clobbering a file, copying binary data, and connecting a program to a Unix command. The examples target the installed Perl 5.38.2 and the perlopentut(1) recipes shipped by perl-doc.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need Perl and a shell. The examples use files under /tmp/perlopentut-demo, so they do not need elevated privileges. Do not replace that path with a real project file until you have checked the mode: > empties an existing file immediately when open succeeds.
Checkpoint 1: use three-argument open
Most file operations start with open. Give it a filehandle, a mode, and a pathname as separate arguments. Check its return value at once; on failure, $! explains the operating system error.
use strict;
use warnings;
my $filename = "/tmp/perlopentut-demo/input.txt";
open(my $fh, "<:encoding(UTF-8)", $filename)
or die "$0: cannot open $filename for reading: $!";
while (my $line = <$fh>) {
print $line;
}
close($fh) or die "$0: cannot close $filename: $!";
The < mode is read-only. The :encoding(UTF-8) layer converts bytes from the file into Perl characters as they are read. A line-reading loop normally ends at EOF, so do not treat an ordinary end of file as an error. Check a read error separately if the distinction matters in your program.
Make a harmless test file and run the example like this:
mkdir -p /tmp/perlopentut-demo
printf 'cafe\njalapeƱo\n' > /tmp/perlopentut-demo/input.txt
perl /tmp/perlopentut-demo/read.pl
Expected output is the two lines, including the accented character. If the path is wrong, the program stops with a message such as No such file or directory, rather than continuing with an invalid handle.
Checkpoint 2: choose append or overwrite deliberately
For text output, put the encoding in the mode as well. Append with >> when existing contents must survive. It creates the file if it does not exist.
my $filename = "/tmp/perlopentut-demo/log.txt";
open(my $log, ">>:encoding(UTF-8)", $filename)
or die "$0: cannot open $filename for appending: $!";
print {$log} "started\n" or die "cannot write $filename: $!";
close($log) or die "cannot close $filename: $!";
Run it twice and verify that the file has two lines:
perl /tmp/perlopentut-demo/append.pl
perl /tmp/perlopentut-demo/append.pl
wc -l /tmp/perlopentut-demo/log.txt
The last command should report 2. Recovery is simple: remove only this disposable demonstration file with rm /tmp/perlopentut-demo/log.txt, then run the example again. In a real log, keep a backup or use the application's normal rotation procedure.
Use > only when replacing the old contents is intended. Opening in that mode truncates an existing file before your first print. There is no undo inside open. For ordinary text, avoid read-write mode: the manpage flags it as unlikely to behave as intended, and a temporary file followed by a rename is usually easier to reason about.
Checkpoint 3: keep binary bytes raw
Text layers are the wrong choice for an image, archive or other byte stream. Open with :raw, or call binmode on an existing handle, and read a fixed-size buffer.
my $source = "/tmp/perlopentut-demo/input.bin";
my $copy = "/tmp/perlopentut-demo/output.bin";
my $buffer;
my $size = 64 * (2 ** 10);
open(my $in, "<:raw", $source)
or die "$0: cannot open $source: $!";
open(my $out, ">:raw", $copy)
or die "$0: cannot open $copy: $!";
while (read($in, $buffer, $size)) {
print {$out} $buffer or die "cannot write $copy: $!";
}
close($in) or die "cannot close $source: $!";
close($out) or die "cannot close $copy: $!";
This example deliberately uses > for the destination, so it replaces an old copy. Point it at a disposable destination first. Verify the result with:
cmp /tmp/perlopentut-demo/input.bin /tmp/perlopentut-demo/output.bin && echo identical
For an already-open standard handle, binmode(STDIN) or binmode(STDOUT) switches it to raw bytes. An explicit binmode($fh, ":encoding(UTF-8)") changes a handle to a text encoding, but that is not binary mode.
Checkpoint 4: connect to a command without accidental shell syntax
open can create a pipe. The mode -| lets Perl read the command's output; |- lets Perl write to the command's input.
open(my $sort_fh, "-|", "sort", "-u", "-f", "/tmp/perlopentut-demo/input.txt")
or die "cannot start sort: $!";
while (my $line = <$sort_fh>) {
print $line;
}
close($sort_fh) or die "sort failed or could not close: $!";
Here the command and each argument are separate list items. Perl invokes sort directly, bypassing the shell. That makes a filename supplied by a variable safer than interpolating it into a command string: spaces are arguments, not word separators, and shell metacharacters are not interpreted. The list form works on Linux, which provides fork.
A single command string is still possible, but it may involve the shell. Treat every variable placed in such a string as untrusted input. Prefer the list form when you do not need shell expansion. If you do need a glob, expand it yourself with Perl's glob and pass the resulting filenames as list arguments.
For a write pipe, close the handle after the last print. Closing tells the child command that its input is complete and also lets you detect a failure reported by the command.
open(my $cat_fh, "|-", "cat", "-n")
or die "cannot start cat: $!";
print {$cat_fh} "first\n", "second\n"
or die "cannot write to cat: $!";
close($cat_fh) or die "cat failed: $!";
Expected output from the command is numbered lines. A pipe starts another process, so it is not merely a file with a different name. Check its exit status at close, and do not send secrets or destructive commands through a shell string.
Common traps
- Do not omit the encoding for text just because the first test file contains ASCII. The default byte-to-character behaviour can become a bug when real input changes.
- Do not use
>for a log or report that must retain earlier output. Use>>and check that the destination is the one you meant. - Do not assume opening a file locks it. Perl's
opennormally does not provide locking; coordinate concurrent writers separately. - Do not confuse a normal EOF with a read failure. A loop ending is expected; inspect the relevant error state when failed reads matter.
- Do not put untrusted filenames into a shell command string. Use list-form pipes and explicit arguments.
Done means
- Text is opened with an explicit encoding and a checked return value.
- Append and overwrite modes are chosen consciously, with overwrite tested only on a disposable destination.
- Binary data uses raw mode and a byte buffer, and the copy passes
cmp. - Pipes use
-|or|-, prefer list arguments, and checkclose.