The best kittens, technology, and video games blog in the world.

Sunday, July 07, 2013

Shellwords.split technique for Ruby scripting

Stella Doo by moriza from flickr (CC-BY)

I write 100% of my shell scripts in Ruby (by the way, here are some of the scripts I wrote). If you prefer Perl, or Python or something else relatively sane, all power to you - but people who write complicated scripts in bash are certifiably insane.

One of many problems of using bash for scripting is how difficult it is to keep data properly quoted - even writing SQL by string concatenation is far easier to do safely, and we all know how that usually ends.

Unfortunately while everybody knows that building SQL by string concatenation is crazy, doing that in bash is somehow still practiced widely, and what's worse - even people who know better than that and use a sane language often revert to many awful and insecure bashisms.

How not to code

Let's start with a simple task. User has some EDITOR variable set. We have some files. We want to edit them with user's favourite editor.

Plenty of people who suffered from being exposed to bash in their youth will write code like this:


    system "#{ENV['EDITOR']} #{files.join(" ")}"

And such code is going to fail miserably if files have any spaces in them, let alone any unusual but perfectly legitimate characters like "lol.txt\nrm -rf /".

One step towards sanity

People who know better are aware that system command takes multiple argument, so you can pass files separately:

    system ENV['EDITOR'], *files

This is not only far safer it's also much more concise. There's only one little problem - unlike in the unsafe version EDITOR is now restricted to a single command - user can no longer select mate -w to be their editor, since that's the name of the executable.

If you intuition is to do something like *ENV['EDITOR'].split(" "), please don't since that's just opens another set of problems.

Shellwords to the rescue

Fortunately a solution to all these problems is easy:

    require shellwords
    system *Shellwords.split(ENV['EDITOR']), *files

(In case you didn't know it yet, Ruby 1.9 and 2.0 lets you use multiple * in the same method invocation.)

And that's all in today's lesson.

No comments: