tl;dr If you want to run a shell command from Ruby and capture its stdout, stderr and return status, check out the Open3.capture3 method. If you'd like to process stdout and stderr data in a streaming fashion, check out Open3.popen3.

So many bad choices

There are literally 492 ways to execute shell commands from ruby and each of them works slightly differently. I bet you've used one of the approaches below. My go-to has always been the back-ticks (``).

exec("echo 'hello world'") # exits from ruby, then runs the command
system('echo', 'hello world') # returns the status code
sh('echo', 'hello world') # returns the status code
`echo "hello world"` # returns stdout
%x[echo 'hello world'] # returns stdout

But these approaches are pretty limited. Suppose that you need to capture not only your shell command's stdout, but also its stderr. You're just plain out of luck. Or suppose you'd like to process stdout data in a stream, and not all at once when the command finishes running? Out of luck.

There is another option. One that gives you the ability to run commands asynchronously, and which gives you stdout, stderr, exit codes, and PIDs. Let's check it out!

Open3

The oddly-named open3 module is part of Ruby's standard library. What does it do?

Open3 grants you access to stdout, stderr, exit codes and a thread to wait for the child process when running another program. You can specify various attributes, redirections, current directory, etc., of the program in the same way as for Process.spawn. (_Source: [Open3 Docs](http://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html))_

Never used it? Never even heard of it? I'm guessing that's because it doesn't come off as the most friendly of libraries. The name itself sounds more like C than Ruby. And the documentation is pretty hard-core neck-beard. But once you give it a try, you'll find that it's not as intimidating as it sounds.

capture3

What if there were an easy way to capture stdout, stderr AND the status code? Well there is. If you don't have time to read the rest of this article, just know that you can use a method called capture3 and call it a day.

Let's take a look at an example. Suppose you want to get a list of files in your current directory. To do that you can run the ls command.

If you were to use the back-tick syntax it's look like this:

puts(`ls`)

With capture3 it looks like so:

require 'open3'
stdout, stderr, status = Open3.capture3("ls")

This will run your command and give you stdout and stderr as strings. No muss no fuss.

Security

You generally don't want to give your users the ability to run arbitrary commands on your web server. That's why code like identify #{ params[:filename] } is such a horrible idea.

Open3 lets you avoid problems like this by separating commands from data. It works just like the system method.

Open3.capture3("identify", params[:filename], other_unsafe_params)

popen3

Under the hood, capture3 uses a much more powerful method called popen3. This method works a little differently than more familiar methods like system().

Here's what it looks like:

require 'open3'
Open3.popen3("ls") do |stdout, stderr, status, thread|
  puts stdout.read
end

It's kind of like when you open and read from a file.  I'm sure you've seen code like this:

File.open("my/file/path", "r") do |f|
  puts f.read
end

Pipes

With Open3, stdout and stderr are all pipes, which behave a lot like file buffers. And like files, they need to be closed when you're done with them. That's the reason for the block syntax. (There's a non-block syntax, but you have to manually call close on stdout,and stderr.)

The read method waits until the pipes are closed before returning a value. But pipes also support reading lines as they become available. Imagine your shell command takes a few seconds to run. During that time, it's printing a status message to stderr. You'd like to capture that and display it to your users.

Here's how you'd capture stderr a line at a time.

require 'open3'
Open3.popen3("sleep 2; ls") do |stdout, stderr, status, thread|
  while line=stderr.gets do 
    puts(line) 
  end
end

Threads

There's one argument we haven't talked about yet. That's thread.

The thread argument gives you a reference to a ruby thread that's waiting on your command to finish. Now, the command isn't running in the thread. It's running in an entirely separate process. The thread just watches the process and waits until it's done.

You can get some useful data from that thread reference though.

  • thread.pid - contains the process id of your shell command. You would need this if you wanted to do additional OS-level operations against that process.

  • thread.status - contains the exit status of the process. 1 or 0 for success or failure.

Caveats

From the Open3 docs:

You should be careful to avoid deadlocks. Since pipes are fixed length buffers,[::popen3](http://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-popen3)(“prog”) {|i, o, e, t| o.read } deadlocks if the program generates too much output on stderr. You should read stdout and stderr simultaneously (using threads or IO.select). However, if you don’t need stderr output, you can use [::popen2](http://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-popen2). If merged stdout and stderr output is not a problem, you can use [::popen2e](http://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-popen2e). If you really need stdout and stderr output as separate strings, you can consider [::capture3](http://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-capture3).

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required