Raku is surprisingly good for CLIs
A while back I wrote Raku: a Language for Gremlins about my first experiences with the language. After three more months of using it I've found that it's quite nice for writing CLIs! This is because of a couple features:
MAIN params become options
Like many other programming languages, Raku has a special-cased MAIN
function that acts as the entry point into your script. But unlike f.ex C, MAIN
can have an arbitrary signatures:
# strcmp.raku
sub MAIN($str1, $str2) {
... # blah blah blah
}
Then when you call the file:
> raku .\strcmp.raku
Usage:
.\strcmp.raku <str1> <str2>
Raku automatically converts MAIN
's parameters into script arguments! That's already pretty useful, and it gets better:
sub MAIN(
$str1, #= first string
$str2 = "foo", #= second string
) {
...
}
> raku .\strcmp.raku
Usage:
.\strcmp.raku <str1> [<str2>]
<str1> first string
[<str2>] second string [default: 'foo']
So now we also have help messages and default values. To get flags and options, just define keyword parameters:
sub MAIN(
$str1, #= first string
$str2, #= second string
Bool :$i, #= case insensitive?
Int :$option #= some integer option
) {
...
}
Usage:
.\strcmp.raku [-i] [--option[=Int]] <str1> <str2>
<str1> first string
<str2> second string
-i case insensitive?
--option[=Int] some integer option
Here's what the same CLI would look like in Python:
import argparse
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("str1", help="first string", type=str)
parser.add_argument("str2", help="second string", type=str)
parser.add_argument("-i", help="case insensitive?", action="store_true")
parser.add_argument("--option", help="some integer option", type=int)
args = parser.parse_args()
That's over twice as long to do the same thing— three times as long without the help strings. That said, there's a few special things I do with argparse
that don't seem possible in Raku. But I still prefer the latter approach, especially when we throw in the other big feature:
Multimethods make subcommands easier
Raku has multiple dispatch: you can define multiple functions with the same name but different parameters, and then when you call the function it will pick the "most appropriate" method based on the supplied arguments.
# Instead of
sub myroot($val, $root = 2) { ... }
# You can write
multi myroot($val, $root) { ... }
multi myroot($val) { myroot($val, 2) }
This is already useful for defining parameter groups, like saying MAIN
can accept either the --foo
option or the --bar
option but not both. But multimethods can also dispatch on values, like exact strings. That gives you subcommands!
multi MAIN(
"profile",
$file,
Bool :$verbose,
:$output = $*OUT
) {
...
}
multi MAIN(
"reformat",
$file,
Bool :$dry-run = False
) {
...
}
Usage:
.\strcmp.raku [--verbose] [--output[=Any]] profile <file>
.\strcmp.raku [--dry-run] reformat <file>
I use this to group a bunch of thematically-similar tools in the same file and distinguish calling them with the subcommands.
Miscellaneous useful things
There's a few small things that aren't as important as those two features, but still help with QoL.
- You can give flags "aliases" by writing
Bool :v($verbose)
. Then you can call it on the CLI as either-v
or--verbose
. - You can define the special hash
%*SUB-MAIN-OPTS
to configure how the arguments are parsed. F.ex if you add the:bundling
key, then it will accept-abc
in addition to-a -b -c
. - Raku provides some hooks to override CLI behavior, like if you define the
ARGS-TO-CAPTURE
function you can preprocess arguments before dispatching toMAIN
.
One thing I've not be able to figure out is how to make variadic options, like having --src
and --dest
both take multiple paths. AFAICT the best you can do is make them both take flat strings and then postprocess them into paths. Maybe that'll get proper support in the future!
If you're reading this on the web, you can subscribe here. Updates are once a week. My main website is here.
My new book, Logic for Programmers, is now in early access! Get it here.