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