I've used Perl for years for all the quick and dirty programs where it's much faster to develop in Perl than e.g. C/C++ or another language like Java which is very verbose and you end up writing tons of source code.
And although I do write Perl scripts, I mostly use so-called Perl one-liners in order to extend the command line and act as glue for other command line run scripts and programs, e.g.:
$ perl -e 'foreach(1..3){printf qq[$_\n];}' | \
perl -lane '
printf qq[>%s\n], $_; $l++; $t+= $_;
sub BEGIN{ printf qq[-start\n]; }
sub END{ printf qq[-end // l=$l t=$t\n]; }'
-start
>1
>2
>3
-end // l=3 t=6
And the other reason why I like Perl one-liners is because if you get into the habit of just writing them on-the-fly, then it's lower friction than having to think up a file name, and think up where to store that file. And it's easier to copy and paste the one-liner into notes or a wiki page, or post it to a colleague via slack, etc.
I've been looking for a more elegant / faster alternative language to aging Perl for writing command line one-liners for many years and have a list of language features that I don't want to do without:
- Execute function before program starts, i.e.
sub BEGIN {...}
. - Execute function before program terminates, i.e.
sub END {...}
. - Handle implicit STDIN loop, e.g.
cat foo.txt | perl -lane 'printf qq[>%s\n], $_;'
. - Handle alternative quoting, e.g.
qq[...]
means"..."
andq[...]
means'...'
. - Handle nested hash table with concise syntax, e.g.
$h{foo}{bar}{baz}
. - Handle constant folding, e.g.
if(0){...}
is compiled away with no run-time overhead. - Handle backticks, e.g.
@lines = `...`
. - Handle regular expression with concise syntax, e.g.
if(m~(...)(...)~){($match1, $m2)=($1,$2); ...}
. - Handle if suffix, e.g.
next if(...);
. - Handle switching off line output buffering, i.e.
$|++
. - Ideally one-liner should be same or shorter as Perl one-liner length.
- Must be at least faster than Perl at run-time.
With this minimal list of Perl one-liner features, I've been very productive over the years with my Perl one-liners, but failed miserably to find any suitable replacement... up until now when Crystal Lang came onto my radar...
Perl seems to have this feature because of its implicit STDIN loop command line option. If you specify on the command line to have the implicit loop, then how would you execute any program before the implicit loop?
Crystal has no implicit loop but never the less has an extremely compact loop anyway:
Perl with implicit loop:
$ perl -e 'foreach(1..3){printf qq[$_\n];}' | perl -lane 'printf qq[>%s\n], $_;'
>1
>2
>3
Perl with explicit loop:
$ perl -e 'foreach(1..3){printf qq[$_\n];}' | perl -e 'while(<STDIN>){ printf qq[>%s], $_; }'
>1
>2
>3
Crystal only has explicit loop:
Note: .each_line
is a method of the STDIN
object.
Note: {|t|...}
is an anonymous function called by .each_line
and |t|
is the local function variable name.
$ perl -e 'foreach(1..3){printf qq[$_\n];}' | crystal eval 'STDIN.each_line {|t| printf %[>%s\n], t; }'
>1
>2
>3
Or alternatively Crystal can interpolate the variable l
into the string:
$ perl -e 'foreach(1..3){printf qq[$_\n];}' | crystal eval 'STDIN.each_line {|t| puts %[>#{t}]; }'
>1
>2
>3
Perl:
$ perl -e 'sub END{ printf qq[bar\n]; } printf qq[foo\n];'
foo
bar
Crystal:
Note: The semi colon needed after the closing curly braces of at_exit{...}
.
$ crystal eval 'at_exit{ printf %[bar\n]; }; printf %[foo\n];'
foo
bar
See answer to 'Crystal: Execute function before program starts' above.
Perl:
$ perl -e 'printf qq[%s %s %s\n], "foo", qq(bar), q<baz>;'
foo bar baz
Crystal:
$ crystal eval 'printf %[%s %s %s\n], "foo", %Q(bar), %q<baz>;'
foo bar baz
Things definitely are a little more forgiving in Perl than in Crystal when it comes to concise syntax for handling nested hash tables.
Consider this Crystal nested hash example:
Note: In Perl the code would look something like $h{two}{foo}
to access the nested hash table.
Note: However, in Crystal the code is h["two"].as(Hash)["foo"]
.
$ crystal eval 'h=Hash{"one" => 1, "two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s< >%s<\n], h["one"], h["two"], h["two"].as(Hash)["foo"];'
debug: >{"one" => 1, "two" => {"foo" => 3}}< >1< >{"foo" => 3}< >3<
The .as(Hash)
is only necessary because the first level of the hash table has two different types of values, numeric "one" => 1
and hash table "two" => Hash{"foo" => 3}
.
With Crystal, if we keep the value type of the hash table value consistent, then the .as(Hash)
is no longer necessary to differentiate between the value type:
$ crystal eval 'h=Hash{"one" => Hash{"baz" => 3}, "two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s< >%s<\n], h["one"], h["two"], h["two"]["foo"];'
debug: >{"one" => {"baz" => 3}, "two" => {"foo" => 3}}< >{"baz" => 3}< >{"foo" => 3}< >3<
But there's more: In Perl you might be tempted to have n keys in your hash table but also have two types of values per key, e.g. $h{$key}{sub_key_1}
and $h{$key}{sub_key_2}
. In this case, we're using a hash key for the 2nd level of the nested hash to emulate a kind of C struct with two struct members. It works but it's not good for performance in Perl because we end up doing one hash table for each hash table level.
Crystal offers something called Named Tuples to avoid the second level hash table lookup in this scenario described:
$ crystal eval 'h=Hash{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"][:foo]; p! h;'
debug: >{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}}< >{foo: 3}< >3<
h # => {"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}}
But, again, if you mix hash table value types, then you'll have to explicitly specify the type you're after, i.e. .as(NamedTuple)
:
Note: You can also 'dump' a variable with p! h;
.
$ crystal eval 'h=Hash{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"].as(NamedTuple)[:foo]; p! h;'
debug: >{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}< >{foo: 3}< >3<
h # => {"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}
And finally, if you want to create a 'real' Crystal record for the next level of your hash table then that's possible too, but the record must be declared causing a longer one-liner:
$ crystal eval 'record A, foo : Int32; h=Hash{"two" => A.new(foo: 3), "three" => A.new(foo: 4)}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"].foo; p! h;'
debug: >{"two" => A(@foo=3), "three" => A(@foo=4)}< >A(@foo=3)< >3<
h # => {"two" => A(@foo=3), "three" => A(@foo=4)}
So in summary, Crystal does offer nested hash tables and with more options and with concise syntax if you pay attention to consistent hash table value types.
Perl:
$ perl -e 'if(0){ printf qq[foo ]; } printf qq[bar\n];'
bar
Crystal using its constant folding:
$ crystal eval 'if false; printf %[foo ]; end; printf %[bar\n];'
bar
Crystal using its macro mechanism:
$ crystal eval '{% if flag?(:mydebug) %} printf %[foo ]; {% end %}; printf %[bar\n];'
bar
$ crystal eval --define=mydebug '{% if flag?(:mydebug) %} printf %[foo ]; {% end %}; printf %[bar\n];'
foo bar
Perl:
Note: Perl fails to chomp
the backticks itself, and needs a scalar variable to chomp
.
$ perl -e 'printf qq[>%s<\n], `echo hello`'
>hello
<
$ perl -e 'printf qq[>%s<\n], chomp(`echo hello`)'
Can't modify quoted execution (``, qx) in chomp at -e line 1, near "`echo hello`)
Execution of -e aborted due to compilation errors.
$ perl -e '$t=`echo hello`; chomp $t; printf qq[>%s<\n], $t'
>hello<
Crystal:
Note: Crystal is more flexible than Perl regarding chomp
.
$ crystal eval 'printf %[>%s<\n], `echo hello`'
>hello
<
$ crystal eval 'printf %[>%s<\n], `echo hello`.chomp'
>hello<
Perl:
$ perl -e '$x = "abcdefg"; $x =~ m/((.*)(c.e).*)/; (undef,$a,$b) = ($1,$2,$3); printf qq[%s %s\n], $a, $b;'
ab cde
Crystal:
$ crystal eval 'x = "abcdefg"; x =~ /(.*)(c.e).*/; _,a,b = $~; printf %[%s %s\n], a, b;'
ab cde
Also with Crystal you can kind of name your captures although the syntax might be a tick longer:
$ crystal eval 'x = "abcdefg"; x =~ /(?<a>.*)(?<b>c.e).*/; printf %[%s %s\n], $~["a"], $~["b"];'
ab cde
Perl:
$ perl -e '$x = 1; printf qq[foo\n] if ($x);'
foo
$ perl -e '$x = 0; printf qq[foo\n] if ($x);'
$
Crystal:
$ crystal eval 'x = true; printf %[foo\n] if x;'
foo
$ crystal eval 'x = false; printf %[foo\n] if x;'
$
Perl:
$ perl -e '$|++;'
Crystal:
$ crystal eval 'STDOUT.flush_on_newline=true;'
As can be seen from the above examples, Crystal is kind of similar to Perl in terms of one-liner length.
This one is kind of tricky. Once compiled the Crystal binary is by all accounts a tick faster than golang, and nearly as fast as C. However, Crystal compilation is not the speediest:
$ rm -rf ~/.cache/crystal/
$ time crystal eval 'printf %[foo\n];'
foo
real 0m1.187s
$ time crystal eval 'printf %[foo\n];'
foo
real 0m0.760s
$ rm -rf ~/.cache/crystal/
$ time crystal eval 'printf %[foo\n];'
foo
real 0m1.176s
$ time crystal eval 'printf %[foo\n];'
foo
real 0m0.760s
Although some amount of caching occurs, even a 'cached' build of printf %[foo\n];
takes 760ms... which his not quick. In comparison Perl manages to compile and run in only 3ms:
$ time perl -e 'printf qq[foo\n]'
foo
real 0m0.003s
The Crystal build is caching a lot of files even for the simple printf %[foo\n];
build:
$ find ~/.cache/crystal/eval/ -type f | wc -l
493
$ wc --bytes ~/.cache/crystal/eval/* | tail -1
4040225 total
However, the Crystal development road-map says they are working on an incremental build system which will hopefully speed this up in the future.