Sonntag, 12. Oktober 2014

Tell shell scripting apart in Ruby!

Ruby scripting for the shell provides multiple ways, depending on the use case.

Kernel#system

The simplest way to fire a shell command in a Ruby script is to send a message to Kernel#system.
In a nutshell:
  1. the command is assigned as a String
  2. the first parameter is the command itself and all following string parameters are the arguments
  3. executes the command in a subshell
  4. is a blocking operation (waits until the result of operation completes)
  5. returns true (for zero exist status), false (for non zero exit status) or nil (if command execution fails)
  6. The commands result is not available for further processing
  7. captures exceptions raised from the child process
  8. the error status (exception) is available in $?, which returns a Process::Status object
Some examples:
system("find *.rb")
# script.rb
# => true
file_name = "script"
system("find", "#{file_name}.rb")
# script.rb
# => true
system("find", "*.rb")
# find: "*.rb": No such file or directory
# => false
$?
# => #<Process::Status: pid 4438 exit 1>
Please note the third command (and compare with the first command): Assigning wild card as a parameter does not work properly.
Kernel#system is great for simple system calls, if the result (e.g. the found files list) is not needed for further processing.

Kernel#exec

Kernel#exec works similar to Kernel#system, with an essential difference: it ends the current process by running the given external command. Running the script.rb:
exec("find *.rb")
puts "End"
will print out script.rb, but not "End". Another example, using IRB:
christian@Trinidad:~$ irb
2.1.1 :001 > exec("date")
Sun, 12 Oct 2014 06:40:23 CEST
christian@Trinidad:~$
exits IRB (the current process), processes the command and returns to bash.

%x() literal

An interpolated shell command can be achieved with the %x() literal as an alternative to the backtick style.
In a nutshell:
  1. the command can be as assigned as a String or even without string notation
  2. executes the command in a subshell
  3. is a blocking operation (waits until the result of operation completes)
  4. returns the command result
  5. raises exception caused by the child process error
  6. the error status (exception) is available in $?, which returns a Process::Status object (with qualified exit status)
Some examples:
%x[find *.rb]
# => "script.rb"
file_name = "script"
%x["find #{file_name}.rb"]
# => script.rb
Time.parse %x[date].chop
# => 2014-10-10 02:23:10 +0200
%x[foo]
# => Errno::ENOENT: No such file or directory - foo
$?
# => #<Process::Status: pid 5052 exit 127>
for comparison reasons, the same stuff with the backtick notation:
`find *.rb`
# => "script.rb"
file_name = "script"
`find #{file_name}.rb`
# => script.rb
Time.parse `date`.chop
# => 2014-10-10 02:23:10 +0200
`foo`
# => Errno::ENOENT: No such file or directory - foo
$?
# => #<Process::Status: pid 5055 exit 127>
The backtick notation is widely used but can be mixed up with String and therefore is not as readable (and intentional) as the %x literal.

Open3#popen3

Another level of granularity is the Open3#popen3.
It allows to deal with the input, output, error and even the wait thread.
  1. the command is assigned as a String
  2. executes the command in a subshell
  3. can be a non blocking operation (run other commands while processing the thread)
  4. full control over the thread
  5. full control over the stdin, stdout and stderr stream
A script.rb could look like:
stdin, stdout, stderr, wait_thr = Open3.popen3('ping www.google.com -c 3')
puts "Thread alive? #{wait_thr.alive?}"
puts "Some calculation: #{1 + 1}"
puts "Output: " + stdout.read
puts "Thread alive? #{wait_thr.alive?}"
# => Thread alive? true
# Some calculation: 2
# PING www.google.com (74.125.136.103) 56(84) bytes of data.
# 64 bytes from ea-in-f103.1e100.net (74.125.136.103): icmp_seq=1 ttl=46 time=26.8 ms
# 64 bytes from ea-in-f103.1e100.net (74.125.136.103): icmp_seq=2 ttl=46 time=24.7 ms
# 64 bytes from ea-in-f103.1e100.net (74.125.136.103): icmp_seq=3 ttl=46 time=23.2 ms
# --- www.google.com ping statistics ---
# 3 packets transmitted, 3 received, 0% packet loss, time 2001ms
# rtt min/avg/max/mdev = 23.229/24.913/26.804/1.477 ms
# Thread alive? false
Please note, that the stdout.read output waited until the thread was successfully ended. That is why there is 2 seconds gap between the calculations output and the stdout.read.
All 3 stdxxx are IO objects. The fourth wait_thr is a Thread object.
Open3#popen3 is the way to deal with system calls, when advanced processing is needed. Especially the full control over its streams is very powerful.
Further articles of interest:

Supported by Ruby 2.1.1