From one-liner to shell alias#

A one-liner is disposable by design. The moment you type the same one twice, something has earned a place in your shell config. This chapter covers the mechanics of lifting a pperl recipe into a bash or zsh shell function, how to parametrise it, and when to stop and write a real script instead.

Assumed baseline: you have a bash or zsh shell, a ~/.bashrc or ~/.zshrc (or an rc.d / functions directory your shell sources), and the patience to close and reopen one terminal after editing it.

Alias versus function#

Bash and zsh have two mechanisms. They are not interchangeable.

  • alias is textual substitution. It runs at the start of a command line, replacing the alias name with its value before the shell re-parses. No arguments, no pipelining quirks beyond what substitution gives you.

  • A shell function is a named piece of shell code. It takes positional arguments ($1, $2, $@), reads stdin like any other command, and can pipe into and out of other commands.

Rule of thumb. If the recipe takes no arguments, alias is enough. If it needs any argument (a column number, a regex, a filename), use a function.

Alias — the zero-argument recipe#

# In ~/.bashrc
alias sum-stdin='pperl -MList::Util=sum -lane '"'"'print sum @F'"'"
alias count-lines='pperl -lne '"'"'END { print $. }'"'"

The quoting is ugly because a bash single-quoted string cannot contain a single quote; the idiom '"'"' closes-and-reopens. This is the main reason to prefer functions: they let you quote the Perl once, cleanly.

Function — the common case#

# In ~/.bashrc
sum-stdin() { pperl -MList::Util=sum -lane 'print sum @F'; }
count-lines() { pperl -lne 'END { print $. }'; }

Usage is the same:

$ printf '1 2 3\n4 5 6\n' | sum-stdin
6
15

Parametrised functions#

The shell interpolates $1, $@, "$1" into the function body before executing it. Inside the Perl program, the shell-level $1 appears as a literal value — Perl’s own $1 (the regex capture) is untouched by shell interpolation only if you quote with single quotes.

The mixture is solvable with care. Two idioms cover almost every case.

Idiom 1: shell-interpolated column / value#

Use double quotes around the Perl program so the shell expands $1, then escape Perl’s own $ with \$.

# Sum column N of stdin: sumcol N
sumcol() {
    pperl -MList::Util=sum -lane "\$s += \$F[$1]; END { print \$s }"
}

# Usage
$ cat data.tsv
10 apple 3
15 pear  7
20 plum  2
$ sumcol 2 < data.tsv
12

Every \$ is a literal dollar sign for Perl; every $1 (unescaped) is the first positional argument expanded by the shell.

Idiom 2: program in single quotes, data via environment#

Single-quoted Perl stays readable, and values reach the program via %ENV.

# Grep with a pperl regex: pgrep PATTERN [FILE...]
pgrep() {
    local pat=$1
    shift
    pat="$pat" pperl -ne 'print if /$ENV{pat}/' "$@"
}

$ pgrep '\berror\b' app.log

This keeps $1 (Perl capture) usable and avoids the backslash cascade.

Idiom 3: mixing both#

# Replace column N's commas with semicolons, in place: csv-fix N FILE...
csv-fix() {
    local col=$1
    shift
    col="$col" pperl -i.bak -F'\t' -lane '
        my $c = $ENV{col};
        $F[$c] =~ tr/,/;/;
        print join("\t", @F);
    ' "$@"
}

The column number arrives in $ENV{col} — no escaping dance. The file list propagates via "$@" so spaces in filenames survive.

Practical aliases worth keeping#

Annotated list of recipes that earn their place in a shell config for anyone who processes text at the command line daily.

sumcol N — sum column N (whitespace-separated)#

sumcol() {
    pperl -MList::Util=sum -lane "\$s += \$F[$1]; END { print \$s }"
}

sumcolf N FS — sum column N with custom field separator#

sumcolf() {
    local col=$1 fs=$2
    col="$col" pperl -MList::Util=sum -F"$fs" -lane '
        $s += $F[$ENV{col}]; END { print $s }
    '
}

$ sumcolf 3 : < /etc/passwd    # sum of UIDs

colstats N — min / max / mean / count for column N#

colstats() {
    col="$1" pperl -MList::Util=qw(min max sum) -lane '
        my $c = $ENV{col};
        push @v, $F[$c];
        END {
            printf "n=%d min=%g max=%g mean=%.4f\n",
                scalar @v, min(@v), max(@v), sum(@v) / @v
        }
    '
}

uniq-stream — streaming unique, keeping first sighting#

uniq-stream() { pperl -ne 'print unless $seen{$_}++'; }

Unlike sort -u, order is preserved. Unlike uniq, input need not be sorted.

extract PAT — print every match of a regex, one per line#

extract() { pat="$1" pperl -lne 'print for /$ENV{pat}/g'; }

$ extract '\b\d{3}-\d{4}\b' < phonebook.txt
555-1212
555-9876

between A B — print everything between regex A and regex B, inclusive#

between() {
    a="$1" b="$2" pperl -ne 'print if /$ENV{a}/ .. /$ENV{b}/'
}

$ between 'BEGIN cert' 'END cert' < bundle.pem

json-pretty — pretty-print a single JSON document#

json-pretty() {
    pperl -MJSON::PP -0777 -ne '
        print JSON::PP->new->pretty->canonical->encode(
            JSON::PP->new->decode($_)
        )
    '
}

passwd-json — /etc/passwd as NDJSON#

passwd-json() {
    pperl -MJSON::PP -F: -lane '
        print JSON::PP->new->encode({
            user => $F[0], uid => 0+$F[2], gid => 0+$F[3],
            home => $F[5], shell => $F[-1]
        })
    ' < /etc/passwd
}

words-by-freq — count word frequencies, sorted#

words-by-freq() {
    pperl -ne '
        $w{lc $_}++ for /\b[[:alpha:]]+\b/g;
        END { print "$w{$_} $_\n" for sort { $w{$b} <=> $w{$a} } keys %w }
    '
}

$ words-by-freq < essay.txt | head -20

strip-comments LANG — strip comments for a few common languages#

strip-comments() {
    case "$1" in
        sh|py|rb|pl) pperl -pe 's/(?<!\\)#.*//' ;;
        c|cpp|rs|java|js) pperl -0777 -pe 's{//[^\n]*|/\*.*?\*/}{}gs' ;;
        sql|lua) pperl -pe 's/--[^\n]*//' ;;
        *) echo "unknown language: $1" >&2; return 1 ;;
    esac
}

When to stop and write a script#

The one-liner → alias transition ends when:

  • The program needs an if / else branch longer than a single ? : conditional.

  • There are more than two or three -M modules.

  • You find yourself debugging via trial-and-error on the command line.

  • The program is the only alias in your config that gets a comment above it explaining what it does.

  • A colleague is going to run it.

Move to a script file. Drop the switches into the shebang line and let the program read naturally:

#!/usr/bin/env pperl
use strict;
use warnings;
use List::Util qw(sum);

my $col = shift @ARGV // die "usage: $0 COLUMN [FILE...]\n";
my $total = 0;
while (<>) {
    my @f = split;
    $total += $f[$col];
}
print "$total\n";

Give it a sensible name, chmod +x, and move on. The one-liner did its job: it was the shortest path from “I have an idea” to “I know this works”; the script is the path from “I know this works” to “my colleague can read it.”

Find out more#

  • progression — the recipes these aliases wrap.

  • gotchas — quoting inside shell functions and aliases.

  • numeric — more candidates for the alias list, especially the date-arithmetic ones.