Command line arguments in Perl 6

July 31, 2018


sub MAIN

In Perl 6, the parsing of command line arguments is done with the MAIN subroutine which is a special subroutine that parses command line arguments based on their signatures. As with other subroutines, you can have named and positional parameters, optional (and required) parameters, multiple dispatch, etc. with it.

With the definition of a MAIN subroutine, a USAGE subroutine is automatically generated by the compiler. This subroutine can be modified to return a customized usage message. All command line arguments are also available in the special variable @*ARGS, which can be mutated before being processed by MAIN.

Named and positional parameters

Named parameters

Let’s begin with a simple program (save as prog.p6):

use v6;

sub MAIN(
    Str :$name = 'John', 
    Str :$last-name = 'Doe',
) {

    my $formatted-name = "$name.tc() $last-name.tc()";
    say $formatted-name;
}

In this MAIN sub, we have created two named parameters, $name and $last-name with type constraints (Str), by prepending : to each variable in the signature of the subroutine. These parameters also have default values which is achieved by assigning a value to the parameter. In this case, we’ve set $name to the default value ‘John’ and $last-name to ‘Doe’. If the command line arguments match the MAIN signature when prog.p6 is executed, then a formatted full name will print out:

$ perl6 prog.p6
John Doe
$ perl6 prog.p6 --name='carl' --last-name='sagan'
Carl Sagan
$ perl6 prog.p6 --last-name='sagan' --name='carl'
Carl Sagan

As you can see, named parameters can be passed in whichever order you want.

If no MAIN signature is matched, then we get a usage message:

 $ p6 prog.p6 --name='Carl' --last-name='Sagan' --career='astronomer'
 prog.p6 [--name=<Str>] [--last-name=<Str>]

Positional parameters

If we wanted to use positional parameters instead, we could redefine the subroutine’s signature to parse only positional parameters. Like the previous version, we’ll have default values for the parameters but these parameters are now positional and must be supplied in the order defined by the signature:

use v6;

sub MAIN(
    Str $name = 'John',       # No colon(:) in the variable 
    Str $last-name = 'Doe',   # No colon(:) in the variable 
) {

    my $formatted-name = "$name.tc() $last-name.tc()";
    say $formatted-name;
}

Executing the prog.p6 with a matching signature will print the following outputs:

$ perl6 prog.p6
John Doe

$ perl6 prog.p6 carl sagan
Carl Sagan

And without one, it gives us the following usage message:

$ perl6 prog.p6 carl sagan astronomer
prog.p6 [<name>] [<last-name>]

Multiple dispatch

We might prefer to use both named and positional parameters in our small program. As we mentioned before, we can make use of multiple dispatch (several subroutines with the same name but with different signatures) to declare multiple MAIN subroutines with their own signatures. To do this, each candidate is declared with the multi keyword instead of sub:

use v6;

multi MAIN(
    Str :$name = 'John', 
    Str :$last-name = 'Doe',
) {

    my $formatted-name = "$name.tc() $last-name.tc()";
    say $formatted-name;
}

multi MAIN(
    Str $name = 'John', 
    Str $last-name = 'Doe',
) {

    my $formatted-name = "$name.tc() $last-name.tc()";
    say $formatted-name;
}

Both MAIN subroutines look pretty much alike but they have different signatures which describe the expected command line arguments.

If we execute prog.p6 with command line arguments that match any of the MAIN signatures, we’ll get our formatted full name:

$ p6 prog.p6 --name='ada' --last-name='lovelace'
Ada Lovelace

$ p6 prog.p6 marcus aurelius
Marcus Aurelius

And without a matching signature, we’ll get a useful usage message detailing the possible signatures for our MAIN subroutine:

$ p6 prog.p6 --name='Ada' --last-name='Lovelace' --title='Ms'
Usage:
  prog.p6 [--name=<Str>] [--last-name=<Str>] 
  prog.p6 [<name>] [<last-name>] 

Combining named and positional parameters

Defining different signatures to tackle different command line arguments, named and positional parameters in our case, is fine. But, what if you wanted to mix named and positional parameters in a single MAIN signature? This can be done quite easily, although the positional parameters must be defined before the named ones.

Let’s update the latest iteration of our simple program prog.p6 by adding a positional parameter to the first multi subroutine:

use v6;

multi MAIN(
    Str $title = 'Mr',      # Our positional parameter defined before named ones
    Str :$name = 'John', 
    Str :$last-name = 'Doe',
) {

    my $formatted-name = "$title.tc() $name.tc() $last-name.tc()";
    say $formatted-name;
}

multi MAIN(
    Str $title,
    Str $name = 'John', 
    Str $last-name = 'Doe',
) {

    my $formatted-name = "$title.tc() $name.tc() $last-name.tc()";
    say $formatted-name;
}

Optional and required parameters

Named parameters are optional by default. Nonetheless, they can be marked as required by appending the respective lexical variable with !. For example, MAIN( :$first, :$second, :$operator ){ ... } wouldn’t print a usage message if called without some command line arguments but MAIN( :$first!, :$second!, :$operator! ){ ... } would do given that the parameters are now required and the caller must pass the necessary arguments.

On the other hand, positional parameters are required by default but can be marked as optional by appending the respective lexical variable with ?. For example, MAIN( $first, $second, $operator ){ ... } would print a usage message if called without command line arguments but MAIN( $first?, $second?, $operator? ){ ... } wouldn’t due to the parameters being now optional.

Positional parameters can also be defined as optional by setting default values like how we did with $name and $last-name in multi MAIN( $title, $name = 'John', $last-name = 'Doe' ) { ... }.

Aliases or alternate named parameters

Named parameters and their aliases are provided by the use of the colon-pair syntax (:). The presence of the colon : will decide whether we are creating a new named parameter or not.

Let’s modify the first multi in prog.p6 to include some aliases:

use v6;

multi MAIN(
    Str $title = 'Mr',
    Str :$name = 'John', 
    Str :last-name($surname) = 'Doe',
    Bool :p(:$print),
) {

    my $formatted-name = "$title.tc() $name.tc() $surname.tc()";
    
    if $print {
        say $formatted-name;
    }
}

...

This MAIN is defining two kind of aliases:

  • :last-name($surname) only aliases the content passed to the command line parameter --last-name to the variable $surname (notice the lack of :). This means $surname will be just the name of the aliased variable which does not create a new named parameter:
$ p6 prog.p6 --name='alan' --last-name='turing' -p
Alan Turing

$ p6 prog.p6 --name='alan' --surname='turing'
Usage:
  pos-named.p6 [--name=<Str>] [--last-name=<Str>] [-p|--print] [<title>]
  • :$print will not only be the name of the aliased variable, but also a new named parameter, alongside :p:
$ p6 prog.p6 --name='alan' --last-name='turing'

$ p6 prog.p6 --name='alan' --last-name='turing' -p
Alan Turing

$ p6 prog.p6 --name='alan' --last-name='turing' -print
Alan Turing

As you may have noticed, the flag -p (or -print) must be now specified if we want to print the person’s formatted full name. This is because the type Bool makes $print a binary flag which is False if absent. If called, then the flag is True, making the execution of our simple if $print { ... } statement possible.

Using aliases is an easy way to create long form and short form option names for parameters. We could further modify the first multi in prog.p6 in order to provide a short form option name for --name and --last-name:

use v6;

multi MAIN(
    Str $title = 'Mr',
    Str :n(:$name) = 'John', 
    Str :l(:last-name($surname)) = 'Doe',
    Bool :p(:$print),
) {

    my $formatted-name = "$title.tc() $name.tc() $surname.tc()";
    
    if $print {
        say $formatted-name;
    }
}
...

By executing prog.p6 with different form options, we get:

p6 prog.p6 --name='alan' --last-name='turing' -print
Mr. Alan Turing

p6 prog.p6 -n='grace' -l='hopper' -p 'Ms'
Ms. Grace Hopper

And without a matching signature, we get our usage message:

p6 prog.p6 -n='alan' -l='turing' -p --career='mathematician'
Usage:
  prog.p6 [-n|--name=<Str>] [-l|--last-name=<Str>] [-p|--print] [<title>] 
  prog.p6 [<title>] [<name>] [<last-name>]

Sub USAGE

Without a matching signature, the latest iteration of our small program prog.p6 would print the following usage message:

Usage:
  prog.p6 [-n|--name=<Str>] [-l|--last-name=<Str>] [-p|--print] [<title>] 
  prog.p6 <title> [<name>] [<last-name>] 

which is due to the USAGE subroutine being called automatically when no matching signature is supplied to the MAIN subroutine. If no such subroutine is found, the compiler will output a default generated usage message which means that we can define it to provide a more detailed (if that’s we want!) usage message.

This is prog.p6 with a modified USAGE sub:

use v6;

multi MAIN(
    Str $title = 'Mr',
    Str :n(:$name) = 'John', 
    Str :l(:last-name($surname)) = 'Doe',
    Bool :p(:$print),
) {

    my $formatted-name = "$title.tc() $name.tc() $surname.tc()";
    
    if $print {
        say $formatted-name;
    }
}

multi MAIN(
    Str $title = 'Mr',
    Str $name = 'John', 
    Str $last-name = 'Doe',
) {

    my $formatted-name = "$title.tc() $name.tc() $last-name.tc()";

    say $formatted-name;
}

sub USAGE() {
print Q:c:to/END/;
Usage:
  {$*PROGRAM-NAME} [-n|--name=<Str>] [-l|--last-name=<Str>] [-p|--print] [<title>] 
  {$*PROGRAM-NAME} [<title>] [<name>] [<last-name>] 

optional arguments:
  -h, --help                     show this help message and exit
  -n=PERSON_NAME, --name=PERSON_NAME
                                 specify person's name
  -l=PERSON_LAST_NAME, --last-name=PERSON_LAST_NAME
                                 specify person's last name
  -p , --print                   print person's full name
  <title>                        specify person's title ('Mr' by default)

  Examples:
    {$*PROGRAM-NAME} --name='richard' --last-name='feynman' -p
    {$*PROGRAM-NAME} --name='sophie' --last-name='germain' -p 'Ms'
    {$*PROGRAM-NAME} 'leonhard' 'euler'
END
}

Notice the mention of the -h (and --help) flags in the usage message which we didn’t need to explicitly define because they are automatically generated. If we now execute prog.p6 with the --help (or -h) flag or provide no matching signature, we get the new usage message:

Usage:
  prog.p6 [-n|--name=<Str>] [-l|--last-name=<Str>] [-p|--print] [<title>] 
  prog.p6 [<title>] [<name>] [<last-name>] 

optional arguments:
  -h, --help                     show this help message and exit
  -n=PERSON_NAME, --name=PERSON_NAME
                                 specify person's name
  -l=PERSON_LAST_NAME, --last-name=PERSON_LAST_NAME
                                 specify person's last name
  -p , --print                   print person's full name
  <title>                        specify person's title ('Mr' by default)

  Examples:
   prog.p6 --name='richard' --last-name='feynman' -p
   prog.p6 --name='sophie' --last-name='germain' -p 'Ms'
   prog.p6 'leonhard' 'euler'

Conclusion

This is certainly just a superficial look at the MAIN and USAGE subroutines. As it is in Perl 6, there are always more things than meets the eye. For example if you want named arguments to be placed anywhere in the command line (even after positional parameters), you could modify the hash %*SUB-MAIN-OPTS to allow this behavior. If you want to go into more details, I’ve provided some useful links right below.


See also: