Shelling out with #system
Ruby offers various ways of shelling out to the command line. In the next few days we'll cover some of them, starting today with system.
The system method is a simple way to execute a command in the shell. It takes a command as a string and returns true if the command was executed successfully or a falsy value if it failed. STDOUT and STDERR from the command are printed directly to the console, so you can see the output of the command:
system "echo Hello, world!"
# Hello, world!
# => true
system "ls non_existent_file"
# ls: non_existent_file: No such file or directory
# => false
system "no-such-command"
# => nil
const { spawnSync } = require("child_process");
function system(command) {
const result = spawnSync(command, { shell: true, stdio: "inherit" });
return result.status === 0;
}
system("echo Hello, world!");
// Hello, world!
// => true
system("ls non_existent_file");
// ls: non_existent_file: No such file or directory
// => false
system("no-such-command");
// /bin/sh: no-such-command: command not found
// => false
We can also have system raise an exception if the command fails by passing exception: true:
system "echo Hello, world!", exception: true
# Hello, world!
# => true
system "ls non_existent_file", exception: true
# ls: non_existent_file: No such file or directory
# => RuntimeError: Command failed with exit 1: ls
const { spawnSync } = require("child_process");
function systemWithException(command) {
const result = spawnSync(command, { shell: true, stdio: "inherit" });
if (result.status !== 0) {
throw new Error(`Command failed: ${command}`);
}
return true
}
systemWithException("echo Hello, world!");
// Hello, world!
// => true
systemWithException("ls non_existent_file");
// ls: non_existent_file: No such file or directory
// => Error: Command failed: ls non_existent_file
There are a whole bunch of other options available that I won't cover here, but feel free to check out the Execution Options documentation for more details.
You may have noticed that in these simple examples I provided the entire command as a single string, but system also supports passing the command and its arguments as separate strings, which can help avoid issues with shell escaping and protect against command injection:
system "echo", "Hello, 'world'!"
# Hello, 'world'!
# => true
unsafe_input = 'hehe; rm -rf /'
system "echo", unsafe_input
# hehe; rm -rf /
# => true
const { spawnSync } = require("child_process");
// NOTE: shell: false is the key to safely pass arguments
function systemWithSafeArguments(command, ...args) {
return spawnSync(
command, args, { shell: false, stdio: "inherit" }
).status === 0;
}
systemWithSafeArguments("echo", "Hello, 'world'!");
// Hello, 'world'!
// => true
const unsafeInput = 'hehe; rm -rf /';
systemWithSafeArguments("echo", unsafeInput);
// hehe; rm -rf /
// => true
You can also pass a leading env hash to set environment variables for the command:
system({ "GREETING" => "Hello" }, "echo $GREETING, world!")
# Hello, world!
# => true
const { spawnSync } = require("child_process");
function systemWithEnv(env, command) {
return spawnSync(
command,
{ shell: true, stdio: "inherit", env: { ...process.env, ...env } }
).status === 0;
}
systemWithEnv({ GREETING: "Hello" }, "echo $GREETING, world!");
// Hello, world!
// => true
History
Kernel#system has been around since Ruby's inception. The original inspiration comes from the POSIX system() function, which is a standard way to execute a command in the shell. Ruby may also have been influenced by Perl's system function.
Support for a leading ENV argument was added in Ruby 1.9. Support for the exception: true option was added in Ruby 2.6.