第十九章. 控制其他程序

Controlling Other Programs

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第十九章. 控制其它程序

有时你需要让其他程序为你做一些工作。 Perl 系列语言被称为“互联网的胶水”。开始一个著名的,稳定的,现有的程序比自己重新实现它更容易,更快。本章介绍了许多启动和控制外部程序的方法,以便根据你的意愿对其进行控制。

快速和容易

shell 例程是运行外部命令或程序的快捷方式。它接受参数并在 shell 中运行它,就像你自己输入它一样。此示例使用类 Unix 的 shell 命令列出所有文件:

shell( 'ls -l' );

If you were on Windows you’d use a different command. There’s an implicit cmd /c in front of your command:

如果你在 Windows 上,你会使用不同的命令。命令前面有一个隐式的 cmd /c

shell( 'dir' );   # actually cmd /c dir

此命令的输出将转到程序输出所在的位置(只要你没有将标准输出或错误重定向到其他内容)。

你可以通过检查 $*DISTRO 变量来选择命令。 Distro 对象有一个 .is-win 方法,如果它认为你的程序在该平台上运行,则返回 True

my $command = $*DISTRO.is-win ?? 'dir' !! 'ls -l';
shell( $command );
警告

注意变量作为 shell 的参数!一定要知道它们里面有什么。如果 shell 中的字符是特殊的,那么它在该值中也是特殊的。稍后详细介绍。

shell 返回一个 Proc 对象。当你在 sink 上下文中使用它(对结果不执行任何操作)并且命令失败时,Proc 对象会抛出异常:

shell( '/usr/bin/false' );  # throws X::Proc::Unsuccessful

当命令以 0 以外的值退出时,命令“失败”。这是一种 Unix 惯例,其中非零数字表示各种错误条件。并非所有程序都遵循该惯例,如果不遵循,你将不得不做更多的工作。

你可以保存结果以避免异常。你可以检查 Proc 对象以查看发生的情况:

my $proc = shell( '/usr/bin/false' );
unless $proc.so {
    put "{$proc.command} failed with exit code: {$proc.exitcode}";
}

这仍然可能不是你想要的。如果你希望它返回非零值,你可能必须自己处理部分过程:

my $proc = shell( '/usr/bin/true' );
given $proc {
    unless .exitcode == 1 {
        put "{.command} returned: {.exitcode}";
        X::Proc::Unsuccessful.new.throw;
    }
}

如果你不关心命令是否失败,则可以在返回的对象上调用 .so。这“处理”对象并阻止 Proc 抛出异常:

shell( '/usr/bin/false' ).so

引起来的命令

有时你想捕获命令的输出或将其保存在变量中。你可以使用带 :x 副词的引用从命令的输出创建一个字符串

my $output = Q:x{ls -1};
my $output = q:x{ls -1};
my $output = qq:x{$command};

这些是它们的稍短版本,可以做同样的事情:

my $output = Qx{dir};
my $output = qx{dir};
my $output = qqx{$command};

这些仅捕获标准输出。如果要合并标准错误,则需要在 shell 中处理。这适用于 Unix 和 Windows,使用 2>&1 。这会在句柄到达你的程序之前合并它们:

my $output = qq:x{$command 2>&1};

更安全的命令

run 例程允许你将命令表示为列表。列表中的第一项是命令名,Raku 直接执行而没有 shell 交互。这个命令并不像它看起来那样令人讨厌,因为没有一个字符对 shell 来说是特殊的。那些分号不会结束命令并启动另一个命令:

# don't do this, just in case
run( '/bin/echo', '-n', ';;;; rm -rf /' );

如果你在 shell 中将其作为单个字符串输入,则可以启动递归操作以删除所有文件。即使在开玩笑中也不要尝试这个(或者使用带有保存快照的虚拟机!)。

run 返回一个 Proc 对象; 以与 shell 相同的方式处理它:

unless run( ... ) {
    put "Command failed";
}

你可能想要使用没有路径信息的裸命令名:

run( 'echo', '-n', 'Hello' );

这也不是特别安全。 run 将在 PATH 环境变量中查找匹配的文件。这是人们可以在你的程序之外设置的东西。有人可能会欺骗你的程序运行一些叫做 echo 的东西。

你可以清除 PATH,强制程序始终指定命令的完整路径:

%*ENV{PATH} = '';  # won't find anything
run( '/bin/echo', '-n', 'Hello' );

PATH 设置为你信任且允许的目录可能更容易:

%*ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin'
run( 'echo', '-n', 'Hello' );

这并不意味着你找到的命令是正确的;有人可能已经篡改过。没有办法提供完美的安全性 - 但你不必太担心。每当你与程序之外的事物进行交互时,请考虑这一点。

shell 一样,run 返回一个 Proc 对象。 :out 参数捕获标准输出并通过 Proc 对象使其可用。使用 .slurp 来提取它:

my $proc = run(
    '/bin/some_command', '-n', '-t', $filename
    :out,
    );
put "Output is 「{ $proc.out.slurp }」";

:err 参数对错误输出执行相同的操作:

my $proc = run(
    '/bin/some_command', '-n', '-t', $filename
    :out, :err,
    );
put "Output is 「{ $proc.out.slurp }」";
put "Error is 「{ $proc.err.slurp }」";

如果你不希望它们作为单独的流,你可以合并它们:

my $proc = run(
    '/bin/some_command', '-n', '-t', $filename
    :out, :err, :merge
    );
put "Output is 「{ $proc.out.slurp }」";

你还可以给它其命名参数以控制编码,环境和当前工作目录(以及其他内容)。

练习19.1 使用 run 来获取当前目录的按文件大小排序的文件列表。输出那个长文件列表。 Unix 命令是 ls -lrS,Windows 命令是 cmd /c dir /OS。一旦你开始工作,过滤行只输出那些带有 7 的行。最后,你能让一个程序在两个平台上都能运行吗?

写入到 Proc

进程可以从你的程序中接收数据。包括 :in 允许你写入到进程:

my $string = 'Hamadryas perlicus';

my $hex = run 'hexdump', '-C', :in, :out;

$hex.in.print: $string;
$hex.in.close;

$hex.out.slurp.put;

在此示例中,你将调用一次 .print,然后关闭输出。这对于 hexdump 来说很好,但其他程序可能表现不同。有些人可能会期待一些输入,给你一些输出,然后在你读取之后期望更多的输入。这如何工作取决于具体的程序,有时可能令人发狂:

my $string = 'Hamadryas perlicus';

my $hex = run 'fictional-program', :in, :out;
$hex.in.print: $string;
$hex.out.slurp;
$hex.in.print: $string;
...;

你可以将一个外部程序的输出重定向到另一个外部程序的输入。此示例获取 raku -v 的输出并使其成为下一个Proc 的输入:

my $proc1 = run( 'raku', '-v', :out );
my $proc2 = run(
    'tr', '-s', Q/[:lower:]/,  Q/[:upper:]/,
    :in($proc1.out)
    );

第二个 run 使用外部的 tr 命令将所有小写字母转换为大写字母:

THIS IS RAKUDO STAR VERSION 2018.04 BUILT ON MOARVM VERSION 2018.04
IMPLEMENTING PERL 6.C.

Procs

Proc 对象处理 shellrun。自己构造对象以获得更多控制。这分两步进行; Proc 设置了稍后运行命令的东西:

my $proc = Proc.new: ...;

设置捕获并合并标准输出和错误流的通用 Proc

my $proc =  Proc.new: :err, :out, :merge;

当你准备好运行命令时,.spawn 它。你生成的进程使用你已经建立的设置。结果是基于程序退出状态的布尔值:

unless $proc.spawn: 'echo', '-n', 'Hello' {
    ... # handle the error
}

如果需要不同的设置,请在调用 .spawn 时指定当前工作目录和环境:

my $worked = $proc.spawn: :cwd($some-dir), :env(%hash);
unless $worked {
    ... # handle the error
}

练习19.2 创建捕获标准输出和错误的 Proc。生成命令以获取目录列表。

异步控制

通过 Proc(以及 shellrun)执行命令会使程序等待,直到外部程序完成其工作。使用 Proc::Async 允许这些程序在自己的 Promise 中运行,而程序的其余部分继续运行。

运行外部 find 并等待它遍历所有文件系统可能几乎永远等不到(至少感觉像):

my $proc = Proc.new: :out;
$proc.spawn: 'find', '/', '-name', '*.txt';

for $proc.out.lines -> $line {
    put $++, ': ', $line;
}

put 'Finished';

运行此程序时,你会看到 find 的所有输出行。完成后,可能需要很长时间,然后你将看到 Finished 消息。你可以异步地执行此操作。

你可以在这些示例中看到 Unix find,但你还在第8章中创建了一个类似的目录列表程序,你可以将其用作外部程序来练习使用 Proc

my $proc = Proc.new: :out;
$proc.spawn: 'raku', 'dir-listing.p6';

for $proc.out.lines -> $line {
    put $++, ': ', $line;
}

put 'Finished';

Proc::Async 的接口与 Proc 的有点不同。一旦有了对象,就可以使用第18章中看到的 SupplyPromise 功能。这个例子使用 .lines 将输出分解为行(而不是缓冲区的块),然后轻敲该 Supply 以处理进来的行:

my $proc = Proc::Async.new: 'find', '/', '-name', '*.txt';

$proc.stdout.lines.Supply.tap: { put $++, ': <', $^line, '>' };
my $promise = $proc.start;

put 'Moving on';

await $promise;

这是 Proc::Async 的简单使用,但你可以将它与你已经看到的并发功能结合使用。调用 .stdout 可以获得输出行,但只能在调用 .start 之后。在中执行这两个操作:

my $proc = Proc::Async.new: 'find', '/', '-name', '*.txt';

react {
    whenever $proc.stdout.lines { put $_;  }
    whenever $proc.start        { put "Finished"; done }
};

.start 返回一个在外部程序完成之前不会保留的 Promise。即使 whenever 在程序开始时运行,Promise 都不会保留到最后,然后 Block 就会完成它的工作。

练习19.3 实现异步 find 程序。修改它,使其在找到你在命令行上指定的文件数后停止。报告找到的文件数。

总结

你可以运行程序并等待它们的输出或在后台触发它们并在它进入时处理它们的输出。扩展它以处理多个程序,你的程序成为外部资源的精细处理程序。你已经看到了它如何工作的机制,但你可以用它来设计更大更好的东西。

comments powered by Disqus