Intermediate

I/O Operations in Perl

Learn console output, reading from standard input, and file reading, writing, and error handling in Perl with Docker-ready examples

Perl was born from the need to shuffle text between programs, files, and terminals, so input and output are woven deep into the language. You have already used print to say “Hello, World!” — this tutorial goes further, covering formatted output, reading from standard input, and the file handling that makes Perl a favorite for log parsing and system administration.

As a dynamic, weakly-typed language, Perl keeps I/O refreshingly direct: there are no stream classes to instantiate and no imports required for the basics. You open a filehandle, read or write scalars and lists through it, and close it. Perl’s context sensitivity shows up here too — reading a filehandle in scalar context gives you one line, while list context slurps every line into an array.

Along the way you will meet three idioms that define Perl I/O: the $! variable that carries the last system error message, the open ... or die pattern for failing loudly, and the three-argument form of open that keeps filenames and modes safely separated.

Console Output: print, say, and printf

You have three main tools for writing to the terminal. print writes exactly what you give it with no automatic newline, say (from use v5.10 or use feature 'say') appends a newline for you, and printf formats values against a template just like C.

Create a file named output.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;

# print writes to STDOUT with no automatic newline
print "No newline here";
print " - still the same line\n";

# say adds a newline automatically
say "This line ends automatically";

# printf produces formatted output
printf "Name: %-10s Age: %3d\n", "Alice", 30;
printf "Pi is roughly %.2f\n", 3.14159;
printf "Hex: %x  Octal: %o\n", 255, 8;

# Build one line from a list
my @items = ("apple", "banana", "cherry");
say join(", ", @items);

The printf format specifiers control width and type: %-10s left-justifies a string in a 10-character field, %3d right-justifies an integer in a 3-character field, and %.2f shows a float with two decimal places. This is the same formatting mini-language used by C and many other languages.

Reading from Standard Input

The <STDIN> operator (also written as <>) reads from standard input. In scalar context it returns one line at a time — including the trailing newline — so chomp is almost always the next step. The while (my $line = <STDIN>) loop is the canonical way to process input line by line until end-of-input.

Create a file named input.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;

# Read one line at a time from STDIN until end-of-input
my $count = 0;
while (my $line = <STDIN>) {
    chomp $line;             # strip the trailing newline
    $count++;
    say "Line $count: $line";
}
say "Total lines read: $count";

When you run a script interactively, you can prompt first with print "Enter your name: "; and then read a single line with my $name = <STDIN>;. In the Docker example below we instead pipe input into the script, which is how Perl programs usually receive data in real automation pipelines.

Reading and Writing Files

The open function connects a filehandle to a file. Always prefer the three-argument form — open($fh, MODE, $filename) — because it keeps the mode (< read, > write/truncate, >> append) separate from the filename, avoiding a whole class of bugs. Pair every open with or die "...: $!" so failures report the underlying system error stored in $!.

The example below writes three lines, then reads them back two different ways: line by line with a while loop, and all at once by evaluating the filehandle in list context.

Create a file named file_io.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;

my $filename = 'notes.txt';

# --- Writing to a file ('>' creates/truncates) ---
open(my $out, '>', $filename) or die "Cannot open $filename for writing: $!";
print $out "Perl was created in 1987\n";
print $out "It excels at text processing\n";
say   $out "say works with filehandles too";
close($out) or die "Cannot close $filename: $!";
say "Wrote 3 lines to $filename";

# --- Reading a file line by line ---
open(my $in, '<', $filename) or die "Cannot open $filename for reading: $!";
my $num = 0;
while (my $line = <$in>) {
    chomp $line;
    $num++;
    say "  Line $num: $line";
}
close($in);

# --- Slurping the whole file into an array ---
open(my $slurp, '<', $filename) or die "Cannot open $filename: $!";
my @lines = <$slurp>;
close($slurp);
say "Total lines: " . scalar(@lines);

Notice that print, printf, and say all accept a filehandle as their first argument (with no comma after it) to redirect output away from the terminal and into a file. Using > truncates any existing file; switching to >> would append instead.

Handling I/O Errors Gracefully

die is perfect when a missing file should halt the program, but sometimes you want to recover and keep going. Because open returns false on failure, you can test it in an if and take a different path. For code that might die deeper down, wrap it in eval { ... } and inspect the result — this is Perl’s classic exception-catching idiom.

Create a file named errors.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;

my $missing = 'does_not_exist.txt';

# Check open's return value instead of dying
if (open(my $fh, '<', $missing)) {
    my @data = <$fh>;
    close($fh);
    say "Read " . scalar(@data) . " lines";
} else {
    warn "Could not open $missing: $!\n";   # goes to STDERR
    say "Handled the error and kept running";
}

# Trap a fatal die with eval
my $ok = eval {
    open(my $fh, '<', $missing) or die "open failed: $!\n";
    1;
};
unless ($ok) {
    say "Caught the exception, program continues";
}

say "Program finished normally";

warn writes to standard error (STDERR) instead of standard output, which is the correct channel for diagnostics — it keeps error text out of a program’s real data output when the two streams are redirected separately.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull perl:5.40-slim

# Console output
docker run --rm -v $(pwd):/app -w /app perl:5.40-slim perl output.pl

# Reading from standard input (pipe data into the script)
printf 'apple\nbanana\ncherry\n' | docker run --rm -i -v $(pwd):/app -w /app perl:5.40-slim perl input.pl

# Writing and reading files
docker run --rm -v $(pwd):/app -w /app perl:5.40-slim perl file_io.pl

# Error handling
docker run --rm -v $(pwd):/app -w /app perl:5.40-slim perl errors.pl

The -i flag on the input example keeps STDIN open so the piped data reaches the container. The -v $(pwd):/app mount lets file_io.pl create notes.txt in your current directory so you can inspect it afterward.

Expected Output

Running output.pl:

No newline here - still the same line
This line ends automatically
Name: Alice      Age:  30
Pi is roughly 3.14
Hex: ff  Octal: 10
apple, banana, cherry

Running input.pl with apple, banana, cherry piped in:

Line 1: apple
Line 2: banana
Line 3: cherry
Total lines read: 3

Running file_io.pl:

Wrote 3 lines to notes.txt
  Line 1: Perl was created in 1987
  Line 2: It excels at text processing
  Line 3: say works with filehandles too
Total lines: 3

Running errors.pl (the first line is written to STDERR):

Could not open does_not_exist.txt: No such file or directory
Handled the error and kept running
Caught the exception, program continues
Program finished normally

Key Concepts

  • Three ways to outputprint (no newline), say (adds a newline), and printf (formatted); all three accept a filehandle as their first argument to write to files instead of the terminal.
  • <$fh> is context-sensitive — reading a filehandle in scalar context returns one line, while list context (my @lines = <$fh>) slurps every remaining line into an array.
  • Always chomp — lines read from <STDIN> or a file keep their trailing newline; chomp removes it so comparisons and concatenation behave as expected.
  • Use three-argument openopen($fh, '<', $file) separates the mode from the filename, which is safer than the older two-argument form.
  • $! carries the error — after a failed system call, $! holds the human-readable reason (e.g. “No such file or directory”); include it in every die or warn message.
  • open ... or die fails loudly — never ignore an open return value; either die to stop or test the return value with if to recover gracefully.
  • STDERR for diagnosticswarn and print STDERR ... send messages to standard error, keeping them separate from a program’s real data on standard output.
  • eval { } catches exceptions — wrap risky I/O in eval and check whether it succeeded to trap a die and continue running.

Running Today

All examples can be run using Docker:

docker pull perl:5.40-slim
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining