命令行界面 - 概述§

Raku 脚本的默认命令行界面由三个部分组成

将命令行参数解析为 Capture§

这将查看 @*ARGS 中的值,根据某些策略解释这些值,并从中创建一个 Capture 对象。开发人员可以提供或使用模块安装另一种解析方法。

使用该捕获调用提供的 MAIN 子例程§

使用标准的 多重分派 来使用生成的 Capture 对象调用 MAIN 子例程。这意味着您的 MAIN 子例程可以是 multi sub,每个候选者负责处理给定命令行参数的某些部分。

如果调用 MAIN 失败,则创建/显示使用信息§

如果多重分派失败,则应尽可能地通知脚本用户失败的原因。默认情况下,这是通过检查每个 MAIN 候选子例程的签名以及任何相关的 Pod 信息来完成的。然后将结果显示给用户,显示在 STDERR 上(如果指定了 --help,则显示在 STDOUT 上)。开发人员可以提供或使用模块安装另一种生成使用信息的方法。

sub MAIN§

具有特殊名称 MAIN 的子例程将在所有相关的入口阶段(BEGINCHECKINITPREENTER)运行完毕且脚本的 主线 执行完毕后执行。如果不存在 MAIN 子例程,则不会发生错误:您的脚本只需在脚本的主线中完成工作,例如参数解析。

MAIN 子例程正常退出将导致退出代码为 0,表示成功。MAIN 子例程的任何返回值将被忽略。如果抛出未在 MAIN 子例程中处理的异常,则退出代码将为 1。如果对 MAIN 的分派失败,则会在 STDERR 上显示使用信息,退出代码将为 2

命令行参数存在于 @*ARGS 动态变量中,可以在调用 MAIN 单位之前在脚本的主线中进行修改。

(多重)子例程 MAIN 的签名决定了使用标准的 多重分派 语义实际调用哪个候选者。

一个简单的例子

# inside file 'hello.raku' 
sub MAIN($name{
    say "Hello $name, how are you?"
}

如果您在没有任何参数的情况下调用该脚本,您将收到以下使用信息

$ raku hello.raku
Usage:
  hello.raku <name>

但是,如果您为参数提供默认值,无论是否指定名称运行脚本,都将始终有效

# inside file 'hello.raku' 
sub MAIN($name = 'bashful'{
    say "Hello $name, how are you?"
}
$ raku hello.raku
Hello bashful, how are you?

$ raku hello.raku Liz
Hello Liz, how are you?

另一种方法是将 sub MAIN 设为 multi

# inside file 'hello.raku' 
multi MAIN()      { say "Hello bashful, how are you?" }
multi MAIN($name{ say "Hello $name, how are you?"   }

这将产生与上述示例相同的输出。您是否应该使用这两种方法来实现所需的目标完全取决于您。

如果您想传递不定数量的参数以在 sub MAIN 中处理,您可以使用 贪婪参数

# inside file 'hello-all.raku' 
sub MAIN(*@all{ @all.map: -> $name { say "Hello, " ~ $name } }
$ raku hello-all.raku peter paul mary
Hello, peter
Hello, paul
Hello, mary

一个更复杂的示例,使用单个位置参数和多个命名参数,并且还显示了 where 子句也可以应用于 MAIN 参数

# inside "frobnicate.raku" 
sub MAIN(
  Str   $file where *.IO.f = 'file.dat',
  Int  :$length = 24,
  Bool :$verbose
{
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

如果存在 file.dat,它将按以下方式工作

$ raku frobnicate.raku
24
file.dat
Verbosity off

或者使用 --verbose 按以下方式工作

$ raku frobnicate.raku --verbose
24
file.dat
Verbosity on

如果文件 file.dat 不存在,或者您指定了另一个不存在的文件名,您将收到从 MAIN 子例程自省创建的标准使用信息

$ raku frobnicate.raku doesnotexist.dat
Usage:
  frobnicate.raku [--length=<Int>] [--verbose] [<file>]

虽然您不必在代码中执行任何操作来实现这一点,但它可能仍然被认为有点简短。但是,有一种简单的方法可以通过使用 pod 功能提供提示来使该使用信息更完善

# inside "frobnicate.raku" 
sub MAIN(
  Str   $file where *.IO.f = 'file.dat',  #= an existing file to frobnicate 
  Int  :$length = 24,                     #= length needed for frobnication 
  Bool :$verbose,                         #= required verbosity 
{
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

这将改进使用信息,如下所示

$ raku frobnicate.raku doesnotexist.dat
Usage:
  frobnicate.raku [--length=<Int>] [--verbose] [<file>]

    [<file>]          an existing file to frobnicate
    --length=<Int>    length needed for frobnication
    --verbose         required verbosity

从 2021.03 版本开始,单个命名参数的值也可以用空格隔开。考虑一个具有以下源代码的 demo 程序

subset name of Any where Str|True;
subset port of Str;
multi MAIN(
    $file,
    name :$profile,    #= Write profile information to a file 
    port :$debug-port#= Listen for debugger connections on the specified port 
    Bool :v($verbose), #= Display verbose output 
 
{}
multi MAIN("--process-files"*@images{}

该程序生成以下使用信息

Usage:
  demo [--profile[=name]] [--debug-port=<port>] [-v] <file>
  demo --process-files [<images> ...]

    --profile[=name]       Write profile information to a file
    --debug-port=<port>    Listen for debugger connections on the specified port
    -v                     Display verbose output

以下是调用 demo 的有效方法

demo --profile ~/foo
demo --profile=/tmp/bar ~/foo
demo --debug-port 4242 ~/foo
demo --debug-port=4242 ~/foo
demo -v ~/foo
demo --process-files *.jpg

然而,这些是不有效的

demo --profile /tmp/bar ~/foo
demo --debug-port ~/foo

第一个无效是因为 /tmp/bar~/foo 都被解析为位置参数,这意味着 demo 被调用了太多位置参数。第二个无效是因为 ~/foo 被解析为 --debug-port 的参数,因此 demo 缺少必需的位置参数。

以下是它的工作原理;Raku 区分三种类型的选项

  • 布尔选项(如 -v),它们从不接受参数;它们要么存在,要么不存在。

  • 带有强制参数的选项(如 --debug-port),它们总是接受参数。如果你用 = 给它们参数,它们会使用它;如果没有,它们会使用下一个参数。

  • 带有可选参数的选项(如 --profile),它们有参数和没有参数都有效。你只能= 语法给这些参数参数;如果选项后面有空格,这意味着它被调用时没有参数。

以下是生成每种类型参数的签名

  • 布尔选项:一个 Bool 类型约束。

  • 带有强制参数的选项:一个不 .ACCEPT Bool 的类型。

  • 带有可选参数的选项:一个 .ACCEPTS True 的类型(因为传递没有参数的选项等同于传递 True

与任何其他子例程一样,MAIN 可以为其命名参数定义 别名

sub MAIN(
  Str   $file where *.IO.f = 'file.dat',  #= an existing file to frobnicate 
  Int  :size(:$length= 24,              #= length/size needed for frobnication 
  Bool :$verbose,                         #= required verbosity 
{
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

在这种情况下,这些别名也会在 --help 中列出为备选方案

Usage:
  frobnicate.raku [--size|--length=<Int>] [--verbose] [<file>]

    [<file>]                 an existing file to frobnicate
    --size|--length=<Int>    length needed for frobnication
    --verbose                required verbosity

Enumeration 可以用在签名中,参数会自动转换为相应的 enum 符号

enum Flag  (
    FLAG_FOO => 0b001,
    FLAG_BAR => 0b010,
    FLAG_BAZ => 0b100,
);
 
sub MAIN(Flag $flag = FLAG_FOO{
    say "Flagging $flag";
}

这将与以下情况一起正常工作

raku MAIN-enum.raku FLAG_BAR

但如果用不是 Flag 的东西调用,则会失败。

%*SUB-MAIN-OPTS§

可以通过在 %*SUB-MAIN-OPTS 哈希中设置选项来更改参数在传递给 sub MAIN {} 之前如何处理。由于动态变量的性质,需要设置 %*SUB-MAIN-OPTS 哈希并用适当的设置填充它。例如

my %*SUB-MAIN-OPTS =
  :named-anywhere,             # allow named variables at any location 
  :bundling,                   # allow bundling of named arguments 
  :coerce-allomorphs-to(Int),  # coerce allomorphic arguments to given type 
  :allow-no,                   # allow --no-foo as alternative to --/foo 
  :numeric-suffix-as-value,    # allow -j2 as alternative to --j=2 
;
sub MAIN ($a$b:$c:$d{
    say "Accepted!"
}

可用选项是

named-anywhere§

默认情况下,传递给程序(即 MAIN)的命名参数不能出现在任何位置参数之后。但是,如果 %*SUB-MAIN-OPTS<named-anywhere> 设置为真值,则命名参数可以在任何地方指定,即使在位置参数之后。例如,上面的程序可以用以下方式调用

$ raku example.raku 1 --c=2 3 --d=4

bundling§

%*SUB-MAIN-OPTS<bundling> 设置为真值时,单个字母的命名参数可以与单个连字符捆绑在一起。以下两个命令是等效的

$ raku example.raku -a -b -c
$ raku example.raku -abc

捆绑参数可以理解为标志,既不能被否定,也不能被赋值

$ raku example.raku -/a       # OK
$ raku example.raku -a=asdf   # OK
$ raku example.raku -abc=asdf # Error
$ raku example.raku -/abc     # Error

此选项仅在 Rakudo 编译器 2020.10 版本及更高版本中可用。

coerce-allomorphs-to§

%*SUB-MAIN-OPTS<coerce-allomorphs-to> 设置为特定类型时,任何 同形 值将被强制转换为该类型。这在任何对 MAIN 的调度问题中都很有用。

此选项仅在 Rakudo 编译器 2020.12 版本及更高版本中可用。

allow-no§

%*SUB-MAIN-OPTS<allow-no> 设置为真值时,命令行上参数的否定也可以用 no- 而不是 / 来表示。

$ raku example.raku --/foo    # named argument "foo" is False
$ raku example.raku --no-foo  # same

此选项仅在 Rakudo 编译器 2022.12 版本及更高版本中可用。

numeric-suffix-as-value§

%*SUB-MAIN-OPTS<numeric-suffix-as-value> 设置为真值时,单个字母参数可以指定数值后缀。

$ raku example.raku --j=2  # named argument "j" is 2
$ raku example.raku -j2    # same

此选项仅在 Rakudo 编译器 2022.12 版本及更高版本中可用。

is hidden-from-USAGE§

有时您可能希望将 MAIN 候选从自动生成的用法消息中排除。这可以通过在您不想显示的 MAIN 候选的规范中添加 hidden-from-USAGE 特性来实现。扩展之前的示例

# inside file 'hello.raku' 
multi MAIN() is hidden-from-USAGE {
    say "Hello bashful, how are you?"
}
multi MAIN($name{  #= the name by which you would like to be called 
    say "Hello $name, how are you?"
}

因此,如果您只使用命名变量调用此脚本,您将获得以下用法

$ raku hello.raku --verbose
Usage:
  hello.raku <name> -- the name by which you would like to be called

如果没有第一个候选上的 hidden-from-USAGE 特性,它将看起来像这样

$ raku hello.raku --verbose
Usage:
  hello.raku
  hello.raku <name> -- the name by which you would like to be called

虽然在技术上是正确的,但读起来并不顺畅。

Unit-scoped definition of MAIN§

如果整个程序体位于 MAIN 内,您可以使用 unit 声明符,如下所示(改编之前的示例)

unit sub MAIN(
  Str   $file where *.IO.f = 'file.dat',
  Int  :$length = 24,
  Bool :$verbose,
);  # <- note semicolon here 
 
say $length if $length.defined;
say $file   if $file.defined;
say 'Verbosity ', ($verbose ?? 'on' !! 'off');
# rest of script is part of MAIN 

请注意,这仅适用于您只需要一个(唯一)sub MAIN 的情况。

sub USAGE§

如果对于给定的命令行参数没有找到 MAIN 的多候选,则调用子 USAGE。如果没有找到此类方法,编译器将输出默认的用法消息。

#|(is it the answer) 
multi MAIN(Int $i{ say $i == 42 ?? 'answer' !! 'dunno' }
#|(divide two numbers) 
multi MAIN($a$b){ say $a/$b }
 
sub USAGE() {
    print Q:c:to/EOH/; 
    Usage: {$*PROGRAM-NAME} [number]
 
    Prints the answer or 'dunno'.
EOH
}

默认的用法消息可以通过只读 $*USAGE 变量在 sub USAGE 内获得。它将根据可用的 sub MAIN 候选及其参数生成。如前所示,您可以使用 #|(...) Pod 块为每个候选指定一个额外的扩展描述,以设置 WHY

拦截 CLI 参数解析 (2018.10, v6.d 及更高版本)§

您可以通过自己提供 ARGS-TO-CAPTURE 子例程,或从生态系统中可用的任何 Getopt 模块导入一个子例程,来替换或增强默认的参数解析方式。

sub ARGS-TO-CAPTURE§

ARGS-TO-CAPTURE 子例程应接受两个参数:一个表示要执行的 MAIN 单元的 Callable(以便可以对其进行内省,如果需要)和一个包含来自命令行的参数的数组。它应该返回一个 Capture 对象,该对象将用于调度 MAIN 单元。以下是一个非常人为的示例,它将根据输入的某个关键字创建一个 Capture(这在测试脚本的命令行界面时可能很方便)

sub ARGS-TO-CAPTURE(&main@args --> Capture{
    # if we only specified "frobnicate" as an argument 
    @args == 1 && @args[0eq 'frobnicate'
      # then dispatch as MAIN("foo","bar",verbose => 2) 
      ?? Capture.newlist => <foo bar>hash => { verbose => 2 } )
      # otherwise, use default processing of args 
      !! &*ARGS-TO-CAPTURE(&main@args)
}

请注意,动态变量 &*ARGS-TO-CAPTURE 可用于将默认命令行参数执行到 Capture 处理中,因此如果您不想重新发明轮子,则不必这样做。

拦截用法消息生成 (2018.10, v6.d 及更高版本)§

您可以通过自己提供 GENERATE-USAGE 子例程,或从生态系统中可用的任何 Getopt 模块导入一个子例程,来替换或增强默认的用法消息生成方式(在 MAIN 调度失败后)。

sub RUN-MAIN§

sub RUN-MAIN(&main$mainline:$in-as-argsfiles)

此例程允许完全控制 MAIN 的处理。它获取一个 Callable,它是应该执行的 MAIN,主线执行的返回值以及其他命名变量::in-as-argsfiles,如果 STDIN 应该被视为 $*ARGFILES,则它将为 True

如果未提供RUN-MAIN,将运行一个默认的,它会查找旧接口的子例程,例如MAIN_HELPERUSAGE。如果找到,它将按照“旧”语义执行。

class Hero {
    has @!inventory;
    has Str $.name;
    submethod BUILD:$name:@inventory ) {
        $!name = $name;
        @!inventory = @inventory
    }
}
 
sub new-main($name*@stuff ) {
    Hero.new(:name($name), :inventory(@stuff) ).raku.say
}
 
RUN-MAIN&new-mainNil );

这将打印生成的对象的名称(第一个参数)。

sub GENERATE-USAGE§

GENERATE-USAGE子例程应该接受一个Callable,它代表由于调度失败而未执行的MAIN子例程。这可以用于自省。所有其他参数都是设置为发送到MAIN的参数。它应该返回您希望显示给用户的用法信息的字符串。一个示例将仅重新创建从处理参数创建的Capture

sub GENERATE-USAGE(&main|capture{
    capture<foo>:exists
      ?? "You're not allowed to specify a --foo"
      !! &*GENERATE-USAGE(&main|capture)
}

您也可以使用多个子例程来创建相同的效果

multi GENERATE-USAGE(&main:$foo!{
    "You're not allowed to specify a --foo"
}
multi GENERATE-USAGE(&main|capture{
    &*GENERATE-USAGE(&main|capture)
}

请注意,动态变量&*GENERATE-USAGE可用于执行默认的用法消息生成,因此如果您不想重新发明轮子,则不必这样做。

拦截 MAIN 调用(在 2018.10 之前,v6.e)§

一个旧的接口允许一个人完全拦截对MAIN的调用。这取决于是否存在一个MAIN_HELPER子例程,如果在程序的主线中找到一个MAIN子例程,则会调用该子例程。

此接口从未记录。但是,任何使用此未记录接口的程序将继续运行,直到v6.e。从 v6.d 开始,使用未记录的 API 将导致DEPRECATED消息。

生态系统模块可以提供新的和旧的接口,以与旧版本的 Perl 6 和 Raku 兼容:如果较新的 Raku 识别新的(已记录)接口,它将使用该接口。如果没有新的接口子例程可用,但旧的MAIN_HELPER接口可用,那么它将使用旧的接口。

如果模块开发人员决定只为v6.d或更高版本提供模块,那么可以从模块中删除对旧接口的支持。