在 Raku 中共享命令行参数

Sharing command line parameters in Raku

Raku 充满了整洁的小技巧和小宝石,可以让程序员的生活变得更简单(或者至少是整个更有趣)。作为一门奖励掌握的语言,这些可以组合成复杂的解决方案来解决常见的问题,这并不奇怪。

这篇文章将用一个这样的例子来说明这一点:在命令行应用程序中共享参数。但首先,我们需要介绍一些允许这样做的整洁的小构件。

命令行接口

Raku 内置了对编写命令行接口的支持。如果一个文件有一个 MAIN 子例程,当文件直接执行时,它会自动调用。而且更有趣的是会使用该子例程的签名自动解析命令行参数。

也就是说,一个名为 cli 的可执行文件,看起来是这样的。

#!/usr/bin/env raku

sub MAIN ( Str $command, Str $path, Bool :$debug ) {
    note "Working on $path" if $debug;

    given $command {
        when 'grep' {
            .say for $path.IO.lines.grep: /raku/;
        }
        when 'count' {
            say "$_ has { .IO.lines.elems } lines" given $path;
        }
        default {
            say "Unknown command: $command";
            say $*USAGE;
        }
    }
}

将导致以下输出:

$ ./cli grep ./cli
#!/usr/bin/env raku
            .say for $path.IO.lines.grep: /raku/;
$ ./cli count ./cli
/home/user/cli has 17 lines
$ ./cli
Usage:
  /home/user/cli [--debug] <command> <path>
$ ./cli reverse ./cli
Unknown command: reverse
Usage:
  /home/user/cli [--debug] <command> <path>

多重分派

这是通过 MAIN 子例程中的签名来实现的,它规定了该子例程可以接受哪些参数(当然也规定了不能接受哪些参数,比如我用的那个 --fake-option)。

但是编译器不仅可以根据函数的签名来判断函数调用是否有效,还可以根据使用的参数来决定调用哪个函数。

我们可以利用这一点来扩展我们的应用。

#!/usr/bin/env raku

multi sub MAIN ( 'grep', Str $path, Bool :$debug ) {
    note "Working on $path" if $debug;
    .say for $path.IO.lines.grep: /raku/;
}

multi sub MAIN ( 'count', Str $path, Bool :$debug ) {
    note "Working on $path" if $debug;
    say "$_ has { .IO.lines.elems } lines" given $path;
}

multi sub MAIN (
    Str $command, Str $path, Bool :$debug,
) is hidden-from-USAGE {
    note "Working on $path" if $debug;
    say "Unknown command: $command";
    say $*USAGE;
}

它将保持上述所有调用的行为,但现在会给我们一个不同的帮助信息:

$ ./cli --help
Usage:
  /home/user/cli [--debug] grep <path>
  /home/user/cli [--debug] count <path>

这个新版本将不同的行为分开,这使得实现一个新的命令变得更加简单。但它也有一些不必要的问题,比如强迫我们在每个条目上重复共享参数,这对于更复杂的应用来说可能会变得非常笨重。

幸运的是,我们还有另一张王牌。

子例程原型

当声明 multi 子例程时,我们可以通过声明一个 proto 来声明共同的行为。例如,当我们想确保不会意外地将 $path 变成一个 Int 时,这就很有用。但我们也可以在我们的应用程序中使用它:

#!/usr/bin/env raku

proto sub MAIN ( $, Str $path, Bool :$debug, | ) {
    note "Working on $path" if $debug;
    {*}
}

multi sub MAIN ( 'grep', Str $path, *% ) {
    .say for $path.IO.lines.grep: /raku/;
}

multi sub MAIN ( 'count', Str $path, *% ) {
    say "$_ has { .IO.lines.elems } lines" given $path;
}

multi sub MAIN ( Str $command, Str $path, *% ) is hidden-from-USAGE {
    say "Unknown command: $command";
    say $*USAGE;
}

proto 定义了 MAIN 的一个公共部分,我们可以在其中放置任何共享行为(包括例如我们可能想要对我们现在的全局参数进行的任何初始化)。不幸的是,这确实意味着我们自动生成的使用信息变得稍微不那么好:

$ ./cli --help
Usage:
  /home/user/cli grep <path>
  /home/user/cli count <path>

TIMTOWTDI

一如既往,有不止一种方法,而作为开发者,你要决定哪种方法是最适合你的需求的。

我只是很高兴 Raku 有所有这些工具供我使用,并给我足够的绳索空间来发展。

请注意我们如何使用 is-hidden-from-USAGE trait 来告诉 Raku 在生成使用消息时忽略一个特定的子例程。这是另一个 Raku 到处都是的小把戏。

proto 声明末尾的 | 声明了一个捕获参数,在这种情况下,它的作用是说这个 proto 的具体实现可能会有其他参数(例如,像子命令的特定选项)。

我在 MAIN 子例程中使用的 *% 是一个 slurpy 参数,它将任何非指定的参数 slurps 进来,让这个签名匹配,即使我不费力地指定一个 :$debug 命名的参数,例如。我本可以用 | 来代替,但那会对使用信息产生一些影响,我想避免这种情况。

幸运的是,我们有办法解决这个问题,我们可以扩展 $*USAGE 中可用的内置消息,或者通过定义我们自己的 USAGE 子例程完全取代它。

你也可以用单元来为每一个分离的命令定义全文件的 MAIN,然后把顶层文件留给正确的文件来调度!

原文链接: https://pinguinorodriguez.cl/blog/raku-argument-parsing/

comments powered by Disqus