Recently I worked on a client project based on the Drupal platform.
The most important part of the job was automating a data import from a remote source, but instead of writing a script to do the job, I created a command for Drush. Quoting from Drush repository site

Drush is a command-line shell and scripting interface for Drupal, a veritable Swiss Army knife designed to make life easier for those who spend their working hours hacking away at the command prompt.

Drush can handle almost every aspect of a Drupal site, from the mundane cache management to user management, from packaging a Drupal install into a makefile to project management and much more, including a CLI for running sql queries an http server for development and an rsync wrapper.
Drush commands can also be executed on remote machines, provided Drush is installed, by specifing the server alias (e.g. drush clear-cache @staging).

There are different ways of creating Drush scripts:

  • prepending the script with the shebang #!/usr/bin/env drush or #!/full/path/to/drush and using Drush commands
  • using Drush php interpreter #!/full/path/to/drush php-script and using the Drush PHP api
  • writing custom commands

This guide is about the last case.
Drush commands are much like Rake or Grunt tasks, you give them a name (more like a namespace) and Drush figures out what function must be called. To create a Drush command, follow these simple steps

  • create a namespace.drush.inc in one of the standard import path
  • implement the namespace_drush_command entry point function
  • implement the command functions. By conventions the command functions are called drush_namespace_commandname

Drush search for commandfiles in the following locations:

  • /path/to/drush/commands folder
  • system-wide drush commands folder, e.g. /usr/share/drush/commands
  • .drush folder in $HOME folder.
  • sites/all/drush in the current Drupal installation
  • all enabled modules folders in the current Drupal installation

Implementing the command

To implement a Drush command, the script must implement the drush_command hook. This function must return a data structure containing all the informations that define your custom command.
As an example we will develop a command that rolls a dice and prints the result.
We’ll use diceroller as namespace and roll-dice as command name.
This is the implementation of the main hook function

<?php
function diceroller_drush_command() {
  $items = array();

  $items['roll-dice'] = array(
    'description' => "Roll a dice for your pleasure.",
    'arguments' => array(
      'faces' => 'How many faces the dice has? Default is 6, max is 100.',
    ),
    'options' => array(
      'rolls' => 'How many times the dice is rolled, default is 1 max is 100',
    ),
    'examples' => array(
      'drush drrd 6 --rolls=2' => 'Rolls a 6 faced dice 2 times',
    ),
    'aliases' => array('drrd'),
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
    // see http://drush.ws/docs/bootstrap.html for detailed informations
    // about the bootstrap values
  );

  return $items;
}

The command is easily implementd this way

<?php

function drush_diceroller_roll_dice($faces=6) {
  $rolls = 1;

  if ($tmp = drush_get_option('rolls')) {
    $rolls = $tmp;
  }  

  drush_print(dt('Rolling a !faces faced dice !n time(s)', array(
    '!faces' => $faces,
    '!n' => $rolls
  )));
  // for n=0..$rolls
  // roll the nth dice
  // print the result
}

In this case we assume that the --rolls option contains a number, but we can guarantee that the function parameters are valid implementing the validate hook (there are others called just before and after the real command function).

<?php

function drush_diceroller_roll_dice_validate($faces=6) {

  if($faces <= 0) {
    return drush_set_error('DICE_WITH_NO_FACES', dt('Cannot roll a dice with no faces!'));
  }
  if($faces > 100) {
    return drush_set_error('DICE_WITH_TOO_MANY_FACES', dt('Cannot roll a sphere!'));
  }

  $rolls = drush_get_option('rolls');
  if(isset($rolls)) {
    if(!is_numeric($rolls))
      return drush_set_error('ROLLS_MUST_BE_INT', dt('rolls value must be a number!'));

    if($rolls <= 0)
      return drush_set_error('NOT_ENOUGH_ROLLS', dt('What you\'re asking cannot be done!'));

    if($rolls > 100)
      return drush_set_error('TOO_MANY_ROLLS', dt('I\'m not your slave, roll it by yourself!'));
  }

}

If we did our job diligently, running drush help roll-dice should give us this ouput

Roll a dice for your pleasure.

Examples:
 drush drrd 6 --rolls=2                    Rolls a 6 faced dice 2 times

Arguments:
 faces                                     How many faces the dice has? Default is 6, max is 100.

Options:
 --rolls                                   How many times the dice is rolled, default is 1 max is 100

Aliases: drrd

Consult the Drush api for a complete list of hooks functions and constants or launch drush topic docs-api from the command line.
For a complete implementation of a command example, see drush topic docs-examplecommand.