<!-- category: one-liners -->
Postfix for as Mass Array Mutator
Mutate every element of an array. In one line. No loop variable, no index, no temporary copies.That stripss~\.example\.com$~~ for @hostnames;
.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
Normalfor loop:
Postfixfor my $host (@hostnames) { $host =~ s~\.example\.com$~~; }
for:
Same result. One line instead of three.s~\.example\.com$~~ for @hostnames;
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:You just doubled every element without writing a loop body. The mutation sticks becausemy @nums = (1, 2, 3, 4, 5); $_ *= 2 for @nums; # @nums is now (2, 4, 6, 8, 10)
$_ IS the element, not a snapshot.
This also means you can break things:
Assigning tomy @names = ("Alice", "Bob", "Charlie"); $_ = uc for @names; # @names is now ("ALICE", "BOB", "CHARLIE")
$_ 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.That's it. Every element chomped. Compare to the block form:chomp for @lines;
Or the map approach:for my $line (@lines) { chomp $line; }
Wait, that map version is wrong.@lines = map { chomp; $_ } @lines;
chomp returns the number of
characters removed, not the modified string. You'd need:
Ugly. The postfix@lines = map { chomp(my $l = $_); $l } @lines;
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:Two substitutions, comma-separated, applied to every element. The comma operator evaluates both expressions for eachs~^\s+~~, s~\s+$~~ for @fields;
$_.
Or if you're on Perl 5.14+ and want to be fancy:
Single regex with alternation. Thes~^\s+|\s+$~~g for @fields;
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.After those three lines: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
Each transformation builds on the last. Clean, readable pipeline of mutations.# ("web01", "db02", "cache03", "app04")
Part 6: NUMERIC TRANSFORMS
Works beautifully with numbers too.Convert Fahrenheit array to Celsius:
Round everything to two decimal places:$_ = ($_ - 32) * 5 / 9 for @temps;
Clamp values to a range:$_ = sprintf("%.2f", $_) for @prices;
Wait, that doesn't work. The$_ = 0 if $_ < 0 for @values; # floor at zero $_ = 100 if $_ > 100 for @values; # cap at 100
if modifier and for modifier can't
both be postfix on the same statement. You need:
Or just use a block:($_ < 0 ? $_ = 0 : $_ > 100 ? $_ = 100 : $_) for @values;
Know when to stop being clever.for (@values) { $_ = 0 if $_ < 0; $_ = 100 if $_ > 100; }
Part 7: CHAINING WITH THE COMMA
You can chain multiple operations using the comma operator:That one line does four things to every element:chomp, s~#.*~~, s~^\s+~~, s~\s+$~~ for @config_lines;
- Remove trailing newline
- Strip comments
- Remove leading whitespace
- 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
Combinegrep and postfix for to filter first, mutate second:
Strip the protocol from all defined URLs. Thes~^https?://~~ for grep { defined } @urls;
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:
Uppercase only the lines that start with "error". The rest stay untouched.$_ = uc for grep { m~^error~i } @log_lines;
Part 9: ONE-LINER FORM
Postfixfor is a natural fit for command-line Perl. Mutate data
streaming through a pipe:
Or withperl -e 's~\t~,~g for @ARGV; print join "\n", @ARGV' *.tsv
-a and -n, mutate the auto-split fields:
That strips trailingperl -lane 's~\.0+$~~ for @F; print join "\t", @F' data.txt
.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:
Oops. That renames each file to itself because we already mutated the names. You need the originals too:my @files = glob("*.HTML"); s~\.HTML$~.html~ for @files; rename $_, $_ for @files; # Wait, that renames nothing
Postfixmy @old = glob("*.HTML"); my @new = @old; s~\.HTML$~.html~ for @new; rename $old[$_], $new[$_] for 0..$#old;
for over an index range. Sometimes you still need the
index.
Part 10: GOTCHAS
Read-only values will blow up:Literals are read-only. You can't mutate them throughs~foo~bar~ for ("literal", "strings"); # Modification of a read-only value attempted
$_. Use an
array variable, not a list of constants.
Don't add or remove elements during iteration:
You're modifying the list you're iterating over. Perl won't stop you. It'll just run forever.push @array, "new" for @array; # Infinite loop. Don't.
Postfix for returns the original list, not the modified values:
Postfixmy @result = (s~foo~bar~ for @items); # Syntax error
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.
map creates a new list. Original untouched (unless you assign back).s~\s+$~~ for @lines; # Modifies @lines directly
Note themy @clean = map { s~\s+$~~r } @lines; # New array, @lines unchanged
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:Normalize file paths: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
Clean up environment variables:s~\\~/~g for @paths; # Backslash to forward slash s~/+~/~g for @paths; # Collapse multiple slashes s~/$~~ for @paths; # Strip trailing slash
That last one is fun. You can use a hash slice with postfixs~\s+$~~ for @ENV{qw(PATH HOME USER)}; # Trim trailing space
for to
mutate specific hash values. @ENV{qw(...)} returns a list of
aliases to those hash values.
Part 13: THE BEAUTY OF IT
Postfixfor as a mutator is peak Perl. It's concise without being
cryptic. It says exactly what it does: apply this operation to every
element.
Read that aloud. "Substitute away .example.com for each hostname." It's almost English.s~\.example\.com$~~ for @hostnames;
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.
perl.gg@array ──────────────┐ | | [elem0] [elem1] [elem2] | | | $_ ──> $_ ──> $_ (aliased, not copied) | | | mutate mutate mutate | | | [new0] [new1] [new2] | | @array ──────────────┘ In-place. No copies. No ceremony.