3

I'm making a Ruby wrapper around a CLI. And I found a neat method, Open3.capture3 (which internally uses Open3.popen3), which lets me execute commands and captures stdout, stderr and exit code.

One thing that I want to detect is if the CLI executable wasn't found (and raise a special error for that). I know that the UNIX shell gives exit code 127 when command wasn't found. And when I execute $ foo in bash, I get -bash: foo: command not found, which is exactly the error message I want to display.

With all that in mind, I wrote code like this:

require "open3"

stdout, stderr, status = Open3.capture3(command)
case status.exitstatus
when 0
  return stdout
when 1, 127
  raise MyError, stderr
end

But, when I ran it with command = "foo", I get an error:

Errno::ENOENT: No such file or directory - foo
  /Users/janko/.rbenv/versions/2.1.3/lib/ruby/2.1.0/open3.rb:193:in `spawn'
  /Users/janko/.rbenv/versions/2.1.3/lib/ruby/2.1.0/open3.rb:193:in `popen_run'
  /Users/janko/.rbenv/versions/2.1.3/lib/ruby/2.1.0/open3.rb:93:in `popen3'
  /Users/janko/.rbenv/versions/2.1.3/lib/ruby/2.1.0/open3.rb:252:in `capture3'

Why does this error occur? I thought Open3.capture3 was supposed to execute that command directly in the shell, why then don't I get a normal STDERR and exit code of 127?

Janko
  • 8,985
  • 7
  • 34
  • 51

1 Answers1

10

Open3.popen3 delegates to Kernel.spawn, which depending on the way the command is passed in, gives the command to shell or directly to OS.

commandline                 : command line string which is passed to the standard shell
cmdname, arg1, ...          : command name and one or more arguments (This form does not use the shell. See below for caveats.)
[cmdname, argv0], arg1, ... : command name, argv[0] and zero or more arguments (no shell)

We might expect that if we call Kernel.spawn("foo"), it would be passed to the shell (and not OS). But it doesn't, documentation for Kernel.exec explains why:

If the string from the first form (exec("command")) follows these simple rules:

* no meta characters
* no shell reserved word and no special built-in
* Ruby invokes the command directly without shell

You can force shell invocation by adding ";" to the string (because ";" is a meta character).

Last paragraph reveals the solution.

require "open3"

stdout, stderr, status = Open3.capture3(command + ";")
case status.exitstatus
when 0
  return stdout
when 1, 127
  raise MyError, stderr
end
Janko
  • 8,985
  • 7
  • 34
  • 51
  • Old but valid. My addition if you want to add parameters include them in the command e.g. `Open3.capture3('ld --version' + ';')`, if you want to add it like this it will not work `Open3.capture3('ld', '--version' + ';')` which is the recommended way to add parameters. – tukan Aug 11 '17 at 10:09