Raku is surprisingly good for CLIs
Raku is surprisingly good for CLIs

                
                    
                        
                            October 26, 2023
                        
                
                
                

                

                
                    
                        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 to MAIN.

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.
                

            