Multiple ways to execute a shell command via Ruby

Running shell commands within your Ruby applications can greatly enhance their functionality. Whether you need to interact with system processes, run scripts, or automate tasks, Ruby offers several methods. In this article, we will look at different approaches to running shell commands from Ruby, their pros and cons, and provide you with a comprehensive guide to choosing the right method for your needs.

Using the system method

The system method is a straightforward way to run shell commands from Ruby. It allows you to execute external commands and display their output directly in your application.

files = system('ls', '-l')
puts "cmd: #{files}" # => true


Pros:

  • Simple and easy to use.
  • Can handle complex commands with arguments and options.


Cons:

  • Runs the command synchronously, which may cause your Ruby application to lock up until the command completes.
  • Lacks finer control over input/output streams.

Using the backtick operator (`)

The backtick operator, also known as the backquote operator, allows you to execute shell commands and capture their output as a string.

files = `ls -l .`
puts files # => "total 3056\n-rw-r--r--..."

# you can also use x%{ command }
files = %x{ ls -l . } # => "total 3056\n-rw-r--r--..."

# prints filenames
puts `ls`.lines.map { |name| name.strip } 

Pros:

  • Provides a convenient way to capture command output.
  • Allows you to use the captured output within your Ruby code.
  • Supports interpolation of Ruby variables in command strings.

Cons:

  • Similar to the system method, it executes the command synchronously, potentially blocking your application.
  • Doesn't provide fine-grained control over input/output streams.
  • May pose security risks if the command contains untrusted user input.

Using the exec method

The exec method replaces the current process with the specified external command, effectively ending the execution of your Ruby script.

files = exec 'ls' # execute and exit


Pros:

  • Immediately passes control to the external command, making it suitable for scenarios where you want the command to take over completely.
  • Saves system resources by terminating the Ruby process.

Cons:

  • The remaining Ruby code after the exec call is not executed.
  • Doesn't capture the output of the command within Ruby.
  • Difficult to manage if you need to continue running Ruby code after the command has finished.

Leveraging the IO Class

Ruby's IO class provides a powerful way to execute shell commands and control their input/output streams. It allows you to programmatically read from and write to the command's I/O channels.

command = 'ls -l'

output = IO.popen(command)
stdout = output.read
output.close

puts "Output:\n#{stdout}" # => "Output: total 3056..."


Pros:

  • Provides fine-grained control over input/output streams.
  • Supports asynchronous execution, allowing your Ruby application to continue processing while the command is running.
  • Allows you to capture command output and handle errors more flexibly.

Cons:

  • Requires additional code to handle input/output streams.
  • May have a steeper learning curve than simpler methods such as system or backticks (`).
  • Requires careful management of resource cleanup and error handling.

Run command via spawn

This command executes the specified command and returns the process ID as output.

pid = spawn("ls -l")
Process.wait pid # => 29813

This will not wait for the process, so we need to add Process.wait

Pros:

  • Supports asynchronous execution

Cons:

  • Lack of control - we only have PID as the output

Most powerful: Open3 popen3

In addition to the previously mentioned methods, another powerful approach to executing shell commands in Ruby is to use the popen3 method from the Open3 module. This method allows you to execute commands and gain fine-grained control over input/output streams, including writing to the command's standard input, reading from its standard output, and capturing any error messages.

The popen3 method takes the command to execute as its first argument cmd and allows you to specify additional arguments args to pass to the command. This flexibility makes it suitable for running commands with complex arguments or options.

require 'open3'

command = 'grep -i ruby'
input_text = "Hello, Ruby!\nRuby programming.\nNext topic should be skipped."

stdin, stdout, stderr, wait_thr = Open3.popen3(command)

# Write input_text to command's standard input
stdin.puts input_text
stdin.close
# Read output from command's standard output
output = stdout.read
# Read error messages from command's standard error
errors = stderr.read
# Wait for the command to complete and obtain its exit status
exit_status = wait_thr.value

if exit_status.success?
  puts "Output: #{output}" # => Output: Hello, Ruby!
else
  puts "Execution failed!"
  puts "Errors: #{errors}"
end

Pros:

  • Fine-grained control: Open3.popen3 provides fine-grained control over input/output streams, allowing you to interact with the command's standard input, read from its standard output, and capture error messages from its standard error stream. This level of control is useful if you need to perform complex interactions with the command or handle different types of data streams.
  • Flexibility with arguments: Open3.popen3 allows you to pass arguments (args) to the command being executed. This flexibility is useful when you need to pass complex arguments or options to the command, allowing you to effectively handle different command line scenarios.
  • Error handling: The Open3.popen3 method provides access to the command's standard error stream, allowing you to capture and handle any error messages generated by the command. This is particularly valuable when you need to distinguish between successful and failed command execution.

Cons:

  • Complexity: Working with Open3.popen3 introduces additional complexity compared to simpler methods such as system, backticks or IO class. You need to manage multiple input/output streams and handle error streams separately, which may require more advanced knowledge of Ruby's stream handling and process management.
  • Learning curve: Using Open3.popen3 effectively may require a deeper understanding of input/output streams, file descriptors, and process communication in Ruby. It may take time and effort to grasp the concepts and techniques necessary to properly handle the command's streams.
  • Resource cleanup: When using Open3.popen3, you manage resource cleanup, including closing file descriptors and terminating child processes. Failure to properly clean up resources can result in resource leaks or unexpected behavior in your application.

Summary

Each approach to executing shell commands in Ruby has its strengths and weaknesses. The system method and backticks (`) are good for simple commands and capturing output but may block your application. The exec method is ideal if you replace the Ruby process with the external command. Finally, popen3 class provides the most flexibility, allowing fine-grained control over input/output streams and asynchronous execution.
Choose the approach that best suits your specific needs. Consider command complexity, desired output handling, application responsiveness and resource usage. By understanding these different methods, you can take advantage of Ruby's ability to execute shell commands efficiently and extend the functionality of your applications.