哈梅林的魔笛手是有史以来最强大的超级英雄之一。他可以通过在烟斗上吹奏一首曲子来带领一大群孩子离开。大多数父母都很难带领一个孩子离开电视。我很确定这就是为什么在 *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