Interacting with Shell Commands in Ruby

May 9, 2025

We often need to run shell commands from within Ruby code, and Ruby provides a number of ways to do that. It’s sligtly confusing at first - specifically because there are different ways of accomplishing the same thing. I found this useful flowchart on StackOverflow that helps you choose the appropriate option. If you’re looking for a detailed writeup of the different options I recommend reading 6 Ways to Run Shell Commands in Ruby by Nate Murray.

SubProcess Flowchart Ruby

One common mistake I’ve seen - and made myself - is to use string interpolation when passing arguments to a shell command. For example, in my recent post on Tweeting new Jekyll Posts I was using Ruby to interact with the git command. I could have written:

Bad
# Git add and commit (unsafe example)
commit_message = "Blogged: #{document.title} #{URI.join(site_config.fetch('url'), document.url)}"
`git add #{post_path}`
`git commit -m "#{commit_message}"`

This is a bad idea because

  1. Can break if it includes double quotes, ampersands, semicolons, etc.
  2. Can lead to command injection if the title includes malicious input.

We could get around (1) by using Shellwords.escape and you could argue I don’t have to worry about (2) because I control the input. However, best practice dictates that you should never interpolate any arguments into shell commands. Instead you should use system and pass the arguments as parameters.

Good
# Git add and commit
commit_message = "Blogged: #{document.title} #{URI.join(site_config.fetch('url'), document.url)}"
system('git', 'add', post_path)
system('git', 'commit', '-m', commit_message)

This principle doesn’t just apply to Ruby - I had the exact same issue with Java code I wrote recently. Instead of building the entire command:

Bad
String command = String.format(
    "pg_dump --dbname=%s --host=%s --port=%s --username=%s",
    connectionProperties.dbName(),
    connectionProperties.host(),
    connectionProperties.port(),
    connectionProperties.username()
);

ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();

Pass the arguments as parameters:

Good
List<String> pgDumpCommand =
    List.of(
        "pg_dump",
        "--dbname",
        connectionProperties.dbName(),
        "--host",
        connectionProperties.host(),
        "--port",
        connectionProperties.port().toString(),
        "--username",
        connectionProperties.username()
    );

ProcessBuilder processBuilder = new ProcessBuilder(
    Stream.of(pgDumpCommand, options).flatMap(Collection::stream).toList()
);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();

In this example I also needed to allow the consumers of this code (which I control) to pass additional options. In that case I would also suggest that you use an explicit whitelist/allowlist for accepted options.

Ruby