Magic Command Invocation with Drush
I've recently been digging into the Aegir Hosting System, both because we're starting to use it at my current NREL gig, and because I've proposed to do a session on it at DrupalCamp LA. In short, Aegir allows you to easily administer a number of Drupal sites from a common site, itself built on Drupal. It's really slick, and a lot of the functionality is built on Drush. (Also see http://drupal.org/project/drush .)
So I'm looking into how Aegir handles tasks, which are just what they sound like - nodes representing commands that Aegir fires off every minute or so from a queue - if the queue's empty then that's that, but otherwise it calls
drush hosting-task #, where the # is a task node id. Wanting to know what that actually does, I looked around, found hosting.drush.inc, and discovered that it didn't have a callback.
Huh. I'd thought that Drush commands had to have a callback, but here I discover that they don't. So, I wondered, what's actually going on there? And, following the advice handed to me by Greg Knaddison when I started in Drupal, I started following the code. And here's what I found:
Generally, you want to use the callbacks on drush commands when they're fairly simple - the task is doing something basic that can be handled easily. If it's something more complex, you can skip the callback, and gain a fair amount of control over how your command is invoked, and even cleanup help if something goes south.
If a drush command doesn't have a callback, then the command info is handed off to a function called drush_invoke - you can find it in the command.inc file. (For the purposes of this, I'm going to call the command
hosting-task, because that's what I was looking for when I found this out, and it's a good example.) The first thing
drush_invoke does is look for an include file named after the reverse of the command name - so our
hosting-task gets inverted and the dashes are converted to dots, leaving us with
task.hosting.inc. It then looks around to find this file in any directory that contains a *.drush.inc file - the folks working on Aegir in this case put task.hosting.inc in the same directory as hosting.drush.inc, which makes sense. Now, there doesn't have to be an include file like this, and if Drush doesn't find one, it'll still continue to the next part. But if you've got a complex command that you're handing off to drush_invoke to handle for you, why not put it in it's own file for neatness' sake?
Continuing, after the possible file load, the drush command is turned into a base $hook by the simple expedience of changing any spaces or dashes into underscores - so for the example, we'd have a base hook of '
hosting_task' With that constructed, drush looks for functions named off of this command as follows:
And then, for each module that defines a module.drush.inc file, drush looks for a
drush_$modulename_$function... which means that for the hosting module, it looks for:
If you look at task.hosting.inc, you'll see that most of these are defined - they're not actually using
drush_hosting_pre_hosting_task, but that's because they're doing their preparation work in the
drush_hosting_hosting_task_validate function. So, the
drush_invoke happily continues along this series of commands, until it either reaches the end (oh, happiness), or one of these functions invokes
drush_set_error() - in which case the whole stack of commands reverses and runs backwards, and
drush_invoke sees if there's a function named exactly the same, only with '_rollback' appended to the name. (For our example, there is a
drush_hosting_hosting_task_rollback() defined in task.hosting.inc.)
A note on naming - although above we've referenced
drush_hosting_hosting_task, the folks who maintain Drush recognize that these are pretty long names to have to construct, and as such they check for when one of these function names they construct starts with drush_
module and change it over to drush_
module. So, in task.hosting.inc, they really should be naming the callback functions
drush_hosting_task... but for now Drush recognizes the old form and still will call them.
So to summarize what I've said so far, if you run into a Drush command without a callback, what you really should be looking for is a file named the reverse of the name in the same directory as the
hook_drush_command implementation, and that file should contain the drush_module_command functions. And alternately, if you want to create a Drush command that can rollback/clean up after itself, then you want to create your commands in a reverse-order name file.
But... there's something else.
drush_invoke goes through all of the modules which define drush hooks when looking for functions to call when invoking a drush command. So, if you wanted to do extra work on hosting-task drush commands, you could create an example.drush.inc in your example.module, and then define your own
drush_example_hosting_task function. Heck, you could define any of:
This is pretty exciting, as it opens up a lot of customization not only in Drush, but also in Aegir. I think as Aegir continues to mature, we're going to start seeing a bunch of modules which extend it in various ways.