Article Image

If you are willing to dig around a bit, you'll find several methods to do this. However, most of these will require some sloppy hacking—that is—trying things you don't understand until something accidentally works. So here's to doing it on purpose. Next time. Probably.

tl;dr

Run shell commands in Ruby like this:

command = "say 'hello from shell'"
process_var = IO.popen(command)

So that you can kill them, also in Ruby, like this:

Process.kill(15, process_var.pid)

The problem: an itch

You want your Ruby CLI to perform shell commands. Maybe you want to run some gross, jazzy background music with afplay. Maybe you've built a robot to insult you, but you want it do so verbally with say.

This all seems reasonable enough, but how can you bridge the gap between the thing that's running, and the thing that's running the thing? You know?

The solution: some RASH

Not a thing. I made it up. Ruby-ass-shell? Whatever.

It's actually quite easy to run a shell command from within your Ruby code. To try it, fork and/or clone this repo:

$ git clone https://github.com/drewprice/rash.git
$ cd rash/

Once you're there, run:

$ ruby rash.rb

Cool, sorta, right? This is what happened:

puts 'shell:'

command = "say 'hello this is shell.'"
system(command)

puts "Ruby: Whoa. I... can't talk."

Ruby tells us that the shell has something to say, the shell says it, and finally, when it's has finished, Ruby has its turn to close out the program.

The new problem: taking turns

The shell took awhile to get through its job, and we had to wait for it the whole time. This might be fine in some circumstances, but let's say you're building a CLI that has a lot of speaking to do, and you want to listen, but you would also like to be able do other things while you listen.

The trouble is, you are stuck listening to the shell until it has finished. Try this:

$ ruby duck_hunt.rb

In this game, ideally, the duck would appear right when the speaker starts trying to convince you not to shoot. That way you can shoot the duck and be done long before you are made to feel bad.

However, Ruby is only going to do one thing at a time: let the shell have its turn, and then you can shoot the duck. You're destined to lose.

The new solution: f* taking turns

The shell actually comes to the rescue here. In the first version of duck_hunt.rb, Ruby ran its shell command like this:

command = "say '#{stuff_to_make_you_feel_bad}'"
system(command)

By simply throwing an & onto the end of that command, shell will move the process into the background, allowing Ruby to continue about its biz:

command = "say '#{stuff_to_make_you_feel_bad}' &"
system(command)

Try it out in duck_hunt_2.rb!

$ ruby duck_hunt_2.rb

The final problem: it lives

That small change almost made it awesome! You killed the duck, you were feeling proud... and then you still had to listen to the whole poem, even though the program itself had stopped running. Which is weird, and a little creepy.

You need someway to tell Ruby to go out, find that process, snag its process id, and kill it before the program exits:

command = "kill -15 $(ps aux | pgrep -l say | egrep -o '\\d+') &> /dev/null"
system(command)

This works, but it's also a mess. And what if you are someone who likes to run multiple say processes simultneously? How can we be sure that we've got the right one?

A final solution: kill it

Instead of creating this strange phantom process that's quite a doozy to kill, let's instantiate an object that willingly surrenders to the proper authority:

command = "say '#{stuff_to_make_you_feel_bad}'"
say_poem = IO.popen(command)

This now utilizes Ruby's IO class, which "is the basis for all input and output in Ruby". The IO::popen method runs the command as a subprocess and stores its process id, which can be accessed through the IO#pid method. So here, I am storing the process in an object that I can access with the variable, say_poem.

Not only is this much cleaner, it's also a much better object-oriented design—in that it actually is object-oriented design. With this, we simply call upon Ruby's Process module to come in and kill the subprocess whenever we see fit:

Process.kill(15, say_poem.pid)

Try it out in duck_hunt_3.rb:

$ ruby duck_hunt_3.rb

Wonderful isn't it? We leave having killed the duck and the process without a bit of guilt. Shootin' ducks and feelin' like a million bucks. Or something?

Check out the project that inspired this post, cosmos-ascii. Built with Stefania Druga, this soon-to-be(?) Ruby gem allows you to view NASA's Astronomy Picture of Day via the command line—playing you sweet space tunes while a robot explains the wonders of the cosmos.

Blog Logo

Drew Price


Published

Image

Blog & stuff

A blog about some things.

Back to Overview