Sed to Perl#
You know sed. You think in s/old/new/, you address lines with /pattern/ and $, you reach for -n and p to print selectively, and -i to edit files in place. This guide maps each of those habits onto Perl, so you can keep every reflex you have and pick up the things sed makes hard.
The one idea to carry in from the first line: Perl is a near-superset of sed for text work. Everything sed does, Perl does - usually with the same s/// syntax - plus a real regex engine, real variables, and arbitrary code in the replacement. The awkward corners of sed (the hold space, multi-line tricks, anything resembling arithmetic) become ordinary Perl that you would write without thinking. This guide leads with the one-to-one mappings and then shows where Perl simply removes a sed limitation.
Every runnable example here was executed on pperl, a Rust reimplementation of Perl 5.42; the # ... comments show its real output. Start scripts with:
use v5.42;
That one line turns on strict, warnings, and the say built-in (a print with a trailing newline), the modern feature set used throughout.
Note
One-liner switches on this pperl build sed’s natural Perl twin is the one-liner: perl -ne and perl -pe. Those switches are correct perl5 5.42 and are shown throughout this guide as the idiomatic form. But the implicit-loop switch family (-n, -p, -a, -F, -i, -l) is not yet recognised by this pperl build - only -e is. So the one-liner examples below are shown without output, and each is paired with an explicit-loop full-script form (while (<$fh>) { ... }) that does run on pperl today and carries the verified output. Use the full-script form until the switches land.
How to read this#
The line cycle - sed’s implicit loop becomes
while (<$fh>)and$_.Substitution:
s///- the same operator, with real regex.Transliteration:
y///becomestr///- theycommand, renamed.Addresses and ranges - line numbers, patterns, and
/a/ .. /b/.Deleting, printing, quitting:
d,p,q-next,print,last.The hold space becomes a variable - where sed is hard and Perl is trivial.
In-place editing and I/O -
-i, reading files, pipes.One-liners and switches -
-n,-p,-e,BEGIN/END.Gotchas coming from sed - scan before you write.
The line cycle#
sed runs your script once per input line, with the current line in its pattern space, and prints the pattern space at the end of the cycle (unless -n). Perl’s equivalent is an explicit loop with the line in $_, the implicit topic variable. The -p switch reproduces “auto-print every line”; -n reproduces “print nothing unless I say so.”
sed 's/bar/BAR/' input.txt
perl -pe 's/bar/BAR/' input.txt
The runnable, full-script form of that same loop - the one that runs on pperl today - opens the file and iterates it explicitly. $_ is the current line, exactly like sed’s pattern space:
use v5.42;
open my $fh, '<', '/tmp/data.txt' or die "open: $!";
while (<$fh>) { # each line lands in $_
s/a/A/g; # operate on $_ by default
print; # print $_ (note: keeps the newline)
}
# Apple
# bAnAnA
# cherry
(The input file held apple, banana, cherry.) The gotcha: a line read with <$fh> keeps its trailing newline, just as sed’s pattern space does. print with no argument prints $_ including that newline, so it round-trips cleanly; chomp only when you intend to strip it. The line number is in $., the sed analogue of the line counter you reach for with $= in GNU sed scripts.
Substitution: s///#
This is the headline: sed’s s/// and Perl’s s/// are the same operator. The flags line up too - g is global, i is case-insensitive - and Perl adds flags sed has no equivalent for.
sed 's/colou\?r/COLOR/g' input.txt
perl -pe 's/colou?r/COLOR/g' input.txt
By default s/// edits its target in place and returns the count of substitutions. Captured groups are $1, $2, … and you can run arbitrary code in the replacement with the /e flag - something sed cannot do at all:
use v5.42;
my $s = "items: 3 and 4";
(my $t = $s) =~ s/(\d+)/$1 * 10/ge; # /e: replacement is Perl code
say $t; # items: 30 and 40
To rearrange captures (sed’s \1, \2 backreferences in the replacement), use $1, $2 and, when you want a new string instead of editing in place, the /r flag, which returns the result and leaves the original untouched:
use v5.42;
my $date = "2026-06-21";
my $us = $date =~ s{(\d+)-(\d+)-(\d+)}{$2/$3/$1}r; # /r: return a copy
say $us; # 06/21/2026
say $date; # 2026-06-21
The gotcha: bare s/// modifies its target and returns a count, not the new string. That is why the examples above either copy first ((my $t = $s) =~ s/.../.../) or use /r. Writing my $new = ($s =~ s/a/b/) puts the substitution count in $new, not the edited string - a classic first-day surprise. Note also that Perl delimiters are free: s{...}{...} and s#...#...# avoid backslashing slashes in a path pattern.
Transliteration: y/// becomes tr///#
sed’s y/abc/xyz/ is Perl’s tr/abc/xyz/. Perl keeps y/// as an exact alias, so y/a-z/A-Z/ works verbatim, but tr is the spelled name you will see in Perl code.
sed 'y/abc/xyz/' input.txt
use v5.42;
my $s = "hello";
(my $t = $s) =~ tr/a-z/A-Z/; # uppercase, like y/a-z/A-Z/
say $t; # HELLO
tr also does things y cannot. With an empty replacement it counts matches, and the s modifier squeezes runs of a character - the common tr -s idiom:
use v5.42;
my $s = "hello";
my $vowels = ($s =~ tr/aeiou//); # count, replacement empty
say $vowels; # 2
my $dots = "a....b...c";
(my $squeezed = $dots) =~ tr/././s; # squeeze repeats
say $squeezed; # a.b.c
The gotcha: tr/// is not a regex - the left side is a literal character set, the same as sed’s y. tr/a-z// means the range a through z, but tr/.*// means the two literal characters . and *, not “any string.” For pattern work, reach for s///. And like s///, tr/// edits in place and returns a count, so copy first or use the /r flag for a non-destructive transliteration.
Addresses and ranges#
A sed command can be prefixed by an address: a line number, $ for the last line, a /pattern/, or a addr1,addr2 range. In Perl an address is just a condition on the statement - a postfix if. Line number is $.; the pattern test is /pat/ against $_; the last line is eof.
sed -n '2p' input.txt # print line 2
sed -n '/banana/p' input.txt # print matching lines
use v5.42;
open my $fh, '<', '/tmp/data.txt' or die "open: $!";
while (<$fh>) {
print if $. == 2; # sed -n '2p'
}
# banana
A sed range /start/,/end/ becomes Perl’s flip-flop range operator /start/ .. /end/, which is true from the line matching the left pattern through the line matching the right, inclusive:
use v5.42;
open my $fh, '<', '/tmp/data.txt' or die "open: $!";
while (<$fh>) {
print if /banana/ .. /cherry/; # sed '/banana/,/cherry/p' with -n
}
# banana
# cherry
The gotcha: in a boolean test .. is the flip-flop (range) operator, not the list-range operator (which is 1 .. 10 building a list). Perl tells them apart by context - in the scalar/boolean position of an if, .. is the line-range selector that mirrors sed’s addr1,addr2. It is stateful: it remembers whether it has seen the start pattern, so it does exactly what a sed range does across the loop.
Deleting, printing, quitting: d, p, q#
sed’s single-letter cycle commands map to Perl loop keywords. d (delete pattern space, skip to next cycle) is next. q (quit) is last. p (print) is print. Because Perl’s loop is explicit, these read as exactly what they do.
sed '/banana/d' input.txt # drop matching lines
sed '/banana/q' input.txt # stop after the match
use v5.42;
open my $fh, '<', '/tmp/data.txt' or die "open: $!";
while (<$fh>) {
next if /banana/; # sed '/banana/d'
print;
}
# apple
# cherry
use v5.42;
open my $fh, '<', '/tmp/data.txt' or die "open: $!";
while (<$fh>) {
print;
last if /banana/; # sed '/banana/q'
}
# apple
# banana
The gotcha: under sed’s -p-style auto-print, d must skip the auto-print, which it does by aborting the cycle. In an explicit Perl loop you control printing yourself, so “delete” is simply not printing the line - next if /pat/ before the print, or guard the print with unless. There is no hidden auto-print to fight; what you print is what comes out.
The hold space becomes a variable#
This is the section where sed stops being convenient and Perl stops being hard. sed has exactly one auxiliary buffer - the hold space - and shuffles data into it with h, H, g, G, x. Anything needing two pieces of remembered state turns into hold-space contortions. In Perl you just declare a variable. Or an array. Or a hash.
The canonical hold-space exercise is reversing a file (tac), which in sed is an opaque 1!G;h;$!d incantation. In Perl it is one obvious line:
sed -n '1!G;h;$!d' input.txt # reverse the file (tac)
use v5.42;
open my $fh, '<', '/tmp/data.txt' or die "open: $!";
my @hold; # the "hold space" is just an array
while (<$fh>) {
unshift @hold, $_; # newest line to the front
}
print @hold;
# cherry
# banana
# apple
The gotcha: there is no gotcha - that is the point. Once you have real variables, the entire category of sed problems that exist only because there is a single hold space disappears. Need to remember the previous line? my $prev. Need a running total? my $sum. Need to dedup? A my %seen hash. When a sed script reaches for h/H/x, the Perl translation is almost always “introduce a named variable and write the obvious thing.”
In-place editing and I/O#
sed’s -i rewrites a file in place. The idiomatic Perl is the same switch on a -p one-liner. Reading several files is sed’s default; Perl spells it <>, the diamond operator, which reads each file named in @ARGV in turn.
sed -i 's/red/crimson/' file.txt # edit in place
perl -i -pe 's/red/crimson/' file.txt
The runnable equivalent today is an explicit read-modify-write: slurp the lines, transform them, write them back. This is exactly what -i does under the hood, and it runs on pperl now:
use v5.42;
my $path = '/tmp/inplace_data.txt'; # holds: red green blue
open my $in, '<', $path or die "read: $!";
my @lines = <$in>; # slurp all lines (list context)
close $in;
s/e/E/g for @lines; # transform each line's $_
open my $out, '>', $path or die "write: $!";
print {$out} @lines; # write them back
close $out;
# file now holds:
# rEd
# grEEn
# bluE
For pipes - sed in the middle of a shell pipeline - Perl opens a pipe filehandle directly: open my $fh, '-|', 'ls', '-l' reads a command’s output, and open my $fh, '|-', 'sort' writes to a command’s input. The gotcha: open returns false on failure rather than raising, so the or die "...: $!" idiom is mandatory - $! is the system error string, sed’s silent failure made explicit. And reading multiple files with <> resets $. per file only if you check eof explicitly; the plain $. keeps counting across the concatenated stream.
One-liners and switches#
The one-liner is sed’s home turf and Perl’s too. The switch family that makes it work: -e supplies the program, -n wraps it in a no-auto-print loop (sed’s -n), -p wraps it in an auto-print loop (sed’s default), -i edits in place, -l handles line endings, and -a/-F autosplit each line into @F (awk territory, useful here for field edits). BEGIN { } and END { } blocks run before and after the loop, the direct analogue of sed’s 1i/$a setup-and-teardown tricks.
sed -n '/error/p' log.txt # print error lines
sed 's/[0-9]\+/N/g' input.txt # mask numbers
perl -ne 'print if /error/' log.txt # print error lines
perl -pe 's/\d+/N/g' input.txt # mask numbers
perl -nE 'END { say $n } $n++ if /error/' log.txt # count, BEGIN/END
These are shown without output because the implicit-loop switches are not recognised by this pperl build yet (see the note at the top). Each maps to a verified explicit-loop form: perl -ne 'CODE' is while (<$fh>) { CODE }, and perl -pe 'CODE' is the same with a trailing print. The runnable “count error lines” form:
use v5.42;
open my $fh, '<', '/tmp/data.txt' or die "open: $!";
my $n = 0;
while (<$fh>) {
$n++ if /banana/; # the body of -ne
}
say $n; # 1
The gotcha: until the switches land on pperl, write the explicit loop (it runs today and is what the switches expand to anyway), and consult the One-Liners guide for the full switch catalogue and dozens of recipes - it is the highest-leverage thing to read coming from a sed background. Perl also has full subroutines, references, modules, and OO if your script outgrows a one-liner; see the Object-oriented Programming guide for that side of the language.
Gotchas coming from sed#
The traps, collected for a final scan before you write:
s///returns a count, not the new string.my $x = $s =~ s/a/b/puts the substitution count in$x. Copy first ((my $t = $s) =~ s/.../.../) or use the/rflag to get a new string.tr///(sed’sy) is a literal character set, not a regex.tr/.*//matches the two characters.and*. For patterns, uses///...in a boolean is the flip-flop range, not a list range. In anif,/a/ .. /b/is the sedaddr1,addr2selector;1 .. 10in list context builds a list. Same operator, decided by context.<$fh>keeps the trailing newline, like sed’s pattern space.printround-trips it;chomponly when you mean to strip it.There is no hidden auto-print. sed’s
-pdefault is something you spell yourself withprint. “Delete a line” is just not printing it (next if /pat/), not a special command.opendoes not raise on failure. Useopen my $fh, '<', $path or die "...: $!".$!is the system error string.The hold space is gone - use a variable. Any sed script reaching for
h/H/xtranslates to a named Perl variable, array, or hash. This is a simplification, not a loss.The implicit-loop switches (
-n/-p/-a/-F/-i/-l) are not yet recognised by thispperlbuild. Only-eworks today. Write the explicitwhile (<$fh>) { ... }loop, which is exactly what the switches expand to, until they land.