Raku 中的命令行参数

Sub MAIN

在 Raku 中,命令行参数的解析是通过 MAIN 子例程完成的,MAIN 子例程是一种特殊的子例程,它根据 MAIN 子例程的签名解析命令行参数。与其他子例程一样,MAIN 子例程可以具有命名参数和位置参数、可选(和必需)参数、多重分派等等。

有了 MAIN 子例程的定义,USAGE 子例程将由编译器自动生成。可以修改此子例程以返回定制的使用消息。所有命令行参数也可以在特殊变量 @*ARGS 中使用,它可以在 MAIN 处理之前发生转变。

命名参数和位置参数

命名参数

让我们从一个简单的程序开始(保存为 prog.p6):

use v6;

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

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

在这个 MAIN 子句中,我们通过前置 : 到子例程签名中的每个变量上,创建了两个带有类型约束(Str)的命名参数,$name$last-name。这些参数也有默认值,这是通过给参数赋值来实现的。在本例中,我们将 $name 设置为默认值 “John”,将 $last-name 设置为 “Doe”。如果执行 prog.p6 时命令行参数与 MAIN 签名匹配,则会打印出一个格式化的全名:

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

如您所见,命名参数可以按任何顺序传递。

如果没有匹配 MAIN 签名,则会得到一条使用信息:

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

位置参数

如果我们想使用位置参数,我们可以重新定义子例程的签名,只解析位置参数。与之前的版本一样,我们将为参数设置默认值,但是这些参数现在是位置的,并且必须按照签名定义的顺序提供:

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;
}

用匹配的签名执行 prog.p6 将打印以下输出:

$ raku prog.p6
John Doe

$ raku prog.p6 carl sagan
Carl Sagan

如果签名不匹配,则它给出如下用法信息:

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

多重分派

我们可能更喜欢在我们的小程序中同时使用命名参数和位置参数。如前所述,我们可以使用多重分派(几个名称相同但签名不同的子例程)来声明具有自己签名的多个 MAIN 子例程。为了做到这一点,每个候选用 multi 关键字来声明,而不是 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;
}

这两个 MAIN 子例程看起来非常相似,但是它们有不同的签名来描述预期的命令行参数。

如果我们执行 prog.p6 的命令行参数匹配任何 MAIN 签名,我们将得到格式化的全名:

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

$ p6 prog.p6 marcus aurelius
Marcus Aurelius

如果没有匹配的签名,我们将得到一个用法消息,详细说明我们的 MAIN 子例程可能的签名:

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

组合命名参数和位置参数

定义不同的签名来处理不同的命令行参数(在我们的示例中是命名参数和位置参数)是可以的。但是,如果您想在 MAIN 签名中混合命名参数和位置参数呢? 这很容易做到,尽管位置参数必须在命名参数之前定义。

让我们通过添加位置参数到第一个 multi 子例程来更新我们的简单程序 prog.p6 到最新版本:

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;
}

可选参数和必须参数

默认情况下,命名参数是可选的。尽管如此,可以通过使用 ! 附加各自的词法变量来将它们标记为必须的。例如,MAIN( :$first, :$second, :$operator ){ ... }, 如果不带某些命令行参数调用 MAIN( :$first!, :$second!, :$operator! ){ ... } 则不会打印用法信息。考虑到参数现在是必须的了,调用者必须传递必须的参数才行。

另一方面,位置参数在默认情况下是必需的,但是可以通过使用 ? 附加相应的词法变量来将它标记为可选的。例如,MAIN( $first, $second, $operator ){ ... }, 如果在没有命令行参数的情况下调用 MAIN( $first?, $second?, $operator? ){ ... } 则不会打印用法信息,因为参数现在是可选的。

位置参数也可以通过设置默认值来定义为可选的,比如在 multi MAIN( $title, $name = 'John', $last-name = 'Doe' ) { ... } 中使用的 $name$last-name

别名或替换命名参数

命名参数及其别名是通过使用冒号对语法(:)提供的。冒号的存在将决定我们是否创建一个新的命名参数。

让我们修改 prog.p6 中的第一个 multi 以包括一些别名:

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;
    }
}

...

MAIN 定义了两种别名:

  • :last-name($surname) 只将传递给命令行参数的内容别名 -—last-name 到变量 $surname (注意缺少 :)。这意味着 $surname 将只是别名变量的名字,而不创建新的命名参数:
$ 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 不仅是别名变量的名称,而且是一个新的命名参数,旁边还有 :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

正如您可能已经注意到的,如果要打印此人的格式化全名,现在必须指定标记 -p (或 -print)。这是因为 Bool 类型使 $print 成为一个二进制标记,如果不存在,则为 False。如果调用,则标志为 True,使执行简单的 if $print { ... } 语句变得可能。

使用别名是为参数创建长形式和短形式选项名的一种简单方法。我们可以进一步修改 prog.p6 中的第一个 multi,以便为 -—name-—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;
    }
}
...

通过执行带有不同形式选项的 prog.p6,我们得到:

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

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

如果没有匹配的签名,我们就会得到使用信息:

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

没有匹配的签名,这是我们小程序 prog.p6 的最新版本会打印以下使用信息:

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

这是由于,在没有向 MAIN 子例程提供匹配的签名时,将自动调用 USAGE 子例程。如果没有找到这样的子例程,编译器将输出一个默认生成的使用消息,这意味着我们可以定义它以提供更详细的(如果我们想要的话!)使用消息。

这是带有修改过的 USAGE sub 的 prog.p6:

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
}

注意,用法消息中提到了 -h(和 --help)标志,我们不需要显式地定义它们,因为它们是自动生成的。如果我们现在执行带有 --help(或-h)标志的 prog.p6,或不提供匹配签名,我们得到新的使用信息:

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'

结论

这当然只是对 MAINUSAGE 子例程的简单介绍。就像在 Raku 中一样,总有比看起来更多的东西。例如,如果希望将命名参数放在命令行中的任何位置(甚至在位置参数之后),可以修改 hash %*SUB-MAIN-OPTS 以允许这种行为。如果你想了解更多细节,我在下面提供了一些有用的链接。

另请参阅