如何将STDOUT输出捕获为字符串?

51
puts "hi"
puts "bye"

我想将代码的标准输出(在这种情况下是 hi \nbye)存储到一个变量中,比如说‘result’,然后打印它。

puts result

我这样做的原因是将一个R代码集成到我的Ruby代码中,输出结果作为R代码运行时给予了STDOUT,但无法在代码中访问输出以进行一些评估。如果造成困惑,请见谅。因此,“puts result”一行应该给我“hi”和“bye”。


3
如果您只想获取外部程序的标准输出(stdout),可以使用result=%x{command}。否则,您可以像@codegnome展示的那样重定向stdio。 - SwiftMango
你正在使用 rinruby 吗?我也试图捕获 rinruby (R) 的输出,但目前没有成功。 - knut
可能是如何在Ruby中暂时重定向stderr?的重复问题。 - Ciro Santilli OurBigBook.com
这只是小问题,但是https://dev59.com/WW855IYBdhLWcg3wPBtx提到了`stderr`,而这个则是`stdout`。你可以将它们合并,但你需要搜索“capture stdout”和“capture stderr”来找到答案。 - fearless_fool
9个回答

84

一个将标准输出捕获到字符串的便捷函数...

下面这个方法是一个方便的通用工具,可以将标准输出捕获并以字符串形式返回。(我经常在单元测试中使用它,以验证是否打印了某些内容。)请特别注意使用ensure子句恢复$stdout(避免出现惊人的结果):

def with_captured_stdout
  original_stdout = $stdout  # capture previous value of $stdout
  $stdout = StringIO.new     # assign a string buffer to $stdout
  yield                      # perform the body of the user code
  $stdout.string             # return the contents of the string buffer
ensure
  $stdout = original_stdout  # restore $stdout to its previous value
end

例如:

>> str = with_captured_stdout { puts "hi"; puts "bye"}
=> "hi\nbye\n"
>> print str
hi
bye
=> nil

2
请注意,如果在您有机会重新分配之前,例如Logger捕获对原始$stdout的引用,则此方法不一定适用。 - David Moles
3
正是我所需要的,谢谢!顺便介绍一个很酷的 Ruby 技巧,如果你的 begin/end 匹配方法的持续时间,你可以跳过它们,只需将 ensure 放在方法的结尾即可(通常 ensure/rescue 与方法的 defend 在同一级别缩进)。 - Brian Underwood
@BrianUnderwood:没错,但仅仅因为你可以在Ruby中做某些事情,并不意味着你总是应该这样做 :). 我主要包含begin/end语句是为了让新手更容易理解。像您这样的老手会知道在这种情况下可以省略begin/end。 - fearless_fool
我同意有些事情你不该做,但如果这是更好的语法,即使对于初学者,我也会使用它,因为我不认为它容易混淆(而且我喜欢给人们“我不知道你可以这样做”的时刻,即使那不是代码的关键)。他们说教孩子复杂单词的最佳方法是不回避它们。 - Brian Underwood
太棒了!一个备注:我会使用 original_stdout,而不是 old_stdout - Artur INTECH
显示剩余2条评论

47

将标准输出重定向到StringIO对象

您可以将标准输出重定向到变量中。例如:

# Set up standard output as a StringIO object.
foo = StringIO.new
$stdout = foo

# Send some text to $stdout.
puts 'hi'
puts 'bye'

# Access the data written to standard output.
$stdout.string
# => "hi\nbye\n"

# Send your captured output to the original output stream.
STDOUT.puts $stdout.string

实际上,这可能不是一个好主意,但至少现在你知道它是可能的。


这正是我需要的..但它不起作用。你能再看一下吗?它会给出一个空白输出..即使puts和hi abd byt insdei也不会被打印出来。我已经包含了require stringio..感谢您的帮助.. - script_kiddie
1
不会被打印,因为您已将标准输出重定向到StringIO。使用 STDOUT.puts $stdout.string 来打印到原始输出流。 - Todd A. Jacobs
1
说实话,这是我唯一一个能够在不将结果输出到STDOUT的情况下调用Rake::Task["mytask"].invoke并捕获结果以供以后使用的方法。我使用ensure立即恢复了STDOUT,以避免任何问题。如果被误用,这可能是危险的,但是当适度使用时它非常有效。 - Joshua Pinter

11
如果您的项目中有activesupport,可以执行以下操作:
output = capture(:stdout) do
  run_arbitrary_code
end

关于 Kernel.capture 的更多信息可以在 这里 找到。


8
ActiveSupport的Kernel#capture已经被弃用,并将在未来的版本中移除,因为它不是线程安全的。请参见代码讨论 - Jakub Jirutka

9
您可以通过在反引号中调用R脚本来实现此操作,如下所示:
result = `./run-your-script`
puts result  # will contain STDOUT from run-your-script

想了解在Ruby中运行子进程的更多信息,请访问这个Stack Overflow问题


这个可以工作,但另一个解决方案才是我想要的。谢谢回复,非常感谢。 - script_kiddie

3
捕获Ruby代码和子进程的标准输出(或标准错误输出)
# capture_stream(stream) { block } -> String
#
# Captures output on +stream+ for both Ruby code and subprocesses
# 
# === Example
#
#    capture_stream($stdout) { puts 1; system("echo 2") }
# 
# produces
# 
#    "1\n2\n"
#
def capture_stream(stream)
  raise ArgumentError, 'missing block' unless block_given?
  orig_stream = stream.dup
  IO.pipe do |r, w|
    # system call dup2() replaces the file descriptor 
    stream.reopen(w) 
    # there must be only one write end of the pipe;
    # otherwise the read end does not get an EOF 
    # by the final `reopen`
    w.close 
    t = Thread.new { r.read }
    begin
      yield
    ensure
      stream.reopen orig_stream # restore file descriptor 
    end
    t.value # join and get the result of the thread
  end
end

我从Zhon得到了灵感。


1

对于大多数实际应用,您可以将任何响应writeflushsyncsync=tty?的内容放入$stdout中。

在此示例中,我使用了stdlib中修改过的Queue

class Captor < Queue

  alias_method :write, :push

  def method_missing(meth, *args)
    false
  end

  def respond_to_missing?(*args)
    true
  end
end

stream = Captor.new
orig_stdout = $stdout
$stdout = stream

puts_thread = Thread.new do
  loop do
    puts Time.now
    sleep 0.5
  end
end

5.times do
  STDOUT.print ">> #{stream.shift}"
end

puts_thread.kill
$stdout = orig_stdout

如果您想要在任务完成之后积极地对数据进行操作,就需要像这样的东西。使用StringIO或文件可能会在多个线程同时尝试同步读写时出现问题。

1

Minitest版本:


assert_output 如果你需要确保是否生成了某些输出:

assert_output "Registrars processed: 1\n" do
  puts 'Registrars processed: 1'
end

assert_output


如果你确实需要捕获它,可以使用capture_io

out, err = capture_io do
  puts "Some info"
  warn "You did a bad thing"
end

assert_match %r%info%, out
assert_match %r%bad%, err

capture_io

Minitest本身可在任何Ruby版本中使用,从1.9.3开始。


0

关于 RinRuby,请注意 R 有 capture.output

R.eval <<EOF
captured <- capture.output( ... )
EOF

puts R.captured 

-2

感谢@girasquid的回答。我将其修改为单文件版本:

def capture_output(string)
  `echo #{string.inspect}`.chomp
end

# example usage
response_body = "https:\\x2F\\x2Faccounts.google.com\\x2Faccounts"
puts response_body #=> https:\x2F\x2Faccounts.google.com\x2Faccounts
capture_output(response_body) #=> https://accounts.google.com/accounts

这是一种远程代码执行漏洞。 - Dorian

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