pl SourceForge

pl – Perl One-Liner Magic Wand

Some tasks are too menial for a dedicated script but still too cumbersome even with the many neat one-liner options of perl -E. This small script fills the gap: various one-letter commands & magic variables (with meaningful aliases too) and more nifty loop options take Perl programming to the command line. Fully imports List::Util. With no program on the command line, starts a pl Shell.

How to echo values, including from @ARGV, with single $quote & double $Quote:

pl 'echo "${quote}Perl$quote", "$Quote@ARGV$Quote"' one-liner
pl 'e "${q}Perl$q", "$Q@A$Q"' one-liner
'Perl' "one-liner"

Same for hard-to-print values:

pl 'echo \"Perl", \@ARGV, undef' one-liner
pl 'e \"Perl", \@A, undef' one-liner
\'Perl' [
  'one-liner'
] undef

Loop over args, printing each with line ending. And same, SHOUTING:

pl -opl '' Perl one-liner
pl -opl '$_ = uc' Perl one-liner
Perl
one-liner
PERL
ONE-LINER

Print up to 3 matching lines, resetting count (and $.) for each file:

pl -rP3 '/Perl.*one.*liner/' file*

Count hits in magic statistics hash %NUMBER:

pl -n '++$NUMBER{$1} while /(Perl|one|liner)/g' file*
pl -n '++$N{$1} while /(Perl|one|liner)/g' file*
       2: one
       7: liner
       9: Perl

Though they're sometimes slightly, sometimes quite a bit more complicated, most Perl one-liners from the internet work, just by omitting -e or -E. There's only one main program in pl, but you can just as well concatenate the -es with ;. See minor differences for exceptions. Let's see many varied examples.

Don't believe everything you read on SourceForge the internet! – Plato 🤔

DESCRIPTION

Pl follows Perl's philosophy for one-liners: the one variable solely used in one-liners, @F, is single-lettered. Because not everyone may like that, pl has it both ways. Everything is aliased both as a word and as a single letter, including Perl's own @F & *ARGV.

Perl one-liners, and hence pl, are by nature bilingual. You must run the command with its options & arguments, typically from Shell. By design, Perl quotes mimic Shell quotes, so here they collide. As Perl also uses Shell meta-characters like $, the best solution is to protect Perl-code from the Shell with single quotes, shown here slightly lighter as shell 'perl'. That means you can't use them inside. (An ugly way around that, is '\'', which ends a string, backslashes a quote and starts another.) For literal quotes use $quote. For quoting use double quotes or q{}.

Shell and Perl, unlike most other languages, don't make you stick your toe up your nose to get newlines into strings. Thus, you see long "one-liners" as legible many-liners. In the veggie-burger menu, you can toggle many-line display. In normal text short and long name variants are initial-bold as XYZ. All examples use the long names, if applicable. Those are in the darker blue upper half. In the lighter lower half, you get the short variant, again selectable in the menu. If there is a play button, hover (or tap) the example to fold out a terminal with the output. The blue sidelines show the structure and act as breadcrumbs: You can hover, click or bookmark them.

DOCUMENTATION

Options

Many of perl's options are also available in pl, sometimes enhanced with extra functionality. And the new options complement what perl offers, specifically oriented towards one-liners.

-0[octal]

perl: Specify record separator with -n/-p (\0, if no argument).

-Aprog

Map program over already available @ARGV (from command line or previous -A) or undef. If you wrap program in {} uses grep instead of map. The result becomes the new @ARGV. You can mix it with -B. The 1st two are equivalent, except that the 1st one isn't limited by Shell line length limitations. The third again greps by file size, reading only the Perl modules less than 1 kB:

pl -nA '<*.pm>' '...'
pl -n '...' *.pm
pl -nA '<*.pm>' -A '{ (stat)[7] < 1000 }' '...'

-a

perl: Autosplit mode with -n/-p (splits $_ into @FIELD).

-bprog

Run program before reading a new file in -n/-P/-p.

-Bprog

Add program before main program in same scope. You can use it to initialise my variables. Whereas, if you define a my variable in the main program of a -n, -p, -P, -o, or -O loop, that's a new variable on each iteration. This doesn't do a BEGIN block unless you wrap program in {}. You may mix it with -A.

-c

perl: Check syntax only (runs BEGIN and CHECK blocks).

-C[number/list]

perl: Enables the listed Unicode features.

--color[=when]

Colorize (people with impairment may adapt their system, terminal, or browser) some of the output; when can be never, always, or auto (the default).

-d[:debugger]

perl: Run program under debugger.

-D[number/list]

perl: Set debugging flags (argument is a bit mask or alphabets).

-eprog

Run program after finishing reading a file in -n/-p.

-Eprog

Add an END block after main-program in same scope. So, my-vars work as follows: the END block is a closure of the 1st $inner variable. Perl warns "Variable "$inner" will not stay shared":

pl -OB 'my $outer' -E 'echo $inner, $outer' 'my $inner = $outer = $ARGV' a b c
pl -OB 'my $outer' -E 'e $inner, $outer' 'my $inner = $outer = $A' a b c
a c

-f

perl: Don't do $sitelib/sitecustomize.pl at startup.

-F/pattern/

perl: Provide split() pattern for -a switch (//'s are optional).

-Idirectory

perl: Specify @INC/#include directory (several -I's allowed).

-i[extension]

perl: Edit <> files in place (makes backup if extension supplied).

As I said before, I never repeat myself. 😂

-n

perl: Assume while (<>) { … } loop around program. It's a little richer than that: if you use last, it closes the current file, leaving you to continue the loop on the next file.

-o[number]

Assume for(@ARGV) { … } loop around main program, and $ARGIND (or $I) is the current position. In this case -p doesn't imply -n. If you give number, passes that many args at once as an array, referencing the original values. If there aren't enough on the last round, fills up @ARGV with undefs.

pl -opl '' I II III IV
pl -o3 'echo $ARGIND, @$_' i ii iii iv v vi vii viii ix
pl -opl '' I II III IV
pl -o3 'e $I, @$_' i ii iii iv v vi vii viii ix
I
II
III
IV
0 i ii iii
3 iv v vi
6 vii viii ix

-O[number]

like -o but use @ARGV as loop variable.

Does pl -penis do pussy?  It implements cat.   🤭

-p[number]

perl+: On each loop print (also -o and -O, in which case you must fill $_) iteration. If you give number, prints at most number times.

-P[number]

Like -p but print only if main program evaluates to true, like grep.

-r

Reset $. and -p/-P counter for each file.

-T

perl: Enable tainting checks.

-t

perl: Enable tainting warnings.

-U

perl: Allow unsafe operations.

-u

perl: Dump core after parsing program.

-v

perl: Print version, patchlevel and license.

-Vversion

Rerun with given perl version, which is just a string appended to perl.

-W

perl: Enable all warnings.

-w

perl: Enable many useful warnings.

-X

perl: Disable all warnings.

Functions

Various functions, always also with a one letter alias, perform little tasks that can be useful in one-liners.

benchmark { } [name[, arg…]]
b { } [name[, arg…]]

Benchmark slow code for 10 s, display name, looping over args.

Benchmark { } [name[, arg…]]
B { } [name[, arg…]]

Same but run code 100 times in benchmark, to reduce overhead.

Config [regexp…]
C [regexp…]

Import and return %Config, e.g., Config->{sitelib}, optionally only part matching regexps.

Why is Halloween Christmas? Because Oct 31 = Dec 25. 🎅

Date [arg…][, tz]
D [arg…][, tz]

Date (from arg [s, us], s{.us}, offset [+-]s{.us}, tz ([+-]0-14{:mm|.ff}). You should pass microseconds as strings because floats have implementation-dependent rounding issues. You must pass positive offsets as strings because otherwise it loses the +. Returns the date, if called in some context, else echoes it.

pl 'Date;
    $_ = Date -86400, "+3600";
    echo $_, " -- ", Date "+8:45"'
pl 'D;
    $_ = D -86400, "+3600";
    e $_, " -- ", D "+8:45"'
Mon Aug 14 23:34:20.425194 2023
Mon Aug 14 00:34:20.425292 2023  --  Tue Aug 15 06:19:20.425333 +08:45 2023

echo [arg…]
e [arg…]

Echo prettified args or $_ with spaces and newline. Prettified means, undef becomes that string, italic if --color is active. Anything that can be stringified, is. Any other reference goes through Data::Dumper, which pl loads only if needed.

If it's called in scalar context (e.g., $x = echo …) instead return the same as it would echo, in one string (inspired by Shell $(…)). If it's called in list context (e.g., @l = echo …) return each arg prettified individually, with a newline on the last one.

Echo [arg…]
E [arg…]

Same but no newline.

form format, [arg…]
f format, [arg…]

Form(at) and echo prettified args or $_ with newline. If it's called in scalar or list context (e.g., $x = form …) instead return the same as it would echo, in one string. Parameter index can be "%1:" instead of "%1\$".

Form format, [arg…]
F format, [arg…]

Same but no newline.

Isodate [arg…][, tz]
I [arg…][, tz]

Same as Date but uses ISO format.

pl 'Isodate;
    $_ = Isodate 7 * -86400;
    echo $_, " -- ", Isodate "+8.75"'
pl 'I;
    $_ = I 7 * -86400;
    e $_, " -- ", I "+8.75"'
2023-08-14T23:34:20.462578
2023-08-07T23:34:20.462666  --  2023-08-15T06:19:20.462695 +08:45

keydiff [key[, value]]
k [key[, value]]

Store value or chomped $_ in $KEYDIFF{key or $1}[$ARGIND]. At the END for each key (which pl sorts numerically if possible) pl diffs all values.

Keydiff [number[, value]]
K [number[, value]]

Same but key is $FIELD[number] or $F[0].

Number [n[, hash_or_array]]
N [n[, hash_or_array]]

Trim hash_or_array (default %NUMBER) values less than n (default 2), or, if negative, more than -n e.g., -EN or -E 'Number -5, @RESULT'. This happens recursively at any depth in nested hashes or arrays.

The first argument can also be a function, where deletion happens for every element where it returns a falsy value. It gets called for each scalar element with 3 arguments: 0 - the current (nested) hash or array, 1 - its key or index, 2 - its value.

pl '%RESULT = (neg => [-9..-1], pos => [1..9]); Number sub { ! ($_[2] % 3) }, %RESULT'
pl '%R = (neg => [-9..-1], pos => [1..9]); N sub { ! ($_[2] % 3) }, %R'
neg:  [
  -9,
  -6,
  -3
]
pos:  [
  3,
  6,
  9
]

piped { } cmd[, arg…]
p { } cmd[, arg…]

Open pipe from cmd and loop over it.

template [tmpl[, hash|key => value…]]
t [tmpl[, hash|key => value…]]

Replace values from hash in template (defaults to $_), which may also be a filehandle of ref to a filename. Hash (defaults to %TEMPLATE) may be given as a reference or key-value pairs. The template may use one of three markup styles: [% x %], {{ x }}, or <?pl x ?>. These are totally equivalent. The 1st one found in the template will be used. If you put a ~ just inside a delimiter (e.g., <?pl~ x ~?>), it will gobble horizontal whitespace on that side, behind also one newline.

Within the markup x may be any valid syntax for a Perl hash key. The 3 characters |, :, or ! mark the end of the key name and introduce 3 kinds of filter. The 1st two, can be ?| or ?:, meaning this item applies only if the key exists. Otherwise the key is optional. If you give a key, its value is locally assigned to $_. If undef it defaults to ''.

You write the filter in Perl. You can abort the filter with last. If the filter starts with | then you pipe its scalar value into the template. If there is no filter after |, you recursively treat the value as a template. If the filter starts with : then you insert the value of $_. An easy way to only do that is [%:%]. The whole template gets compiled to an anonymous sub, the 1st time. As this happens inside the function, filters have no access to your surrounding my variables.

You insert the Perl code following ! into the code the template compiles to verbatim. This is useful for flow control. E.g., if $TEMPLATE{x} (or $T{x}) is an array, you can loop over its values as @$_. The loop logic is entirely in Perl, which again localizes $_. If do-nothing [%!%] starts the document, it "declares" its markup syntax. And with ~ it suppresses surrounding space including one following newline. (See PLDUMP)

pl 'template q(A: {{ a }} B: {{ | "no" }}{{ b ?| "yes" }} C: {{ lc "C" }} C+1: {{ c|$_ + 1}}), qw(a 1 c 3)'
pl 'template q([%x!for( @$_ ) { %] X: [% : %] [%~ ! } %][% y %]), { x => [1..5] }'
pl 'template q(<ul><?pl l!for( @$_ ) { ?> <li><?pl:?></li><?pl ! } ?> </ul>), { l => [1..3] }'
pl 't q(A: {{ a }} B: {{ | "no" }}{{ b ?| "yes" }} C: {{ lc "C" }} C+1: {{ c|$_ + 1}}), qw(a 1 c 3)'
pl 't q([%x!for( @$_ ) { %] X: [% : %] [%~ ! } %][% y %]), { x => [1..5] }'
pl 't q(<ul><?pl l!for( @$_ ) { ?> <li><?pl:?></li><?pl ! } ?> </ul>), { l => [1..3] }'
A: 1 B: no C: 3 C+1: 4
 X: 1 X: 2 X: 3 X: 4 X: 5
<ul> <li>1</li> <li>2</li> <li>3</li> </ul>

Template [tmpl[, hash|key => value…]]
T [tmpl[, hash|key => value…]]

Same but no newlines, also not on nested templates.

pl '$_ = q( [ <?pl~inner|~?> ]);
    $TEMPLATE{inner} = q([ {{ who | $_ || "Jack" }} in the box ]);
    Template;
    $TEMPLATE{who} = "Sue";
    template'
pl '$_ = q( [ <?pl~inner|~?> ]);
    $T{inner} = q([ {{ who | $_ || "Jack" }} in the box ]);
    T;
    $T{who} = "Sue";
    t'
 [[ Jack in the box ]] [[ Sue in the box ]
]

Variables

Various variables, always also with a one letter alias, often perform magic tasks at the END.

*ARGV
*A

perl: ARGV, $ARGV & @ARGV are all aliased to A, $A & @A.

$ARGIND
$I

Index of ARG which -o, -n, or -p loop is currently processing.

@FIELD
@F

perl: This is an alias to loop autosplit variable @F.

$quote
$q

Predefined to a single quote ' without any magic. Perl's q() makes it easy to integrate functional quotes under all circumstances. This does the same for literal quotes.

$Quote
$Q

Predefined to a double quote " without any magic. Perl's qq() makes it easy to integrate functional quotes under all circumstances. This does the same for literal quotes.

%KEYDIFF
%K

At END, sort by keys, print keydiff of $ARGIND array elements. Filled by keydiff.

%NUMBER
%N

At END, sort numerically by values.

*RESULT
*R

At END, echo $RESULT if defined, then @RESULT one per line if not empty, then %RESULT sorted by keys.

$ENV{PLDUMP}

Since pl -MO=Deparse won't show your parts of the program, it can be quite baffling when things go wrong. If you export this with value 1 before starting pl, you see how your parts get embedded in various bits of generated stuff. If you install perltidy, pl will use it. Some options get handled by perl, so they won't show up here:

PLDUMP=1 \
    pl 'say "Hello Perld!"'
use feature ':' . substr $^V, 1;

sub pl::prog {
    $pl::last = 1;
  LINE: {
#line 1 "main program"
        say "Hello Perld!";
    } continue {
        $pl::last = 0;
    }
    if ( $pl::last || eof ) {
        ++$ARGIND;
        if ($pl::last) { my $d = $.; close ARGV; $. = $d }
        exit if $pl::last == 2;
    }
}

If you export this with value 2, it will instead show what a template would compile to:

PLDUMP=2 \
    pl 'template q([%x!for( @$_ ) {%] X: [% : %] [%~!}%]), x => [1..5]'
PLDUMP=2 \
    pl 't q([%x!for( @$_ ) {%] X: [% : %] [%~!}%]), x => [1..5]'
my @_template = [
  '',
  ' X: ',
  ''
] ;
#line 1 "template"
sub { my $_template = $_template[0]
; for((local $_ = $TEMPLATE{x} // '')?():(), @$_ ) {
;$_template .= $_template[1] . (do {{ 
; $_ }} // '');$_template .= $_template[2]
; }
;
; $_template }

To steal ideas from one person is plagiarism. To steal from many is research. 😉

EXAMPLES

Here's a wide variety of examples, many solving real-life problems. Often you can copy & paste them as-is. Or you need to make minor changes, e.g., to adapt them to your search expression. Many of these examples started out quite small, illustrating the power of pl. But to be generally useful, they're extended to cope with border cases. See canned commands for how you can make your favourite ones easier to use.

Only some of these are original. Many are adaptations from the various Perl one-liner web pages (Tom Christiansen, Peteris Krumins, CatOnMat, joyrexus, Richard Socher, eduonix, Geoff Broadwell (Windows syntax), Oracle, PerlMonks, perloneliner, Sundeep Agarwal: part 1, part 2 & cookbook) or videos (Walt Mankowski, Techalicious, David Oswald). This is no attempt to appropriate ownership, just to show how things are even easier and more concise with pl.

Dealing with Files

People say the back of my head looks really nice – but I don't see it. 😂

Heads …

If you want just n, e.g., 10, lines from the head of each file, use the optional number argument to -p, along with -r to reset the count. The program can be empty but must be present if you provide additional args:

pl -rp10 '' file*

If you want the head up to a regexp, use the flip-flop operator, starting with line number 1. Use the print-if-true -P loop option, again with -r to reset the count:

pl -rP '1../last/' file*

You can combine the two if you want at most n lines, e.g., 10:

pl -rP10 '1../last/' file*

What has a head, a tail, but no legs?  A penny.   😂

… or Tails?

If you want a bigger number of last lines, you need to stuff them in a list; not worth it. But, if you want just 1 last line from each file, the end-of-file -e code (no need to quote, as it has no special characters) can Echo it for you, capitalized to not add another newline (yes, Perl is case sensitive):

pl -e Echo '' file*
pl -e E '' file*

If you want the tail from a line-number (e.g., 99) or a regexp, use the flip-flop operator, starting with your regexp and going till each end-of-file:

pl -P '99..eof' file*
pl -P '/first/..eof' file*

You can even get head and tail (which in programming logic translates to print if in 1st or 2nd range) if last line of head comes before 1st line of tail (or actually any number of such disjoint ranges):

pl -rP '1../last/ or /first/..eof' file*

Remove Trailing Whitespace in Each File

This print-loops (-p) over each file, replacing it (-i) with the modified output. Line ends get stripped on reading and added on printing (-l) because they're also whitespace (\s). At each end of line, substitute one or more spaces of any kind (incl. DOS newlines) with nothing:

pl -pli 's/\s+$//' file*

Tabify/Untabify Each File

This print-loops (-p) over each file, replacing it (-i) with the modified output. At beginning of line and after each tab, this converts 8 spaces or less than 8 followed by a tab to a tab:

pl -pi '1 while s/(?:^|\t)\K(?: {1,7}\t| {8})/\t/' file*

To go the other way, subtract the tab-preceding length modulo 8, to get the number of spaces to replace with:

pl -pi '1 while s/^([^\t\n]*)\K\t/" " x (8 - length($1) % 8)/e' file*

Fans of half-width tabs make that:

pl -pi '1 while s/(?:^|\t)\K(?: {1,3}\t| {4})/\t/' file*
pl -pi '1 while s/^([^\t\n]*)\K\t/" " x (4 - length($1) % 4)/e' file*

This counts repetitions of lines in a hash. Print only when the expression is true (-P), i.e. the count was 0:

pl -P '!$a{$_}++' file*

If you want this per file, you must empty the hash in the end-of-file -e code:

pl -Pe '%a = ()' '!$a{$_}++' file*

Remove Empty Lines

Or, actually the opposite, printing back to the same files (-Pi) all lines containing non-whitespace \S:

pl -Pi '/\S/' file*

Move a Line Further Down in Each File

Assume we have lines matching "from" followed by lines matching "to". The former shall move after the latter. This loops over each file, replacing it with the modified output. The flip-flop operator becomes true when matching the 1st regexp. Capture something in there to easily recognize it's the first, keep the line in a variable and empty $_. When $1 is again true, it must be the last matching line. Append the keep variable to it.

pl -pi 'if( /(f)rom/.../(t)o/ ) {
        if( $1 eq "f" ) { $k = $_; $_ = "" } elsif( $1 ) { $_ .= $k }
    }' file*

Rename a File Depending on Contents

This reads each file in an -n loop. When it finds the package declaration, which gives the logical name of this file, it replaces double colons with slashes. It renames the file to the result. The last statement then makes this the last line read of the current file, continuing with the next file:

pl -n 'if( s/^\s*package\s+([^\s;]+).*/$1/s ) {
        s!::!/!g;
        rename $ARGV, "$_.pm" or warn "$ARGV -> $_.pm: $!\n";
        last;
    }' *.pm
pl -n 'if( s/^\s*package\s+([^\s;]+).*/$1/s ) {
        s!::!/!g;
        rename $A, "$_.pm" or warn "$A -> $_.pm: $!\n";
        last;
    }' *.pm

This assumes all files are at the root of the destination directories. If not, you must add the common part of the target directories before $_.

On Windows this won't quite work because that locks the file while reading. There you must add close ARGV; (or close A;) before the rename.

For Java it's a bit more complicated because the full name is split into a package followed by a class or similar statement. Join them when we find the latter:

pl -n 'if( /^\s*package\s+([^\s;]+)/ ) {
        $d = $1 =~ tr+.+/+r;
    } elsif( /^\s*(?:(?:public|private|protected|abstract|sealed|final)\s+)*(?:class|interface|enum|record)\s+([^\s;]+)/ ) {
        rename $ARGV, "$d/$1.java" or warn "$ARGV -> $d/$1.java: $!\n";
        last;
    }' *.java
pl -n 'if( /^\s*package\s+([^\s;]+)/ ) {
        $d = $1 =~ tr+.+/+r;
    } elsif( /^\s*(?:(?:public|private|protected|abstract|sealed|final)\s+)*(?:class|interface|enum|record)\s+([^\s;]+)/ ) {
        rename $A, "$d/$1.java" or warn "$A -> $d/$1.java: $!\n";
        last;
    }' *.java

Delete Matching Files, Except Last One

pl -o 'unlink if $ARGIND < $#ARGV' file*
pl -o 'unlink if $I < $#A' file*
pl -oA '<file*>' -B pop unlink
pl -oA 'grep !/keep-me/, <file*>' unlink

42% of statistics are made up! 😂

File Statistics

Count Files per Suffix

Find and pl both use the -0 option to allow funny filenames, including newlines. Sum up encountered suffixes in sort-numerically-at-end hash %NUMBER:

find -type f -print0 |
    pl -0ln 'm@[^/.](\.[^/.]*)?$@;
        ++$NUMBER{$1 // "none"}'
find -type f -print0 |
    pl -0ln 'm@[^/.](\.[^/.]*)?$@;
        ++$N{$1 // "none"}'
       4: .3
       4: .SHs
       4: .act
      …
      20: .tar
      24: .com
      24: .e2x
      28: .aux
      28: .utf
      32: .enc
      32: .typemap
      36: .SH
      40: .cpp
      52: .bat
      64: .plx
      72: .inc
      80: .xr
      88: .json
     108: .tml
     136: .xml
     224: .yml
     332: .xs
     376: .sh
     412: .ucm
     444: .PL
     512: .c
     640: .h
     696: .txt
     950: .pod
    1392: .pl
    2988: none
    3264: .pm
   10846: .t

There are three types of people: those who can count and those who can't. 🙃

Count Files per Directory per Suffix

Match to first or last / and from last dot following something, i.e. not just a dot-file. Store sub-hashes in sort-by-key-and-stringify-at-end hash %RESULT. Count in a nested hash of directory & suffix:

find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+?)/.*?[^/.](\.[^/.]*)?$@;
        ++$RESULT{$1}{$2 // "none"}'
find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+?)/.*?[^/.](\.[^/.]*)?$@;
        ++$R{$1}{$2 // "none"}'
perl-5.30.0:  {
  '.1' => 3,
  '.3' => 1,
  '.PL' => 111,
  …
  '.yml' => 56,
  none => 747
}
perl-5.30.1:  {
  …
  '.c' => 128,
  …
  '.h' => 160,
  …
  '.pm' => 816,
  '.pmc' => 2,
  …
}
perl-5.30.2:  {
  …
  '.pl' => 348,
  …
  '.t' => 2712,
  …
}
perl-5.30.3:  {
  '.1' => 3,
  '.3' => 1,
  '.PL' => 111,
  '.SH' => 9,
  …'
  '.perl' => 1,
  '.perldb' => 2,
  '.ph' => 1,
  '.pht' => 1,
  '.pkg' => 1,
  '.pl' => 348,
  '.yml' => 56,
  none => 747
}
find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$RESULT{$1}{$2 // "none"}'
find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$R{$1}{$2 // "none"}'
perl-5.30.3:  {
  '.SH' => 8,
  '.act' => 1,
  …
  '.yml' => 3,
  none => 15
}
perl-5.30.3/Cross:  {
  '.new' => 1,
  '.patch' => 2,
  '.sh-arm-linux' => 1,
  '.sh-arm-linux-n770' => 1,
  none => 9
}
…
perl-5.30.3/lib:  {
  '.pl' => 5,
  '.pm' => 36,
  '.pod' => 2,
  '.t' => 41
}
perl-5.30.3/lib/B:  {
  '.pm' => 2,
  '.t' => 3
}
perl-5.30.3/lib/Class:  {
  '.pm' => 1,
  '.t' => 1
}
perl-5.30.3/lib/Config:  {
  '.pm' => 1,
  '.t' => 1
}
…
perl-5.30.3/t:  {
  '.pl' => 4,
  '.supp' => 1,
  none => 3
}
perl-5.30.3/t/base:  {
  '.t' => 9
}
perl-5.30.3/t/benchmark:  {
  '.t' => 1
}
…

This is the same pivoted, grouping by suffix and counting per directory:

find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$RESULT{$2 // "none"}{$1}'
find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$R{$2 // "none"}{$1}'
…
.pl:  {
  'perl-5.30.3' => 8,
  …
  'perl-5.30.3/dist/Attribute-Handlers/demo' => 11,
  'perl-5.30.3/dist/Devel-PPPort/devel' => 3,
  'perl-5.30.3/dist/Devel-PPPort/parts' => 2,
  'perl-5.30.3/dist/Devel-PPPort/t' => 1,
  'perl-5.30.3/dist/IO/hints' => 1,
  'perl-5.30.3/dist/Storable/hints' => 4,
  …
}
…
.pm:  {
  'perl-5.30.3' => 1,
  'perl-5.30.3/Porting' => 2,
  …
  'perl-5.30.3/dist/Attribute-Handlers/lib/Attribute' => 1,
  'perl-5.30.3/dist/Carp/lib' => 1,
  'perl-5.30.3/dist/Carp/lib/Carp' => 1,
  'perl-5.30.3/dist/Data-Dumper' => 1,
  'perl-5.30.3/dist/Data-Dumper/t/lib' => 1,
  …
}
…
.pod:  {
  'perl-5.30.3/Porting' => 8,
  'perl-5.30.3/cpan/CPAN-Meta/lib/CPAN/Meta/History' => 5,
  'perl-5.30.3/cpan/CPAN/lib/CPAN/API' => 1,
  …
  'perl-5.30.3/dist/ExtUtils-ParseXS/lib' => 3,
  'perl-5.30.3/dist/ExtUtils-ParseXS/lib/ExtUtils' => 1,
  'perl-5.30.3/dist/Locale-Maketext/lib/Locale' => 1,
  'perl-5.30.3/dist/Locale-Maketext/lib/Locale/Maketext' => 2,
  …
}
…

This is similar but stores in sort-by-number-at-end %NUMBER. Therefore, it sorts by frequency, only secondarily by directory & suffix (pl sorts stably):

find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$NUMBER{"$1 " . ($2 // "none")}'
find -type f -print0 |
    pl -0ln 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$N{"$1 " . ($2 // "none")}'
       1: perl-5.30.3 .act
       1: perl-5.30.3 .aix
       1: perl-5.30.3 .amiga
       1: perl-5.30.3 .android
       1: perl-5.30.3 .bs2000
       …
       2: perl-5.30.3/Porting .c
       2: perl-5.30.3/Porting .pm
       …
     138: perl-5.30.3/cpan/Unicode-Collate/t .t
     149: perl-5.30.3/pod .pod
     206: perl-5.30.3/t/op .t

The function Number can trim %NUMBER, to those entries at least the argument (default 2):

find -type f -print0 |
    pl -0lnE Number 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$NUMBER{"$1 " . ($2 // "none")}'
find -type f -print0 |
    pl -0lnE N 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$N{"$1 " . ($2 // "none")}'
find -type f -print0 |
    pl -0lnE 'Number 80' 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$NUMBER{"$1 " . ($2 // "none")}'
find -type f -print0 |
    pl -0lnE 'N 80' 'm@^(?:\./)?(.+)/.*?[^/.](\.[^/.]*)?$@;
        ++$N{"$1 " . ($2 // "none")}'
      82: perl-5.30.3/cpan/Math-BigInt/t .t
      82: perl-5.30.3/hints .sh
      84: perl-5.30.3/cpan/IO-Compress/t .t
      87: perl-5.30.3/cpan/Unicode-Collate/Collate/Locale .pl
     103: perl-5.30.3/cpan/Encode/ucm .ucm
     117: perl-5.30.3/ext/XS-APItest/t .t
     137: perl-5.30.3/dist/Devel-PPPort/parts/base none
     137: perl-5.30.3/dist/Devel-PPPort/parts/todo none
     138: perl-5.30.3/cpan/Unicode-Collate/t .t
     149: perl-5.30.3/pod .pod
     206: perl-5.30.3/t/op .t

Sum up File-sizes per Suffix

This illustrates a simpler approach: rather than the complicated regexps above, let Perl split each filename for us. Find separates output with a dot and -F splits on that. The \\ is to escape one backslash from the Shell. No matter how many dots the filename contains, 1st element is the size and last is the suffix. Sum it in %NUMBER, which gets sorted numerically at the end:

find -type f -printf "%s.%f\0" |
    pl -0lF\\. '$NUMBER{@FIELD > 2 ? ".$FIELD[-1]" : "none"} += $FIELD[0]'
find -type f -printf "%s.%f\0" |
    pl -0lF\\. '$N{@FIELD > 2 ? ".$FIELD[-1]" : "none"} += $FIELD[0]'
       0: .configure
      16: .perldb
      85: .xsh
      90: .inf
     118: .pmc
     138: .plugin
…
 7167163: .c
 7638677: .pod
 7794749: .h
 9742749: .ucm
11124074: .t
11617824: .pm
12259742: .txt

I feel more like I do now than I did a while ago. 🙃

Count Files per Date

Incredibly, find has no ready-made ISO date, so specify the 3 parts. If you don't want days, just leave out -%Td. Sum up encountered dates in sort-value-numerically-at-end hash %NUMBER:

find -type f -printf "%TY-%Tm-%Td\n" |
    pl -ln '++$NUMBER{$_}'
find -type f -printf "%TY-%Tm-%Td\n" |
    pl -ln '++$N{$_}'
       1: 2018-07-19
       1: 2019-04-10
       …
      34: 2020-02-11
      93: 2020-02-29
    2816: 2018-06-27
    3307: 2019-05-11
    6024: 2019-10-21
   12159: 2019-10-24

Learn sign language! It's very handy. 😂

Count Files per Date with Rollup

Rollup means, additionally to the previous case, sum up dates with the same prefix. The trick here is to count both for the actual year, month, and day, as well as replacing once only the day, once also the month with "__", and once also the year with "____". This sorts after numbers and gives a sum for all with the same leading numbers. Use the sort-by-key-and-stringify-at-end hash %RESULT:

find -type f -printf "%TY-%Tm-%Td\n" |
    pl -ln 'do { ++$RESULT{$_} }
        while s/[0-9]+(?=[-_]*$)/"_" x length $&/e'
find -type f -printf "%TY-%Tm-%Td\n" |
    pl -ln 'do { ++$R{$_} }
        while s/[0-9]+(?=[-_]*$)/"_" x length $&/e'
2018-06-27:  2816
2018-06-__:  2816
2018-07-19:  1
2018-07-__:  1
2018-__-__:  2817
2019-04-10:  1
2019-04-__:  1
…
2019-11-10:  11
2019-11-25:  6
2019-11-__:  17
2019-12-05:  4
2019-12-__:  4
2019-__-__:  21581
…
2020-05-14:  33
2020-05-15:  1
2020-05-17:  5
2020-05-29:  4
2020-05-__:  43
2020-__-__:  206
____-__-__:  24604

Always remember you're unique, just like everyone else. 😂

Diff Several Inputs by a Unique Key

The function keydiff stores the 2nd arg or chomped $_ in %KEYDIFF keyed by 1st arg or $1 and the arg counter $ARGIND (or $I). Its sibling Keydiff does the same using 1st arg or 0 as an index into @FIELD for the 1st part of the key. At the end these show only the rows differing between files. If you write to a terminal or specify --color the difference gets color-highlighted in per-character detail with Algorithm::Diff, or in just one red blob without. Here are examples for how to alias these as canned commands.

Diff Several CSV, TSV, or passwd Files by 1st Field

This assumes commaless key fields and no newline in any field. Else you need a CSV-parser package. -F implies -a, which implies -n (even using older than Perl 5.20, which introduced this idea). -F, splits each line on commas, and Keydiff by default takes the 1st field as your unique key:

pl -F, Keydiff *.csv
pl -F, K *.csv
1
	1,H,Hydrogen,1:H & alkali metal,1.008
	n/a
	1,H,Hydrogen,1:alkali metal,1
4
	4,Be,Beryllium,2:alkaline earth metal,9.012
	4,Pl,Perlium,2:pl basis,5.32.0
	n/a
8
	8,O,Oxygen,16:O & chalcogen,16
	8,O,Oxygen,16:O & chalcogen,16
	8,O,Oxygen,16:O and chalcogen,16
41
	41,Nb,Niobium,5:no name,92.906
	n/a
	41,Nb,Columbium,5:no name,93
42
	42,Ve,Veritasium,6:an element of truth,i
	n/a
	n/a
74
	74,W,Tungsten,6:transition metal,183.84
	74,W,Wolfram,6:transition metal,183.8
	n/a
80
	80,Hg,Mercury,12:no name,200.592
	80,Hg,Quicksilver,12:no name,200.6
	80,Hg,Hydrargyrum,12:no name,201
110
	n/a
	110,Ds,Darmstadtium,10:transition metal,[281]
	110,Ds,Darmstadtium,10:transition metal,281

This is similar but removes the key from the stored value, so it doesn't get repeated for each file. Note how keydiff by default uses $1 as a key for $_. Additionally, in a -B begin program, show the filenames one per line:

pl -nB 'echo for @ARGV' 'keydiff if s/(.+?),//' *.csv
pl -nB 'e for @A' 'k if s/(.+?),//' *.csv
atom-weight-1.csv
atom-weight-2.csv
atom-weight-3.csv
1
	H,Hydrogen,1:H & alkali metal,1.008
	n/a
	H,Hydrogen,1:alkali metal,1
4
	Be,Beryllium,2:alkaline earth metal,9.012
	Pl,Perlium,2:pl basis,5.32.0
	n/a
8
	O,Oxygen,16:O & chalcogen,16
	O,Oxygen,16:O & chalcogen,16
	O,Oxygen,16:O and chalcogen,16
41
	Nb,Niobium,5:no name,92.906
	n/a
	Nb,Columbium,5:no name,93
42
	Ve,Veritasium,6:an element of truth,i
	n/a
	n/a
74
	W,Tungsten,6:transition metal,183.84
	W,Wolfram,6:transition metal,183.8
	n/a
80
	Hg,Mercury,12:no name,200.592
	Hg,Quicksilver,12:no name,200.6
	Hg,Hydrargyrum,12:no name,201
110
	n/a
	Ds,Darmstadtium,10:transition metal,[281]
	Ds,Darmstadtium,10:transition metal,281

A variant of CSV is TSV, with tab as separator. Tab is \t, which you must escape from the Shell as \\t, either with or without repeated keys:

pl -F\\t Keydiff *.tsv
pl -F\\t K *.tsv
1
	1	H	Hydrogen	1:H & alkali metal	1.008
	n/a
	1	H	Hydrogen	1:alkali metal	1
4
	4	Be	Beryllium	2:alkaline earth metal	9.012
	4	Pl	Perlium	2:pl basis	5.32.0
	n/a
8
	8	O	Oxygen	16:O & chalcogen	16
	8	O	Oxygen	16:O & chalcogen	16
	8	O	Oxygen	16:O and chalcogen	16
41
	41	Nb	Niobium	5:no name	92.906
	n/a
	41	Nb	Columbium	5:no name	93
42
	42	Ve	Veritasium	6:an element of truth	i
	n/a
	n/a
74
	74	W	Tungsten	6:transition metal	183.84
	74	W	Wolfram	6:transition metal	183.8
	n/a
80
	80	Hg	Mercury	12:no name	200.592
	80	Hg	Quicksilver	12:no name	200.6
	80	Hg	Hydrargyrum	12:no name	201
110
	n/a
	110	Ds	Darmstadtium	10:transition metal	[281]
	110	Ds	Darmstadtium	10:transition metal	281
pl -n 'keydiff if s/(.+?)\t//' *.tsv
pl -n 'k if s/(.+?)\t//' *.tsv
1
	H	Hydrogen	1:H & alkali metal	1.008
	n/a
	H	Hydrogen	1:alkali metal	1
4
	Be	Beryllium	2:alkaline earth metal	9.012
	Pl	Perlium	2:pl basis	5.32.0
	n/a
8
	O	Oxygen	16:O & chalcogen	16
	O	Oxygen	16:O & chalcogen	16
	O	Oxygen	16:O and chalcogen	16
41
	Nb	Niobium	5:no name	92.906
	n/a
	Nb	Columbium	5:no name	93
42
	Ve	Veritasium	6:an element of truth	i
	n/a
	n/a
74
	W	Tungsten	6:transition metal	183.84
	W	Wolfram	6:transition metal	183.8
	n/a
80
	Hg	Mercury	12:no name	200.592
	Hg	Quicksilver	12:no name	200.6
	Hg	Hydrargyrum	12:no name	201
110
	n/a
	Ds	Darmstadtium	10:transition metal	[281]
	Ds	Darmstadtium	10:transition metal	281

The same, with a colon as separator, if you want to compare passwd files from several hosts. Here we additionally need to ignore commented out lines:

pl -F: 'Keydiff unless /^#/' /etc/passwd passwd*
pl -n 'keydiff if s/^([^#].*?)://' /etc/passwd passwd*
pl -F: 'K unless /^#/' /etc/passwd passwd*
pl -n 'k if s/^([^#].*?)://' /etc/passwd passwd*

Growing old you forget to zip up your fly. Later you forget to unzip your fly. 🤓

Diff Several zip Archives by Member Name

This uses the same mechanism as the CSV example. Additionally, through the piped block, it reads the output of unzip -vql for each archive. That has an almost fixed format, except with extreme member sizes:

pl -oB 'echo for @ARGV' 'piped {
        keydiff if s@.{29,}% .{16} [\da-f]{8}\K  (.+)@@;
    } "unzip", "-vqq", $_' *.zip
pl -oB 'e for @A' 'p {
        k if s@.{29,}% .{16} [\da-f]{8}\K  (.+)@@;
    } "unzip", "-vqq", $_' *.zip
perl-5.30.0.zip
perl-5.30.1.zip
perl-5.30.2.zip
perl-5.30.3.zip
AUTHORS
	   48831  Defl:N    22282  54% 2019-05-11 11:50 cc2a1286
	   48864  Defl:N    22297  54% 2019-10-24 23:27 b793bcc5
	   48927  Defl:N    22338  54% 2020-02-29 12:55 8cecd35e
	   48927  Defl:N    22338  54% 2020-02-11 14:31 8cecd35e
Artistic
	    6321  Defl:N     2400  62% 2019-05-11 11:50 fa53ec29
	    6321  Defl:N     2400  62% 2019-10-24 22:17 fa53ec29
	    6321  Defl:N     2400  62% 2019-10-24 22:17 fa53ec29
	    6321  Defl:N     2400  62% 2019-10-21 13:20 fa53ec29
Changes
	    3168  Defl:N     1273  60% 2018-06-27 13:17 66a9af3e
	    3111  Defl:N     1246  60% 2019-10-27 10:52 f826c349
	    3111  Defl:N     1246  60% 2019-10-27 10:52 f826c349
	    3111  Defl:N     1246  60% 2019-10-28 09:05 f826c349
Configure
	  587687  Defl:N   148890  75% 2019-05-11 11:50 144c0f25
	  587687  Defl:N   148890  75% 2019-10-24 23:27 144c0f25
	  587825  Defl:N   148954  75% 2020-02-29 12:55 6761d877
	  587825  Defl:N   148954  75% 2020-02-27 18:40 6761d877
Copying
	   12632  Defl:N     4924  61% 2018-06-27 13:17 7117fcb9
	   12632  Defl:N     4924  61% 2019-10-24 22:17 7117fcb9
	   12632  Defl:N     4924  61% 2019-10-24 22:17 7117fcb9
	   12632  Defl:N     4924  61% 2019-10-21 13:20 7117fcb9
EXTERN.h
	    1652  Defl:N      575  65% 2019-05-11 11:50 725e28ae
	    1652  Defl:N      575  65% 2019-10-24 23:27 725e28ae
	    1652  Defl:N      575  65% 2019-10-24 23:27 725e28ae
	    1652  Defl:N      575  65% 2019-10-21 13:20 725e28ae
INSTALL
	  108059  Defl:N    37351  65% 2019-05-11 11:50 45af5545
	  108085  Defl:N    37371  65% 2019-10-24 23:27 e5f2f22b
	  107649  Defl:N    37211  65% 2020-02-29 12:55 9db83c1e
	  107649  Defl:N    37211  65% 2020-05-14 13:35 16726160
INTERN.h
	    1309  Defl:N      505  61% 2019-05-11 11:50 2bf79a27
	    1309  Defl:N      505  61% 2019-10-24 22:17 2bf79a27
	    1309  Defl:N      505  61% 2019-10-24 22:17 2bf79a27
	    1309  Defl:N      505  61% 2019-10-21 13:20 2bf79a27
MANIFEST
	  331194  Defl:N    64654  81% 2019-05-12 01:02 7c361194
	  331248  Defl:N    64661  81% 2019-10-24 23:27 9e4f98b4
	  331368  Defl:N    64708  81% 2020-03-14 13:13 c858ba6a
	  331422  Defl:N    64714  81% 2020-05-14 11:15 28a30dfa

Java .jar, .ear & .war files (which are aliases for .zip), after a clean build have many class files with the identical CRC but a different date. This excludes the date. Here are examples for how to combine these variants as Shell functions:

pl -o 'piped {
        keydiff $2 if s@.{16} ([\da-f]{8})  (.+)@$1@;
    } "unzip", "-vqq", $_' *.zip
pl -o 'p {
        k $2 if s@.{16} ([\da-f]{8})  (.+)@$1@;
    } "unzip", "-vqq", $_' *.zip
AUTHORS
	   48831  Defl:N    22282  54% cc2a1286
	   48864  Defl:N    22297  54% b793bcc5
	   48927  Defl:N    22338  54% 8cecd35e
	   48927  Defl:N    22338  54% 8cecd35e
Changes
	    3168  Defl:N     1273  60% 66a9af3e
	    3111  Defl:N     1246  60% f826c349
	    3111  Defl:N     1246  60% f826c349
	    3111  Defl:N     1246  60% f826c349
Configure
	  587687  Defl:N   148890  75% 144c0f25
	  587687  Defl:N   148890  75% 144c0f25
	  587825  Defl:N   148954  75% 6761d877
	  587825  Defl:N   148954  75% 6761d877
INSTALL
	  108059  Defl:N    37351  65% 45af5545
	  108085  Defl:N    37371  65% e5f2f22b
	  107649  Defl:N    37211  65% 9db83c1e
	  107649  Defl:N    37211  65% 16726160

Browsers have a bug of not checking for updated CSS & JavaScript. A common workaround is to add a hex number to those file names. In that case use only the meaningful part of the filename as a key:

pl -o 'piped {
        keydiff $2
            if s@.{16} ([\da-f]{8})  (.+?)(?:\.([0-9a-f]{20})(\..[a-z]+))?$@if( $3 ) {
                $n = "$2.\$x$4"; "$1  \$x=$3"
            } else {
                $n = $2; $1
            }@e
    } "unzip", "-vqq", $_' *.jar
pl -o 'p {
        k $2
            if s@.{16} ([\da-f]{8})  (.+?)(?:\.([0-9a-f]{20})(\..[a-z]+))?$@if( $3 ) {
                $n = "$2.\$x$4"; "$1  \$x=$3"
            } else {
                $n = $2; $1
            }@e
    } "unzip", "-vqq", $_' *.jar

Actually I'm very different. But I rarely find time for it. – von Horváth 😂

Diff Several Tarballs by Member Name

This is like the zip example. Alas, tar gives no checksums, so this is less reliable. Exclude directories, by taking only lines not starting with a d. Each time pl sees a wider owner/group or file size, columns shift right. Reformat the columns, to not show this as a difference:

pl -oB 'echo for @ARGV' 'piped {
        keydiff $4
            if s!^[^d]\S+ \K(.+?) +(\d+) (.{16}) (.+)!Form "%-20s %10d %s", $1, $2, $3!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
pl -oB 'e for @A' 'p {
        k $4
            if s!^[^d]\S+ \K(.+?) +(\d+) (.{16}) (.+)!F "%-20s %10d %s", $1, $2, $3!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
perl-5.30.0.txz
perl-5.30.1.txz
perl-5.30.2.txz
perl-5.30.3.txz
…
cpan/Compress-Raw-Bzip2/bzip2-src/decompress.c
	-r--r--r-- pfeiffer/pfeiffer         20948 2018-06-27 13:17
	-r--r--r-- pfeiffer/pfeiffer         20948 2019-10-24 22:17
	-r--r--r-- pfeiffer/pfeiffer         21287 2020-02-29 12:55
	-r--r--r-- pfeiffer/pfeiffer         21287 2020-02-12 18:41
cpan/Compress-Raw-Bzip2/bzip2-src/huffman.c
	-r--r--r-- pfeiffer/pfeiffer          6991 2018-06-27 13:17
	-r--r--r-- pfeiffer/pfeiffer          6991 2019-10-24 22:17
	-r--r--r-- pfeiffer/pfeiffer          6986 2020-02-29 12:55
	-r--r--r-- pfeiffer/pfeiffer          6986 2020-02-12 18:41
cpan/Compress-Raw-Bzip2/bzip2-src/randtable.c
	-r--r--r-- pfeiffer/pfeiffer          3866 2018-06-27 13:17
	-r--r--r-- pfeiffer/pfeiffer          3866 2019-10-24 22:17
	-r--r--r-- pfeiffer/pfeiffer          3861 2020-02-29 12:55
	-r--r--r-- pfeiffer/pfeiffer          3861 2020-02-12 18:41
cpan/Compress-Raw-Bzip2/fallback/constants.h
	-r--r--r-- pfeiffer/pfeiffer          7238 2018-06-27 13:17
	-r--r--r-- pfeiffer/pfeiffer          7238 2019-10-24 22:17
	-r--r--r-- pfeiffer/pfeiffer          7238 2019-10-24 22:17
	-r--r--r-- pfeiffer/pfeiffer          7238 2019-10-21 13:20

Same without the date:

pl -o 'piped {
        keydiff $3
            if s!^[^d]\S+ \K(.+?) +(\d+) .{16} (.+)!Form "%-20s %10d", $1, $2!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
pl -o 'p {
        k $3
            if s!^[^d]\S+ \K(.+?) +(\d+) .{16} (.+)!F "%-20s %10d", $1, $2!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
cpan/Compress-Raw-Bzip2/bzip2-src/decompress.c
	-r--r--r-- pfeiffer/pfeiffer         20948
	-r--r--r-- pfeiffer/pfeiffer         20948
	-r--r--r-- pfeiffer/pfeiffer         21287
	-r--r--r-- pfeiffer/pfeiffer         21287
cpan/Compress-Raw-Bzip2/bzip2-src/huffman.c
	-r--r--r-- pfeiffer/pfeiffer          6991
	-r--r--r-- pfeiffer/pfeiffer          6991
	-r--r--r-- pfeiffer/pfeiffer          6986
	-r--r--r-- pfeiffer/pfeiffer          6986
cpan/Compress-Raw-Bzip2/bzip2-src/randtable.c
	-r--r--r-- pfeiffer/pfeiffer          3866
	-r--r--r-- pfeiffer/pfeiffer          3866
	-r--r--r-- pfeiffer/pfeiffer          3861
	-r--r--r-- pfeiffer/pfeiffer          3861
cpan/Compress-Raw-Bzip2/lib/Compress/Raw/Bzip2.pm
	-r--r--r-- pfeiffer/pfeiffer         10783
	-r--r--r-- pfeiffer/pfeiffer         10783
	-r--r--r-- pfeiffer/pfeiffer         11009
	-r--r--r-- pfeiffer/pfeiffer         11009

Tarballs from the internet have a top directory of name-version/, which across versions would make every member have a different key. Exclude the 1st path element from the key by matching [^/]+/ before the last paren group:

pl -o 'piped {
        keydiff $4
            if s!^[^d]\S+ \K(.+?) +(\d+) (.{16}) [^/]+/(.+)!Form "%-20s %10d %s", $1, $2, $3!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
pl -o 'p {
        k $4
            if s!^[^d]\S+ \K(.+?) +(\d+) (.{16}) [^/]+/(.+)!F "%-20s %10d %s", $1, $2, $3!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
.dir-locals.el
	-r--r--r-- sawyer/sawyer               208 2018-06-27 13:17
	-r--r--r-- Steve/None                  208 2019-10-24 22:17
	-r--r--r-- Steve/None                  208 2019-10-24 22:17
	-r--r--r-- Steve/None                  208 2019-10-21 13:20
.lgtm.yml
	-r--r--r-- sawyer/sawyer               347 2019-05-11 11:50
	-r--r--r-- Steve/None                  347 2019-10-24 22:17
	-r--r--r-- Steve/None                  347 2019-10-24 22:17
	-r--r--r-- Steve/None                  347 2019-10-21 13:20
.metaconf-exclusions.txt
	-r--r--r-- sawyer/sawyer              1317 2019-05-11 11:50
	-r--r--r-- Steve/None                 1317 2019-10-24 22:17
	-r--r--r-- Steve/None                 1317 2019-10-24 22:17
	-r--r--r-- Steve/None                 1317 2019-10-21 13:20
.travis.yml
	-r--r--r-- sawyer/sawyer              2203 2019-05-11 11:50
	-r--r--r-- Steve/None                 2203 2019-10-24 23:27
	-r--r--r-- Steve/None                 2203 2019-10-24 23:27
	-r--r--r-- Steve/None                 2203 2019-10-21 13:20
AUTHORS
	-r--r--r-- sawyer/sawyer             48831 2019-05-11 11:50
	-r--r--r-- Steve/None                48864 2019-10-24 23:27
	-r--r--r-- Steve/None                48927 2020-02-29 12:55
	-r--r--r-- Steve/None                48927 2020-02-11 14:31
Artistic
	-r--r--r-- sawyer/sawyer              6321 2019-05-11 11:50
	-r--r--r-- Steve/None                 6321 2019-10-24 22:17
	-r--r--r-- Steve/None                 6321 2019-10-24 22:17
	-r--r--r-- Steve/None                 6321 2019-10-21 13:20
Changes
	-r--r--r-- sawyer/sawyer              3168 2018-06-27 13:17
	-r--r--r-- Steve/None                 3111 2019-10-27 10:52
	-r--r--r-- Steve/None                 3111 2019-10-27 10:52
	-r--r--r-- Steve/None                 3111 2019-10-28 09:05

Again, without the date and owner/group, which can also vary:

pl -o 'piped {
       keydiff $2
            if s!^[^d]\S+ \K.+? +(\d+) .{16} [^/]+/(.+)!Form "%10d", $1!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
pl -o 'p {
       k $2
            if s!^[^d]\S+ \K.+? +(\d+) .{16} [^/]+/(.+)!F "%10d", $1!e;
    } "tar", "-tvf", $_' *.tar *.tgz *.txz
AUTHORS
	-r--r--r--      48831
	-r--r--r--      48864
	-r--r--r--      48927
	-r--r--r--      48927
Changes
	-r--r--r--       3168
	-r--r--r--       3111
	-r--r--r--       3111
	-r--r--r--       3111
Configure
	-r-xr-xr-x     587687
	-r-xr-xr-x     587687
	-r-xr-xr-x     587825
	-r-xr-xr-x     587825

Diff ELF Executables by Loaded Dependencies

You get the idea: you can do this for any command that outputs records with a unique key. This one looks at the required libraries and which file they came from. For a change, loop with -O and $ARGV to avoid the previous examples' confusion between outer $_ which were the CLI args, and the inner one, which were the read lines:

pl -O 'piped {
       keydiff if s/^\t(.+\.so.*) => (.*) \(\w+\)/$2/;
    } ldd => $ARGV' exe1 exe2 lib*.so
pl -O 'p {
       k if s/^\t(.+\.so.*) => (.*) \(\w+\)/$2/;
    } ldd => $A' exe1 exe2 lib*.so

It's even more useful if you use just the basename as a key because version numbers may change:

pl -O 'piped {
       keydiff $2 if s/^\t((.+)\.so.* => .*) \(\w+\)/$1/;
    } ldd => $ARGV' exe1 exe2 lib*.so
pl -O 'p {
       k $2 if s/^\t((.+)\.so.* => .*) \(\w+\)/$1/;
    } ldd => $A' exe1 exe2 lib*.so

A pig looking at an electric socket: "Oh no, who put you into that wall?" 🐽

Looking at Perl

VERSION of a File

Print the first line (-P1) where the substitution was successful. To avoid the hassle of protecting them from (sometimes multiple levels of) Shell quoting, there are variables for single $quote & double $Quote:

pl -P1 's/(?:\bpackage\s+[\w:]+\s+|.+\bVERSION\s*=\s*)[v$Quote$quote]{0,2}([0-9.]+).+/$1/' pl
pl -P1 's/(?:\bpackage\s+[\w:]+\s+|.+\bVERSION\s*=\s*)[v$Q$q]{0,2}([0-9.]+).+/$1/' pl
0.91.1

For multiple files, add the filename, and reset (-r) the -P count for each file:

pl -rP1 's/(?:\bpackage\s+[\w:]+\s+|.+\bVERSION\s*=\s*)[v$Quote$quote]{0,2}([0-9.]+).+/$ARGV: $1/' *.pm
pl -rP1 's/(?:\bpackage\s+[\w:]+\s+|.+\bVERSION\s*=\s*)[v$Q$q]{0,2}([0-9.]+).+/$A: $1/' *.pm

Only POD or non-POD

You can extract either parts of a Perl file, with these commands. Note that they don't take the empty line before into account. If you want that, and you're sure the files adhere strictly to this convention, use the option -00P instead (not exactly as desired, the empty line comes after things but still, before next thing). If you want only the 1st POD (e.g., NAME & SYNOPSIS) use the option -P1 or -00P1:

pl -P '/^=\w/../^=cut/' file*
pl -P 'not /^=\w/../^=cut/' file*

Count Perl Code

This makes __DATA__ or __END__ the last inspected line of (unlike in perl -n!) each file. It strips any comment (not quite reliably, also inside a string). Then it strips leading whitespace and adds the remaining length to print-at-end $RESULT:

pl -ln 'last if /^__(?:DATA|END)__/;
    s/(?:^|\s+)#.*//s;
    s/^\s+//;
    $RESULT += length' *.pm
pl -ln 'last if /^__(?:DATA|END)__/;
    s/(?:^|\s+)#.*//s;
    s/^\s+//;
    $R += length' *.pm

If you want the count per file, instead of $RESULT use either sort-lexically $RESULT{$ARGV} (or $R{$A}) or sort-numerically $NUMBER{$ARGV} (or $N{$A}).

Content of a Package

Pl's echo can print any item. Packages are funny hashes, with two colons at the end. Backslashing the variable passes it as a unit to Data::Dumper, which gets loaded on demand in this case. Otherwise, all elements would come out just separated by spaces:

pl 'echo \%List::Util::'
pl 'e \%List::Util::'
{
  BEGIN => *List::Util::BEGIN,
  EXPORT => *List::Util::EXPORT,
  EXPORT_FAIL => *List::Util::EXPORT_FAIL,
  EXPORT_OK => *List::Util::EXPORT_OK,
  EXPORT_TAGS => *List::Util::EXPORT_TAGS,
  ISA => *List::Util::ISA,
  RAND => *List::Util::RAND,
  REAL_MULTICALL => *List::Util::List::Util,
  VERSION => *List::Util::VERSION,
  XS_VERSION => *List::Util::XS_VERSION,
  '_Pair::' => *{'List::Util::_Pair::'},
  all => *List::Util::all,
  any => *List::Util::any,
  bootstrap => *List::Util::bootstrap,
  first => *List::Util::first,
  head => *List::Util::head,
  import => *List::Util::import,
  max => *List::Util::max,
  maxstr => *List::Util::maxstr,
  mesh => *List::Util::mesh,
  mesh_longest => *List::Util::mesh_longest,
  mesh_shortest => *List::Util::mesh_shortest,
  min => *List::Util::min,
  minstr => *List::Util::minstr,
  none => *List::Util::none,
  notall => *List::Util::notall,
  pairfirst => *List::Util::pairfirst,
  pairgrep => *List::Util::pairgrep,
  pairkeys => *List::Util::pairkeys,
  pairmap => *List::Util::pairmap,
  pairs => *List::Util::pairs,
  pairvalues => *List::Util::pairvalues,
  product => *List::Util::product,
  reduce => *List::Util::reduce,
  reductions => *List::Util::reductions,
  sample => *List::Util::sample,
  shuffle => *List::Util::shuffle,
  sum => *List::Util::sum,
  sum0 => *List::Util::sum0,
  tail => *List::Util::tail,
  uniq => *List::Util::uniq,
  uniqint => *List::Util::uniqint,
  uniqnum => *List::Util::uniqnum,
  uniqstr => *List::Util::uniqstr,
  unpairs => *List::Util::unpairs,
  zip => *List::Util::zip,
  zip_longest => *List::Util::zip_longest,
  zip_shortest => *List::Util::zip_shortest
}

Library Loading

Where does perl load from, and what exactly has it loaded?

pl 'echo \@INC, \%INC'
pl 'e \@INC, \%INC'
[
  '/etc/perl',
  '/usr/local/lib/x86_64-linux-gnu/perl/5.36.0',
  '/usr/local/share/perl/5.36.0',
  '/usr/lib/x86_64-linux-gnu/perl5/5.36',
  '/usr/share/perl5',
  '/usr/lib/x86_64-linux-gnu/perl-base',
  '/usr/lib/x86_64-linux-gnu/perl/5.36',
  '/usr/share/perl/5.36',
  '/usr/local/lib/site_perl'
] {
  'Carp.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/Carp.pm',
  'Data/Dumper.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.36/Data/Dumper.pm',
  'Exporter.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/Exporter.pm',
  'Exporter/Heavy.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/Exporter/Heavy.pm',
  'List/Util.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/List/Util.pm',
  'XSLoader.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/XSLoader.pm',
  'bytes.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/bytes.pm',
  'constant.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/constant.pm',
  'feature.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/feature.pm',
  'overloading.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/overloading.pm',
  'sort.pm' => '/usr/share/perl/5.36/sort.pm',
  'strict.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/strict.pm',
  'warnings.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/warnings.pm',
  'warnings/register.pm' => '/usr/lib/x86_64-linux-gnu/perl-base/warnings/register.pm'
}

Same, for a different Perl version, e.g., if you have perl5.28.1 in your path. This time copy %INC before calling echo, which for an array or hashref needs to load Data::Dumper on the fly and thus its dependencies:

pl -V5.28.1 '$orig = {%INC}; echo \@INC, $orig, \%INC'
pl -V5.28.1 '$orig = {%INC}; e \@INC, $orig, \%INC'
[
  '/etc/perl',
  '/usr/local/lib/x86_64-linux-gnu/perl/5.28.1',
  '/usr/local/share/perl/5.28.1',
  '/usr/lib/x86_64-linux-gnu/perl5/5.28',
  '/usr/share/perl5',
  '/usr/lib/x86_64-linux-gnu/perl/5.28',
  '/usr/share/perl/5.28',
  '/usr/local/lib/site_perl',
  '/usr/lib/x86_64-linux-gnu/perl-base'
] {
  'Exporter.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/Exporter.pm',
  'Exporter/Heavy.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/Exporter/Heavy.pm',
  'List/Util.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/List/Util.pm',
  'XSLoader.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/XSLoader.pm',
  'feature.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/feature.pm',
  'sort.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/sort.pm',
  'strict.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/strict.pm',
  'warnings.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/warnings.pm'
} {
  'Carp.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/Carp.pm',
  'Data/Dumper.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/Data/Dumper.pm',
  'Exporter.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/Exporter.pm',
  'Exporter/Heavy.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/Exporter/Heavy.pm',
  'List/Util.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/List/Util.pm',
  'XSLoader.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/XSLoader.pm',
  'bytes.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/bytes.pm',
  'constant.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/constant.pm',
  'feature.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/feature.pm',
  'overloading.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/overloading.pm',
  'sort.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/sort.pm',
  'strict.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/strict.pm',
  'warnings.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/warnings.pm',
  'warnings/register.pm' => '/usr/lib/x86_64-linux-gnu/perl/5.28/warnings/register.pm'
}

Configuration

You get %Config::Config loaded on demand and returned by Config:

pl 'echo Config'
pl 'e C'
{
  Author => '',
  CONFIG => 'true',
  Date => '',
  Header => '',
  Id => '',
  Locker => '',
  Log => '',
  PATCHLEVEL => '30',
  PERL_API_REVISION => '5',
  PERL_API_SUBVERSION => '0',
  PERL_API_VERSION => '30',
  …
}

It returns a hash reference, from which you can look up an entry:

pl 'echo Config->{sitelib}'
pl 'e C->{sitelib}'
/usr/local/share/perl/5.36.0

You can also return a sub-hash, of only the keys matching any regexps you pass:

pl 'echo Config "random", qr/stream/'
pl 'e C "random", qr/stream/'
{
  d_random_r => 'define',
  d_srandom_r => 'define',
  d_stdio_stream_array => undef,
  random_r_proto => 'REENTRANT_PROTO_I_St',
  srandom_r_proto => 'REENTRANT_PROTO_I_TS',
  stdio_stream_array => ''
}

Tables

Number Bases

Perl natively handles the 4 different bases common to programming. If you want to list them side by side, quadruple them and form them with the 4 corresponding formats. Note the alternate parameter index syntax "1:":

pl 'form "0b%08b  0%1:03o  %1:3d  0x%1:02x"
        for 0..0xff'
pl 'f "0b%08b  0%1:03o  %1:3d  0x%1:02x"
        for 0..0xff'
0b00000000  0000    0  0x00
0b00000001  0001    1  0x01
0b00000010  0002    2  0x02
0b00000011  0003    3  0x03
0b00000100  0004    4  0x04
0b00000101  0005    5  0x05
0b00000110  0006    6  0x06
0b00000111  0007    7  0x07
0b00001000  0010    8  0x08
0b00001001  0011    9  0x09
0b00001010  0012   10  0x0a
0b00001011  0013   11  0x0b
0b00001100  0014   12  0x0c
0b00001101  0015   13  0x0d
0b00001110  0016   14  0x0e
0b00001111  0017   15  0x0f
0b00010000  0020   16  0x10
0b00010001  0021   17  0x11
0b00010010  0022   18  0x12
0b00010011  0023   19  0x13
0b00010100  0024   20  0x14
0b00010101  0025   21  0x15
0b00010110  0026   22  0x16
0b00010111  0027   23  0x17
0b00011000  0030   24  0x18
0b00011001  0031   25  0x19
0b00011010  0032   26  0x1a
0b00011011  0033   27  0x1b
0b00011100  0034   28  0x1c
0b00011101  0035   29  0x1d
0b00011110  0036   30  0x1e
0b00011111  0037   31  0x1f
0b00100000  0040   32  0x20
0b00100001  0041   33  0x21
0b00100010  0042   34  0x22
…
0b11110101  0365  245  0xf5
0b11110110  0366  246  0xf6
0b11110111  0367  247  0xf7
0b11111000  0370  248  0xf8
0b11111001  0371  249  0xf9
0b11111010  0372  250  0xfa
0b11111011  0373  251  0xfb
0b11111100  0374  252  0xfc
0b11111101  0375  253  0xfd
0b11111110  0376  254  0xfe
0b11111111  0377  255  0xff

That makes a rather long table. You can get a better overview with 4 nested tables, looping over a preset @ARGV:

pl -OA '0..0x3f' 'say join "\t",
    map Form( "0b%08b  0%1:03o  %1:3d  0x%1:02x", $ARGV+$_ ), 0, 0x40, 0x80, 0xc0'
pl -OA '0..0x3f' 'say join "\t",
    map F( "0b%08b  0%1:03o  %1:3d  0x%1:02x", $A+$_ ), 0, 0x40, 0x80, 0xc0'
0b00000000  0000    0  0x00	0b01000000  0100   64  0x40	0b10000000  0200  128  0x80	0b11000000  0300  192  0xc0
0b00000001  0001    1  0x01	0b01000001  0101   65  0x41	0b10000001  0201  129  0x81	0b11000001  0301  193  0xc1
0b00000010  0002    2  0x02	0b01000010  0102   66  0x42	0b10000010  0202  130  0x82	0b11000010  0302  194  0xc2
0b00000011  0003    3  0x03	0b01000011  0103   67  0x43	0b10000011  0203  131  0x83	0b11000011  0303  195  0xc3
0b00000100  0004    4  0x04	0b01000100  0104   68  0x44	0b10000100  0204  132  0x84	0b11000100  0304  196  0xc4
0b00000101  0005    5  0x05	0b01000101  0105   69  0x45	0b10000101  0205  133  0x85	0b11000101  0305  197  0xc5
0b00000110  0006    6  0x06	0b01000110  0106   70  0x46	0b10000110  0206  134  0x86	0b11000110  0306  198  0xc6
0b00000111  0007    7  0x07	0b01000111  0107   71  0x47	0b10000111  0207  135  0x87	0b11000111  0307  199  0xc7
0b00001000  0010    8  0x08	0b01001000  0110   72  0x48	0b10001000  0210  136  0x88	0b11001000  0310  200  0xc8
0b00001001  0011    9  0x09	0b01001001  0111   73  0x49	0b10001001  0211  137  0x89	0b11001001  0311  201  0xc9
0b00001010  0012   10  0x0a	0b01001010  0112   74  0x4a	0b10001010  0212  138  0x8a	0b11001010  0312  202  0xca
…
0b00111010  0072   58  0x3a	0b01111010  0172  122  0x7a	0b10111010  0272  186  0xba	0b11111010  0372  250  0xfa
0b00111011  0073   59  0x3b	0b01111011  0173  123  0x7b	0b10111011  0273  187  0xbb	0b11111011  0373  251  0xfb
0b00111100  0074   60  0x3c	0b01111100  0174  124  0x7c	0b10111100  0274  188  0xbc	0b11111100  0374  252  0xfc
0b00111101  0075   61  0x3d	0b01111101  0175  125  0x7d	0b10111101  0275  189  0xbd	0b11111101  0375  253  0xfd
0b00111110  0076   62  0x3e	0b01111110  0176  126  0x7e	0b10111110  0276  190  0xbe	0b11111110  0376  254  0xfe
0b00111111  0077   63  0x3f	0b01111111  0177  127  0x7f	0b10111111  0277  191  0xbf	0b11111111  0377  255  0xff

If you prefer to enumerate sideways, it's easier. @ARGV is in the right order so we can loop in chunks of 4:

pl -O4 -A '0..0xff' 'form join( "\t", map "0b%08b  0%$_:03o  %$_:3d  0x%$_:02x", 1..4 ), @$ARGV'
pl -O4 -A '0..0xff' 'f join( "\t", map "0b%08b  0%$_:03o  %$_:3d  0x%$_:02x", 1..4 ), @$A'
0b00000000  0000    0  0x00	0b00000001  0001    1  0x01	0b00000010  0002    2  0x02	0b00000011  0003    3  0x03
0b00000100  0004    4  0x04	0b00000101  0005    5  0x05	0b00000110  0006    6  0x06	0b00000111  0007    7  0x07
0b00001000  0010    8  0x08	0b00001001  0011    9  0x09	0b00001010  0012   10  0x0a	0b00001011  0013   11  0x0b
…
0b11111000  0370  248  0xf8	0b11111001  0371  249  0xf9	0b11111010  0372  250  0xfa	0b11111011  0373  251  0xfb
0b11111100  0374  252  0xfc	0b11111101  0375  253  0xfd	0b11111110  0376  254  0xfe	0b11111111  0377  255  0xff

Inflation

Inflation is rearing its head. Here's the relative value of 1000 euros, dollars, … after n years, with 1-20% inflation:

pl -MList::MoreUtils=pairwise '@a = (1000) x (@b = map 1 - $_ / 100, 1..20);
    form "  " . ("  " . (" %3d%%" x 5)) x 4, 1..20;
    form "%2d" . ("  " . (" %4d" x 5)) x 4, $_, pairwise { $a *= $b } @a, @b for 1..30'
pl -MList::MoreUtils=pairwise '@a = (1000) x (@b = map 1 - $_ / 100, 1..20);
    f "  " . ("  " . (" %3d%%" x 5)) x 4, 1..20;
    f "%2d" . ("  " . (" %4d" x 5)) x 4, $_, pairwise { $a *= $b } @a, @b for 1..30'
       1%   2%   3%   4%   5%     6%   7%   8%   9%  10%    11%  12%  13%  14%  15%    16%  17%  18%  19%  20%
 1    990  980  970  960  950    940  929  920  910  900    890  880  870  860  850    840  830  820  810  800
 2    980  960  940  921  902    883  864  846  828  810    792  774  756  739  722    705  688  672  656  640
 3    970  941  912  884  857    830  804  778  753  729    704  681  658  636  614    592  571  551  531  512
 4    960  922  885  849  814    780  748  716  685  656    627  599  572  547  522    497  474  452  430  409
 5    950  903  858  815  773    733  695  659  624  590    558  527  498  470  443    418  393  370  348  327
 6    941  885  832  782  735    689  646  606  567  531    496  464  433  404  377    351  326  304  282  262
 7    932  868  807  751  698    648  601  557  516  478    442  408  377  347  320    295  271  249  228  209
 8    922  850  783  721  663    609  559  513  470  430    393  359  328  299  272    247  225  204  185  167
 9    913  833  760  692  630    572  520  472  427  387    350  316  285  257  231    208  186  167  150  134
10    904  817  737  664  598    538  483  434  389  348    311  278  248  221  196    174  155  137  121  107
11    895  800  715  638  568    506  450  399  354  313    277  245  216  190  167    146  128  112   98   85
12    886  784  693  612  540    475  418  367  322  282    246  215  188  163  142    123  106   92   79   68
13    877  769  673  588  513    447  389  338  293  254    219  189  163  140  120    103   88   75   64   54
14    868  753  652  564  487    420  362  311  267  228    195  167  142  121  102     87   73   62   52   43
15    860  738  633  542  463    395  336  286  243  205    174  146  123  104   87     73   61   50   42   35
16    851  723  614  520  440    371  313  263  221  185    154  129  107   89   74     61   50   41   34   28
17    842  709  595  499  418    349  291  242  201  166    137  113   93   76   63     51   42   34   27   22
18    834  695  577  479  397    328  270  222  183  150    122  100   81   66   53     43   34   28   22   18
19    826  681  560  460  377    308  251  205  166  135    109   88   70   56   45     36   29   23   18   14
20    817  667  543  442  358    290  234  188  151  121     97   77   61   48   38     30   24   18   14   11
21    809  654  527  424  340    272  217  173  137  109     86   68   53   42   32     25   19   15   11    9
22    801  641  511  407  323    256  202  159  125   98     77   60   46   36   28     21   16   12    9    7
23    793  628  496  391  307    240  188  146  114   88     68   52   40   31   23     18   13   10    7    5
24    785  615  481  375  291    226  175  135  103   79     61   46   35   26   20     15   11    8    6    4
25    777  603  466  360  277    212  162  124   94   71     54   40   30   23   17     12    9    7    5    3
26    770  591  452  345  263    200  151  114   86   64     48   36   26   19   14     10    7    5    4    3
27    762  579  439  332  250    188  140  105   78   58     43   31   23   17   12      9    6    4    3    2
28    754  567  426  318  237    176  131   96   71   52     38   27   20   14   10      7    5    3    2    1
29    747  556  413  306  225    166  121   89   64   47     34   24   17   12    8      6    4    3    2    1
30    739  545  401  293  214    156  113   81   59   42     30   21   15   10    7      5    3    2    1    1

ISO replaced 8 standards by one. Now we have 9 standards. 😢

ISO Paper Sizes

Since @ARGV is initially empty, -A isn't looping. We can do other things as a side effect, rather than in a separate -B. Use Perl's lovely list assignment to swap and alternately halve the numbers. Because halving happens before echoing, start with double size:

pl -oA '($w, $h) = (1189, 1682); 0..10' \
    'form "A%-2d  %4dmm x %4dmm", $_, ($w, $h) = ($h / 2, $w)'
pl -oA '($w, $h) = (1189, 1682); 0..10' \
    'f "A%-2d  %4dmm x %4dmm", $_, ($w, $h) = ($h / 2, $w)'
A0    841mm x 1189mm
A1    594mm x  841mm
A2    420mm x  594mm
A3    297mm x  420mm
A4    210mm x  297mm
A5    148mm x  210mm
A6    105mm x  148mm
A7     74mm x  105mm
A8     52mm x   74mm
A9     37mm x   52mm
A10    26mm x   37mm

You could widen the table to cover B- & C-formats, by extending each list of 2, to a corresponding list of 6, e.g., ($Aw, $Ah, $Bw, …). But a more algorithmic approach seems better. Triple the format (with cheat spaces at the beginning). The main program loops over @ARGV, thanks to -O, doing the same as above but on anonymous elements of @d, which -A sets as a side effect:

pl -OA '@d = (["A", 1189, 1682], ["B", 1414, 2000], ["C", 1297, 1834]); 0..10' \
    'form "  %3s  %4dmm x %4dmm" x 3,
        map +("$$_[0]$ARGV", ($$_[1], $$_[2]) = ($$_[2] / 2, $$_[1])), @d'
pl -OA '@d = (["A", 1189, 1682], ["B", 1414, 2000], ["C", 1297, 1834]); 0..10' \
    'f "  %3s  %4dmm x %4dmm" x 3,
        map +("$$_[0]$A", ($$_[1], $$_[2]) = ($$_[2] / 2, $$_[1])), @d'
   A0   841mm x 1189mm   B0  1000mm x 1414mm   C0   917mm x 1297mm
   A1   594mm x  841mm   B1   707mm x 1000mm   C1   648mm x  917mm
   A2   420mm x  594mm   B2   500mm x  707mm   C2   458mm x  648mm
   A3   297mm x  420mm   B3   353mm x  500mm   C3   324mm x  458mm
   A4   210mm x  297mm   B4   250mm x  353mm   C4   229mm x  324mm
   A5   148mm x  210mm   B5   176mm x  250mm   C5   162mm x  229mm
   A6   105mm x  148mm   B6   125mm x  176mm   C6   114mm x  162mm
   A7    74mm x  105mm   B7    88mm x  125mm   C7    81mm x  114mm
   A8    52mm x   74mm   B8    62mm x   88mm   C8    57mm x   81mm
   A9    37mm x   52mm   B9    44mm x   62mm   C9    40mm x   57mm
  A10    26mm x   37mm  B10    31mm x   44mm  C10    28mm x   40mm

What color is a mirror? It depends whom you ask. 😉

ANSI Background;Foreground Color Table

You get numbers to fill into "\e[BGm", "\e[FGm", or "\e[BG;FGm" to get a color and close it with "\e[m". This shows two times twice 8 different colors for dim & bright and for background & foreground. Hence the multiplication of escape codes and of values to fill them.

This fills @ARGV in -A twice, the 2nd time looping over the 1st list, as though it came from the command line. It maps it to the 16-fold number format to print the header, swallowing every other number with 0-width. Then the main program loops over it pairwise with $ARGV, thanks to -o2, to print the body. It duplicates numbers with (N)x2, once to go into the escape sequence, once to display them:

pl -o2A 1..8 -A '$_, $_+39, $_+8, $_+99' -B 'form "co:   bg;fg"."%4d%.0s"x16, @ARGV;
    $b = Form "\e[%dm%3d "x16, map +(($_)x2, ($_+60)x2), 30..37' \
    'form "%2d: %4d;   \e[%2:dm$b\e[m", @$_'
pl -o2A 1..8 -A '$_, $_+39, $_+8, $_+99' -B 'f "co:   bg;fg"."%4d%.0s"x16, @A;
    $b = F "\e[%dm%3d "x16, map +(($_)x2, ($_+60)x2), 30..37' \
    'f "%2d: %4d;   \e[%2:dm$b\e[m", @$_'
co:   bg;fg   1   9   2  10   3  11   4  12   5  13   6  14   7  15   8  16
 1:   40;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 9:  100;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 2:   41;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
10:  101;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 3:   42;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
11:  102;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 4:   43;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
12:  103;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 5:   44;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
13:  104;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 6:   45;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
14:  105;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 7:   46;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
15:  106;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
 8:   47;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 
16:  107;    30  90  31  91  32  92  33  93  34  94  35  95  36  96  37  97 

Terminal Rulers

If you need a ruler to better count the width of some other output, you can print out one of the following. These are either decimal, by groups of 5 or hex by groups of 4 and 8. The latter two do the same. But instead of one static ruler of length 100 or 256, they repeat and adapt. Depending on your terminal emulator and Shell, the variable $COLUMNS may track the current width. If so, pass it in a single argument to "loop" over with -o, else provide the desired width yourself:

pl 'say map "$_...:.....", 0..9'
pl 'say map "$_..:...|...:....", 0..9, "A".."F"'
pl -o 'say substr join( "", map "$_...:.....", 0..9 ) x ($_ / 100 + 1), 0, $_' $COLUMNS
pl -o 'say substr join( "", map "$_..:...|...:....", 0..9, "A".."F" ) x ($_ / 256 + 1), 0, $_' $COLUMNS
0...:.....1...:.....2...:.....3...:.....4...:.....5...:.....6...:.....7...:.....8...:.....9...:.....
0..:...|...:....1..:...|...:....2..:...|...:....3..:...|...:....4..:...|...:....5..:...|...:....6..:...|...:....7..:...|...:....8..:...|...:....9..:...|...:....A..:...|...:....B..:...|...:....C..:...|...:....D..:...|...:....E..:...|...:....F..:...|...:....
0...:.....1...:.....2...:.....3...:.....4...:.....5...:.....6...:.....7...:.....
0..:...|...:....1..:...|...:....2..:...|...:....3..:...|...:....4..:...|...:....

Math

Minimum and Maximum

The List::Util functions min and max are imported for you:

pl 'echo max 1..5'
pl 'e max 1..5'
5

If you have just several numbers on each line and want their minimums, you can autosplit (-a) to @FIELD:

pl -a 'echo min @FIELD' file*
pl -a 'e min @FIELD' file*

If on the same you just want the overall minimum, you can use the print-at-end variable $RESULT, which you initialise to infinity in a -B begin program:

pl -aB '$RESULT = "inf"' '$RESULT = min $RESULT, @FIELD' file*
pl -aB '$R = "inf"' '$R = min $R, @FIELD' file*

Likewise, for overall maximum, you start with negative infinity:

pl -aB '$RESULT = "-inf"' '$RESULT = max $RESULT, @FIELD' file*
pl -aB '$R = "-inf"' '$R = max $R, @FIELD' file*

Median, Quartiles, Percentiles

The median is the number where half the list is less, and half is greater. Similarly, the 1st quartile is where 25% are less and the 3rd where 25% are greater. Use a list slice to extract these 3 and a 97th percentile, by multiplying the fractional percentage with the list length:

pl -A 0..200 'echo @ARGV[map $_*@ARGV, .25, .5, .75, .97]'
pl -A 0..200 'e @A[map $_*@A, .25, .5, .75, .97]'
50 100 150 194

If you'd rather have names associated, assign them to hash slice in sort-numerically-at-end %NUMBER, whose key order must match the percentages. Note how <…> without wildcard characters is like qw(…):

pl -A 0..200 '@NUMBER{<lower median upper 97%>} = @ARGV[map $_*@ARGV, .25, .5, .75, .97]'
pl -A 0..200 '@N{<lower median upper 97%>} = @A[map $_*@A, .25, .5, .75, .97]'
      50: lower
     100: median
     150: upper
     194: 97%

80% of people consider themselves to be above average. 😂

Triangular Number, Factorial, and Averages

The triangular number is defined as the sum of all numbers from 1 to n, e.g., 1 to 5. Factorial is the equivalent for products:

pl 'echo sum 1..5;
    echo product 1..5'
pl 'e sum 1..5;
    e product 1..5'
15
120

The sum of all list elements divided by the length of the list gives the linear average. Alternately the root mean square can be a fairer average because it accounts for the weights:

pl -A 11..200 'echo sum( @ARGV ) / @ARGV;
     echo sqrt sum( map $_ ** 2, @ARGV ) / @ARGV'
pl -A 11..200 'e sum( @A ) / @A;
     e sqrt sum( map $_ ** 2, @A ) / @A'
105.5
118.905424602917

Add Pairs or Tuples of Numbers

If you have a list of number pairs and want to add each 1st and each 2nd number, reduce is your friend. Inside it map over the pair elements 0..1:

pl 'echo reduce {
        [map $a->[$_] + $b->[$_], 0..1]
    } [1, 11], [2, 12], [3, 13]'
pl 'e reduce {
        [map $a->[$_] + $b->[$_], 0..1]
    } [1, 11], [2, 12], [3, 13]'
[
  6,
  36
]

If your list is a variable and is empty the result is undef. You can insert a fallback zero element if you'd rather receive that for an empty list:

pl 'echo reduce {
        [map $a->[$_] + $b->[$_], 0..1]
    } [0, 0], @list'
pl 'e reduce {
        [map $a->[$_] + $b->[$_], 0..1]
    } [0, 0], @list'
[
  0,
  0
]

The above adds pairs because we iterate 0..1. You can generalize this to tuples by iterating to the length of the 1st array:

pl 'echo reduce {
        [map $a->[$_] + $b->[$_], 0..$#$a]
    } [1, 11, 21], [2, 12, 22], [3, 13, 23]'
pl 'e reduce {
        [map $a->[$_] + $b->[$_], 0..$#$a]
    } [1, 11, 21], [2, 12, 22], [3, 13, 23]'
[
  6,
  36,
  66
]

2 + 2 = 5 for extremely large values of 2. 😂

Big Math

With the bigint, bignum and bigrat modules you can do arbitrary precision and semi-symbolic fractional math:

pl -Mbigint 'echo 123456789012345678901234567890 * 123456789012345678901234567890'
pl -Mbigint 'e 123456789012345678901234567890 * 123456789012345678901234567890'
15241578753238836750495351562536198787501905199875019052100
pl -Mbignum 'echo 1.23456789012345678901234567890 * 1.23456789012345678901234567890'
pl -Mbignum 'e 1.23456789012345678901234567890 * 1.23456789012345678901234567890'
1.52415787532388367504953515625361987875019051998750190521
pl -Mbigrat 'echo 1/23456789012345678901234567890 * 1/23456789012345678901234567890'
pl -Mbigrat 'e 1/23456789012345678901234567890 * 1/23456789012345678901234567890'
1/550220950769700970248437984536198787501905199875019052100

Primes

This calculates all primes in a given range, e.g., 2 to 99:

pl 'echo grep {
        $a = $_;
        all { $a % $_ } 2..$_/2;
    } 2..99'
pl 'e grep {
        $a = $_;
        all { $a % $_ } 2..$_/2;
    } 2..99'
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

Fibonacci Numbers

These are, seeded with 1 and 1, all sums of the previous two. Show all up to a given maximum:

pl '$a = $_ = 1; echo;
    while( $_ < $ARGV[0] ) {
        echo; ($a, $_) = ($_, $a + $_)
    }' 50000
pl '$a = $_ = 1; e;
    while( $_ < $A[0] ) {
        e; ($a, $_) = ($_, $a + $_)
    }' 50000
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368

What's 3n plus n?  Hmm, sounds 4n to me.   🙃

Collatz 3n + 1

Collatz' unproven & unrefuted conjecture is that the function f(n) = if odd: 3n + 1 else n/2 will always lead to 1, when applied repeatedly to any natural number n. Since if odd: 3n + 1 is even, we can skip it and go straight to the postsuccessor (3n + 1)/2 = (2n + n-1 + 2)/2 = n + int(n/2) + 1. Loop with -o over remaining args in $_. Having simplified the factor 3, inline the function as more efficient bit-ops:

pl -o 'echo "---" if $ARGIND; echo;
    while( $_ > 1 ) {
        if( $_ & 1 ) { ++($_ += $_ >> 1) } else { $_ >>= 1 }
        echo;
    }' 23 27
pl -o 'e "---" if $I; e;
    while( $_ > 1 ) {
        if( $_ & 1 ) { ++($_ += $_ >> 1) } else { $_ >>= 1 }
        e;
    }' 23 27
23
35
53
80
40
20
10
5
8
4
2
1
---
27
41
62
31
47
71
107
161
242
121
182
91
137
206
103
155
233
350
175
263
395
593
890
445
668
334
167
251
377
566
283
425
638
319
479
719
1079
1619
2429
3644
1822
911
1367
2051
3077
4616
2308
1154
577
866
433
650
325
488
244
122
61
92
46
23
35
53
80
40
20
10
5
8
4
2
1

Add some fancy indent to show the turns, starting far enough right to let the snakes wiggle in both directions:

pl -o 'echo "---" if $ARGIND;
    $i = 40;
    while( 1 ) {
        form "%*d", $i, $_;
    last if $_ < 2;
        if( $_ & 1 ) { ++$i; ++($_ += $_ >> 1) } else { --$i; $_ >>= 1 }
    }' 15 48 49
pl -o 'e "---" if $I;
    $i = 40;
    while( 1 ) {
        f "%*d", $i, $_;
    last if $_ < 2;
        if( $_ & 1 ) { ++$i; ++($_ += $_ >> 1) } else { --$i; $_ >>= 1 }
    }' 15 48 49
                                      15
                                       23
                                        35
                                         53
                                          80
                                         40
                                        20
                                       10
                                       5
                                        8
                                       4
                                      2
                                     1
---
                                      48
                                     24
                                    12
                                    6
                                   3
                                    5
                                     8
                                    4
                                   2
                                  1
---
                                      49
                                       74
                                      37
                                       56
                                      28
                                     14
                                     7
                                     11
                                      17
                                       26
                                      13
                                       20
                                      10
                                      5
                                       8
                                      4
                                     2
                                    1

Powers of 2 only get halved, so check only as long as the result has more than one bit on (n & (n - 1)). Furthermore, this tricky problem alternates between the binary & ternary realms which never meet, as no power of three is even. At least we can show both bases, their square and product bases, as well as, out of habit, decimal. For bases use bigint as of Perl 5.30. That would also allow astronomical numbers . Sadly, form can't go beyond 64 bits, so this is also how you'd get around that. Also add direction indicators:

pl -OMbigint -B 'form $f = "%s %26s %16s %14s %12s %9s %8s", " ", @b = <2 3 4 6 9 10>' '
    $ARGV += 0; $b = 2; echo "---" if $ARGIND;
    while( 1 ) {
        form $f, qw(\\ / +)[$b], map $ARGV->to_base($_), @b;
    last unless $ARGV & ($ARGV - 1);
        if( $b = $ARGV & 1 ) { ++($ARGV += $ARGV >> 1) } else { $ARGV >>= 1 }
    }' 255 511
pl -OMbigint -B 'f $f = "%s %26s %16s %14s %12s %9s %8s", " ", @b = <2 3 4 6 9 10>' '
    $A += 0; $b = 2; e "---" if $I;
    while( 1 ) {
        f $f, qw(\\ / +)[$b], map $A->to_base($_), @b;
    last unless $A & ($A - 1);
        if( $b = $A & 1 ) { ++($A += $A >> 1) } else { $A >>= 1 }
    }' 255 511
                           2                3              4            6         9       10
+                   11111111           100110           3333         1103       313      255
/                  101111111           112012          11333         1435       465      383
/                 1000111111           210022          20333         2355       708      575
/                 1101011111          1011222          31133         3555      1158      863
/                10100001111          1202222         110033         5555      1688     1295
/                11110010111          2122222         132113        12555      2588     1943
/               101101100011         10222222         231203        21255      3888     2915
/              1000100010101         12222222        1010111        32125      5888     4373
/              1100110100000         22222222        1212200        50212      8888     6560
\               110011010000         11111111         303100        23104      4444     3280
\                11001101000          2020202         121220        11332      2222     1640
\                 1100110100          1010101          30310         3444      1111      820
\                  110011010           120012          12122         1522       505      410
\                   11001101            21121           3031          541       247      205
/                  100110100           102102          10310         1232       372      308
\                   10011010            12201           2122          414       181      154
\                    1001101             2212           1031          205        85       77
/                    1110100            11022           1310          312       138      116
\                     111010             2011            322          134        64       58
\                      11101             1002            131           45        32       29
/                     101100             1122            230          112        48       44
\                      10110              211            112           34        24       22
\                       1011              102             23           15        12       11
/                      10001              122            101           25        18       17
/                      11010              222            122           42        28       26
\                       1101              111             31           21        14       13
/                      10100              202            110           32        22       20
\                       1010              101             22           14        11       10
\                        101               12             11            5         5        5
/                       1000               22             20           12         8        8
---
+                  111111111           200221          13333         2211       627      511
/                 1011111111          1001102          23333         3315      1042      767
/                10001111111          1120122         101333         5155      1518     1151
/                11010111111          2100222         122333        11555      2328     1727
/               101000011111         10112222         220133        15555      3488     2591
/               111100101111         12022222         330233        25555      5288     3887
/              1011011000111         21222222        1123013        42555      7888     5831
/             10001000101011        102222222        2020223       104255     12888     8747
/             11001101000001        122222222        3031001       140425     18888    13121
/            100110011100010        222222222       10303202       231042     28888    19682
\             10011001110001        111111111        2121301       113321     14444     9841
/             11100110101010        202020202        3212222       152202     22222    14762
\              1110011010101        101010101        1303111        54101     11111     7381
/             10101101000000        120012002        2231000       123132     16162    11072
\              1010110100000         21121001        1112200        41344      7531     5536
\               101011010000         10210112         223100        20452      3715     2768
\                10101101000          1220021         111220        10224      1807     1384
\                 1010110100           221122          22310         3112       848      692
\                  101011010           110211          11122         1334       424      346
\                   10101101            20102           2231          445       212      173
/                  100000100           100122          10010         1112       318      260
\                   10000010            11211           2002          334       154      130
\                    1000001             2102           1001          145        72       65
/                    1100010            10122           1202          242       118       98
\                     110001             1211            301          121        54       49
/                    1001010             2202           1022          202        82       74
\                     100101             1101            211          101        41       37
/                     111000             2002            320          132        62       56
\                      11100             1001            130           44        31       28
\                       1110              112             32           22        15       14
\                        111               21             13           11         7        7
/                       1011              102             23           15        12       11
/                      10001              122            101           25        18       17
/                      11010              222            122           42        28       26
\                       1101              111             31           21        14       13
/                      10100              202            110           32        22       20
\                       1010              101             22           14        11       10
\                        101               12             11            5         5        5
/                       1000               22             20           12         8        8

This lead to exciting findings here.

Separate Big Numbers with Commas, …

Loop and print with line-end (-opl) over remaining args in $_. If reading from stdin or files, instead of arguments, use only -pl. After a decimal dot, insert a comma before each 4th commaless digit. Then do the same backwards from end or decimal dot, also for Perl style with underscores:

n='1234567 12345678 123456789 1234.5678 3.141 3.14159265358'
pl -opl '1 while s/[,.]\d{3}\K(?=\d)/,/;
    1 while s/\d\K(?=\d{3}(?:$|[.,]))/,/' $n
pl -opl '1 while s/[._]\d{3}\K(?=\d)/_/;
    1 while s/\d\K(?=\d{3}(?:$|[._]))/_/' $n
1,234,567
12,345,678
123,456,789
1,234.567,8
3.141
3.141,592,653,58
1_234_567
12_345_678
123_456_789
1_234.567_8
3.141
3.141_592_653_58

The same for languages with a decimal comma, using either a dot or a space as spacer:

n='1234567 12345678 123456789 1234,5678 3,141 3,141592653589'
pl -opl '1 while s/[,.]\d{3}\K(?=\d)/./;
    1 while s/\d\K(?=\d{3}(?:$|[.,]))/./' $n
pl -opl '1 while s/[, ]\d{3}\K(?=\d)/ /;
    1 while s/\d\K(?=\d{3}(?:$|[ ,]))/ /' $n
1.234.567
12.345.678
123.456.789
1.234,567.8
3,141
3,141.592.653.589
1 234 567
12 345 678
123 456 789
1 234,567 8
3,141
3,141 592 653 589

Web

HTML Encoding

Sometimes you need to en- or decode HTML entities, e.g. for HTML displayed in a <pre>. Only the 1st 2 characters are essential, but often these & < > " ' get encoded:

echo "<p class=\"example\">Planet pl's plucky &amp; pleasant plasticity</p>" |
    pl -pB '%ent = split /\b/, "&amp<lt>gt\"quot${q}apos";
        $ent = join "", keys %ent' \
        's/([$ent])/&$ent{$1};/go'
echo "&lt;p class=&quot;example&#x22;&#62;Planet pl&apos;s plucky &amp;amp; pleasant plasticity&lt;/p&gt;" |
    pl -pB '%ent = split /\b/, "amp&lt<gt>quot\"apos$q";
        $ent = join "|", keys %ent' \
        's/&(?:#x([0-9a-f]+)|#([0-9]+)|($ent));/$3 ? $ent{$3} : chr( $1 ? hex $1 : $2 )/egio'
&lt;p class=&quot;example&quot;&gt;Planet pl&apos;s plucky &amp;amp; pleasant plasticity&lt;/p&gt;
<p class="example">Planet pl's plucky &amp; pleasant plasticity</p>

URL Encoding

URL (or URI, or percent) encoding is used to embed arbitrary data, like another URL, into a URL. This makes it hard to read. So, let's decode it, also treating the non-standard, but widespread '+':

echo https%3a%2f%2frt%2ecpan%2eorg%2FDist%2FDisplay%2ehtml%3FStatus%3D%5F%5FActive%5F%5F%3BQueue%3DApp-pl |
    pl -p 'tr/+/ /;
        s/%([0-9a-f]{2})/chr hex $1/egi'
https://rt.cpan.org/Dist/Display.html?Status=__Active__;Queue=App-pl

Many characters pose no problem and are often left unencoded. But let's encode according to rfc3986, whereby only a very limited set of characters is exempt. Here -l matters, otherwise the newline would also get encoded:

echo 'https://rt.cpan.org/Dist/Display.html?Status=__Active__;Queue=App-pl' |
    pl -pl 's/[^0-9a-z~_.-]/Form "%%%02X", ord $&/egi'
echo 'https://rt.cpan.org/Dist/Display.html?Status=__Active__;Queue=App-pl' |
    pl -pl 's/[^0-9a-z~_.-]/F "%%%02X", ord $&/egi'
https%3A%2F%2Frt.cpan.org%2FDist%2FDisplay.html%3FStatus%3D__Active__%3BQueue%3DApp-pl

What do you call a sheep with no legs?  A cloud.   🐑

DNS Lookup

The hosts function deals with the nerdy details and outputs as a hosts file. This canonically sorts the file by address type (localhost, link local, private, public), version (IPv4, IPv6) and address. You tack on any number of IP-addresses or hostnames, either as Perl arguments or on the command-line via @ARGV:

pl 'hosts qw(perl.org 127.0.0.1 perldoc.perl.org cpan.org)'
pl 'h qw(perl.org 127.0.0.1 perldoc.perl.org cpan.org)'
127.0.0.1 localhost
139.178.67.96 perl.org
146.75.118.132 perldoc.perl.org
151.101.1.55 cpan.org
151.101.65.55 cpan.org
151.101.129.55 cpan.org
151.101.193.55 cpan.org
2a04:4e42:8d::644 perldoc.perl.org
pl 'hosts @ARGV' perl.org 127.0.0.1 perldoc.perl.org cpan.org
pl 'h @A' perl.org 127.0.0.1 perldoc.perl.org cpan.org

If you don't want it to merge & sort, call hosts for individual addresses:

pl 'hosts for qw(perl.org 127.0.0.1 perldoc.perl.org cpan.org)'
pl 'h for qw(perl.org 127.0.0.1 perldoc.perl.org cpan.org)'
139.178.67.96 perl.org
127.0.0.1 localhost
146.75.118.132 perldoc.perl.org
2a04:4e42:8d::644 perldoc.perl.org
151.101.1.55 cpan.org
151.101.65.55 cpan.org
151.101.129.55 cpan.org
151.101.193.55 cpan.org
pl -o hosts perl.org 127.0.0.1 perldoc.perl.org cpan.org
pl -o h perl.org 127.0.0.1 perldoc.perl.org cpan.org

If your input comes from files, collect it in a list and perform at end (-E):

pl -lnE 'hosts @list' 'push @list, $_' file*
pl -lnE 'h @list' 'push @list, $_' file*

Miscellaneous

There are various other examples here.

Renumber Shell Parameters

If you want to insert another parameter before $2, you have to renumber $2 - $8 respectively to $3 - $9. The same applies to Perl regexp match variables. This matches and replaces them, including optional braces. Apply in your editor to the corresponding region:

echo 'random Shell stuff with $1 - $2 - x${3}yz' |
    pl -p 's/\$\{?\K([2-8])\b/$1 + 1/eg'
random Shell stuff with $1 - $3 - x${4}yz

System Errors

Assign Unix error numbers to Perl's magic variable $!, which in string context gives the corresponding message. Instead of explicit individual numbers, you could fill @ARGV programmatically with -A 1..133 (the biggest errno on my system):

pl -o 'form "%3d %1:s", $! = $_' 1 14 64 111
pl -o 'f "%3d %1:s", $! = $_' 1 14 64 111
  1 Operation not permitted
 14 Bad address
 64 Machine is not on the network
111 Connection refused

Find Palindromes

This assumes a dictionary on your machine. It loops over the file printing each match -P. It eliminates trivia like I, mom & dad with a minimum length:

pl -Pl 'length > 3 and $_ eq reverse' /usr/share/dict/words
boob
civic
deed
deified
kayak
kook
level
ma'am
madam
minim
noon
peep
poop
radar
redder
refer
rotor
sagas
sees
sexes
shahs
solos
stats
tenet
toot

Lottery: a tax on people who are bad at math. 😂

Generate a Random UUID

This gives a hex number with the characteristic pattern of dashes. The hex format takes only the integral parts of the random numbers. If you need to further process the UUID, you can retrieve it instead of echoing, by giving a scalar context, e.g., $x = form …:

pl '$x = "%04x";
    form "$x$x-$x-$x-$x-$x$x$x", map rand 0x10000, 0..7'
pl '$x = "%04x";
    f "$x$x-$x-$x-$x-$x$x$x", map rand 0x10000, 0..7'
9ed6347a-f52d-74eb-6f7f-609ea30ed42a

To be RFC 4122 conformant, the 4 version & 2 variant bits need to have standard values. As a different approach, -o "loops" over the one parameter in $_. This transforms the template into a format:

pl -o '@u = map rand 0x10000, 0..7;
    ($u[3] /= 16) |= 0x4000;
    ($u[4] /= 4) |= 0x8000;
    form s/x/%04x/gr, @u' xx-x-x-x-xxx
pl -o '@u = map rand 0x10000, 0..7;
    ($u[3] /= 16) |= 0x4000;
    ($u[4] /= 4) |= 0x8000;
    f s/x/%04x/gr, @u' xx-x-x-x-xxx
be9957bb-1c70-46c1-8530-db3f7fb17a93

Why should you trust atoms? They make up everything. 😂

Generate a Random Password

Use say, which doesn't put spaces between its arguments. Generate 15 random characters from 33 to 126, i.e. printable ASCII characters. Note that rand 94 will always be less than 94 and chr will round it down:

pl 'say map chr(33 + rand 94), 1..15'
m'<|:FVS_u}&-0r

If you can't convince 'em, confuse 'em! 😉

Just another pl hacker,

Just another Perl hacker, adapted. This obfuscated mock turtle soup JAPH is left for you to figure out. You may wonder y "y", y things start or end in "y":

pl -ploiy y' ya-zy You, Turtleneck phrase Jar. Yoda? Yes! 'y yhvjfumcjslifrkfsuoplie
Just another pl hacker,

If canned goods expire, are they canned bads? 🙃

CANNED COMMANDS

It's nice to have so many examples, but copying some of the more frequently used ones in time of need would be cumbersome. Luckily there are better ways.

Let's explore two ways to make them more accessible from the Shell. You can put these in one of your Shell startup files like .bashrc to have them permanently available. The first, more compact but also limited, way is with aliases. The second, more flexible and powerful, way is with functions. However, depending on how flexible you want them to be, they require more or less Shell programming know how.

That smart pig's alias is Mr. Cunning Ham. 🐽

Shell Aliases

Right in the spirit of one-liners are Shell aliases. These have drawbacks:

  • You will usually need to quote them. If you pass in Perl code containing dollar signs, you should single quote them to protect them from the Shell when running and yet again to protect them from the Shell when defining the alias.

  • Their value literally replaces that one word at the beginning of a command. You can add arguments only at the end.

  • Your IDE might not give you syntax highlighting for the value.

Either you can alias just initial options you use frequently. In that case you'd give some Perl code when invoking the alias, e.g., here to process the output of find:

alias pl0='pl -nl0'
find -type f -print0 |
    pl0 '...'

Or you alias a full command, in which case the Shell passes only the run-time arguments in @ARGV. Since there are no dollars it doesn't matter which quote type you embed within the other:

alias hosts="pl 'hosts @ARGV'"
alias h="pl 'h @A'"

Here's an unnecessarily complex example where both the inner and outer strings need single quoting. We clumsily achieve that by leaving the outer single quoted string, appending a backslashed single quote and reentering the single quote again. Possible but a function would have been more readable here:

alias whereis='pl '\''$p = $ENV{PATH} =~ s!/*:!,!gr;
    @x = grep { -f && -x } map <{$p}/$_>, @ARGV;
    exec ls => -fl => @x if @x'\'
alias whereis='pl '\''$p = $ENV{PATH} =~ s!/*:!,!gr;
    @x = grep { -f && -x } map <{$p}/$_>, @A;
    exec ls => -fl => @x if @x'\'

Here are a few of the key-based diff examples. The 3rd name is unfortunate, as CSV commonly means "comma separated values," so "c" for "colon" was taken. That name might not work in all Shells, in which case you could use diffCsv:

alias diffcsv="pl -nB 'echo for @ARGV' 'k if s/(.+?),//'"
alias difftsv="pl -nB 'echo for @ARGV' 'k if s/(.+?)\t//'"
alias diff:sv="pl -nB 'echo for @ARGV' 'k if s/^([^#].*?)://'"
alias diffelf='pl -OB "echo for @ARGV" '\''p { k $2 if s/^\t((.+)\.so.* => .*) \(\w+\)/$1/ } ldd => $ARGV'\'
alias diffcsv="pl -nB 'e for @A' 'k if s/(.+?),//'"
alias difftsv="pl -nB 'e for @A' 'k if s/(.+?)\t//'"
alias diff:sv="pl -nB 'e for @A' 'k if s/^([^#].*?)://'"
alias diffelf='pl -OB "e for @A" '\''p { k $2 if s/^\t((.+)\.so.* => .*) \(\w+\)/$1/ } ldd => $A'\'

We can import some of the pl functionality directly to the Shell. My favourite is the smart echo, only by short name to avoid collision. Shell meta-characters like * need quoting. I chose to treat multiple arguments as though they were one, therefore needing a comma if I have more than one expression. Since the expression needs eval I can even have multiple statements, the last one being the list that echo sees:

alias e="pl 'e eval qq{@A}; die \$@ if \$@'"
e 10 / 3, '7 * 3'
e '$x = 5; 10 / $x, $x * 2'
3.33333333333333 21
2 10

Template has a prototype, for the 1st argument to default to $_. This is how to still pass all alias arguments as a list:

alias template="pl '&template( @ARGV )'"
template "Don't {{verb}} the {{noun}}, make the {{noun}} {{verb}}!" verb count noun days
alias t="pl '&t( @A )'"
t "Don't {{verb}} the {{noun}}, make the {{noun}} {{verb}}!" verb count noun days
Don't count the days, make the days count!

On the Shell, you may be more interested in reading from a file, whose name shall get passed backslashed as a ref (multiply, because of "):

alias Template="pl -B '\$file = shift' '&Template( \\\$file, @ARGV )'"
Template filename key value ...
alias T="pl -B '\$file = shift' '&T( \\\$file, @A )'"
T filename key value ...

I can't function without my glasses. Especially when they're empty! 🤓

Shell Functions

Functions (here using the classical syntax understood by various Shells) directly contain normal Shell code. They avoid the double quoting problem of aliases. In the simplest case, you just wrap any pl command like this, with a special sigil meaning pass the function's parameters to pl here:

somename() {
    pl ... "$@"
}

You can jazz up some more key-based diff examples as functions. That way we can have options, like -d for "eliminate date" or for zip -x for "extract hex part of filename."

diffzip() {
    local prg='if s/ [0-9a-f]{8}\K  (.+)//'
    if [ "x$1" = x-x ]; then        # randomized hex web resource
        shift
        prg='$n if s/.{16} ([0-9a-f]{8})  (.+?)(?:\.([0-9a-f]{20})(\..[a-z]+))?$/if( $3 ) { $n = "$2.\$x$4"; "$1  \$x=$3" } else { $n = $2; $1 }/e'
    elif [ "x$1" = x-d ]; then      # dateless
        shift
        prg='$2 if s/.{16} ([0-9a-f]{8})  (.+)/$1/'
    fi
    pl -oB 'echo for @ARGV' 'piped { keydiff '"$prg"' } "unzip", "-vqq", $_' "$@"
}
difftar() {
    local prg='s/^\S+ \K(.+?) +(\d+) (.{16}) (.+)/sprintf "%-20s %10d %s", $1, $2, $3/e; keydiff $4'
    if [ "x$1" = x-d ]; then      # dateless
        shift
        prg='s/^\S+ \K(.+?) +(\d+) .{16} (.+)/sprintf "%-20s %10d", $1, $2/e; keydiff $3'
    fi
    pl -oB 'echo for @ARGV' 'piped { '"$prg"' } "tar", "-tvf", $_' "$@"
}
diffzip() {
    local prg='if s/ [0-9a-f]{8}\K  (.+)//'
    if [ "x$1" = x-x ]; then        # randomized hex web resource
        shift
        prg='$n if s/.{16} ([0-9a-f]{8})  (.+?)(?:\.([0-9a-f]{20})(\..[a-z]+))?$/if( $3 ) { $n = "$2.\$x$4"; "$1  \$x=$3" } else { $n = $2; $1 }/e'
    elif [ "x$1" = x-d ]; then      # dateless
        shift
        prg='$2 if s/.{16} ([0-9a-f]{8})  (.+)/$1/'
    fi
    pl -oB 'e for @A' 'p { k '"$prg"' } "unzip", "-vqq", $_' "$@"
}
difftar() {
    local prg='s/^\S+ \K(.+?) +(\d+) (.{16}) (.+)/sprintf "%-20s %10d %s", $1, $2, $3/e; k $4'
    if [ "x$1" = x-d ]; then      # dateless
        shift
        prg='s/^\S+ \K(.+?) +(\d+) .{16} (.+)/sprintf "%-20s %10d", $1, $2/e; k $3'
    fi
    pl -oB 'e for @A' 'p { '"$prg"' } "tar", "-tvf", $_' "$@"
}

COMPATIBILITY

Even if it's rare nowadays, you can still find Perl 5.16 out in the wild (e.g., in RHEL 7). Pl accommodates it gracefully, falling back to what works. It has shims for any, all, none, notall, product & sum0. (Some Unices maintain even older Perl versions, e.g., AIX or Solaris: you can go back till Perl 5.10 with pl 0.63.2.)

Minor Differences with perl -E

Known minor differences are:

  • by design in an -n loop last is per file instead of behaving like exit

  • don't goto LINE, but next LINE is fine

  • using pop, etc. to implicitly modify @ARGV works in -B begin code but not in your main program (which gets compiled to a function)

  • shenanigans with unbalanced braces won't work

Work Is Never Done On Windows Systems 😉

Windows Notes

Do yourself a favour and get a real Shell, e.g., from Cygwin, git, MinGW, MSYS, or WSL! If you can't avoid command.com or cmd.exe, you will have to first convert all inner quotes to qq or \". Then convert the outer single quotes to double quotes:

pl "echo qq{${quote}Perl$quote}, \"$Quote@ARGV$Quote\"" one-liner
pl "e qq{${q}Perl$q}, \"$Q@A$Q\"" one-liner
Perl one-liner

PowerShell is weirder. (Did I mention you'd be better off with a real Shell?) You must use outer single quotes, but you still need to protect double quotes:

pl 'echo qq{${quote}Perl$quote}, \"$Quote@ARGV$Quote\"' one-liner
pl 'e qq{${q}Perl$q}, \"$Q@A$Q\"' one-liner
Perl one-liner

While the old Windows 10 terminal understands ANSI escape sequences, it makes it horribly hard to activate them. Therefore, they're off by default, requiring --color to override that choice.