Shell Piping

Shell::Piping 模块

Shell::Piping

Shell pipes without a shell but Raku.

概要

use v6.d;
use Shell::Piping;

my int $exitcode = 0;
my &RED = $*OUT.t ?? { „\e[31m$_\e[0m“ } !! { $_ };

sub MAIN(Str $where = ‚/tmp/.‘) {
    my @result;
    my @err;

    px«find $where» |» { /a/ ?? $_ !! Nil } |» px<sort -r> |» @result :stderr(@err) :done({$exitcode ⚛= 1 if .exitcodes});

    .say for @result.head(10);

    if $exitcode {
        $*ERR.put: @err».&RED.join(„\n“);
    }

    exit $exitcode;
}

默认查找 /tmp 目录下文件名带有字母 a 的文件, 找到就打印前 10 行, 报错就用红色强调出错信息。

用法

本模块提供了操作符 (别名为 |>>)来实现类似于 shell 的管道,使用 Proc::Async 对象、Code 对象、ChannelSupply和自定义对象。提供了一个类似于操作符 px 的引号结构来创建 Proc::Async 实例。

px<>, px"", px{}

这些运算符接受一个单一的参数,在 px 和参数之间没有空格。然后,它将在空格上分割参数。第一个元素被认为是一个命令,其余元素是该命令的参数。

如果命令不包含目录分隔符,%*ENV<PATH> 将搜索该命令,并将第一个命中的内容用于创建 Proc::Async。如果使用了目录分隔符,则假设第一个参数是 IO::Path

在这两种情况下,生成的文件都会被检查是否存在,是否有文件系统访问权限来执行它。当这些测试失败时,将抛出异常 X::Shell::CommandNotFoundX::Shell::CommandNoAccess。请注意,在这个检查和实际执行命令之间,文件可能会被删除。所提供参数的语义遵循一般的 Raku 下标规则。因此,px<foo bar>px«foo $bar» 将自动生成一个参数列表。而在 px{foo, bar} 中的代码必须通过你的努力来返回那个列表。

my $proc = px<foo $not-interpolated>; # 不插值, 需要 $PATH
my $var = "42";
$proc = px«/usr/bin/meaning $var»; # 插值, 并且不需要 $PATH
$proc = px{ 'C:/WINDOWS/SYSTEM32/VIOLATE-PRIVACY'.subst('/', '\') ~ '.exe', secrets.txt };
await $proc.start;

px 不负责对产生的 Proc::Async 实例做任何事情。

副词 :timeout(Numerical) 用小数表示秒数,当至少过了这个时间,就会杀死生成的进程。如果使用副词, 会返回 Proc::Async::Timeout 而不是 Proc::Async。当超时时,X::Proc::Async::Timeout 将被抛出。

my @a;

loop {
    (px<curl https://www.raku.org>:timeout(60)) |» @a;
    last;
    CATCH {
        when X::Proc::Async::Timeout {
            put „{.command} timed out. Trying again and again and again.“;
        }
    }
}

multi infix:<|»>multi infix:«|>>»

该操作符的 MMD 候选对象取两个参数,并返回一个 Shell::Pipe 对象。这个对象实现了 .sink.start,据此前者将调用后者。当 .sink 被调用时,管道的所有成员都将被连接起来,以正确的顺序启动并等待。在 sink 上下文中,整个管道将阻塞,直到最后一个 Proc::Async 从其 .start 方法返回。

管道的成员可以是 Proc::AsyncCode 对象、ChannelSupplyIO::HandleIO::Path 和类似 Array 的对象。后者由一个 subset 子集来标识。

subset Arrayish of Any where { !.isa('Code') && .^can(‚push‘) && .^can(‚list‘) }

Proc::Async 的 STDOUT 被逐行送入管道中的下一个元素。如果它是一个 RHS 参数,那么它的 STDIN 会和 LHS 的输出一起被写入。除非使用副词 :quiet:stderr,否则 STDERR 将不被触及。

px<find /tmp> |» px<sort> :quiet; # equivalent to `find /tmp 2>/dev/null | sort 2>/dev/null`;

Code 对象可以在管道的任何地方使用。然而语义有所不同。在管道的开头,对象必须返回一个 Iterable 或实现 .list。它将被调用一次,并对其返回值进行迭代。因此,我们支持 gather/take,序列操作符和许多内置函数。每一个从迭代中返回的值都会加一个新行符,编码为 utf8,并反馈给管道的下一个成员。如果一个代码对象在管道的中间,它将在每次产生一行文本时被调用,并将其返回值反馈到右边。如果返回的是 Nil,这个值将被跳过。在管道的末端,代码对象会在其左邻的每一行文字产生时被调用。

my @a;
{ 2,4,8 … 2**32 } |» px<sha256sum> |» @a;
px<find /tmp> |» { /a/ ?? .lc !! Nil } |» px<sort>;
px<find /tmp> |» { .say } :quiet;

ChannelSupplier/Supply 可以用在管道的开始和结束。如果它们被关闭,整个管道会把 STDIN/STDOUT 关闭。这使得管道可以从外部进行控制。因此,任何对 Code 对象来说太复杂的情况都应该用 Channel 来处理。

my $c = Channel.new;
my $sort = px<sort>;
start {
    await $sort.ready; # this line is optional
    for 1..∞ {
        $c.send: $^a;
    }
}

Promise.in(60).then: { $c.close }; # a timeout
$c |» $sort |» px<uniq> |» { .say };

IO::Path 对象在管道开始时被打开读取,在结束时被打开写入。IO::Handle 对象被认为已经打开,并且必须在最后打开以备写入。文件句柄不会被管道关闭。

px<find /tmp> |» px<sort> |» { .uc  } |» ‚/tmp/sorted.txt‘.IO :quiet;

类似于数组的对象可以用在管道的两端。如果作为第一个元素使用,其 .list 方法将被调用和迭代。在管道的末端,会调用 .push 方法。这意味着来自 LHS 的行总是被添加到这个对象中。

class Custom {
    has @.buffer;
    method push -> \v { @.buffer.push: v; @.buffer.shift if +@.buffer > 100; self }
    method list { @.buffer.list }
}

my $c = Custom.new;
px<find /usr -iname *.txt> |» $c;
$c |» px<sort> |» { .say };

副词

:done(&c(Shell::Pipe $pipe))

将在管道的最后一条命令退出后和 X::Shell::NonZeroExitcode 被抛出之前被调用。参数 $pipe 可以通过 .exitcodes 进行错误处理,通过 .pipees 进行自省。

:stderr(Arrayish|Code|Channel|IO::Path|IO::Handle|Capture)

这个副词将所有的 STDERR 重定向为类似于 ‚|»‘ 接受的对象。错误文本被逐行处理并以一对 (Int $index, Str $text) 的形式转发。其中 $index 是产生文本的 pipee 的位置,从0开始。

px<find /usr> |» px<sort> |» @a :stderr(@err) :done({.exitcodes});
for @err.grep({.head == 0}) {
    say ‚find warned about: ‘, .Str;
}

要记录到一个文件 :stderr() 需要一个已打开的 IO::Handle 或一个将打开的 IO::Path。要关闭句柄,可以在 :done() 回调中调用 .stderr.close

STDERR 流的多个目标可以用一个 Junction 来提供。目前只支持 & junction。所有目标都将收到相同的文本行。在此,不应假定特定的顺序。

px<find /usr> |» px<sort> |» @a :stderr('logfile.txt'.IO & @err & Capture);

Capture 值可以混入一个 Int,以限制捕获到最后 n 行。

my $n = 10; # at most $n lines of STDERR are captured
px<find /usr> |» px<sort> |» @a :stderr(Capture but $n);

:quiet

副词 :quiet 将吞噬所有 STDERR 流并丢弃它们。通过将 $*quiet 设置为输出符号 on,可以将其作为默认值。

my $*quiet = on;
my @a;
px<find -iname *.raku> |» @a;

错误处理

当管道中的任何 Proc::Async 以一个非零的 exitcode 完成时,管道会返回一个 X::Shell::NonZeroExitcode 的 Failure。调用管道上的 .exitcode 会将这个 Failure 标记为已处理。:done() 中的回调会在 Failure 抛出之前被调用。手工处理 exitcode 必须去那里。管道命令的单个出口代码存储在一个 Array 中,其索引与命令在管道中的位置相对应。如果用 :stderr(Capture) 捕获 STDERR 输出。每条命令的文本是可用的,同样是一个列表 ($idx, $text)。这可以通过设置 $*capture-stderr 为输出符号上的默认值。

sub error-handler($pipe) {
    my @a = $pipe.exitcodes;
    for @a {
        .command.say;
        .exitcode.say;
        .STDERR.say;
    }
}
px«find /usr» |» px«sort» :done(&error-handler) :stderr(Capture);

Shell::Pipe::Exitcode 类支持对 IntStrRegex 进行智能匹配。这可以用于处理异常。

px«find /usr» |» px«sort» :stderr(Capture);

CATCH {
    when X::Shell::NonZeroExitcode { 
        for .pipe.exitcodes {
            when ‚find‘ & 1 & /‘(<![‘]>+)‘: Permission denied/ {
                say „did not look in $0“;
            }
        }
    }

}

异常

CATCH {
    when X::Shell::CommandNotFound {
        say .cmd ~ ‚was not found‘;
    }
    when X::Shell::CommandNoAccess {
        say .cmd ~ ‚was unaccessable‘;
    }
    when X::Shell::NonZeroExitcode {
        for .pipe.exitcodes {
            say .command, .exitcode, .pipe.stderr ~~ Capture ?? .STDERR !! ‚‘;
            when ‚find‘ & 1 & /‘(<![‘]>+)‘: Permission denied/ {
                say „did not look in $0“;
            }
        }
    }
    when X::Shell::NoExitcodeYet {
        say .^name, „\n“, .message;
    }
}

完善异常

异常 X::Shell::CommandNotFoundX::Shell::CommandNoAccess 是可以精炼的。这意味着错误信息可以通过类方法 .refine 进行调整。这个方法需要两个 Callable。当 .message 与异常实例一起被调用,并期望返回 Bool。在 True 时,第2个回调与异常实例一起被调用,并且应该返回一个文本。这个文本将被用来代替默认的文本并从 .message 中返回。替换这个消息将作用于类,甚至作用于已创建但尚未抛出的异常。

X::Shell::CommandNotFound.refine(
    (my &b = {.cmd eq ‚raku‘}),
    { ‚Please install Rakudo with `apt install rakudo`.‘ }
);
X::Shell::CommandNotFound.refine(&b, :revert);
X::Shell::CommandNotFound.refine(:revert-all);

方法 .revert 也需要一个 Callable 和副词 :revert 来删除一个精炼或用 :revert-all 删除所有精炼。

X::Shell::CommandNotFound

将被 px«» 抛出,或者当管道启动时,如果作为命令使用的文件没有找到。“not found” 的含义取决于操作系统。如果命令是在 %*ENV<PATH> 中搜索的,那么该路径将在异常消息中显示。这个异常也会检查悬空的符号链接,并为这种情况提供一个备用的错误信息。

X::Shell::CommandNoAccess

将被 px«» 抛出,或者当管道启动时,如果用作命令的文件存在但无法执行。文件系统的访问权限取决于操作系统。

X::Shell::NonZeroExitcode

这将在最后一个 pipee 退出并在 .pipe 中持有 Shell::Pipe 后抛出。如果使用 :stderr(Capture),异常信息包含所有按 shell 命令名分组的错误文本。当混入一个 Int 时,只有那么几行会被捕获。

命令行将被剪切为180个字符。这个限制可以通过设置 $*max-exitcode-command 为任何 IntInf 来改变。

X::Shell::NoExitcodeYet

如果在管道完成之前访问了 .exitcodes,将被抛出。请注意,对底层 Array 的填充不是原子性的。当或在调用 .done 之后使用 .exitcodes 就可以了。

颜色控制

如果将异常发送到终端,会将其错误信息以红色打印到 STDERR。这可以通过 %*ENV<SHELLPIPINGNOCOLOR>$*colored-exceptions 来控制。环境变量可以设置为任何值。动态变量用于输出符号的开启和关闭,当该变量没有被任何调用者声明时,默认为开启。

use Shell::Piping;
use Shell::Piping::Whereceptions;

sub s(IO(Str) $f where &it-is-a-file) {
}

my $*colored-exceptions = off;
s('/foo/bar');

my @a;
px<find /tmp/> |» @a;

Wherecetions

是要在 where 子句中使用的子句,用于测试以后会抛出的条件。除非 %*ENV<SHELLPIPINGNOCOLOR> 被设置为任何值,否则 Where 子句将以红色输出到 STDERR。在合理的情况下,会对悬空的符号链接进行检查,并且异常会返回一个备用的错误信息。所有异常都是 X::IO::Whereception 的子类。

概要

sub works-with-files(IO::Path(Str) $file where &it-is-a-file) {
    say ‚answer‘ for $file.lines.grep(42);
}

sub works-with-directories(IO::Path(Str) $dir where &it-is-a-directory) {
    for $dir {
        .&works-with-files when .IO.f;
        .IO.dir()».&?BLOCK when .IO.d;
    }
}

}

sub will-shell-out(IO::Path(Str) $file where &it-is-executable) {
    px<find -iname '42'> |» px«$file» |» (my @stdout);
}

sub it-is-a-file(IO() $f)

将调用 .e.f 并抛出 X::IO::FileNotFound

sub it-is-a-directory(IO() $d)

将调用 .d 并抛出 X::IO::DirectoryNotFound

sub it-is-executable(IO() $exec)

将调用 .x 并抛出 X::IO::FileNotExecutable

许可证

所有文件(除非另有说明)都可以在 Artistic License Version 2 的条款下使用、修改和重新分发。例子(在文档中、测试中或作为单独的文件分发)可被视为公共领域。

ⓒ2020 Wenzel P. P. Peppmeyer

comments powered by Disqus