Perl binary files
last modified November 7, 2026
A binary file is a sequence of bytes that is not meant to be read as lines of
text. Images, archives, databases, executables and network captures are all
binary files. Perl reads and writes them with the same open
function it uses for text, but the program must make sure that no byte is
changed on the way in or out, and it usually reads the data in blocks of a
known size instead of line by line.
This article covers the tools for working with binary data:
- the
:rawlayer andbinmode, which turn off any translation of the bytes; read, which reads a given number of bytes;seekandtell, which move around a file;sysreadandsyswrite, which bypass Perl's buffering;packandunpack, which convert between Perl values and their byte representation.
The pack and unpack articles describe the templates in detail. The examples were run with Perl 5.44 on Linux.
Bytes and I/O layers
Every Perl filehandle has a stack of I/O layers that process the data. The
:encoding(UTF-8) layer encodes characters to bytes on output and
decodes them on input; the :crlf layer translates line endings.
The :raw layer passes bytes through unchanged. The following
program writes a string with an encoding layer and reads it back as raw bytes.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $text = "caf\x{e9} \x{20ac}5\n";
open my $fh, '>:encoding(UTF-8)', 'word.txt' or die "Cannot open word.txt: $!";
print $fh $text;
close $fh;
say "characters written: ", length $text;
say "file size: ", -s 'word.txt';
open $fh, '<:raw', 'word.txt' or die "Cannot open word.txt: $!";
my $bytes = do { local $/; <$fh> };
close $fh;
say "bytes read: ", length $bytes;
say join ' ', map { sprintf '%02x', ord } split //, $bytes;
The string has eight characters, but é takes two bytes in UTF-8
and the euro sign takes three, so the file has eleven bytes. Reading with
<:raw gives back exactly these bytes, which we print in
hexadecimal. A binary file must always be opened with :raw (or
have binmode called on its handle); otherwise a layer such as
:crlf on Windows or an encoding set by the open
pragma would silently change the data.
$ perl layers.pl characters written: 8 file size: 11 bytes read: 11 63 61 66 c3 a9 20 e2 82 ac 35 0a
Writing and reading bytes
The pack function builds a string of bytes from numbers. The
template C* turns each number from 0 to 255 into one byte.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $file = 'bytes.bin';
open my $fh, '>:raw', $file or die "Cannot open $file: $!";
print $fh pack 'C*', 0 .. 255;
close $fh;
say "$file has ", -s $file, " bytes";
open $fh, '<:raw', $file or die "Cannot open $file: $!";
my $data = do { local $/; <$fh> };
close $fh;
my @bytes = unpack 'C*', $data;
say "first: @bytes[0 .. 7]";
say "last: @bytes[-8 .. -1]";
say "newline byte at offset ", index $data, "\n";
The file gets all 256 possible byte values. To read the whole file, we set the
input record separator $/ to undef, so the readline
operator returns everything at once. The unpack 'C*' call turns
the bytes back into a list of numbers. Byte 10 is the newline character; in a
binary file it is just an ordinary byte, which is why reading binary data line
by line makes no sense.
$ perl bytes.pl bytes.bin has 256 bytes first: 0 1 2 3 4 5 6 7 last: 248 249 250 251 252 253 254 255 newline byte at offset 10
Reading in chunks
Slurping is fine for small files. Large files are read in blocks with the
read function, which takes a filehandle, a buffer variable and the
number of bytes to read.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $file = shift // 'bytes.bin';
my $size = 100;
open my $fh, '<:raw', $file or die "Cannot open $file: $!";
my ($total, $chunks) = (0, 0);
while (1) {
my $buf;
my $n = read $fh, $buf, $size;
die "Read error: $!" unless defined $n;
last if $n == 0;
$chunks++;
$total += $n;
say "chunk $chunks: $n bytes";
}
close $fh;
say "total: $total bytes";
The read function returns the number of bytes actually read, 0 at
the end of the file, and undef on an error. The last block is
usually shorter than the requested size, so the program must use the return
value and not assume a full buffer. The buffer variable is declared on its own
line; writing read $fh, my $buf, $size as a statement triggers a
"Parentheses missing around my list" warning.
$ perl chunks.pl chunk 1: 100 bytes chunk 2: 100 bytes chunk 3: 56 bytes total: 256 bytes
Fixed-size records
Many binary formats store a sequence of records of the same length. Each record
is written with pack and read back with unpack, using
the same template.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $file = 'users.dat';
my $tpl = 'A12 C n';
my $len = length pack $tpl;
my @users = (
['Alice', 34, 1250],
['Bob', 27, 980],
['Carol', 45, 3120],
['Dave', 52, 410],
);
open my $fh, '>:raw', $file or die "Cannot open $file: $!";
print $fh pack $tpl, @$_ for @users;
close $fh;
say "record length: $len bytes";
say "file size: ", -s $file, " bytes";
open $fh, '<:raw', $file or die "Cannot open $file: $!";
while (read $fh, my $rec, $len) {
my ($name, $age, $points) = unpack $tpl, $rec;
printf "%-6s %3d %6d\n", $name, $age, $points;
}
close $fh;
The template A12 C n describes a 12-byte space-padded name, an
unsigned byte for the age, and a 16-bit big-endian number for the points.
Calling pack with the template alone and no values gives a record
filled with defaults, whose length is the record size of 15 bytes. Four records
make a 60-byte file. When reading, A12 strips the padding spaces
from the name.
$ perl records.pl record length: 15 bytes file size: 60 bytes Alice 34 1250 Bob 27 980 Carol 45 3120 Dave 52 410
Random access with seek and tell
Because all records have the same size, record number i starts at
byte offset i × record length. The seek function
moves the file position there directly, without reading the records before it.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:seek);
my $file = 'users.dat';
my $tpl = 'A12 C n';
my $len = length pack $tpl;
open my $fh, '<:raw', $file or die "Cannot open $file: $!";
my $count = (-s $fh) / $len;
say "records: $count";
for my $i (2, 0, 3) {
seek $fh, $i * $len, SEEK_SET or die "Cannot seek: $!";
my $rec;
read $fh, $rec, $len;
my ($name, $age) = unpack $tpl, $rec;
say "record $i: $name ($age)";
}
seek $fh, -$len, SEEK_END;
say "last record starts at offset ", tell $fh;
seek $fh, 12, SEEK_CUR;
my $age;
read $fh, $age, 1;
say "age in last record: ", ord $age;
close $fh;
The third argument of seek says what the offset is relative to.
The constants come from the Fcntl module: SEEK_SET
counts from the start of the file, SEEK_CUR from the current
position, and SEEK_END from the end, usually with a negative
offset. The tell function returns the current position. The number
of records is the file size divided by the record length; the -s
test also accepts an open filehandle. After moving to the last record, we skip
its 12-byte name with a relative seek and read the single age byte.
$ perl seek.pl records: 4 record 2: Carol (45) record 0: Alice (34) record 3: Dave (52) last record starts at offset 45 age in last record: 52
Updating a record in place
The +< mode opens a file for reading and writing without
truncating it. Combined with seek, it lets us change one record and
leave the rest of the file untouched.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:seek);
my $file = 'users.dat';
my $tpl = 'A12 C n';
my $len = length pack $tpl;
my ($index, $bonus) = (1, 500);
open my $fh, '+<:raw', $file or die "Cannot open $file: $!";
seek $fh, $index * $len, SEEK_SET or die "Cannot seek: $!";
my $rec;
read $fh, $rec, $len;
my ($name, $age, $points) = unpack $tpl, $rec;
say "before: $name $points";
$points += $bonus;
seek $fh, $index * $len, SEEK_SET or die "Cannot seek: $!";
print $fh pack $tpl, $name, $age, $points;
seek $fh, 0, SEEK_SET;
while (read $fh, my $r, $len) {
my ($n, undef, $p) = unpack $tpl, $r;
say "$n $p";
}
close $fh;
say "file size: ", -s $file, " bytes";
The program reads Bob's record, adds 500 points, seeks back to the start of the
record and writes the new version over the old one. Because the new record has
the same length, the file size does not change. A seek is required between
reading and writing on the same handle; it also flushes Perl's buffer, which is
why we seek to the beginning before reading the records again. Never use
+> for this purpose: it truncates the file first.
$ perl update.pl before: Bob 980 Alice 1250 Bob 1480 Carol 3120 Dave 410 file size: 60 bytes
Writing binary data to standard output
Standard output is a text handle by default. A program that prints binary data,
for example so that it can be piped to another tool, calls binmode
on STDOUT first.
#!/usr/bin/perl use strict; use warnings; use v5.34.0; binmode STDOUT; print pack 'a4 n N', 'DEMO', 2026, 0xCAFEBABE; print pack 'C*', 0x0d, 0x0a, 0xff, 0x00;
The binmode function without a layer argument sets the handle to
raw mode. The program prints a four-byte tag, the number 2026 as a 16-bit
big-endian value (07ea), the 32-bit number
0xCAFEBABE, and four more bytes including a carriage return,
newline and zero byte. The xxd tool shows the bytes in
hexadecimal; the Perl hex dump article builds a
similar tool in Perl.
$ perl stdout.pl | xxd 00000000: 4445 4d4f 07ea cafe babe 0d0a ff00 DEMO..........
Unbuffered I/O with sysread and syswrite
The read and print functions go through Perl's I/O
buffers. The sysread and syswrite functions call the
operating system's read and write directly. They are
useful for copying large files in big blocks and for sockets and pipes, where
the program must handle data as soon as it arrives.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my ($src, $dst) = @ARGV;
die "Usage: $0 source destination\n" unless defined $dst;
open my $in, '<:raw', $src or die "Cannot open $src: $!";
open my $out, '>:raw', $dst or die "Cannot open $dst: $!";
my ($total, $calls) = (0, 0);
while (1) {
my $buf;
my $n = sysread $in, $buf, 64 * 1024;
die "Read error: $!" unless defined $n;
last if $n == 0;
$calls++;
my $off = 0;
while ($off < $n) {
my $w = syswrite $out, $buf, $n - $off, $off;
die "Write error: $!" unless defined $w;
$off += $w;
}
$total += $n;
}
close $in;
close $out or die "Cannot close $dst: $!";
say "copied $total bytes in $calls sysread calls";
The program copies a file in blocks of 64 KiB. Like read,
sysread returns the number of bytes read, 0 at the end of file and
undef on an error. A syswrite call may write fewer
bytes than requested, so the inner loop passes the remaining length and an
offset into the buffer until the whole block is written. Never mix
sysread or syswrite with buffered functions such as
read, print or eof on the same handle;
the buffered data would be skipped or written out of order. We copy a
300,000-byte file of random data and compare the copy with the
cmp tool.
$ perl syscopy.pl data.bin copy.bin && cmp data.bin copy.bin && echo identical copied 300000 bytes in 5 sysread calls identical
Best practices
- Always use :raw: open binary files with
<:raw,>:rawor+<:raw, or callbinmodeon the handle. - Check the return value of read: it may return fewer bytes
than requested, 0 at the end of file and
undefon an error. - Keep the template in one place: use one variable for the
record template and compute the record length with
length pack $tpl. - Specify the byte order: use
n/Norv/Vinstead of nativesandlso that files are portable between machines. - Seek between reads and writes: when a handle is opened with
+<, callseekwhen switching direction. - Do not mix buffered and unbuffered I/O: use either
read/printorsysread/syswriteon a handle.
In this article we have worked with binary files in Perl. We have used the
:raw layer and binmode, read files whole and in
chunks, stored fixed-size records with pack, accessed them randomly
with seek and tell, updated a record in place, and
copied a file with sysread and syswrite.
Source
perlfunc: binmode · perlfunc: read · perlfunc: seek · perlfunc: sysread · PerlIO
Author
List all Perl tutorials.