Chris Collins

Lab · Playground

Perl playground

Perl 5 running as WebAssembly in your browser — regular expressions, hashes, text munging, and the context rules that make Perl Perl.

Code blocks on this page are editable — change them and press Run.

Larry Wall released Perl in 1987 to pull reports out of text files when awk and sed ran out of road, and for a decade it was the glue of the internet: CGI scripts, sysadmin tools, the bioinformatics pipelines of the Human Genome Project. Its regular expressions became the standard everyone else copied (PCRE is literally "Perl Compatible"). It is still on nearly every Unix box, and still the fastest way to turn a messy log into a tidy table.

These blocks run Perl 5 with its core modules, compiled to WebAssembly (zeroperl) and run in a Web Worker. The first Run downloads about 25 MB; after that it is quick. Each run starts a fresh interpreter. Every block starts with use strict; use warnings;, as any real script should.

Regular expressions with named captures: parse a log
use strict; use warnings;

my @log = (
  '2026-10-29 08:14:02 GET /tools/sandbox/ 200 1834ms',
  '2026-10-29 08:14:09 GET /lab/ 200 212ms',
  '2026-10-29 08:15:41 POST /api/feedback 500 95ms',
  '2026-10-29 08:16:03 GET /tools/graph/?f=sin(x) 200 640ms',
  'garbage line',
  '2026-10-29 08:17:30 GET /missing 404 12ms',
);

for (@log) {
  if (m{^ (?<date>\S+) \s (?<time>\S+) \s (?<verb>[A-Z]+) \s (?<path>\S+) \s (?<status>\d{3}) \s (?<ms>\d+)ms $}x) {
    my $flag = $+{status} >= 500 ? '!!' : $+{ms} > 500 ? 'slow' : '';
    printf "%-5s %-28s %3d %5d %s\n", $+{verb}, $+{path}, $+{status}, $+{ms}, $flag;
  } else {
    print "skipped: $_\n";
  }
}
Hashes: count words and sort by frequency
use strict; use warnings;

my $text = <<'END';
It was the best of times, it was the worst of times, it was the age of
wisdom, it was the age of foolishness, it was the epoch of belief.
END

my %count;
$count{lc $1}++ while $text =~ /(\w+)/g;     # every word, lower-cased

my @top = (sort { $count{$b} <=> $count{$a} || $a cmp $b } keys %count)[0 .. 5];
printf "%-8s %s\n", $_, '#' x $count{$_} for @top;
print scalar(keys %count), " distinct words\n";
Substitution: s///e runs code, tr/// counts and maps characters
use strict; use warnings;

my $recipe = "Mix 200g flour, 3 eggs and 150ml milk; rest 30 min.";
(my $doubled = $recipe) =~ s/(\d+)(?=\s*(?:g|ml|eggs))/$1 * 2/ge;   # double quantities, not minutes
print "$doubled\n";

(my $title = "the perl cookbook") =~ s/(\w+)/\u$1/g;                # \u: upper-case the next letter
print "$title\n";

my $dna = "ATGCGATACGCTTGA";
(my $comp = reverse $dna) =~ tr/ATGC/TACG/;                          # reverse complement
my $gc = ($dna =~ tr/GC//);                                          # tr returns how many it matched
printf "%s -> %s, GC %.0f%%\n", $dna, $comp, 100 * $gc / length $dna;

for my $n (1234567, 98765.4321, -1000) {
  my $c = $n;
  1 while $c =~ s/^(-?\d+)(\d{3})/$1,$2/;     # keep inserting a comma until none fits
  print "$n -> $c\n";
}
References: nested data structures
use strict; use warnings;

my %tools = (
  graph   => { category => 'math',   tags => [qw(plot function)],  uses => 1240 },
  sandbox => { category => 'dev',    tags => [qw(code run)],       uses => 3105 },
  regex   => { category => 'encoding', tags => [qw(text match)],   uses => 980 },
);

push @{ $tools{regex}{tags} }, 'perl';        # autovivify into the nested array
$tools{bqn} = { category => 'dev', tags => ['arrays'], uses => 12 };

for my $name (sort { $tools{$b}{uses} <=> $tools{$a}{uses} } keys %tools) {
  my $t = $tools{$name};
  printf "%-8s %-9s %5d  [%s]\n", $name, $t->{category}, $t->{uses}, join ', ', @{ $t->{tags} };
}

my %by_cat;
push @{ $by_cat{ $tools{$_}{category} } }, $_ for sort keys %tools;
print "$_: @{ $by_cat{$_} }\n" for sort keys %by_cat;
Context: the same expression in list and scalar context
use strict; use warnings;

my @langs = qw(perl python php ruby);
my $n = @langs;                       # an array in scalar context is its length
my ($first) = @langs;                 # parentheses make list context: the first element
print "n=$n first=$first last=$langs[-1]\n";

my $when = localtime(0);              # scalar context: a readable string
my @parts = gmtime(1_000_000_000);    # list context: the fields
print "$when\n";
printf "year %d, day of year %d\n", $parts[5] + 1900, $parts[7];

sub how { wantarray ? 'list' : defined(wantarray) ? 'scalar' : 'void' }
my @a = how(); my $s = how();
print "called in $a[0] and $s context\n";

my @sorted = sort { length($a) <=> length($b) or $a cmp $b } @langs;
print "@sorted\n";
print "has p-words: ", scalar(grep { /^p/ } @langs), "\n";
A report with formats-by-hand: group, total, and align
use strict; use warnings;
use List::Util qw(sum max);

my @sales = map { [split /,/] } split /\n/, <<'CSV';
north,widgets,1200
south,widgets,800
north,gadgets,450
east,widgets,300
south,gadgets,975
east,gizmos,1500
CSV

my (%by_region, %by_item);
for (@sales) {
  my ($region, $item, $amt) = @$_;
  $by_region{$region} += $amt;
  $by_item{$item}{$region} = $amt;
}
my @regions = sort keys %by_region;
my $w = max map { length } keys %by_item;

printf "%-${w}s" . (" %7s" x @regions) . "\n", 'item', @regions;
for my $item (sort keys %by_item) {
  printf "%-${w}s" . (" %7s" x @regions) . "\n", $item, map { $by_item{$item}{$_} // '-' } @regions;
}
printf "%-${w}s" . (" %7d" x @regions) . "\n", 'total', @by_region{@regions};
print "grand total: ", sum(values %by_region), "\n";