The MAIN Thing

If you're coming to Perl 6 from Perl 5, the global variable @*ARGS will be familiar to you as the place to get the command-line arguments to your program:

$ cat main1.pl6
#!/usr/bin/env perl6

put "ARGS = ", @*ARGS.join(', ');
$ ./main1.pl6 foo bar baz
ARGS = foo, bar, baz

The @ is the sigil that denotes the variable as an array, and the * is the "twigle" that denotes that the variable is a global.  If you follow the above link to the documentation, you'll find a whole host of other dynamic and environmental variables like user, hostname, PID, cwd, etc.

Dealing directly with @*ARGS is fine your program accepts a few positional arguments where the first argument means one thing (name), the second argument another thing (rank), the third another (serial number), etc:

$ cat main2.pl6
#!/usr/bin/env perl6

my ($name, $rank, $serial-num) = @*ARGS;
put "name ($name) rank ($rank) serial number ($serial-num)";
$ ./main2.pl6 Patch Private 1656401
name (Patch) rank (Private) serial number (1656401)

Or if all the arguments are homogenous (e.g., a list of files to process), this works reasonably well:

$ cat main3.pl6
#!/usr/bin/env perl6

for @*ARGS -> $file {
    put "Processing '$file'";
}

put "Done."
$ ./main3.pl6 foo bar baz
Processing 'foo'
Processing 'bar'
Processing 'baz'
Done.

If we declare a MAIN subroutine, we can automatically assign the variables instead of taking them from @*ARGS:

sub MAIN ($name, $rank, $serial-num) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}

Perl will enforce the number of arguments and generate help when supplied the wrong number or when requested:

$ ./main4.pl6 Patch Private
Usage:
    ./main4.pl6 <name> <rank> <serial-num>
$ ./main4.pl6 Patch Private 1656401
name (Patch) rank (Private) serial number (1656401)
$ ./main4.pl6 -h
Usage:
  ./main4.pl6 <name> <rank> <serial-num>

You can also add type constraints:

$ cat main5.pl6
#!/usr/bin/env perl6

sub MAIN (Str $name, Str $rank, Int $serial-num) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}
$ ./main5.pl6 foo bar baz
Usage:
  ./main5.pl6 <name> <rank> <serial-num>
$ ./main5.pl6 foo bar 123
name (foo) rank (bar) serial number (123)

To make them named arguments, simply prefix with a : and then you can supply the arguments in any order:

$ cat main6.pl6
#!/usr/bin/env perl6

sub MAIN (Str :$name, Str :$rank, Int :$serial-num) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}
$ ./main6.pl6 -h
Usage:
  ./main6.pl6 [--name=<Str>] [--rank=<Str>] [--serial-num=<Int>]
$ ./main6.pl6 --name=Patch --serial-num=1656401 --rank=Private
name (Patch) rank (Private) serial number (1656401)

There's a problem if I don't supply any arguments to this last version:

$ ./main6.pl6
Use of uninitialized value of type Str in string context.
Methods .^name, .perl, .gist, or .say can be used to stringify it to something meaningful.
  in sub MAIN at ./main6.pl6 line 3
Use of uninitialized value of type Str in string context.
Methods .^name, .perl, .gist, or .say can be used to stringify it to something meaningful.
  in sub MAIN at ./main6.pl6 line 3
Use of uninitialized value of type Int in string context.
Methods .^name, .perl, .gist, or .say can be used to stringify it to something meaningful.
  in sub MAIN at ./main6.pl6 line 3
name () rank () serial number ()

Perl is complaining that it can't print uninitialized values. I didn't declare that any of the arguments are required -- which I can do by post-fixing the variables with ! -- nor did I provide defaults for those that are not -- which I can do with =:

$ cat main7.pl6
#!/usr/bin/env perl6

sub MAIN (Str :$name!, Str :$rank='NA', Int :$serial-num=0) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}
$ ./main7.pl6
Usage:
  ./main7.pl6 --name=<Str> [--rank=<Str>] [--serial-num=<Int>]

The absence of [] around the --name indicates to the user that the argument is required, while --rank and --serial-num are optional:

$ ./main7.pl6 --name=Patch
name (Patch) rank (NA) serial number (0)
$ ./main7.pl6 --name=Patch --serial-num=1656401 --rank=Private
name (Patch) rank (Private) serial number (1656401)

Even though Perl is checking the types (string, integer) for our arguments, it's not enforcing any validation on the value. We can add a where clause to ensure we only get a positive value for --serial-num:

$ cat main8.pl6
#!/usr/bin/env perl6

sub MAIN (
    Str :$name!, 
    Str :$rank='NA', 
    Int :$serial-num where * > 0 = 1
) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}
$ ./main8.pl6 --name=Patch --serial-num=-10
Usage:
  ./main8.pl6 --name=<Str> [--rank=<Str>] [--serial-num=<Int>]
$ ./main8.pl6 --name=Patch
name (Patch) rank (NA) serial number (1)

As it turns out, the built-in UInt type will work for ensuring positive, so maybe we would be better off ensuring that the serial number is of the correct length:

$ cat main9.pl6
#!/usr/bin/env perl6

sub MAIN (
    Str :$name!,
    Str :$rank='NA',
    UInt :$serial-num where *.Str.chars == 7 = 1111111
) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}
$ ./main9.pl6 --name=Patch --serial-num=12345
Usage:
  ./main9.pl6 --name=<Str> [--rank=<Str>] [--serial-num=<Int>]
$ ./main9.pl6 --name=Patch --serial-num=1234567
name (Patch) rank (NA) serial number (1234567)

Unfortunately the usage doesn't include information about the length, so we can override the default USAGE:

$ cat main10.pl6
#!/usr/bin/env perl6

sub MAIN (
    Str :$name!,
    Str :$rank='NA',
    UInt :$serial-num where *.Str.chars == 7 = 1111111
) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}

sub USAGE {
    printf "Usage:\n  %s --name=<Str> [--rank=<Str>] [--serial-num=<Int>]\n",
        $*PROGRAM.basename;

    put "Note: --serial-num must be 7 digits in length."
}
$ ./main10.pl6
Usage:
  main10.pl6 --name=<Str> [--rank=<Str>] [--serial-num=<Int>]
Note: --serial-num must be 7 digits in length.

I can move the --serial-num code into a subset and further constrain --rank to a list of acceptable values:

$ cat main11.pl6
#!/usr/bin/env perl6

subset SerialNum of UInt where *.Str.chars == 7;
my @ranks = <NA Recruit Private PSC PFC Specialist>;
subset Rank of Str where * eq any(@ranks);

sub MAIN (
    Str :$name!,
    Rank :$rank='NA',
    SerialNum :$serial-num=1111111
) {
    put "name ($name) rank ($rank) serial number ($serial-num)";
}

sub USAGE {
    printf "Usage:\n  %s --name=<Str> [--rank=<Str>] [--serial-num=<Int>]\n",
        $*PROGRAM.basename;

    put "--rank must be one of: {@ranks.join(', ')}";
    put "--serial-num must be 7 digits in length."
}
$ ./main11.pl6 --name=Patch --serial-num=1234567 --rank=XXX
Usage:
  main11.pl6 --name=<Str> [--rank=<Str>] [--serial-num=<Int>]
--rank must be one of: NA, Recruit, Private, PSC, PFC, Specialist
--serial-num must be 7 digits in length.
$ ./main11.pl6 --name=Patch --serial-num=1234567 --rank=Private
name (Patch) rank (Private) serial number (1234567)

Leave a comment

About Ken Youens-Clark

user-pic I work for Dr. Bonnie Hurwitz at the University of Arizona where I use Perl quite a bit in bioinformatics and metagenomics. I am also trying to write a book at https://www.gitbook.com/book/kyclark/metagenomics/details. Comments welcome.