Adding Arguments And Options To Deployer Tasks

I have been adding to my custom Deployer scripts for a number of months and I have now been using it to do more than just deploy my sites. Since it acts as a connection to my website server I have been using Deployer to perform other tasks like creating backups and clearing Drupal caches without having to log into the server to do it. What has helped me here is that I have set out my deployment tasks in a very modular fashion, so although my deployment runs a database backup, I there is nothing to stop me running the database backup command on it's own without doing a full deployment.

This has led me to realise that I sometimes need to control how those tasks run. I might, for example, need to backup my database with a different option so that I include the cache tables if I want to make a full backup of my Drupal site. I have a number of custom tasks in my deploy.php file, but I don't want to have nearly duplicate tasks just to run a slightly different flavour of database backup.

This lead me to look at adding options and arguments to Deployer. Although adding arguments is technically documented on the Deployer site, I had a few problems figuring out what the examples meant, so I decided to create a post to add to the documentation.

If you were not already aware, custom tasks are pretty easy to write. All you need to do is define your task in a task() function, passing in the name of the task and one of the following.

  • A string that defines a command to run.
  • A closure that gets run when the task is run.
  • An array that lists other tasks within the system.

If you use a closure then there are a number of functions that are available within the task scope to run commands or return output to the screen. As a simple example, here is my custom task for clearing the caches on a Drupal site.

task('drush:cr', function () {
    $output = run('{{deploy_path}}/current/vendor/bin/drush cr');
    writeln('<info>' . $output . '</info>');
});

I could have used the string argument to run this command, but I like to see the output being produced and so use a closure to print the output. I tend to namespace tasks like this, so all of my drush based functions have the format "drush:command", this also helps to group the commands together. In the above example, the run() command runs the command on the remote server and returns the output. The writeln() function will print out the output of the command run. Running the dep command will show this task available in the list of other commands.

$ ../vendor/bin/dep
Deployer v6.8.0

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -f, --file[=FILE]     Specify Deployer file
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  autocomplete  Install command line autocompletion capabilities
  help          Displays help for a command
  init          Initialize deployer in your project
  list          Lists commands
  run           Run any arbitrary command on hosts
  ssh           Connect to host through ssh
 debug
  debug:task    Display the task-tree for a given task
 drush
  drush:cr

To run a backup on my site before I deploy it I have a custom task. This wraps the drush sql-dump command, but also has some other setup to exclude the common cache tables, session tables and watch dog tables that aren't critical to a disaster recovery.

task('drush:backup', function () {
  if (test('test -f {{deploy_path}}/current/vendor/bin/drush')) {
    // Backup database.
    $file = 'hashbangcode-' . date('d-m-Y-Hi') . '.sql';
    $output = run('{{deploy_path}}/current/vendor/bin/drush sql-dump --gzip --structure-tables-list=cache,cache_bootstrap,cache_config,cache_container,cache_data,cache_default,cache_discovery,cache_filter,cache_menu,cache_page,history,search_dataset,search_index,search_total,sessions,watchdog --result-file=~/backup/' . $file);
    writeln('<info>' . $output . '</info>');

    // (re)Create symlink.
    $symlink = '~/backup/latest.sql.gz';
    if (test('test -f ' . $symlink)) {
      run('rm -f ' . $symlink);
    }
    $output = run('ln -s ~/backup/' . $file . '.gz ' . $symlink);
    writeln('<info>' . $output . '</info>');
  }
});

The final step here is to create a symlink to the backup so that I can download the latest backup without having to remember or figure out the name of the backup file. This means I can just rsync the file latest.sql.gz instead of trying to figure out the name of the file that was created during the backup.

To listen to user input outside of the command being run there are two options, both of which are defined globally. That is key to understanding how they work as you define your user input at the top of your deploy.php file and all tasks have access to potentially listen to that input. The two types of input we have are 'option' and 'argument'. I'll go into each of those now as they both have differences.

Argument

An argument works by appending additional arguments to the end of the command line, after the name of the task being run.

For example, if we want to change the database function using an argument we define it in the deploy.php file like this.

use Symfony\Component\Console\Input\InputArgument;

argument('include-cache-backup', InputArgument::OPTIONAL, 'Adds the cache tables to the backup command.', false);

As you can probably tell, Deployer uses Symfony components. This means that the argument() function is a wrapper around the creation of an InputArgument object. Most of the parameters to the argument() function are self explanatory, but the final parameter here is the default option that is set if the user hasn't entered the paramter.

Note that Deployer already has one argument called "stage", which means that this argument needs to be set first in the command line. The stage argument is the name of the server you are deploying to, as defined in your host() function and is useful when deploying to different servers. For example, let's say that you have two servers setup, one called prod, and one called stage. You might define them like this.

host('prod')
    ->hostname('stage.domain.com')
    ->set('deploy_path', '/var/www/html/website')
    
host('stage')
    ->hostname('domain.com')
    ->set('deploy_path', '/var/www/html/website')

With this in place you provide the 'stage' argument to deploy to the relevant host. For example, to deploy to 'prod' you would supply the 'prod' parameter to the command.

../vendor/bin/dep deploy prod

By adding additional arguments you need to introduce them before the server you want to deploy to, otherwise the argument you defined would receive the text 'prod', which was intended for the first argument. The previous command, with the addition of the include-cache-backup would become this.

../vendor/bin/dep deploy yes prod

This passes the value of 'yes' to the include-cache-backup argument and 'prod' to the stage argument. There is also absolutely no information on the command line output that you have added an additional argument to your script. Because of this it is really important that you document any additional arguments you add to your Deploy script. Without that you would have to read the code to figure out that the 'prod' argument is being passed to the wrong argument.

To listen to the arguments being sent to the command you need to use the input() function to extract the incoming input from the user. In the following example we are extracting data from the supplied argument. Arguments aren't named, just extracted in the order they are defined, so include-cache-backup will map to the first argument sent to the command.

task('test:argument', function () {  
  $includeCacheBackup = null;
  if (input()->hasArgument('include-cache-backup')) {
      $includeCacheBackup = input()->getArgument('include-cache-backup');
  }
  writeln('include-cache-backup ' . var_export($includeCacheBackup, true));
});

This will print out "yes" on the command line when running the examples above against this task. Obviously you will need to do some careful checking to make sure that the incoming argument is correct before trying to use it. The hasArgument() method is really important to include as it means you can run your commands without any arguments. I find it best practice to assume some sensible default value instead of throwing an error if the argument isn't present. As we provided a default for our argument when setting things up we will always receive 'false' if the argument is not present.

This is probably not the right type of input to use for a switch that changes how a task operates.

Option

An option works by listening to flags that you add to the command and work entirely separately from the arguments input type. For example, if we want to change the database backup task using an option we define it in the deploy.php file like this.

use Symfony\Component\Console\Input\InputOption;

option('include-cache-backup', 'i', InputOption::VALUE_OPTIONAL, 'Adds the cache tables to the backup command.', false);

With this in place, Deploy adds our custom option to the list of options in the command line output. Again, the 'false' at the end of the parameter list is the default value of the option. Running the dep command without any input shows this in the list of options.

$ ../vendor/bin/dep
Deployer v6.8.0

Usage:
  command [options] [arguments]

Options:
  -h, --help                                           Display this help message
... snip ...
  -i, --include-cache-backup[=INCLUDE-CACHE-BACKUP]  Adds the cache tables to the backup command. [default: false]

To use the new option we just add it as a flag to the command. It doesn't matter if this goes before or after the 'stage' argument as it will not interfere with the arguments.

$ ../vendor/bin/dep deploy --include-cache-backup=true

Note that the second parameter to the option() function is the shortcut. This can be used to simplify the option on the command line. The following command is equivalent to the above example.

$ ../vendor/bin/dep deploy -i true

To listen to the options being used with the command you need to use the input() function to extract the incoming input from the user. In the following example we are extracting data from the supplied option with the name 'include-cache-backup'. Options are named so they can be extracted by the name of the option set when defined.

task('test:option', function () {  
  $includeCacheBackup = null;
  if (input()->hasOption('include-cache-backup')) {
      $includeCacheBackup = input()->getOption('include-cache-backup');
  }
  writeln('include-cache-backup ' . var_export($includeCacheBackup, true));
});

This will print out the string 'true' when running the examples above against this task. Again, you should probably make sure that the incoming parameters are correctly formatted before trying to use them. The option is passed to the task as a string so you need to do your own casting to ensure it's the correct type. Essentially, don't trust user input.

As options don't interfere with the default running of Deployer, and are self documenting on the command line, they make sense to use before looking at arguments. In fact, I would highly recommend you don't use arguments unless you have a very good reason to do so. Options should be your go to option when accepting user input to a Deployer script. 

The New Backup Function

With that knowledge in hand, let's look at recreating my database backup task with the option of either including or excluding the cache tables. By default, I don't want the script to include the cache tables in the backup, so the default option is false. If the user adds the --include-cache-backup option then they can either pass in 'yes' or nothing at all as we detect that option as well. This means that just passing the flag to the script is enough to enable the option and turn off the cache table backup.

Note that I'm not directly using the incoming value from the user, I'm deliberately only using it to detect what I should be doing later in the script. This is especially important when running raw commands on a remote server, which is essentially what we are doing here.

use Symfony\Component\Console\Input\InputOption;

option('include-cache-backup', 'i', InputOption::VALUE_OPTIONAL, 'Adds the cache tables to the backup command.', false);

task('drush:backup', function () {
  if (test('test -f {{deploy_path}}/current/vendor/bin/drush')) {
    // Define filename.
    $file = 'hashbangcode-' . date('d-m-Y-Hi') . '.sql';

    // Detect option.
    $includeCacheBackup = false;
    if (input()->hasOption('include-cache-backup')) {
        $includeCacheBackupInput = input()->getOption('include-cache-backup');
        if ($includeCacheBackupInput == 'yes' || $includeCacheBackupInput == '') {
          $includeCacheBackup = true;
        }
    }

    // React to option.
    if ($includeCacheBackup === TRUE) {
      $structureTablesList = '';
    }
    else {
      $structureTablesList = '--structure-tables-list=cache,cache_bootstrap,cache_config,cache_container,cache_data,cache_default,cache_discovery,cache_filter,cache_menu,cache_page,history,search_dataset,search_index,search_total,sessions,watchdog ';
    }

    // Backup database.
    $output = run('{{deploy_path}}/current/vendor/bin/drush sql-dump ' . $structureTablesList . '--gzip --result-file=~/backup/' . $file);
    writeln('<info>' . $output . '</info>');

    // (re)Create symlink.
    $symlink = '~/backup/latest.sql.gz';
    if (test('test -f ' . $symlink)) {
      run('rm -f ' . $symlink);
    }
    $output = run('ln -s ~/backup/' . $file . '.gz ' . $symlink);
    writeln('<info>' . $output . '</info>');
  }
});

This works very well. I could improve this though better generating the backup command instead of just plugging strings together. Also, because the --include-cache-backup option is defined as a global option then it can be used no matter now the drush:backup task is called. Which means that I can either use it directly by calling drush:backup or indirectly through the default drush task.

More in this series

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
1 + 7 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.