命令行界面 - 概述§
Raku 脚本的默认命令行界面由三个部分组成
将命令行参数解析为 Capture
§
这将查看 @*ARGS 中的值,根据某些策略解释这些值,并从中创建一个 Capture
对象。开发人员可以提供或使用模块安装另一种解析方法。
使用该捕获调用提供的 MAIN
子例程§
使用标准的 多重分派 来使用生成的 Capture
对象调用 MAIN
子例程。这意味着您的 MAIN
子例程可以是 multi sub
,每个候选者负责处理给定命令行参数的某些部分。
如果调用 MAIN
失败,则创建/显示使用信息§
如果多重分派失败,则应尽可能地通知脚本用户失败的原因。默认情况下,这是通过检查每个 MAIN
候选子例程的签名以及任何相关的 Pod 信息来完成的。然后将结果显示给用户,显示在 STDERR 上(如果指定了 --help
,则显示在 STDOUT 上)。开发人员可以提供或使用模块安装另一种生成使用信息的方法。
sub MAIN§
具有特殊名称 MAIN
的子例程将在所有相关的入口阶段(BEGIN
、CHECK
、INIT
、PRE
、ENTER
)运行完毕且脚本的 主线 执行完毕后执行。如果不存在 MAIN
子例程,则不会发生错误:您的脚本只需在脚本的主线中完成工作,例如参数解析。
从 MAIN
子例程正常退出将导致退出代码为 0
,表示成功。MAIN
子例程的任何返回值将被忽略。如果抛出未在 MAIN
子例程中处理的异常,则退出代码将为 1
。如果对 MAIN
的分派失败,则会在 STDERR 上显示使用信息,退出代码将为 2
。
命令行参数存在于 @*ARGS
动态变量中,可以在调用 MAIN
单位之前在脚本的主线中进行修改。
(多重)子例程 MAIN
的签名决定了使用标准的 多重分派 语义实际调用哪个候选者。
一个简单的例子
# inside file 'hello.raku'sub MAIN()
如果您在没有任何参数的情况下调用该脚本,您将收到以下使用信息
$ raku hello.raku
Usage:
hello.raku <name>
但是,如果您为参数提供默认值,无论是否指定名称运行脚本,都将始终有效
# inside file 'hello.raku'sub MAIN( = 'bashful')
$ 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()multi MAIN()
这将产生与上述示例相同的输出。您是否应该使用这两种方法来实现所需的目标完全取决于您。
如果您想传递不定数量的参数以在 sub MAIN
中处理,您可以使用 贪婪参数
# inside file 'hello-all.raku'sub MAIN(*)
$ raku hello-all.raku peter paul mary
Hello, peter
Hello, paul
Hello, mary
一个更复杂的示例,使用单个位置参数和多个命名参数,并且还显示了 where
子句也可以应用于 MAIN
参数
# inside "frobnicate.raku"sub MAIN(Str where *.IO.f = 'file.dat',Int : = 24,Bool :)
如果存在 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 where *.IO.f = 'file.dat', #= an existing file to frobnicateInt : = 24, #= length needed for frobnicationBool :, #= required verbosity)
这将改进使用信息,如下所示
$ 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
程序
of Any where Str|True;of Str;multi MAIN(,name :, #= Write profile information to a fileport :, #= Listen for debugger connections on the specified portBool :v(), #= Display verbose output)multi MAIN("--process-files", *)
该程序生成以下使用信息
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
类型约束。带有可选参数的选项:一个
.ACCEPTS
True
的类型(因为传递没有参数的选项等同于传递True
)
与任何其他子例程一样,MAIN
可以为其命名参数定义 别名。
sub MAIN(Str where *.IO.f = 'file.dat', #= an existing file to frobnicateInt :size(:) = 24, #= length/size needed for frobnicationBool :, #= required verbosity)
在这种情况下,这些别名也会在 --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
符号
(FLAG_FOO => 0b001,FLAG_BAR => 0b010,FLAG_BAZ => 0b100,);sub MAIN(Flag = FLAG_FOO)
这将与以下情况一起正常工作
raku MAIN-enum.raku FLAG_BAR
但如果用不是 Flag
的东西调用,则会失败。
%*SUB-MAIN-OPTS
§
可以通过在 %*SUB-MAIN-OPTS
哈希中设置选项来更改参数在传递给 sub MAIN {}
之前如何处理。由于动态变量的性质,需要设置 %*SUB-MAIN-OPTS
哈希并用适当的设置填充它。例如
my =: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 (, , :, :)
可用选项是
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-USAGEmulti MAIN()
因此,如果您只使用命名变量调用此脚本,您将获得以下用法
$ 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 where *.IO.f = 'file.dat',Int : = 24,Bool :,); # <- note semicolon heresay if .defined;say if .defined;say 'Verbosity ', ( ?? 'on' !! 'off');# rest of script is part of MAIN
请注意,这仅适用于您只需要一个(唯一)sub MAIN
的情况。
sub USAGE§
如果对于给定的命令行参数没有找到 MAIN
的多候选,则调用子 USAGE
。如果没有找到此类方法,编译器将输出默认的用法消息。
multi MAIN(Int )multi MAIN(, )sub USAGE()
默认的用法消息可以通过只读 $*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(, --> Capture)
请注意,动态变量 &*ARGS-TO-CAPTURE
可用于将默认命令行参数执行到 Capture
处理中,因此如果您不想重新发明轮子,则不必这样做。
拦截用法消息生成 (2018.10, v6.d 及更高版本)§
您可以通过自己提供 GENERATE-USAGE
子例程,或从生态系统中可用的任何 Getopt 模块导入一个子例程,来替换或增强默认的用法消息生成方式(在 MAIN 调度失败后)。
sub RUN-MAIN§
sub RUN-MAIN(, , :)
此例程允许完全控制 MAIN
的处理。它获取一个 Callable
,它是应该执行的 MAIN
,主线执行的返回值以及其他命名变量::in-as-argsfiles
,如果 STDIN 应该被视为 $*ARGFILES
,则它将为 True
。
如果未提供RUN-MAIN
,将运行一个默认的,它会查找旧接口的子例程,例如MAIN_HELPER
和USAGE
。如果找到,它将按照“旧”语义执行。
sub new-main(, * )RUN-MAIN( , Nil );
这将打印生成的对象的名称(第一个参数)。
sub GENERATE-USAGE§
GENERATE-USAGE
子例程应该接受一个Callable
,它代表由于调度失败而未执行的MAIN
子例程。这可以用于自省。所有其他参数都是设置为发送到MAIN
的参数。它应该返回您希望显示给用户的用法信息的字符串。一个示例将仅重新创建从处理参数创建的Capture
sub GENERATE-USAGE(, |capture)
您也可以使用多个子例程来创建相同的效果
multi GENERATE-USAGE(, :!)multi GENERATE-USAGE(, |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
或更高版本提供模块,那么可以从模块中删除对旧接口的支持。