perl.gg / one-liners

<!-- category: one-liners -->

Postfix for as Mass Array Mutator

2026-03-20

Mutate every element of an array. In one line. No loop variable, no index, no temporary copies.
s~\.example\.com$~~ for @hostnames;
That strips .example.com from every hostname in the array. Not from copies. From the actual elements. The array is permanently changed.

The secret is postfix for. When you write EXPR for @array, Perl aliases $_ to each element. Not a copy. The actual slot in the array. So any mutation to $_ mutates the original.

One line. Whole array transformed.

Part 1: HOW IT WORKS

Normal for loop:
for my $host (@hostnames) { $host =~ s~\.example\.com$~~; }
Postfix for:
s~\.example\.com$~~ for @hostnames;
Same result. One line instead of three.

The key insight: $_ in a for loop is not a copy. It's an alias. A direct reference to the array element. When s~~~ modifies $_, it modifies the element in place.

@hostnames = ("web.example.com", "db.example.com", "cache.example.com") | | | $_ ──────────────> alias ──────────────> alias | | | s~\.example\.com$~~ s~\.example\.com$~~ s~\.example\.com$~~ | | | @hostnames = ( "web", "db", "cache" )

Part 2: THE ALIAS TRAP

This aliasing behavior catches people off guard. Watch:
my @nums = (1, 2, 3, 4, 5); $_ *= 2 for @nums; # @nums is now (2, 4, 6, 8, 10)
You just doubled every element without writing a loop body. The mutation sticks because $_ IS the element, not a snapshot.

This also means you can break things:

my @names = ("Alice", "Bob", "Charlie"); $_ = uc for @names; # @names is now ("ALICE", "BOB", "CHARLIE")
Assigning to $_ replaces the element entirely. Not just modifying, replacing.

Part 3: MASS CHOMP

The classic use case. You read lines from a file and every single one has a trailing newline.
chomp for @lines;
That's it. Every element chomped. Compare to the block form:
for my $line (@lines) { chomp $line; }
Or the map approach:
@lines = map { chomp; $_ } @lines;
Wait, that map version is wrong. chomp returns the number of characters removed, not the modified string. You'd need:
@lines = map { chomp(my $l = $_); $l } @lines;
Ugly. The postfix for version is cleaner because chomp modifies in place, and $_ is already aliased. No return value gymnastics.

Part 4: TRIMMING WHITESPACE

Strip leading and trailing whitespace from every element:
s~^\s+~~, s~\s+$~~ for @fields;
Two substitutions, comma-separated, applied to every element. The comma operator evaluates both expressions for each $_.

Or if you're on Perl 5.14+ and want to be fancy:

s~^\s+|\s+$~~g for @fields;
Single regex with alternation. The g flag catches both ends.

Real-world example. You split a CSV line and get padded fields:

my @fields = split m~,~, " Alice , Bob , Charlie "; # (" Alice ", " Bob ", " Charlie ") s~^\s+|\s+$~~g for @fields; # ("Alice", "Bob", "Charlie")

Part 5: NORMALIZING DATA

Sysadmin scenario. You pull a list of hostnames from a config file and they're a mess. Mixed case, trailing dots, some with FQDNs, some without.
my @hosts = qw( Web01.Example.COM. DB02.example.com CACHE03 app04.EXAMPLE.COM. ); $_ = lc for @hosts; # lowercase everything s~\.$~~ for @hosts; # strip trailing dots s~\.example\.com$~~ for @hosts; # strip domain suffix
After those three lines:
# ("web01", "db02", "cache03", "app04")
Each transformation builds on the last. Clean, readable pipeline of mutations.

Part 6: NUMERIC TRANSFORMS

Works beautifully with numbers too.

Convert Fahrenheit array to Celsius:

$_ = ($_ - 32) * 5 / 9 for @temps;
Round everything to two decimal places:
$_ = sprintf("%.2f", $_) for @prices;
Clamp values to a range:
$_ = 0 if $_ < 0 for @values; # floor at zero $_ = 100 if $_ > 100 for @values; # cap at 100
Wait, that doesn't work. The if modifier and for modifier can't both be postfix on the same statement. You need:
($_ < 0 ? $_ = 0 : $_ > 100 ? $_ = 100 : $_) for @values;
Or just use a block:
for (@values) { $_ = 0 if $_ < 0; $_ = 100 if $_ > 100; }
Know when to stop being clever.

Part 7: CHAINING WITH THE COMMA

You can chain multiple operations using the comma operator:
chomp, s~#.*~~, s~^\s+~~, s~\s+$~~ for @config_lines;
That one line does four things to every element:
  1. Remove trailing newline
  2. Strip comments
  3. Remove leading whitespace
  4. Remove trailing whitespace

Config file cleanup in a single statement. Each operation runs left to right on the same $_.

.--. |o_o | "One line to clean them all" |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/

Part 8: FILTERING AND MUTATING

Combine grep and postfix for to filter first, mutate second:
s~^https?://~~ for grep { defined } @urls;
Strip the protocol from all defined URLs. The grep returns a list of aliases to the elements that pass the test, and for iterates those aliases. The original array is mutated, but only the elements that matched the grep.

Another pattern. Selectively mutate:

$_ = uc for grep { m~^error~i } @log_lines;
Uppercase only the lines that start with "error". The rest stay untouched.

Part 9: ONE-LINER FORM

Postfix for is a natural fit for command-line Perl. Mutate data streaming through a pipe:
perl -e 's~\t~,~g for @ARGV; print join "\n", @ARGV' *.tsv
Or with -a and -n, mutate the auto-split fields:
perl -lane 's~\.0+$~~ for @F; print join "\t", @F' data.txt
That strips trailing .000 from every field in a tab-delimited file. The -a flag splits each line into @F, and postfix for walks through every field, trimming zeros in place.

Bulk rename files:

my @files = glob("*.HTML"); s~\.HTML$~.html~ for @files; rename $_, $_ for @files; # Wait, that renames nothing
Oops. That renames each file to itself because we already mutated the names. You need the originals too:
my @old = glob("*.HTML"); my @new = @old; s~\.HTML$~.html~ for @new; rename $old[$_], $new[$_] for 0..$#old;
Postfix for over an index range. Sometimes you still need the index.

Part 10: GOTCHAS

Read-only values will blow up:
s~foo~bar~ for ("literal", "strings"); # Modification of a read-only value attempted
Literals are read-only. You can't mutate them through $_. Use an array variable, not a list of constants.

Don't add or remove elements during iteration:

push @array, "new" for @array; # Infinite loop. Don't.
You're modifying the list you're iterating over. Perl won't stop you. It'll just run forever.

Postfix for returns the original list, not the modified values:

my @result = (s~foo~bar~ for @items); # Syntax error
Postfix for is a statement modifier, not an expression. If you need a return value, use map.

Part 11: POSTFIX FOR VS MAP

When do you use which?

Postfix for mutates in place. No new array created. No return value.

s~\s+$~~ for @lines; # Modifies @lines directly
map creates a new list. Original untouched (unless you assign back).
my @clean = map { s~\s+$~~r } @lines; # New array, @lines unchanged
Note the r flag on the substitution. Without it, map would mutate $_ (the alias) AND return the match count. With r, it returns the modified copy and leaves the original alone.

Rule of thumb: if you want to change the array in place, postfix for. If you want a new array, map with /r.

Part 12: REAL-WORLD MASS MUTATIONS

Sanitize user input fields:
s~<[^>]*>~~g for @form_fields; # Strip HTML tags s~['";]~~g for @form_fields; # Strip dangerous chars s~^\s+|\s+$~~g for @form_fields; # Trim whitespace
Normalize file paths:
s~\\~/~g for @paths; # Backslash to forward slash s~/+~/~g for @paths; # Collapse multiple slashes s~/$~~ for @paths; # Strip trailing slash
Clean up environment variables:
s~\s+$~~ for @ENV{qw(PATH HOME USER)}; # Trim trailing space
That last one is fun. You can use a hash slice with postfix for to mutate specific hash values. @ENV{qw(...)} returns a list of aliases to those hash values.

Part 13: THE BEAUTY OF IT

Postfix for as a mutator is peak Perl. It's concise without being cryptic. It says exactly what it does: apply this operation to every element.
s~\.example\.com$~~ for @hostnames;
Read that aloud. "Substitute away .example.com for each hostname." It's almost English.

The aliasing behavior that makes it work is the same aliasing that powers for loops everywhere in Perl. Postfix for just strips away the ceremony and lets you say what you mean in one breath.

@array ──────────────┐ | | [elem0] [elem1] [elem2] | | | $_ ──> $_ ──> $_ (aliased, not copied) | | | mutate mutate mutate | | | [new0] [new1] [new2] | | @array ──────────────┘ In-place. No copies. No ceremony.
perl.gg