Ruby管道:如何将两个子进程的输出连接在一起?

18

有没有一种自动化的方式在Ruby中进行shell管道操作?我正在尝试将以下shell代码转换为Ruby:

a | b | c... > ...

但目前我所找到的唯一解决方案就是自己进行缓冲区管理(简化、未经测试,希望表达我的意思):

a = IO.popen('a')
b = IO.popen('b', 'w+')
Thread.new(a, b) { |in, out|
    out.write(in.readpartial(4096)) until in.eof?
    out.close_write
}
# deal with b.read...

我想我正在寻找的是一种告诉popen使用现有流而不是创建新流的方法?或者,一个IO#merge方法来将a的输出连接到b的输入?当过滤器数量增加时,我的当前方法变得相当笨重。

我知道显然有Kernel#system('a | b'),但我需要以通用的方式混合Ruby过滤器和外部程序过滤器。


在Ruby中,使用spawn命令有一个解决方案。您应该打开几个管道,然后使用spawn选项重定向子进程的标准输入和标准输出。您可以在我的答案这里找到更详细的示例:http://stackoverflow.com/questions/11898528/marshal-ruby-pipes-sending-serialized-object-to-child-processes/13258047#13258047 - Pavel Chernov
Open3的管道似乎非常理想。https://ruby-doc.org/stdlib-2.4.1/libdoc/open3/rdoc/Open3.html#method-c-pipeline_rw - kch
5个回答

12

虽然这是一个老问题,但由于它在 Google 上排名靠前,因此我在这里给出答案:http://devver.wordpress.com/2009/10/12/ruby-subprocesses-part_3/(第 8 种方法)。

简而言之:

sh = Shell.new
sh.system("a") | sh.system("b") | sh.system("c")

而且您还可以做更复杂的事情,例如

sh.echo(my_string) | sh.system("wc") > "file_path"
xml = (sh.echo(html) | sh.system("tidy", "-q")).to_s

从 Ruby 2.7 开始,“shell” 不再是标准库,需要安装 gem。请参见 https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-released/(“自 2.6 以来的其他重要更改”部分)。 - knarewski

5

使用纯 Ruby,spawn 提供了重定向选项,您可以使用这些选项连接进程和管道。

1)创建一个管道

r,w = IO.pipe

2) 使用它来连接两个已生成的进程

spawn(*%w[echo hello world], out: w)
spawn(*%w[tr a-z A-Z], in: r)
# => HELLO WORLD

当然,您可以将此内容封装在Shell库中的sh.system之类的东西中,并创建一个|()方法来进行互连。

标准库的open3模块有一些非常好用的工具,包括创建完整管道。


3
如果abc是通常从命令行访问的命令,则可以使用以下命令:
captured_output = `a | b | c`

Ruby会在子shell中运行命令,并捕获标准输出。

如果你需要将输出路由到文件中,那么你可以将重定向添加到命令中。这种情况下,标准输出不会返回给你,但是你可以打开文件并手动处理它:

`a | b | c > captured_output`
File.foreach('captured_output') do |li|
  print li
end

虽然不如使用systempopen3那么灵活,但它很方便:

>> sin, sout, serr = Open3.popen3('ls -al | tail -1') #=> [#<IO:fd 4>, #<IO:fd 5>, #<IO:fd 7>, #<Thread:0x00000100bb8798 run>]
>> sout.read #=> "drwxr-xr-x   3 greg  staff    102 Nov  2 21:01 python\n"

我会使用大循环来代替使用线程,这是一个不好的问题的不好的解决方案。 - mpapis

-1

可悲的是,在 shell 中进行管道传输是一件严肃的事情,确实需要相当多的代码。不必生成带有读写循环的线程,但仍需要大量的工作。

我找到的最简单的例子是dash(Debian Almquist Shell)中的重定向实现。通常,如果您想在 Ruby 中执行相同操作,则需要使用 Ruby 的IO#dupIO#filenoIO#pipeIO#reopen等来复制这些fd操作技巧。使用 C .so 库为 Ruby 解释器重用 shell(例如 dash)代码可能比尝试仅使用 Ruby 原语组合相同的内容更容易。

我不知道是否存在任何现有的通用 Ruby API 用于复杂的进程间管道传输/重定向。如果您可以建议一个好的 API,您想要使用的,也许我可以参与实现。


-1

我知道这是一个超级老的问题,但我刚刚经历了这种痛苦。

基本上,我想出了一对实用方法,使用fork以及popen运行命令,并通过管道传递结果,实现类似于:

echo 'ABC12345 234234 24523' | wc | grep '1'

在Ruby中实现:

# Setup two pipes (the two we see in the shell command line above)
# plus one more to receive the result from.
pipe_pair1 = IO.pipe
pipe_pair2 = IO.pipe
pipe_pair_result = IO.pipe
pipe_in_out nil, pipe_pair1,  ['echo', 'ABC12345 234234 24523']
pipe_in_out pipe_pair1, pipe_pair2, ['wc']
pipe_in_out pipe_pair2, pipe_pair_result, ['grep', '1']

# Get the result from the final pipe
puts "Output: #{pipe_result(pipe_pair_result)}"

这些方法的代码在此Gist上:https://gist.github.com/philayres/c0d96cd263329e41fa84c2e3c7b9ae7b


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接