随着在 Raku 中写的 shell 脚本越来越多,我意识到我以一种非常间接的方式调用 MAIN。我在想,能不能找到一种减少间接性的方法,以摆脱额外的进程和至少一些 Proc::Async 的开销。首先我们需要一个脚本来调用。
#! /usr/bin/env raku
constant \frame = gather while my $frame = callframe($++) {
take $frame
}
sub MAIN {
say frame[0];
.note for lines;
exit 42;
say 'alive';
}
这个脚本会打印其调用者。然后通过 lines
间接读取 $*IN
并阻塞。如果进程能通过 exit,它将打印 alive
。请注意,MAIN 是这个编译单元的最后一条语句。这让我们可以对脚本进行 slurp 和 EVAL。
my &main := $path.IO.slurp.&EVAL;
这就是了! 我们可以从另一个脚本中调用 MAIN。不过有几个注意事项。我们可以从一个脚本到另一个脚本的数据管道,因为它们共享 $*IN
和 $*OUT
。另外,STDERR 可能会让人感到困惑。exit
的语义可能是错误的。很可能我们想让外层脚本继续运行。捕获内脚本的返回值很容易,exitcode 就不容易了。
由于我们调用的是函数词法的,可以帮助解决大部分的问题。在 Raku 中,我们不能轻易地捕获 c-land exit。但我们可以防止它被调用。
my &*EXIT = sub ($exitcode) {
CapturedExitcode.new(:$exitcode).throw;
}
这里的 exitcode 被封装在一个异常中,所以我们可以从 MAIN 中提取它。这个子程序可能会以返回的方式退出,所以我们也要捕捉到它。
$out.exitcode = main();
CATCH {
when CapturedExitcode {
$out.exitcode = .exitcode;
}
default {
say .^name, ': ', .message;
put .backtrace».Str
}
}
现在我们需要处理 $*IN
和 $*OUT
。由于目标脚本只是调用 lines
和转发到 $*ARGFILES.lines
,我们可以使用 Channel。其中 Channel 是存储 exitcode 的好地方。
my $out = Channel.new but role :: { has $.exitcode is rw; };
my $in = Channel.new but role :: { method put(Any:D \value){ self.send: value ~ $?NL } };
由于 lines 需要一个 Str,我们提供了熟悉的 put 方法。其他方法如 say 也会进入匿名角色。当内部 MAIN 终止时,我们要关闭 $out
通道。我们可以在一个 LEAVE 块中进行。整个过程可以包装成一个 sub
,它为外脚本提供了一个很好的接口。
my ($out, $in) = embed-script('./script.raku');
start {
for ^10 {
$in.put: $++;
}
$in.close;
}
.print for $out.list;
say $out.exitcode;
处理 multi MAIN
是很棘手的。如果脚本中的最后一条语句是 &MAIN
,那么它将指的是调度器。对于最后的任何多候选者,我们只能通过下降到 nqp-land 来掌握 proto。
my &main := $path.IO.slurp.&EVAL;
use nqp;
my &disp = &main.multi ?? nqp::getattr(&main, Routine, '$!dispatcher') !! &main;
然后我们可以调用 disp()
来调度到正确的 MAIN 候选。我不知道这是不是很脆弱。每一个 multi 例程都有一个 dispatcher。然而 Routine.dispatcher
并没有被 CORE 暴露出来。
整个例子可以在这里找到。
避开 Proc::Async 确实加快了不少速度。因为我们可以使用 Channel,所以在移动数据的时候,我们不必进行字符串化和解析输出。在这种情况下,被调用的 MAIN 需要配合,因此需要知道它没有被运行时调用。我们可以引入一个新的词法,或者对 Channel 检查 $*IN
。还可以选择检查 EVAL 的调用框架。
constant \frame = gather while my $frame = callframe($++) {
take $frame
}
say 'indirect' if any frame».gist».starts-with('EVAL');
与 Perl 5 颇为不同的是,对于 Raku,我从不怎么使用 EVAL。不是因为我害怕 - 代码生成的理由不多。毕竟,在我们两个人之间,我一直是比较邪恶的双胞胎。
更新
正如 jnthn 指出的 Routine.dispatcher 已经暴露了。因此,我们可以很好地避开 nqp:
my &disp = &main.multi ?? &main.dispatcher() !! &main;
到目前为止,还不清楚是否应该规范化,从而记录下来。