命令行接口 - 概述

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

将命令行参数解析为捕获

这将查看 @*ARGS 中的值,根据某些策略解释这些值,并创建一个 Capture 对象。解析器的替代方式可以由开发者提供或使用模块安装。

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

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

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

如果多重分派失败,则应尽可能通知脚本的用户失败的原因。默认情况下,这是通过检查每个 MAIN 候选 sub 的签名以及任何关联的 pod 信息来完成的。然后在 STDERR 上向用户显示结果(如果指定了 --help,则在 STDOUT 上显示)。生成使用信息的替代方式可以由开发者提供或使用模块安装。

sub MAIN

在运行所有相关的输入phasers(BEGINCHECKINITPREENTER)并执行脚本的主线之后,将执行具有特殊名称 MAIN 的子程序。如果没有 MAIN sub,则不会发生错误:您的脚本只需要在脚本的主线中执行工作,例如参数解析。

从 MAIN sub 的任何正常退出将导致退出代码为 0,表示成功。 MAIN 子的任何返回值都将被忽略。如果抛出未在 MAIN 子内部处理的异常,则退出代码将为 1。如果调度到 MAIN 失败,则在 STDERR 上将显示一条用法消息,退出代码将为 2。

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

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

一个简单的例子:

  1. # inside file 'hello.p6'
  2. sub MAIN($name) {
  3. say "Hello $name, how are you?"
  4. }

如果您调用该脚本没有任何参数:

  1. $ raku hello.p6
  2. Usage:
  3. hello.p6 <name>

但是,如果为参数指定默认值,则无论是否指定名称,运行脚本始终有效:

  1. # inside file 'hello.p6'
  2. sub MAIN($name = 'bashful') {
  3. say "Hello $name, how are you?"
  4. }
  1. $ raku hello.p6
  2. Hello bashful, how are you?
  1. $ raku hello.p6 Liz
  2. Hello Liz, how are you?

另一种方法是使 sub MAIN 成为一个 multi sub

  1. # inside file 'hello.p6'
  2. multi sub MAIN() { say "Hello bashful, how are you?" }
  3. multi sub MAIN($name) { say "Hello $name, how are you?" }

这将提供与上述示例相同的输出。您是否应该使用任何一种方法来实现预期目标完全取决于您。

使用单个位置和多个命名参数的更复杂的示例:

  1. # inside "frobnicate.p6"
  2. sub MAIN(
  3. Str $file where *.IO.f = 'file.dat',
  4. Int :$length = 24,
  5. Bool :$verbose
  6. ) {
  7. say $length if $length.defined;
  8. say $file if $file.defined;
  9. say 'Verbosity ', ($verbose ?? 'on' !! 'off');
  10. }

有了 file.dat,这将以这种方式工作:

  1. $ raku frobnicate.p6
  2. 24
  3. file.dat
  4. Verbosity off

或者这样 --verbose

  1. $ raku frobnicate.p6 --verbose
  2. 24
  3. file.dat
  4. Verbosity on

如果文件 file.dat 不存在,或者您指定了另一个不存在的文件名,您将获得从 MAIN 子的内省创建的标准用法消息:

  1. $ raku frobnicate.p6 doesntexist.dat
  2. Usage:
  3. frobnicate.p6 [--length=<Int>] [--verbose] [<file>]

虽然您不必在代码中执行任何操作,但它仍然可能被视为有点简洁。但是通过使用 pod 功能提供提示,有一种简单的方法可以更好地使用该消息:

  1. # inside "frobnicate.p6"
  2. sub MAIN(
  3. Str $file where *.IO.f = 'file.dat', #= an existing file to frobnicate
  4. Int :$length = 24, #= length needed for frobnication
  5. Bool :$verbose, #= required verbosity
  6. ) {
  7. say $length if $length.defined;
  8. say $file if $file.defined;
  9. say 'Verbosity ', ($verbose ?? 'on' !! 'off');
  10. }

哪个会改善这样的用法消息:

  1. $ raku frobnicate.p6 doesntexist.dat
  2. Usage:
  3. frobnicate.p6 [--length=<Int>] [--verbose] [<file>]
  4. [<file>] an existing file to frobnicate
  5. --length=<Int> length needed for frobnication
  6. --verbose required verbosity

%*SUB-MAIN-OPTS

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

  1. my %*SUB-MAIN-OPTS =
  2. :named-anywhere, # allow named variables at any location
  3. # other possible future options / custom options
  4. ;
  5. sub MAIN ($a, $b, :$c, :$d) {
  6. say "Accepted!"
  7. }

可用选项包括:

named-anywhere

默认情况下,传递给程序的命名参数(即 MAIN)在任何位置参数后都不会出现。但是,如果将 %*SUB-MAIN-OPTS<named-anywhere> 设置为 true 值,则可以在任何位置指定命名参数,即使在位置参数之后也是如此。例如,可以使用以下命令调用上述程序:

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

is hidden-from-USAGE

有时您希望排除MAIN候选者显示在任何自动生成的使用消息中。这可以通过向您不想显示的 MAIN 候选者的规范添加 hidden-from-USAGE 特征来实现。扩展前面的例子:

  1. # inside file 'hello.p6'
  2. multi sub MAIN() is hidden-from-USAGE {
  3. say "Hello bashful, how are you?"
  4. }
  5. multi sub MAIN($name) { #= the name by which you would like to be called
  6. say "Hello $name, how are you?"
  7. }

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

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

没有第一个候选者 hidden-from-USAGE 特征,它看起来像这样:

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

虽然技术上是正确的,但也不能读。

MAIN 的单位作用域定义

如果整个程序体驻留在 MAIN 中,则可以使用单位声明符,如下所示(调整前面的示例):

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

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

sub USAGE

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

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

通过只读 $*USAGE 变量,sub USAGE 内的默认用法消息可用。它将基于可用的 sub MAIN 候选者及其参数生成。如前所示,您可以使用 #|(…​) Pod 块为每个候选项指定其他扩展描述以设置 WHY

拦截 CLI 参数解析(2018.10, v6.d and later)

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

sub ARGS-TO-CAPTURE

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

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

请注意,动态变量 &*ARGS-TO-CAPTURE 可用于执行捕获处理的默认命令行参数,因此如果您不想,则不必重新发明整个轮子。

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

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

sub RUN-MAIN

定义为:

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

该程序允许完全控制 MAIN 的处理。它得到一个 Callable,它是应该执行的 MAIN,主线执行的返回值和其他命名变量

in-as-argsfiles 如果 STDIN 应该被视为 $*ARGFILES,它将为 True

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

  1. class Hero {
  2. has @!inventory;
  3. has Str $.name;
  4. submethod BUILD( :$name, :@inventory ) {
  5. $!name = $name;
  6. @!inventory = @inventory
  7. }
  8. }
  9. sub new-main($name, *@stuff ) {
  10. Hero.new(:name($name), :inventory(@stuff) ).perl.say
  11. }
  12. RUN-MAIN( &new-main, Nil );

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

sub GENERATE-USAGE

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

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

您还可以使用 multi 子例程来创建相同的效果:

  1. multi sub GENERATE-USAGE(&main, :$foo!) {
  2. "You're not allowed to specify a --foo"
  3. }
  4. multi sub GENERATE-USAGE(&main, |capture) {
  5. &*GENERATE-USAGE(&main, |capture)
  6. }

请注意,动态变量 &*GENERATE-USAGE 可用于执行默认使用消息生成,因此您不必重新发明整个轮子。

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

较旧的接口使得一个接口完全拦截对 MAIN 的调用。这取决于是否存在 MAIN_HELPER 子程序,如果在程序的主线中找到 MAIN 子程序,则该子程序将被调用。

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

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

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