Running shell commands from Ruby
Ruby is an excellent language offering us a simple and human friendly interface, however for system administration or simple task automation the shell is a much better alternative. Luckily, combining them is easy.
There are many ways to interact with the shell (back-ticks, system, exec, open3, …), but I am not going to list and explain every one of them (go read the docs or this thread) and instead I will focus on the interaction of the two languages.
Let’s start with a simple example and list the content of the directory. In
ruby we will use the back-ticks syntax to capture the output of the
output = `ls` puts output
OK, that was easy. In this next example let’s execute a shell action that will
install Vim on a Ubuntu
machine. We will use the
system command instead of the back-ticks, so we can
join the output of the command with the output of our ruby script.
system "sudo apt-get -y install vim"
This should work fine. But not always… If your internet connection is broken, or if the vim package changes its name, the above command will fail without our Ruby script knowing anything about it.
Hopefully, there is an easy solution. The
$? global variable always contains
the status code of the last executed shell command. Let’s use to show an error
message to the user:
system "sudo apt-get -y install vim" if $?.exitstatus > 0 puts "I failed to install Vim, I am very sorry :'(" end
With the above knowledge you can do a lot. But sooner or later you will
stumble on one little detail — the system and the back-ticks execute
sh commands, and not Bash commands. And there are a
lot of differences
between the two of them.
For example in Bash you have process substitution that is very handy, yet not available in Sh. Let’s write a Ruby script that uses it.
A good use case for process substitution is to check if two directories have the same files in them. In the command line we would write such a test like this:
cmp <( ls ~/images ) <( ls ~/images-backup )
Following the above example, a naive Ruby implementation would be:
system "cmp <( ls ~/images ) <( ls ~/images-backup )" if $?.exitstatus == 0 puts "They are the same, yay!" else puts "They are not the same" end
But this will fail with a weird error that looks similar to this:
sh: 1: Syntax error: "(" unexpected
Hopefully, the system command can take multiple arguments and will threat the
first one as the command and the rest as its arguments. For example we can list
/etc directory with:
system "ls", "/etc"
If you are familiar with the command line you probably know that you can run commands in an alternative shell with:
bash -c "echo 'running from bash shell!'"
Now let’s use the above knowledge to compare two directories from a Ruby script:
system "bash", "-c", "cmp <( ls ~/images ) <( ls ~/images-backup )" if $?.exitstatus == 0 puts "They are the same, yay!" else puts "They are not the same" end
Hooray, this works!
I hope I have given you the incentive to go and explore this technology even further. Happy hacking!