# 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: ```perl 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](#the-line-cycle) - sed's implicit loop becomes `while (<$fh>)` and `$_`. - [Substitution: `s///`](#substitution-s) - the same operator, with real regex. - [Transliteration: `y///` becomes `tr///`](#transliteration-y-becomes-tr) - the `y` command, renamed. - [Addresses and ranges](#addresses-and-ranges) - line numbers, patterns, and `/a/ .. /b/`. - [Deleting, printing, quitting: `d`, `p`, `q`](#deleting-printing-quitting-d-p-q) - `next`, `print`, `last`. - [The hold space becomes a variable](#the-hold-space-becomes-a-variable) - where sed is hard and Perl is trivial. - [In-place editing and I/O](#in-place-editing-and-io) - `-i`, reading files, pipes. - [One-liners and switches](#one-liners-and-switches) - `-n`, `-p`, `-e`, `BEGIN`/`END`. - [Gotchas coming from sed](#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 sed 's/bar/BAR/' input.txt ``` ```perl 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: ```perl 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 sed 's/colou\?r/COLOR/g' input.txt ``` ```perl 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: ```perl 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: ```perl 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 sed 'y/abc/xyz/' input.txt ``` ```perl 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: ```perl 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 sed -n '2p' input.txt # print line 2 sed -n '/banana/p' input.txt # print matching lines ``` ```perl 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: ```perl 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 sed '/banana/d' input.txt # drop matching lines sed '/banana/q' input.txt # stop after the match ``` ```perl use v5.42; open my $fh, '<', '/tmp/data.txt' or die "open: $!"; while (<$fh>) { next if /banana/; # sed '/banana/d' print; } # apple # cherry ``` ```perl 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 sed -n '1!G;h;$!d' input.txt # reverse the file (tac) ``` ```perl 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 sed -i 's/red/crimson/' file.txt # edit in place ``` ```perl 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: ```perl 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 sed -n '/error/p' log.txt # print error lines sed 's/[0-9]\+/N/g' input.txt # mask numbers ``` ```perl 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: ```perl 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](../oneliner/index) 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](../oop/index) 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 `/r` flag to get a new string. - **`tr///` (sed's `y`) is a literal character set, not a regex.** `tr/.*//` matches the two characters `.` and `*`. For patterns, use `s///`. - **`..` in a boolean is the flip-flop range, not a list range.** In an `if`, `/a/ .. /b/` is the sed `addr1,addr2` selector; `1 .. 10` in list context builds a list. Same operator, decided by context. - **`<$fh>` keeps the trailing newline,** like sed's pattern space. `print` round-trips it; `chomp` only when you mean to strip it. - **There is no hidden auto-print.** sed's `-p` default is something you spell yourself with `print`. "Delete a line" is just not printing it (`next if /pat/`), not a special command. - **`open` does not raise on failure.** Use `open 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`/`x` translates 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 this `pperl` build.** Only `-e` works today. Write the explicit `while (<$fh>) { ... }` loop, which is exactly what the switches expand to, until they land.