Chapter 3. Customizing Drush

Drush is highly configurable. With the contents of this chapter and some practice you will feel that you are doing magic with your console. Imagine that you can download a whole production database ignoring cache tables, resetting user emails and passwords to your local database with just one short command such as drush sql-sync @prod @local. Yes, it is possible.

In this chapter, you will learn about the following topics:

  • Write, test, and validate our first Drush command
  • Altering and taking action when a command is executed
  • Running PHP code directly on the command line or in PHP scripts after bootstrapping a Drupal site
  • Create an alias for our testing Drupal site and issue commands to it
  • Executing commands against remote systems and synchronizing files, code and databases
  • Defining Drush configuration files for a user, a Drupal installation or a single site
  • Optimizing our terminal in order to run even shorter commands

These are advanced topics which will need some systems administration skills such as knowing where your home path is, how to set up, and use SSH and how to authenticate against a remote host with a Public Key. Some Drush commands dealing with remote systems have limitations when being executed from Windows but workarounds will be explained in these cases.

Writing a custom command

So far you have seen most of the Drush command toolkit. Now it is time for us to think about how Drush can help us accomplish tasks that cannot be done with a few commands. Hence, it is time to write our own Drush command.

Here are some examples where you should choose to write a Drush command:

  • To run a periodic task that needs to be completely isolated because it can take a long time to complete and therefore cannot be executed through Cron
  • To extend the capabilities of an existing command in order to perform extra tasks needed in the production environments of your websites
  • To perform a task without a graphic interface, such as the content generator command of the Devel module

Drush commands follow a syntax very similar to Drupal. They are defined within a hook as an array of properties and a callback function does the processing. They also have hooks before, during, and after their execution. These will be explained in the next section.

Commands in Drush have the following structure (based on http://www.drush.org/docs/commands.html):

  • A file named COMMANDFILE.drush.inc where COMMANDFILE is the namespace of the group of commands that will be implemented
  • An implementation of the hook COMMANDFILE_drush_help() which optionally describes each command and how they are categorized in the output of the drush help command
  • An implementation of the hook COMMANDFILE_drush_command() where the basic properties of each command are defined
  • A callback function for each defined command at COMMANDFILE_drush_command() that will do the actual processing following the function name drush_COMMANDFILE_COMMANDNAME()

Let's write a very simple but illustrative command. Imagine that we have a website where we need to block user accounts based on the following requirements:

  1. By default, accounts which have not logged in during the last two years should be blocked (except the administrator account)
  2. If languages are provided, then filter user accounts by these languages
  3. If a period of time is given, then filter user accounts which logged in for the last time earlier than the one provided

Based on the previous requirements, we will create a custom command to perform these operations as the existing user-block command works only on specific users and cannot be extended to suit the previous requirements.

Writing our command

We are about to create the file where our command will reside. It is important that we carefully follow the structure COMMANDFILE.drush.inc so that Drush can discover it, and also that think about a good name for COMMANDFILE as Drush will use this part of the filename to invoke the callback function that will actually do the processing.

Custom Drush command files can be placed in several locations in our system:

  • In a custom directory defined by the --include option or by a drushrc.php configuration file. More on Using Configuration Files later in this chapter.
  • At the shared system commands, residing in $SHARE_PREFIX/share/drush/commands. For example, at /usr/share/drush/commands.
  • In the $HOME/.drush folder, where $HOME is an environment variable that defines your home path. For example, /home/juampy/.drush/ COMMANDFILE.drush.inc. This is the recommended location for general purpose commands, which we do not need to share with other users in the system.

For this scenario, we will go with the last option. We will create a file with the filename user_blocker.drush.inc inside the .drush folder within our home path (for example, at /home/juampy/.drush/user_blocker.drush.inc). This file has a hook for defining the command properties and a callback function that actually does the processing. The first part defines the command properties, such as the command description, examples, arguments, and options:

<?php
/**
* @file
* Blocks users with no activity in a period of time
*/
/**
* Implementation of hook_drush_command().
*/
function user_blocker_drush_command() {
$items['user-blocker'] = array(
'description' => 'Blocks user accounts with no activity in a period of time.',
'aliases' => array('ub'),
'examples' => array(
'drush user-blocker' => 'Blocks user accounts who did not log in in the last two years.',
'drush user-blocker en es' =>
'Blocks user accounts who did not log in in the last two years whose default language is English or Spanish.',
'drush user-blocker --since="two months ago" es' => 'Blocks user accounts which have Spanish as their default language and an account age ' .' of more than two months without activity.',
),
'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL,
'arguments' => array(
'languages' => 'A list of languages to filter user accounts.',
),
'options' => array(
'since' =>
'Specifies last time a user account should have logged in ' .
'so it won't get blocked. Defaults to 2 years ago. Accepts ' .
'all date formats described at ' .
'http://www.php.net/manual/en/dtetime.formats.php.show.',
),
);
return $items;
}

The second part implements the command callback. It first gathers arguments and options and then performs the SQL query to block users:

/**
* Callback implementation for user-blocker command
*/
function drush_user_blocker() {
// Grab all languages given. If any.
$languages = func_get_args();
// See if we received a date from which we should filter
$since = strtotime(drush_get_option('since', '2 years ago'));
// Perform the update over the users table
$query= db_update('users')
->fields(array('status' => 0,))
->condition('uid', array(0, 1), 'NOT IN')
->condition('access', array(1, $since), 'BETWEEN'),
// Add the condition to filter by language
if (count($languages)) {
$query->condition('language', $languages, 'IN'),
}
$total_blocked = $query->execute();
drush_log(dt("Blocked !total_blocked users", array('!total_blocked' => $total_blocked)), 'success'),
}

Save the file. Let's verify that the command was loaded by reading its help information in the main Drush help:

$ drush help
...
Other commands: (userblocker)
user-blocker (ub) Blocks user accounts with no activity

Our command is listed at the end of the output. This is fine for just one command, but if you define many commands and they do not fit in any of the existing categories, you can classify it within the drush help output by implementing hook_drush_help(). More information about this hook can be seen by executing drush topic docs-examplecommand. Now let's see the detailed description of our command:

$ drush help user-blocker
Blocks user accounts with no activity
Examples:
drush user-blocker Blocks user accounts who did not log in in the last two years.
drush user-blocker en es Blocks user accounts who did not log in in the last two years and whose default language is English or Spanish.
drush user-blocker Blocks user accounts which have Spanish --since="two months ago" es as their default language and an account age of more than two months without activity.
Arguments:
roles of roles. Note that you have to wrap a role between quotes if it has a space in its name.
Options:
--since Specifies the date that marks the last time a user account should have logged in so it wont get blocked. Defaults to 2 years ago. Accepts all date formats described at http://www.php.net/manual/en/ datetime.formats.php.show.
Aliases: ub

The previous information is extracted from the first hook implemented at the command file: hook_drush_command(). It shows its basic description, examples, arguments, options, and command aliases.

When writing commands, keep an eye at the Drush API as it describes a lot of functions that will be very useful for your scripts http://api.drush.org/api/functions.

Following is how we can quickly test our command before studying its implementation:

$ drush user-create user1
$ drush sql-query "update {users} set access=1 where name = 'user1';"
$ drush user-blocker
Blocked 1 users.

Note

Several date fields in Drupal such as the created, access, and login fields in the users table are stored as Unix Timestamps. A Unix Timestamp represents time through the number of seconds since 1st of January, 1970. This means that if we set the age of a user account to 1, it means one second after the first of January of 1970.

Analyzing the definition of our command

Now we are going to analyze the source code of our command to understand how it works and then improve it.

The first function defined contains the implementation of hook_drush_command(). This hook is used to tell Drush about commands implemented in the file. They are defined as a keyed array; the key of each element being the command name. Following is a description of each of the properties defined within the key user-blocker:

  • description: This is used to describe our command when drush help is executed and at the top of the description when drush help user-blocker is executed.
  • aliases: This is an array with a list of shortcuts for our command.
  • examples: This is an array with pairs of command => expected result examples given. You should try to cover the most illustrative cases as it will help users to understand what your command is capable of.
  • arguments: This is an array with each argument name and its description. Single valued arguments (such as integers or strings) are normally defined here and then received in the callback function as variables. Variable length arguments (such as in our command) are obtained in the callback function through func_get_args() as an array.
  • options: This is an array with a list of options that our command accepts and their respective descriptions. Options can be accessed within the callback function using drush_get_option('option name', 'default value').

There are other important options not defined in our command, such as:

  • callback: This is the function that will be called to process different commands. If not defined, Drush looks for a function with the structure drush_COMMANDFILE_commandname() with underscores instead of dashes. If you choose to define a callback, it must start with drush_COMMANDFILE_ (for example, drush_user_blocker_block). It is recommended to omit this option.
  • bootstrap: This defines up to which level a Drupal site should be bootstrapped. Defaults to DRUSH_BOOTSTRAP_DRUPAL_FULL, which does a full Drupal bootstrap similar to when we open a site in a web server. There are several levels of bootstrapping such as DRUSH_BOOTSTRAP_DRUSH, which is used by commands that do not need a Drupal directory to be executed. Read the contents of drush topic bootstrap for detailed information about other levels.
  • core: If this command has limited version support, specify it here. Examples of this are 6, 7 or 6+.
  • drupal dependencies and drush dependencies: Drupal modules or Drush command files that this command may require, respectively.

Analyzing the implementation of our command

After a quick look at the code of our callback function, we can see that apart from drush_get_option() and drush_print() the rest is day-to-day Drupal code. This is good news because it means that we can use the Drupal APIs within our command when a Drupal site is bootstrapped. So far, the code is very simple and it just grabs the list of languages, calculates the date of the last time a user must have logged in, and finally performs a SQL query to block user accounts, which match the criteria. Let's set a user with Spanish as default language; an old last access date and then test our command again:

$ drush sql-query "update {users} set status=1, access=1,  language='es' where name = 'user1';"
$ drush user-blocker es
Blocked 1 users.

The previous command blocked one user account that also matched to Spanish as its default language. Languages are grabbed in the command at the following lines as an array and then added to the SQL query:

// Grab all languages given
$languages = func_get_args();

Drush provides the command drush_get_option() to catch options if they are given. Let's first see a working example and then examine the code. We will set the last access date of our test user account to one hour ago and run the command again:

$ drush sql-query "update {users} set status=1, 
access=(unix_timestamp() - 3600) where name = 'user1';"
$ drush user-blocker --since="1 hour ago" es
Blocked 1 users.

The previous command blocked the account as we changed the default last access date from two hours to one with the --since option.

Options and arguments can be used together as we saw in Chapter 1, Installation and Basic Usage. Following is a command example of how we could block user accounts, which last accessed the site more than a year ago, and have Spanish, English, or French as their default language. We will also use the command alias that we defined:

$ drush ub --since="1 year ago" es en fr

As you can see, options and arguments provide higher flexibility so our commands can cover the widest range of scenarios. In the next section, we will validate them.

Validating input

We are assuming that all the arguments and options given through the command line are safe and valid, but in order to make our command robust we have to validate the input. We will implement drush_hook_COMMAND_validate() to add some logic that ensures that the options and arguments given are valid. Add the following code below userblocker_drush_command() in the file userblocker.drush.inc:

/**
* Implementation of drush_hook_COMMAND_validate()
*/
function drush_userblocker_user_blocker_validate() {
// Validates language arguments if they were given
$languages = func_get_args();
if (count($languages)) {
$languages = array_unique($languages);
$valid_languages = array_intersect( $languages, array_keys(language_list()));
if (count($languages) != count($valid_languages)) {
return drush_set_error('INVALID_LANGUAGE', dt( 'At least one of the languages given does not exist.'));
}
}
// Validates if a valid date was given
if (drush_get_option('since')) {
if (!strtotime(drush_get_option('since'))) {
return drush_set_error('INVALID_SINCE_DATE', dt( 'The date given as --since option is invalid.'));
}
}
}

The previous code validates the languages and date given. Note that the function name follows the format that we explained before: hook is the name of our command file (userblocker), and COMMAND is the name of our command using underscores instead of dashes (user_blocker). We added some logic to make sure that all languages provided exist in our site (in case the user misspelled one of them) and that the date provided in the --since option is a valid date that can be converted to a timestamp through strtotime(). If any of this validations failed then we set an error by calling to drush_set_error(), which will stop the execution and print a message. Drush can translate strings (such as the error descriptions) using the function dt(), which is a wrapper for the well know t() function from Drupal. Now we will test our command to verify that it validates user input:

$ drush user-blocker --since="nonsense date"
The date given as --since option is invalid [error]
$ drush user-blocker en es
At least one of the languages given do not exist [error]
$ drush user-blocker --since="1970" en
Blocked 0 users [success]

First, we executed the command with a string date that cannot be converted to a timestamp, hence we did not pass validation. Then we attempted to block user accounts with English and Spanish as their default languages, but the fact is that Spanish language has not been enabled nor the Locale module so again an error was reported. We finally verified that the command works as expected by giving it a valid language and a valid date, which returned a success message although we did not block any user account.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.163.13