Running External Programs#

Backticks#

The simplest way to run external command is to surround it with backticks, exactly like in the Shell scripts. This will return string containing stdout of the command. You can use expression substitution #{} inside a backtics, just like in strings.

`pwd`
#=> "/Users/turbo\n"   # note ls returns string ending with a newline
`pwd`.chomp        # so it is good to get rid of it
#=> "/Users/turbo"

path = '/etc'
#=> "/etc"
`ls #{path}`               # you can use expression subsitution
#=> "afpovertcp.cfg\naliases\naliases.db\napache2\..."
`ls #{path}`.split(/\n/)   # convert the output to an array
#=> ["afpovertcp.cfg", "aliases", "aliases.db", "apache2"]

`ls /tmmp`   # non-existing dir
ls: /tmmp: No such file or directory
#=> ""           # stdout is empty, stderr is not passed (but displayed above)

Exactly like in the Shell, you may get the return code of the last command. There is a pre-defined global variable $? for that, but unlike in the Shell it returns not only the return code, but the whole Process::Status objects. You can get the exit code integer using existstatus method or just check if the command ends with success?:

`ls /nonexistent`
ls: /nonexistent: No such file or directory
#=> ""
$?
#=> #<Process::Status: pid 98527 exit 1>
$?.pid
#=> 98527
$?.exitstatus
#=> 1
$?.success?
#=> false

Digression: Other Pre-defined Globals#

There is more pre-defined global variables and constants in Ruby. For example, $ contains the PID of the current process, $0 is a name of the process. There are “last seen” globals, like $! - last exception raised or $~ - last match (equivalent to Regexp.last_match).

$$     # current PID
#=> 98108

$0     # process name
#=> "irb"

I would recommend to avoid using this globals. Most of them can be replaced by more human-readable construct. The other reason is that in multi-threaded applications using globals may provide confusion: for example, another thread may run external program and overwrite the $0 before your read it.

exec#

The Kernel.exec method replaces the current process with another one. In the other words, it terminates the current process and runs the other one. Try to run it in IRB:

exec 'ls', '-l', '/etc'  # or Kernel.exec 'ls -l /etc'
lrwxr-xr-x@ 1 root  wheel  11 Oct 23  2013 /etc -> private/etc
$

system#

The Kernel.system command runs the specified program, wait for it to finish and returns true if the command exits with status zero, false when it exit with the other status and nil if the execution fails.

system('sleep 10; pwd')  # wait 10 seconds before
/Users/turbo
#=> true

spawn#

Kernel.spawn runs specified command in a background. It returns the PID of the process.

spawn('sleep 10; pwd')
#=> 869                 # pid of the background process
/Users/turbo            # shows on the console after 10 seconds

System, exec and spawn Additional Options#

This three methods described above takes more options than the command name - the full syntax is spawn([env,] command… [,options]).

The env is a hash with the environment variable set to this process. Use nil if you want to unset the environment variable. The options hash contains additional flags, like :unsetenv_others to clear up all the environment variables, :umask to set up the command umask or :chdir to start the command in the different directory.

See the full description of the options in ri Kernel.spawn documentation.

system({'ORACLE_SID' => 'testdb'}, 'env|grep ORACLE')
ORACLE_SID=testdb
ORACLE_HOME=/oracle/ohome/
#=> true

system({'ORACLE_SID' => 'testdb', 'ORACLE_HOME' => nil}, 'env|grep ORACLE')
ORACLE_SID=testdb
#=> true

system({'ORACLE_SID' => 'testdb'}, 'env', unsetenv_others: true)
ORACLE_SID=testdb
#=> true

system('umask')  # default system umask
0022
#=> true
system('umask', umask: 0777)  # set it up to octal 0777, then run 'umask'
0777
#=> true

system('pwd', chdir: '/')   # run 'pwd' in root
/
#=> true

By default the command inherits all the I/O descriptors opened by Ruby. Option :close_others => true forces closing all non standard (all except stdout, stdin and stderr) file descriptors.

You can redirect std* as well:

spawn('ls /this_will_fail', err: '/dev/null')  # silent error, stderr redirected to /dev/null
#=> 2042

spawn('sleep 2; date; sleep 2; date', out: '/tmp/test')  # like '> /tmp/test'
#=> 2095

open#

From the previous chapters we know how to open a file for reading and writing. The Kernel.open(filename) method is more general than File.open(filename) - when the filename begins with the pipe, it treats the rest of filename as a command to run (otherwise it tries just to open file). It returns IO object, so you can use it just like a normal file: read methods would have an access to the processes stdout and write command to its stdin.

Remember you can read the process stdout only once; there is no way to rewind or set seek position. And it is very important to close this descriptor after use, because in the other case the command will not finish:

f = open('|ls /etc')  # run 'ls /etc' and create IO object for reading
#=> #<IO:fd 7>

f.readlines           # read just like from the ordinary file
#=> ["afpovertcp.cfg\n", "aliases\n", "aliases.db\n", ...]

f.rewind              # but no way to rewind
Errno::ESPIPE: Illegal seek

# 'ls' still exists in a process table
puts `ps -ef|grep #{Process.pid}`   # Process.pid is a PID of the current process
  506  3031 99339   0  8:46PM ttys001    0:00.12 irb
  506  3056  3031   0  8:46PM ttys001    0:00.00 (ls)
  506  3092  3031   0  8:48PM ttys001    0:00.01 sh -c ps -ef|grep 3031
  506  3094  3092   0  8:48PM ttys001    0:00.00 grep 3031
#=> nil

f.close               # closing the IO object
#=> nil

# 'ls' finished
puts `ps -ef|grep #{Process.pid}`
  506  3031 99339   0  2:46PM ttys001    0:00.13 irb
  506  3256  3031   0  2:53PM ttys001    0:00.00 sh -c ps -ef|grep 3031
  506  3258  3256   0  2:53PM ttys001    0:00.00 grep 3031
#=> nil

Open3.popen3#

Kernel.open is useful, but what if we want to read stderr as well as stdout of the process? In this case we can use Open3 object. Its method popen3 spawns the process in a background and returns a descriptors for stdin, stdout and stderr.

Notice that you can control popen3 with the same options as Kernel.spawn.

require 'open3'  # must load Open3 object first
#=> true

sin, sout, serr = Open3.popen3('ls /etc')
#=> [#<IO:fd 10>, #<IO:fd 12>, #<IO:fd 14>, #<Thread:0x007ff3e42b1440 sleep>]
sout.readline   # read like from the ordinary file
#=> "aliases\n"
sout.readline
#=> "aliases.db\n"

sin, sout, serr = Open3.popen3('ls /nonexists/')
#=> [#<IO:fd 10>, #<IO:fd 12>, #<IO:fd 14>, #<Thread:0x007ff3e588a3c0 sleep>]
serr.readlines   # reads from sederr
#=> ["ls: /nonexists/: No such file or directory\n"]

Open4 or How to Run Sudo Programs in Ruby#

What if we want to run some command with root privilages using sudo? In this case Open3 could be useful, because it is giving an access to the commands stdin. But there is more convinient way to deal with this: open4 gem. Install it with gem install open4.

require 'open4'

Open4::popen4("sudo -S whoami") do |pid, stdin, stdout, stderr|
  stdin.puts "your password, bad hardcoded in the script"
  stdin.close
  puts stdout.read.strip
end

This way is easier beacuse when running in block, popen4 spawns the external command synchronously - waiting for it to finish (where popen3 runs it at the background).

Note we had to use -S switch for sudo to force it to read the password from stdin. By default, sudo reads it directly from the terminal.