James Britt

Maximum R&D

Getting lazy with bash

September 2012

I’m pretty lazy. I spend a lot of time at a keyboard, and while I cop to finding mindless text manipulation often therapeutic I get the willies thinking about truly wasted effort.

For example: I’ve spent enough time writing Ruby apps that I’ve taken for granted having a Rakefile around to help automate tasks. Even for non-Ruby projects, if I find myself re-typing the same lengthy commands I’ll whip up a Rakefile and start adding helper tasks.

Because of my laziness even typing the full four characters in “rake” became tedious. I wrote a small script, called “r”, that essentially aliased rake (but did a few other helpful things as well).

But laziness never sleeps.

A little while ago I learned about the mechanism used by bash (on Ubuntu/Debian, at least; I’m too lazy to go check OSX) to handle command line calls to non-existent programs.

If you open up a terminal window, and assuming you are running a bash shell, and happen to invoke some imaginary script, the shell will often ask if you did not in fact mean this or that other program, with some pointers on how to acquire them.

It’s clever.

I learned of this in a roundabout way. I forget the original motivation; I think I read about someone who had in mind a special shell to turn all command output into JSON, or maybe it was to re-write or overload basic commands to do that. Whatever it was (or whatever it was I happen to recall it as), it felt like overkill, a bit clumsy to maintain or extend (or avoid, when it wasn’t what you wanted).

Having seen the “Did you mean …?” bash stuff I wondered if I could hook into it, so that if I requested specially-formated non-existent commands the shell would ingeniously interpret it as something else. (BTW, hat tip to Darrin Chandler for suggesting this.)

For example, if I run ls -la I get a directory listing, formated in plain text columns. What if I entered ls.js -la; could I get bash to execute that as something like ls -la | to-json.sh (assuming I provide the to-json.sh code)?

Yes. There’s a special bash function, command_not_found_handle. There’s a default version in /etc/bash.bashrc. Mine looks like this:

# if the command-not-found package is installed, use it
if [ -x /usr/lib/command-not-found -o -x /usr/share/command-not-found ]; then
	function command_not_found_handle {
    # check because c-n-f could've been removed in the meantime
    if [ -x /usr/lib/command-not-found ]; then
      /usr/bin/python /usr/lib/command-not-found -- $1
      return $?
    elif [ -x /usr/share/command-not-found ]; then
	    /usr/bin/python /usr/share/command-not-found -- $1
      return $?
		else
		  return 127
		fi
	}
fi

There’s some “see if we can even do this” wrapper stuff, then the function definition (which, interestingly, has more of that “see if we can even do this” wrapper stuff).

We can ignore what’s in the actual command-not-found Python script. What’s interesting is that this bash function can do whatever you like.

You probably don’t want to edit the default bash.bashrc. You can get custom behavior for yourself by editing your own bash files. I use a .bash_scripts file that gets sourced by bash when I start a shell.

The first thing I did was copy the code from the system version. I then added some trivial code just to demonstrate that my own version was getting called and that it had access to the command-line call. This extra code was a simple Ruby script to echo the whatever was passed on the command line.

It worked. My goofy demo would run, and the bash function would continue on to handle absent programs just as it always did.

Then I just left it because I didn’t have any pressing ideas for exploiting it.

I’m currently writing some books about technology for artists. These include Kinect Hacking for Artists and OSC for Artists. I’m writing code in Processing, but I’m still using Rakefiles to handles routine tasks, such as converting Textile to HTML and epub, and updating the remote Web site with the current book contents. I’m typing a lot “r this” and " r that" (though I recently added a small task that uses inotifywaitinotifywait to automagically regenerate stuff). I got to thinking: that “r” I have to type; why can’t I get bash to just assume I’m invoking a Rake task and Do The Right Thing?"

So I did.

First, I created a Ruby script, command_not_found_ng.rb in my local bin directory:

  puts `rake #{ARGV.join(" ")}`; exit 0 if File.exist? "#{Dir.pwd}/Rakefile"
  exit 1

If you know ruby then there’s little to explain. It’s an “if” expression that, if the condition is true, does stuff and then exits the script.

The exit 0 is there so that the bash function knows to terminate (see below). If there is no Rakefile then exit 1 is executed and bash knows to continue handling a call to a missing program.

I then changed my local version of command_not_found_handle in my .bash_scripts file:

function command_not_found_handle {

  if ~/bin/command_not_found_ng.rb $* ; then
      return 0
   fi
  
  # check because c-n-f could've been removed in the meantime
  if [ -x /usr/lib/command-not-found ]; then
    /usr/bin/python /usr/lib/command-not-found -- $1
    return $?
  elif [ -x /usr/share/command-not-found ]; then
    /usr/bin/python /usr/share/command-not-found -- $1
    return $?
  else
    return 127
  fi
}

It works mostly pretty well. If I’m in a folder that has a Rakefile I can invoke a task just by task name. If there’s no Rakefile then the usual missing program behavior kicks in.

However, if a task is designed to prompt me for input I don’t see it. For example, I have a Rake task that generates a skeleton blog post file. It prompts for a post title before creating the file. I never see that prompt unless I explicitly invoke Rake.

I also don’t get output when calling Rake command line switches, such as -T. That’s not such a big deal for me since some time ago I wrote (yes, another) helper script, rt, for that (plus, as usual, some additional behavior).

So basically it works OK most of the time, and in the few cases it doesn’t I can fall back to using my r thing. Also, this won’t work if you are calling a rake task that has the same name as a real program.

A bigger concern is whether there is some other odd behavior that only manifests under certain conditions. I just started using my clever bash hack so I need to keep an eye out for this as well as consider the ramifications of using Ruby to meta-execute Rake.

So there you have it. I get to type one less character. Two, really, since I omit the space as well.

OK, that’s not a big deal. The big deal is that the same idea can be used to test for other conditions and react accordingly. For example, if you wanted to execute a command on a remote machine using ssh you could add a hook that looks to see if a missing program name ends with .ssh, and then use that to invoke the proper command on some pre-define remote server. I’ve ended up writing a set of helper scripts to do precisely that (e.g., run df -h on my server to check free space), and the idea of having a general template that would allow me to turn almost anything into a remote call is enticing.

There are a few things I’m curious about. I don’t like the cut-n-paste dupe code in my local copy of command_not_found_handle. Is there a form of “super” for bash scripting that would let me invoke the default version from my custom version? I tried some aliasing but ended up with an infinite loop.

I also wonder whether I’m being too clever for my own good. For example, are there better ways to generalize calls to remote machines?

Mostly, though, I want to know what other good/fun/amusing/appropriate uses can you think of for this hack?