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.