Ruby
Shell Commands
Programming
Scripting Language
Coding Tutorial

How to call shell commands from Ruby

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Ruby gives you several process APIs, and they are not interchangeable. The right choice depends on whether you need a simple success flag, captured output, streaming control, or full replacement of the current process. The main engineering concern is safety: avoid building one large shell string when you can pass command arguments directly.

Use system for Simple Execution

system runs a command and returns true on success, false on non-zero exit, or nil if the command could not start.

ruby
ok = system("ls", "-l", "/tmp")
puts "success=#{ok}"
puts "exit=#{$?.exitstatus}"

This is a good fit when the child process should inherit your terminal and you only care whether it worked.

Use Backticks or %x When You Need Stdout

Backticks and %x capture standard output into a string. They are convenient, but they invoke a shell when given shell syntax, so they are easy to misuse with untrusted input.

ruby
1current_branch = `git rev-parse --abbrev-ref HEAD`.strip
2puts current_branch
3
4files = %x(find . -maxdepth 1 -type f)
5puts files

These forms are acceptable for quick scripts under your control. They are a poor choice when command arguments come from users or external data.

Use Open3 for Output and Error Streams

Open3 is the most practical standard-library option when you need stdout, stderr, and exit status separately.

ruby
1require "open3"
2
3stdout, stderr, status = Open3.capture3("ruby", "-e", "warn 'bad'; puts 'ok'")
4puts "stdout=#{stdout.inspect}"
5puts "stderr=#{stderr.inspect}"
6puts "success=#{status.success?}"

This is usually the right API for automation tasks because it gives you structured results without forcing you to parse mixed console output.

Pass Arguments as Separate Values

If any part of the command is dynamic, use the array-style argument form. That avoids shell expansion and greatly reduces command-injection risk.

ruby
1require "open3"
2
3filename = "report.txt"
4stdout, stderr, status = Open3.capture3("grep", "error", filename)
5puts stdout
6puts stderr unless stderr.empty?
7puts status.exitstatus

Compare that with a dangerous pattern such as interpolating filename into one shell string. The separate-argument form treats the value as data, not shell syntax.

Use spawn for Long-Running Background Work

spawn starts a process and returns its process id immediately. You can then wait for it or let it continue independently.

ruby
pid = spawn("sleep", "1")
Process.wait(pid)
puts "finished with #{$?.exitstatus}"

This is useful when you want manual process control or need to connect pipes yourself.

Use exec Only When Replacing the Current Process

exec does not create a child that Ruby continues to manage. It replaces the current Ruby process entirely.

ruby
# After this line, Ruby does not continue.
# exec("ruby", "-e", "puts 'replacement process'")

That is appropriate in wrappers or launch scripts, but it surprises people who expect code after exec to run.

A Safe Helper Method

Many applications benefit from a small wrapper that fails loudly and returns structured data.

ruby
1require "open3"
2
3Result = Struct.new(:stdout, :stderr, :status, keyword_init: true)
4
5def run_command(*args)
6  stdout, stderr, status = Open3.capture3(*args)
7  Result.new(stdout: stdout, stderr: stderr, status: status.exitstatus)
8end
9
10result = run_command("ruby", "-e", "puts 2 + 2")
11puts result.stdout

A wrapper like this keeps subprocess handling consistent across the codebase.

Common Pitfalls

The most common mistake is interpolating user input into a shell string. That turns data into executable syntax and creates injection bugs. Another problem is using backticks when you really need stderr or an exit code, which leads to fragile error handling.

It is also easy to forget that system writes directly to the terminal unless you redirect it, while Open3.capture3 buffers output in memory. For very large outputs, streaming with pipes may be better than capturing everything at once. Finally, remember that exec replaces the current process, so any cleanup logic after it will never run.

Summary

  • use system when you only need success or failure
  • use backticks or %x only for simple stdout capture in trusted scripts
  • use Open3.capture3 when you need stdout, stderr, and exit status separately
  • pass command arguments as separate values instead of one shell string
  • use spawn for background processes and exec only to replace the current process

Course illustration
Course illustration

All Rights Reserved.