管道变得简单

Piping made easy

哈梅林的魔笛手是有史以来最强大的超级英雄之一。他可以通过在烟斗上吹奏一首曲子来带领一大群孩子离开。大多数父母都很难带领一个孩子离开电视。我很确定这就是为什么在 *nix 上被称为烟斗的原因。这也意味着,超级英雄电影只是童话故事。

它确实是一个非常强大的工具,允许编写可以处理文本行形式的数据的程序。如果你的程序缺乏过滤其输出的能力,你可以直接用管道来 grep。你甚至可以重复使用你为不同目的而编写的程序来实现。

管道其实是一个非常简单的结构。我们启动两个程序,将第一个程序的 STDOUT 和第二个程序的 STDIN 连接起来。从程序的角度来看,它们是在没有文件名的情况下向打开的文件柄写入。Raku 允许我们通过使用 Proc::Async 来做到这一点。

my $find = Proc::Async.new('/usr/bin/find', '/usr');
my $grep = Proc::Async.new('/bin/grep', 'lib');

$grep.bind-stdin: $find.stdout;

await $find.start, $grep.start;

这比巴什的做法要啰嗦的多。

find /usr | grep lib

我们不会像 Bash 那样密集。Raku 不是一个 shell 脚本语言。然而,在一个面向操作符的语言中,我们应该能够定义一个操作符来完成 STDOUT 和 STDIN 的绑定。最好是用启动线程并等待它们完成。我们也希望能够将这个操作符链接起来。

role Shell::Pipe {
    method sink {
        say [self[0].command, self[1].command, "sinking"];
        await self[1].start, self[0].start;
    }
}
my multi infix:«|>»(Proc::Async:D $out, Proc::Async:D $in) {
    $in.bind-stdin: $out.stdout;
    [$out, $in] does Shell::Pipe
}

$find |> $grep
# OUTPUT: [(/usr/bin/find /usr) (/bin/grep lib) sinking]
#         <lots of lines found by find containing 'lib'>

链是棘手的部分。我们要在这里处理两种情况。sink 上下文列表上下文。Raku 允许我们处理一个没有分配到任何东西或有方法被调用的列表。运行时会在裸露的列表上调用方法 .sink。我们可以用它来告诉我们必须开始处理管道。通过定义另一个操作符,我们可以将整个管道的输出捕获为一个 Array 中的文本行。

my multi infix:«|>»(@pipe where @pipe ~~ Shell::Pipe, @array) {
    @pipe[1].stdout.lines.tap: -> $line is raw { @array.push: $line };
    @pipe.sink;
}
my @a;
$find |> $grep |> @a;

在这种情况下,我们手动调用 .sink。链式管道需要更多的工作。要使用 sink 上下文,一个单一的管道会返回一个混有角色的 Array。我们可以用它来写一个多候选者,期望只是作为它的左边操作数,右边是一个 Proc::Async。棘手的是,我希望能够处理任何奇数的 Proc::Async 对象。既要连接其中的两个对象,又要将其中的一个对象连接到一个 Array 上,以便从 Array 输入数据。我尝试了一个自定义的 IO::Handle,但失败了,因为 Proc::Async.bind-stdin 想要调用 .native-descriptor。如果我从一个 Array 中输入数据,我就没有这个功能。我相信这是一个 Rakudo bug,因为如果我们调用 Proc::Async.write,它就会工作。所以它显然不需要那个本地描述符。我得到了帮助,找到了一个变通的方法。只要在调用 .start-internal 之前修改 Proc::Async.w,回调就能正确设置。遗憾的是,这个属性没有一个公共的写访问器。在 MOP 的帮助下,我们可以写一个变通方法。

my multi infix:«|>»(@array where @array !~~ Shell::Pipe, Proc::Async:D $in) {
    my $h = Shell::ArrayHandle.new(:@array);
    # HERE BE DRAGONS!
    $in.^attributes.grep(*.name eq '$!w')[0].set_value($in, True);
    my $out = class {
        method command { @array.WHAT.gist ~ ' ↦ ' ~ $in.command }
        method start {
            my $p_out = start {
                LEAVE $in.close-stdin;
                $in.write: ($_ ~ "\n").encode for @array;
            }
            slip $p_out
        }
    }
    # $in.bind-stdin: $h;
    [$out, $in] does Shell::Pipe
}

my $sort = Proc::Async.new('/usr/bin/sort');
my @a;
$find |> $grep |> @a;
# fiddle-with(@a);
@a |> $sort;

如果你关注我的博客,你可能已经发现,这是我"持有之袋"中的另一个任务项目。看来我已经很接近一个模块了,它使 Raku 成为 Bash 的一个很好的替代品。

当我开始思考如何在 Raku 中实现简单的 Unix 管道时,我以为这是一项艰巨的任务。其实不然。我得出的结论是,“Raku” 其实是一个动词,意思是"让事情落到实处"。

by gfldex

comments powered by Disqus