9 Ways to Run System Commands in Ruby

When to use which methods to run shell commands

Kirill Shevchenko
5 min readAug 26, 2024

Whether you need to execute shell commands, manage processes, or capture output, Ruby provides several methods to accomplish these tasks. There is a short breakdown of nine different ways to run system commands in Ruby, considering their pros and cons, with simple code examples to illustrate each method.

Spoiler: If you’re looking for a short answer on what to use in production for most cases, you can choose Open3.popen3.

source: https://stackoverflow.com/a/30463900

1. Backticks

One of the simplest ways to execute a system command in Ruby is by using backticks. This method captures the output of the command as a string.

output = `ls -l`
puts output

In this example, the ls-l command lists the files in the current directory in long format, and the output is stored in the output variable.

Pros:

  • Captures output directly as a string.

Cons:

  • Does not provide access to the exit status of the command.
  • Can lead to security issues if user input is not sanitized.

2. System

The system method executes a command in a subshell and returns true if the command was successful, or false if it failed. Unlike backticks, it does not capture the output.

result = system("mkdir new_folder")
puts "Folder created: #{result}"

In this example, the mkdir new_folder command creates a new directory, and the success of the operation is stored in the result variable.

Pros:

  • Returns the exit status of the command.
  • More safe than backticks for user input.

Cons:

  • Does not capture the command’s output.

3. Exec

The exec method replaces the current process with the command being executed. This means that the Ruby script will not continue after the command runs.

exec("echo 'Hello, World!'")

In this example, the echo command outputs “Hello, World!” to the console, and the Ruby script terminates after executing this command.

Pros:

  • Directly replaces the current process, which can be more efficient.
  • Useful for running commands that do not need to return to running script such as one time operations.

Cons:

  • Cannot return to the Ruby runtime after execution.

4. IO.popen

The IO.popen method opens a pipe to a command, allowing you to read from or write to it. This is useful for interacting with commands that require input or produce output.

IO.popen("grep 'test'", "w") do |pipe|
pipe.puts "This is a test."
pipe.puts "This is another line."
end

In this example, the grep command filters lines containing the word test. The input is sent to the command through the pipe.

Pros:

  • Allows for bidirectional communication with the command.
  • Can handle both input and output streams.

Cons:

  • Requires careful handling of input and output streams.

5. Open3

The Open3 module provides methods to run commands and capture their output, error messages, and exit status. This is particularly useful for more complex command executions.

require 'open3'

stdout, stderr, status = Open3.capture3('ls -l')
puts "Output: #{stdout}"
puts "Error: #{stderr}" if stderr
puts "Exit Status: #{status.exitstatus}"

In this example, the capture3 method captures the standard output, standard error, and exit status of the ls-l command.

This also one more method, Open3.popen3 which provides a more flexible approach by allowing you to interact with the subprocess's input, output, and error streams directly. It returns an array containing three IO objects (for stdin, stdout, and stderr) and the process status. This method is useful when you need to send input to the command or read output incrementally.

require 'open3'

Open3.popen3('ruby hello.py') do |stdin, stdout, stderr, wait_thr|
stdin.puts 'Alice'
stdin.close

puts "Output: #{stdout.read}"
puts "Error: #{stderr.read}" unless error.empty?
puts "Status: #{wait_thr.value.exitstatus}"
end

Pros:

  • Captures output, error messages, and exit status.
  • More control over command execution.

Cons:

  • More verbose than simpler methods.

6. Spawn

The spawn method creates a new process to run a command. It returns the process ID (PID) of the spawned process, allowing for more advanced process management.

pid = spawn('sleep 5')
puts "Spawned process ID: #{pid}"

In this example, the sleep 5 command runs in the background, and the PID of the spawned process is printed.

Pros:

  • Allows for asynchronous execution of commands.
  • Can manage multiple processes simultaneously.

Cons:

  • More complex to manage than synchronous methods.
  • Requires additional handling for process termination and output.

7. %x

The %x syntax is an alternative for backticks, allowing you to execute system commands and capture their output as a string.

output = %x(ls -l)
puts output

This example is functionally identical to using backticks, capturing the output of the ls -l command.

Pros:

  • Concise and easy to read.
  • Captures output directly as a string.

Cons:

  • Does not provide access to the exit status of the command.
  • Can lead to security issues if user input is not sanitized.

8. Fork

The fork method creates a child process that can run concurrently with the parent process. This is useful for running commands in the background while continuing execution in the main program.

pid = fork do
exec("echo 'Running in the background'")
end
Process.wait(pid)
puts "Child process finished."

In this example, the echo command runs in a child process, and the parent process waits for it to finish.

Pros:

  • Allows for concurrent execution of commands.
  • Can manage child processes effectively.

Cons:

  • More complex than other methods.
  • Requires careful management of process termination.

9. PTY.open

The PTY.open method allows you to create a pseudo-terminal, which can be useful for running commands that require a terminal interface. This method is particularly beneficial when you need to interact with commands that expect a terminal environment, such as ssh or top.

require 'pty'

PTY.spawn('bash') do |stdout, stdin, pid|
stdin.puts "echo 'Hello from PTY!'"
stdin.puts 'exit'
stdout.each { |line| puts line }
end

In this example, we spawn a new bash shell and send commands to it through the pseudo-terminal. The output is captured and printed to the console.

Pros:

  • Allows interaction with commands that require a terminal.
  • Can handle both input and output streams.

Cons:

  • More complex to set up than other methods.
  • Requires careful handling of input and output streams.

Summary

While methods like backticks and system are simple and easy to use, they may not provide the level of control needed for more complex tasks, but still good for one time operations in scripting. On the other hand, Open3.popen3 offer a strong preference for production environments due to their ability to capture output, error messages, and exit statuses effectively.

--

--

Kirill Shevchenko

Software Engineer. Interested in Microservices, Distributed Systems and Cloud Services.