在Ruby中获取system()调用的输出

Getting output of system() calls in Ruby

如果我使用Ruby中的内核系统调用一个命令,如何获得它的输出?

1
system("ls")


我想扩大和澄清一下混乱的答案。

如果用反勾号包围命令,那么根本不需要(显式)调用System()。backticks执行命令并以字符串形式返回输出。然后可以将该值赋给变量,如下所示:

1
2
output = `ls`
p output

1
printf output # escapes newline chars


请注意,将包含用户提供值的字符串传递给system%x[]等的所有解决方案都是不安全的!不安全实际上意味着:用户可以触发代码在上下文中运行,并拥有程序的所有权限。

据我所知,在Ruby1.8中,只有systemOpen3.popen3提供了一个安全/转义变量。在Ruby1.9中,IO::popen还接受一个数组。

只需将每个选项和参数作为数组传递给其中一个调用。

如果您不仅需要退出状态,还需要使用Open3.popen3的结果:

1
2
3
4
5
6
7
require 'open3'
stdin, stdout, stderr, wait_thr = Open3.popen3('usermod', '-p', @options['shadow'], @options['username'])
stdout.gets(nil)
stdout.close
stderr.gets(nil)
stderr.close
exit_code = wait_thr.value

请注意,块表单将自动关闭stdin、stdout和stderr,否则必须显式关闭它们。

这里的更多信息:在Ruby中形成卫生外壳命令或系统调用


对于记录,如果您同时需要(输出和操作结果),可以执行以下操作:

1
output=`ls no_existing_file` ;  result=$?.success?


正确和安全地执行此操作的直接方法是使用Open3.capture2()Open3.capture2e()Open3.capture3()

如果与不受信任的数据一起使用,在任何情况下,使用Ruby的backticks和它的%x别名都是不安全的。这是危险的、朴素的和简单的:

1
2
3
4
5
6
7
8
untrusted ="; date; echo"
out = `echo #{untrusted}`                              # BAD

untrusted = '"; date; echo"'
out = `echo"#{untrusted}"`                            # BAD

untrusted ="'; date; echo'"
out = `echo '#{untrusted}'`                            # BAD

相反,如果正确使用system函数,则会正确地转义参数:

1
2
ret = system"echo #{untrusted}"                       # BAD
ret = system 'echo', untrusted                         # good

问题是,它返回退出代码而不是输出,捕获后者是复杂和混乱的。

到目前为止,这个线程中最好的答案提到了open3,但没有提到最适合该任务的函数。Open3.capture2capture2ecapture3的工作方式与system类似,但返回两个或三个参数:

1
2
3
4
5
out, err, st = Open3.capture3("echo #{untrusted}")     # BAD
out, err, st = Open3.capture3('echo', untrusted)       # good
out_err, st  = Open3.capture2e('echo', untrusted)      # good
out, st      = Open3.capture2('echo', untrusted)       # good
p st.exitstatus

另一个提到了IO.popen()。语法可能很笨拙,因为它希望数组作为输入,但它也可以工作:

1
out = IO.popen(['echo', untrusted]).read               # good

为了方便起见,您可以将Open3.capture3()包装在一个函数中,例如:

1
2
3
4
5
6
7
8
9
10
#
# Returns stdout on success, false on failure, nil on error
#
def syscall(*cmd)
  begin
    stdout, stderr, status = Open3.capture3(*cmd)
    status.success? && stdout.slice!(0..-(1 + $/.size)) # strip trailing eol
  rescue
  end
end

例子:

1
2
3
4
5
6
p system('foo')
p syscall('foo')
p system('which', 'foo')
p syscall('which', 'foo')
p system('which', 'which')
p syscall('which', 'which')

生成以下内容:

1
2
3
4
5
6
7
nil
nil
false
false
/usr/bin/which         <— stdout from system('which', 'which')
true                   <- p system('which', 'which')
"/usr/bin/which"       <- p syscall('which', 'which')


根据需要的结果类型,可以使用System()或%x[]。

如果找到命令并成功运行,则返回true,否则返回false。

1
2
3
4
5
6
7
>> s = system 'uptime'
10:56  up 3 days, 23:10, 2 users, load averages: 0.17 0.17 0.14
=> true
>> s.class
=> TrueClass
>> $?.class
=> Process::Status

%另一方面,x[..]将命令结果保存为字符串:

1
2
3
4
5
6
7
8
>> result = %x[uptime]
=>"13:16  up 4 days,  1:30, 2 users, load averages: 0.39 0.29 0.23
"

>> p result
"13:16  up 4 days,  1:30, 2 users, load averages: 0.39 0.29 0.23
"

>> result.class
=> String

Jay Fields的博客文章详细解释了使用System、Exec和%x的区别。


如果需要转义参数,在Ruby1.9io.popen中还接受一个数组:

1
p IO.popen(["echo","it's escaped"]).read

在早期版本中,您可以使用open3.popen3:

1
2
3
require"open3"

Open3.popen3("echo","it's escaped") { |i, o| p o.read }

如果您还需要通过stdin,那么这应该在1.9和1.8中都有效:

1
2
3
4
5
6
out = IO.popen("xxd -p","r+") { |io|
    io.print"xyz"
    io.close_write
    io.read.chomp
}
p out #"78797a"


另一种方法是:

1
2
f = open("|ls")
foo = f.read()

注意,这是"ls"前面的"pipe"字符。这也可用于将数据输入程序的标准输入并读取其标准输出。


使用倒计时:

1
`ls`


如果您需要返回值,我发现以下内容很有用:

1
2
result = %x[ls]
puts result

我特别想在我的机器上列出所有Java进程的PID,并使用它:

1
ids = %x[ps ax | grep java | awk '{ print $1 }' | xargs]


正如Simon H_rlimann已经解释的那样,open3比backticks等更安全。

1
2
require 'open3'
output = Open3.popen3("ls") { |stdin, stdout, stderr, wait_thr| stdout.read }

请注意,块表单将自动关闭stdin、stdout和stderr,否则必须显式关闭它们。


当你真正想要的往往是使用backticks或popen时,它实际上并没有回答你提出的问题。捕获system输出(可能用于自动测试)可能是有正当理由的。有一点谷歌搜索到了一个答案,我想我会为了其他人的利益张贴在这里。

由于我在测试时需要这样做,因此我的示例使用块设置来捕获标准输出,因为实际的system调用隐藏在被测试的代码中:

1
2
3
4
5
6
7
8
9
10
11
require 'tempfile'

def capture_stdout
  stdout = $stdout.dup
  Tempfile.open 'stdout-redirect' do |temp|
    $stdout.reopen temp.path, 'w+'
    yield if block_given?
    $stdout.reopen stdout
    temp.read
  end
end

此方法使用tempfile来存储实际数据,捕获给定块中的任何输出。示例用法:

1
2
3
4
captured_content = capture_stdout do
  system 'echo foo'
end
puts captured_content

您可以将system调用替换为任何内部调用system的调用。如果需要,也可以使用类似的方法来捕获stderr


如果希望使用Kernel#system将输出重定向到文件,可以这样修改描述符:

在附加模式下将stdout和stderr重定向到文件(/tmp/log):


system('ls -al', :out => ['/tmp/log', 'a'], :err => ['/tmp/log', 'a'])

对于长时间运行的命令,这将实时存储输出。您还可以使用io.pipe存储输出,并将其从内核系统重定向。


作为直接系统(…)替换,您可以使用open3.popen3(…)

进一步讨论:http://tech.natemurray.com/2007/03/ruby-shell-commands.html


我在这里没有找到这个,所以加上它,我在获得完整输出时遇到了一些问题。

You can redirect STDERR to STDOUT if you want to capture STDERR using
backtick.

output = `grep hosts /private/etc/* 2>&1`

来源:http://blog.bigbinary.com/2012/10/18/backtick-system-exec-in-ruby.html


1
2
3
4
5
6
puts `date`
puts $?


Mon Mar  7 19:01:15 PST 2016
pid 13093 exit 0