Sharing command line parameters in Raku
Raku is full of neat little tricks and gems that can make the programmer’s life simpler (or at least a whole more entertaining). And as a language that rewards mastery, it’s not a surprise that these can combine into sophisticated solutions to common problems.
This post will illustrate this point with one such example: sharing parameters in a command line application. But first, we’ll need to cover some of the neat little building blocks that allow for this.
Command line interfaces
Raku has built-in support for writing command line interfaces. If a
file has a MAIN
subroutine, it will called automatically when the file
is directly executed. And more interestingly will use that subroutine’s
signature to automatically parse the command line arguments.
That means that an executable file named cli
that looks like this:
#!/usr/bin/env raku
sub MAIN ( Str $command, Str $path, Bool :$debug ) {
note "Working on $path" if $debug;
given $command {
when 'grep' {
.say for $path.IO.lines.grep: /raku/;
}
when 'count' {
say "$_ has { .IO.lines.elems } lines" given $path;
}
default {
say "Unknown command: $command";
say $*USAGE;
}
}
}
will result in the following output:
$ ./cli grep ./cli
#!/usr/bin/env raku
.say for $path.IO.lines.grep: /raku/;
$ ./cli count ./cli
/home/user/cli has 17 lines
$ ./cli
Usage:
/home/user/cli [--debug] <command> <path>
$ ./cli reverse ./cli
Unknown command: reverse
Usage:
/home/user/cli [--debug] <command> <path>
Multiple dispatch
This is made possible by the signature in the MAIN
subroutine, which
specifies what parameters that subroutine can take (and of course, which ones
it cannot, like that --fake-option
one I used).
But the compiler not only can determine whether a function call is valid or not based on its signature, it can also decide which function to call based the arguments used.
We can use this to expand our application:
#!/usr/bin/env raku
multi sub MAIN ( 'grep', Str $path, Bool :$debug ) {
note "Working on $path" if $debug;
.say for $path.IO.lines.grep: /raku/;
}
multi sub MAIN ( 'count', Str $path, Bool :$debug ) {
note "Working on $path" if $debug;
say "$_ has { .IO.lines.elems } lines" given $path;
}
multi sub MAIN (
Str $command, Str $path, Bool :$debug,
) is hidden-from-USAGE {
note "Working on $path" if $debug;
say "Unknown command: $command";
say $*USAGE;
}
which will keep the behaviour for all the above calls, but will now give us a different help message:1
$ ./cli --help
Usage:
/home/user/cli [--debug] grep <path>
/home/user/cli [--debug] count <path>
This new version keeps different behaviours separate, which makes implementing a new command a lot simpler. But it also has some unwanted issues, like forcing us to repeat the shared parameters on each entry, which can get pretty unwieldy with more complex applications.
Luckily, we have another ace up our sleeves.
Subroutine prototypes
When declaring multi
subroutines we can declare common behaviours by
declaring a proto
. This can be useful when we want to make sure that
we don’t accidentally make the $path
into an Int
, for example. But
we can also use it for our application:2
#!/usr/bin/env raku
proto sub MAIN ( $, Str $path, Bool :$debug, | ) {
note "Working on $path" if $debug;
{*}
}
multi sub MAIN ( 'grep', Str $path, *% ) {
.say for $path.IO.lines.grep: /raku/;
}
multi sub MAIN ( 'count', Str $path, *% ) {
say "$_ has { .IO.lines.elems } lines" given $path;
}
multi sub MAIN ( Str $command, Str $path, *% ) is hidden-from-USAGE {
say "Unknown command: $command";
say $*USAGE;
}
The proto
defines a common portion of MAIN
in which we can place any
shared behaviours (including for example any initialisation we may want
to do with our now global parameters). Unfortunately, it does mean that
our automatically generated usage message gets slightly less nice:3
$ ./cli --help
Usage:
/home/user/cli grep <path>
/home/user/cli count <path>
TIMTOWTDI
As always, there is more than one way to do it,4 and it’s up to you as the developer to decide which one is the most appropriate for your needs.
I’m just glad that Raku has all these tools at my disposal, and gives me
enough rope room to grow.
-
Note how we can use the
is-hidden-from-USAGE
trait to tell Raku to ignore a particular subroutine when generating the usage message. This is another of those nice little tricks Raku is littered with. ↩ -
The
|
at the end of theproto
declaration declares a capture parameter, and in this case has the effect of saying that the specific implementations of thisproto
may have other parameters (like sub-command-specific options, for example).The
*%
I used in theMAIN
subs is a slurpy parameter which slurps in any non-specified parameters to lets that signature match even if I don’t bother to specify a:$debug
named parameter, for example. I could have used a|
instead, but that has some effects on the usage message that I wanted to avoid. ↩ -
Fortunately, there are ways in which we can solve this by either expanding the built-in message available in
$*USAGE
or replacing it entirely by defining our ownUSAGE
subroutine. ↩ -
You could also us
unit
to define file-wideMAIN
s for each separete command, and leave the top level file to dispatch to the right file! ↩