In Progress
Unit 1, Lesson 21
In Progress

Subprocesses Part 1: Basics

Video transcript & code

More than many other general purpose programming languages, Ruby is often used to "glue" other programs together. And in order to fulfill this need, Ruby comes with a rich-set of tools for spawning and communicating with external subprocesses.

We've touched on these tools from time to time in various episodes. But given that Ruby provides an almost bewildering array of options for managing subprocesses, I thought it might be helpful to cover these tools in a more systematic and comprehensive fashion.

Today, we start with the basics.

Let's say we want to pop up a message on the screen of this linux machine.

To do this from the command line, we'd use the notify-send command.

$ notify-send "Hello there!"

This is an example of perhaps the simplest use case for starting a subprocess. We just want to fire off this command for its side effects. We don't care about its output. We don't care about its exit status. We aren't worried about it running for a long time.

For cases like this, in Ruby we use the system method.

system "notify-send 'Hello there!'"

system is a Kernel method, so it's available everywhere. We give it a command, and it runs the command, simple as that.

notify-send sysIt blocks waiting for the command to finish, so if we have a long-running command it's probably not the best choice.

system "sleep 3"
puts "done"

# >> done

What about output? system just shares its standard output stream with the parent process. Meaning that anything the command outputs is just going to show up mixed in with whatever our Ruby script outputs.

puts "Here's who is logged in:"
system "who"

# >> Here's who is logged in:
# >> avdi     tty7         2016-04-25 09:22 (:0)

The same goes for the standard error stream.

puts "Here comes some STDERR..."
system "ls /NOTEXIST"

# >> Here comes some STDERR...

# !> ls: cannot access '/NOTEXIST': No such file or directory

Of course, we could always use shell redirection in the command itself to, for instance, re-route standard error to the null device.

system "ls /NOTEXIST 2>/dev/null"

As we learned in episode #389, under the covers Ruby auto-detects this shell syntax and switches from running the bare command, to running it within the system default shell.

But whenever we do this, we tie our code more closely to a particular operating system and shell program. For this and other reasons, if we find ourselves wanting to do fancy I/O redirection we may be better served using one of Ruby's other subprocess utilities that we'll be talking about in upcoming episodes.

Another feature we saw in episode #389 is that we can force the method to never invoke a subshell as an intermediary by feeding it a command as an array instead of as a single string.

For instance, this command comes out strangely due to it being run in an implicit subshell, and the resulting shell variable interpolation.

system "echo Time to make some $$!"

# >> Time to make some 19638!

We avoid these kinds of effects by separating the program and its arguments into separate strings.

system "echo", "Time to make some $$!"

# >> Time to make some $$!

In addition to controlling whether a subshell is invoked, we can also control the command's environment variables. For instance, here's a command that changes directory to the current user's home directory, then prints the directory it is in.

system "cd ~; pwd"

# >> /home/avdi

When we add a hash as the first argument to system, mapping the HOME environment variable to /tmp, and then run the command again, we get a different result:

system({"HOME" => "/tmp"}, "cd ~; pwd")

# >> /tmp

system also supports a large number of special options specified in a hash after the command.

Just as an example, we could redirect all output to /dev/null without using shell redirection, like this:

puts "before"
system("ls", [:out, :err] => "/dev/null")
puts "after"

There are a huge number of options supported by this form of the call. They are inherited from Ruby's Process.spawn method, and we'll talk about them in much greater detail in upcoming episodes.

There's one other type of command that system is particularly good for.

Let's say we want to execute a git commit command from a script, and we need to know if it succeeded.

system returns false if the command it tried to execute exited with an error status.

system "git commit -m 'checkpoint'" # => false

# !> fatal: Not a git repository (or any of the parent directories): .git

Conversely, it returns true if the process exited indicating success.

system "cd ~; pwd"                    # => true

# >> /home/avdi

This makes it really easy to use system as part of an if/else construct switching on whether the command succeeded or not.

if system "git commit -m 'checkpoint'"
  # ...
else
  # ...
end

While system simplifies the process exit status down to a boolean value, that doesn't mean the original exit status is lost. As long as the command is the last one executed, we can get at its status using the $? special variable.

system "git commit -m 'checkpoint'" # => false

$?                              # => #<Process::Status: pid 20905 exit 128>

# !> fatal: Not a git repository (or any of the parent directories): .git

If we require the English module, we can use the alternative alias $CHILD_STATUS to get at the same information.

require "English"
system "git commit -m 'checkpoint'" # => false

$?                         # => #<Process::Status: pid 32330 exit 128>
$CHILD_STATUS              # => #<Process::Status: pid 32330 exit 128>

# !> fatal: Not a git repository (or any of the parent directories): .git

And that about wraps it up for the system method. I hope you've learned something new about its capabilities and behavior today. More importantly, I hope you have a sense now of what problems system is best suited for. system is ideal for firing off short-running subprocesses where we either don't care about the results, or where we only care whether they succeeded or failed. For these applications, system is the right choice. For more complex or more specialized subprocess scenarios, Ruby has other, better facilities. And we'll learn about them, in future episodes.

Happy hacking!

Responses