Error Handling and Debugging - Drush for Developers Second Edition (2015)

Drush for Developers Second Edition (2015)

Chapter 4. Error Handling and Debugging

Up to this point in the book, we have covered many topics about running code with Drush. The next step is to make sure that our code runs smoothly by ensuring that input data is correct and by implementing error handling. We will also see a few tools to help us understand Drush's bootstrap even further.

In this chapter, we will cover the following topics to meet the preceding goals:

· Validating input

· Rolling back on errors

· Browsing Drush's available hooks

· Monitoring Drush's bootstrap process

· Inspecting Drupal's hooks and function implementations

Validating input

Drush can validate input arguments before handing them over to the command's callback. In this section, we will see how to process arguments and options in order to make sure that the command's callback (the function that actually does the processing of a command) receives the right input data.

Validating an argument

By default, Drush won't require any input arguments to execute a command, not even when you define them in the command callback. We can see this in the following example, which defines a command that expects one argument named $argument_1. We have placed this file at sites/all/drush/testcommand.drush.inc in our sample Drupal project:

<?php

/**

* @file

* Sample Drush command to test arguments.

*/

/**

* Implements hook_drush_command().

*/

function testcommand_drush_command() {

$items = array();

$items['testcommand'] = array(

'description' => "Tests Drush command arguments",

'arguments' => array(

'argument_1' => 'This is a sample argument.',

),

);

return $items;

}

/**

* Implements drush_hook_COMMAND().

*/

function drush_testcommand($argument_1) {

var_dump($argument_1);

}

We have defined a command called testcommand. Now, let's execute it without arguments:

$ drush testcommand

Missing argument 1 for drush_testcommand()

testcommand.drush.inc:26 [warning]

NULL

Drush logged a warning that came from PHP regarding an undefined variable expected by our command's callback, which we did not enter. As a consequence, when we printed the value of $argument_1, we got a NULL value. As you can see, Drush did not do any validation. If we want it to, we have to be explicit by adding the 'required-arguments' => TRUE option at the command definition (testcommand_drush_command()). Here is our command definition after we add it:

$items['testcommand'] = array(

'description' => 'Tests Drush command arguments',

'arguments' => array(

'argument_1' => 'This is a sample argument.',

),

'required-arguments' => TRUE,

);

Here is the output when we run our command again without any input arguments:

$ drush testcommand

Missing required argument: 'argument_1'. See

`drush help testcommand` for information on usage. [error]

Thanks to the required-arguments setting, Drush now forces us to enter a value for the required argument $argument_1. If our command expects more than one argument and only some of them are required, the required-arguments setting can also be set to a number, which defines the minimum amount of arguments that the command expects. Here is an updated version of our sample command, where the first and second arguments are required and the third one is optional:

/**

* Implements hook_drush_command().

*/

function testcommand_drush_command() {

$items = array();

$items['testcommand'] = array(

'description' => 'Tests Drush command arguments',

'arguments' => array(

'argument_1' => 'This is a sample argument.',

'argument_2' => 'This is a sample argument.',

'argument_3' => 'This is a sample argument.',

),

'required-arguments' => 2,

);

return $items;

}

/**

* Implements drush_hook_COMMAND().

*/

function drush_testcommand($argument_1, $argument_2, $argument_3 = NULL) {

var_dump(array($argument_1, $argument_2, $argument_3));

}

Our command callback signature matches with the required-arguments setting: the first two arguments are required and the third one is optional (hence, it defaults to NULL). Now, we will test it with different input arguments to see how it behaves. First, we will run it with no arguments and then with just one argument:

$ drush testcommand

Missing required arguments: 'argument_1, argument_2'.

See `drush help testcommand` for information on usage. [error]

$ drush testcommand one

Missing required arguments: 'argument_1, argument_2'.

See `drush help testcommand` for information on usage. [error]

We can see in the preceding command executions that we must provide at least two arguments to our command or else Drush will fail to process it. Now, we will run the command with two and then three arguments, which will pass validation and then print the values:

$ drush testcommand one two

array(3) {

[0] => string(1) "one"

[1] => string(1) "two"

[2] => NULL

}

$ drush testcommand one two three

array(3) {

[0] => string(3) "one"

[1] => string(3) "two"

[2] => string(5) "three"

}

As we expected, validation is successful and our command prints the input values.

Validating options

Drush has a stricter behavior for options than for arguments. It will evaluate all given options and if any of them is not supported by Drush core or the command being executed, it will throw an error. Here is an example:

$ drush version --foo

Unknown option: --foo. See `drush help version` for

available options. To suppress this error, add the

option --strict=0. [error]

As we can see in the error message, this validation can be disabled by appending the --strict=0 option to the command invocation:

$ drush --strict=0 version --foo

Drush Version : 7.0.0-alpha5

When defining a command, there are two settings that alter how Drush processes its options. These are mentioned in the following sections.

Ignoring options after the command name

The strict-option-handling command can be set to TRUE at the command definition when we want to allow extra options that are not known by Drush. Drush uses this setting for the core-rsync command, which accepts custom options for the rsync command that gets executed in the background to perform a recursive directory copy. Here is a simplified version of the core-rsync command definition:

$items['core-rsync'] = array(

'description' => 'Rsync the Drupal tree to/from another server using ssh.',

'arguments' => array(

'source' => 'May be rsync path or site alias. See rsync documentation and example.aliases.drushrc.php.',

'destination' => 'May be rsync path or site alias. See rsync documentation and example.aliases.drushrc.php.',

),

'options' => array(

...

'{rsync-option-name}' => "Replace {rsync-option-name} with the rsync option (or option='value') that you would like to pass through to rsync.",

),

'strict-option-handling' => TRUE,

);

The core-rsync command definition accepts rsync specific options and uses strict-option-handling. Here is a sample command invocation with some options for Drush and others that are to be passed to rsync:

$ drush --yes core-rsync -v -az --exclude-paths='.git:.svn' local-files/ @site:%files

We mentioned in Chapter 1, Introduction, Installation, and Basic Usage, that you can place options either before or after the command name as Drush will evaluate them all. When strict-option-handling is set, all the options placed before the command name are processed by Drush, while options placed after the command are processed by the command. In the preceding example, -v -az --exclude-paths='.git:.svn' are all options that will be passed to the rsync command.

The core-rsync command calls drush_get_original_cli_args_and_options() in order to obtain the list of options provided in the command line and pass them to rsync. If you ever need to build a wrapper for a system command and want to accept its options, this function will come in handy.

Allowing additional options

The allow-additional-options setting can be used at the command definition and depending on whether it is a TRUE value or an array, it means different things for Drush.

If allow-additional-options equals TRUE, then Drush won't validate options at all. This setting is used, for example, by the help command to give you the freedom to copy and paste any command after drush help, no matter which arguments and options it has. It will simply extract the command name and print back its full description:

$ drush help core-status --full --foo --bar

Provides a birds-eye view of the current Drupal installation, if any.

Examples:

drush core-status version Show all status lines that

contain version information.

...

Alternatively, allow-additional-options might contain an array of command names whose options will be supported too. This is useful when your command calls other commands using drush_invoke() and needs to support its options as well. For example, sql-cli is a Drush command that opens an interactive connection with the database. Internally, it calls the sql-connect command in order to build a connection string. Here, we can see the definition of sql-cli taken from Drush core:

$items['sql-cli'] = array(

'description' => "Open a SQL command-line interface using Drupal's credentials.",

'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,

'allow-additional-options' => array('sql-connect'),

'aliases' => array('sqlc'),

'examples' => array(

'drush sql-cli' => "Open a SQL command-line interface using Drupal's credentials.",

'drush sql-cli --extra=-A' => "Open a SQL CLI and skip reading table information.",

),

'remote-tty' => TRUE,

);

The sql-cli command supports the options defined by the sql-connect command thanks to allow-additional-options' => array('sql-connect'). This is why, in the examples section, there is an example where it uses the --extra option. This approach is way more flexible than manually defining the --extra option because if sql-connect adds further options in the future, we won't need to make any changes in the command definition of sql-cli to support them.

Adding custom validation to a command

If we need to make custom validation of our input parameters, then it is time to implement drush_hook_COMMAND_validate(). This hook gets executed right before a command's callback. We will now add this hook to the contributed module: Node Revision Delete, which we worked with in previous chapters. Let's first see how the command works:

$ cd /home/juampy/projects/drupal

$ drush help node-revision-delete

Deletes old node revisions for a given content type.

Examples:

drush nrd article 50 Keeps the latest 50 revisions of every

article. Deletes the rest.

Arguments:

type A content type's machine name.

revisions The maximum amount of revisions

to keep per node for this content type.

Aliases: nrd

The node-revision-delete command accepts two arguments: a content type name and a number of revisions to keep for each node. These two arguments are set to be required through the 'required-arguments' => TRUE option, but we are not checking whether the content type exists or if the amount of revisions is a positive integer. Here is our validate hook that does so:

// sites/all/modules/contrib/node_revision_delete/node_revision_delete.drush.inc

/**

* Implements drush_hook_COMMAND_validate().

*/

function drush_node_revision_delete_validate($content_type,

$revisions_to_keep) {

// Make sure the content type exists.

$content_types = array_keys(node_type_get_types());

if (!in_array($content_type, $content_types)) {

drush_set_error('NODE_REVISION_DELETE_WRONG_TYPE', dt('The content type "!type" does not exist. Available content types are !types', array(

'!type' => $content_type,

'!types' => implode(', ', $content_types),

)));

}

// Make sure the number of revisions is a positive integer.

if (!is_numeric($revisions_to_keep) ||

intval($revisions_to_keep) != $revisions_to_keep ||

$revisions_to_keep <= 0) {

drush_set_error('NODE_REVISION_DELETE_WRONG_REVISIONS', dt('The amount of revisions to keep must be a positive integer.'));

}

}

Our drush_node_revision_delete_validate()validate hook takes the command arguments as input variables. Drush takes care of capturing input arguments from the command line and setting them into these two variables ($content_type and $revisions_to_keep). If the validate function returns FALSE or drush_set_error() is called, Drush won't execute the command.

The drush_set_error() function accepts three arguments:

· A machine name version of the error; this is useful when you want to classify errors and reuse error messages

· An optional error message to be printed to STDERR

· An optional label to add before the error message

Let's test our validation callback now:

$ drush node-revision-delete basic_page 1.5

The content type "basic_page" does not exist. Available

content types are article, feed, feed_item, page [error]

The amount of revisions to keep must be a positive

integer. [error]

There we are. Our validation callback calls drush_set_error() as we did not enter valid arguments, which writes to STDERR and makes Drush stop processing the command and trigger the rollback mechanism, which we will explain in the following section.

Tip

You can find additional documentation about error codes at drush topic docs-errorcodes.

Rolling back when an error happens

When drush_set_error() is called during a command execution, the rollback mechanism jumps into action. The rollback mechanism gives us a chance to exit gracefully if something goes wrong. It is especially useful when we only want to perform a final action if a command is completed successfully. Drush itself uses the rollback mechanism when dealing with core and module upgrades, performing actions such as restoring original files back in place, and deleting the downloaded files of the new version if there is an error.

Here is the full sequence of invocations for a given command. In the following list, hook is the filename where the Drush command is implemented (excluding the .drush.inc extension) and COMMAND is the actual command name:

# 1. hook_drush_init()

# 2. drush_COMMAND_init()

# 3. drush_hook_COMMAND_pre_validate()

# 4. drush_hook_COMMAND_validate()

# 5. drush_hook_pre_COMMAND()

# 6. drush_hook_COMMAND()

# 7. drush_hook_post_COMMAND()

# 8. hook_drush_exit()

Also, here is the list of rollback functions that Drush will attempt to call if there is an error. Notice that it goes in backward order as the preceding list:

# 1. drush_hook_post_COMMAND_rollback()

# 2. drush_hook_COMMAND_rollback()

# 3. drush_hook_pre_COMMAND_rollback()

# 4. drush_hook_COMMAND_validate_rollback()

# 5. drush_hook_COMMAND_pre_validate_rollback()

If there is an error, Drush will stop the execution and attempt to call rollback functions for every hook that was executed. For example, if an error happens at drush_hook_pre_COMMAND(), then Drush will call drush_hook_pre_COMMAND_rollback(),drush_hook_COMMAND_validate_rollback(), and drush_hook_COMMAND_pre_validate_rollback(). You can find plenty of documentation about these hooks at drush topic docs-api.

Turning the update path into a single command

In order to see a practical example, we will retake the update path script that we covered in Chapter 2, Keeping Database Configuration and Code Together. The update path was a Bash script that called a few Drush commands in order to keep the configuration in the database in sync with the exported configuration in code. Here it is:

# 1. Registry Rebuild.

drush --verbose registry-rebuild --no-cache-clear

# 2. Run database updates.

drush --verbose --yes updatedb

# 3. Clear the Drush cache.

# Sometimes Features may need this due to a bug in Features module.

drush cache-clear drush

# 4. Revert all features.

drush --verbose --yes features-revert-all

# 5. Clear all caches.

drush --verbose cache-clear all

What we will do now is to work on a new iteration for the preceding piece of logic. We will implement the following features:

· We will wrap these commands within a custom Drush command (a Drush command can call other commands).

· We will implement drush_hook_pre_COMMAND() and drush_hook_post_COMMAND() in order to enable and disable Drupal's maintenance mode, respectively, as a measure of precaution when we update the database.

· If something goes wrong during our command, drush_hook_post_COMMAND() won't be invoked and instead drush_hook_COMMAND_rollback() will do, so our site will stay in maintenance mode. This is ideal as we do not want to show visitors a broken site. We will simply log an alert in the rollback callback for the administrator to take action.

Here is our update path command that we will implement within our sample Drupal project at sites/all/drush/updatepath.drush.inc. We will now explain it hook by hook. The first thing at the top of the file is the command definition:

<?php

/**

* @file

* Drush implementation of the update path.

*/

/**

* Implements hook_drush_command().

*/

function updatepath_drush_command() {

$items = array();

$items['updatepath'] = array(

'description' => 'Runs the update path in the current site performing tasks such as database update, reverting features, etc.',

);

return $items;

}

Next, we will implement drush_hook_pre_command(), where we will enable Drupal's maintenance mode and kill user sessions in order to make sure that Drupal won't accept web requests when the update path command runs:

/**

* Implements drush_hook_pre_command().

*/

function drush_updatepath_pre_updatepath() {

drush_log('Enabling maintenance mode and killing active sessions.', 'status');

variable_set('maintenance_mode', 1);

db_query('truncate table {sessions}');

}

Now, we will actually implement each of the steps of our update path. We are making extensive use of drush_invoke_process() here, which is a Drush function that runs commands as subprocesses. Ideally, we should evaluate the result of these invocations in order to stop the executions if there are errors, but for simplicity, we will skip this check for now:

/**

* Implements drush_hook_command().

*/

function drush_updatepath() {

drush_invoke_process('@self', 'registry-rebuild', array(), array(

'no-cache-clear' => TRUE,

));

drush_invoke_process('@self', 'updatedb', array(), array('yes' => true));

drush_invoke_process('@self', 'cc', array('type' => 'drush'));

drush_invoke_process('@self', 'features-revert-all', array(), array(

'yes' => true,

));

drush_invoke_process('@self', 'cc', array('type' => 'all'));

}

The @self argument is a Drush site alias. It is used to reference a Drupal project and is covered in detail in Chapter 5, Managing Local and Remote Environments.

If everything goes well with the previous callbacks, then drush_hook_post_command() will be invoked. Here, we are implementing it in order to disable the maintenance mode and logging a message to inform that the site is not in maintenance mode anymore:

/**

* Implements drush_hook_post_command().

*/

function drush_updatepath_post_updatepath() {

drush_log('Disabling maintenance mode.', 'status');

variable_del('maintenance_mode');

}

Alternatively, if there was an error during our command, our site will stay in maintenance mode because drush_hook_post_command() won't be invoked, but drush_hook_command_rollback() will. We are implementing this hook in the following code just to alert that maintenance mode is still on:

/**

* Implements drush_hook_command_rollback().

*/

function drush_updatepath_rollback() {

drush_log('Oh no! Something went wrong. Review the above log and disable maintenance mode when done.', 'error');

}

Let's run drush updatepath in our sample Drupal project and verify its output. Note that we are using the --verbose options to see [status] messages. The following output is a simplified version for clarity:

$ cd /home/juampy/projects/drupal

$ drush --verbose updatepath

Enabling maintenance mode and killing active sessions. [status]

The registry has been rebuilt via registry_rebuild (A). [success]

The Drupal caches have NOT been cleared after all

registry rebuilds. [warning]

It is highly recommended you clear the Drupal caches as

soon as possible. [warning]

All registry rebuilds have been completed. [success]

No database updates required [success]

'all' cache was cleared. [success]

Finished performing updates. [ok]

'drush' cache was cleared. [success]

Current state already matches defaults, aborting. [ok]

'all' cache was cleared. [success]

Disabling maintenance mode. [success]

We can see that the first and last messages of the preceding output are our pre and post command hooks. There is also a warning from the Registry Rebuild module telling us to clear caches as soon as possible, which we do right after we run database updates. This was a smooth run. Now, let's suppose that there is an error. The last message, instead of being Disabling maintenance mode. [success], would be the following one:

Oh no! Something went wrong. Review the above log and

disable maintenance mode when done. [error]

The preceding message is called due to Drush's rollback mechanism. We are simply alerting that the maintenance mode is still active. If you need to take action when a command fails, then the rollback mechanism is the place to do it.

Browsing hook implementations

So far, we saw some hooks that Drush supports before and after running a command. In order to discover them, Drush offers a debugging mode to view all the hooks that we can implement for a given command and check whether they were executed or not on runtime.

In the following example, we will define a very simple command that we will use to test the handy option --show-invoke, which prints all the function callbacks where Drush attempts to find a match. We will create this command under $HOME/.drush/testhooks.drush.inc, which makes it available for us everywhere in the command-line interface for our user:

<?php

/**

* @file

* Sample Drush command to test hook invocations.

*/

/**

* Implements hook_drush_command().

*/

function testhooks_drush_command() {

$items = array();

$items['testhooks'] = array(

'description' => 'Dummy command to test command invocations.',

// No bootstrap at all.

'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,

);

return $items;

}

/**

* Implements drush_hook_COMMAND().

*/

function drush_testhooks() {

// Leaving it empty. Just want to see what happens before and after.

}

Now that we have our sample command, which Drush hooks do we have available? How should we name them after? Let's run the command with the --show-invoke option to see them:

$ cd /home/juampy

$ drush --show-invoke testhooks

Available drush_invoke() hooks for testhooks: [ok]

drush_testhooks_pre_validate

drush_archive_testhooks_pre_validate

drush_browse_testhooks_pre_validate

...

drush_testhooks_validate

drush_archive_testhooks_validate

drush_browse_testhooks_validate

...

drush_testhooks_pre_testhooks

drush_archive_pre_testhooks

drush_browse_pre_testhooks

...

drush_testhooks [* Defined in /home/juampy/.drush/testhooks.drush.inc]

drush_archive_testhooks

drush_browse_testhooks

...

drush_testhooks_post_testhooks

drush_archive_post_testhooks

drush_browse_post_testhooks

...

Available rollback hooks for testhooks: [ok]

drush_testhooks_rollback

Drush first checks whether each hook has been implemented at testhooks.drush.inc and then looks at all the command files in core and the following locations (see drush topic docs-commands for further details):

· Drush's core commands directory. For example, /home/juampy/.composer/vendor/drush/drush/commands.

· Directories added manually through the --include option, such as drush --include=/home/juampy/projects/drupal/sites/all/drush testhooks.

· The system-wide shared directory. For example, /usr/share/drush/commands.

· The .drush folder in our home directory, which is where we implemented our testhooks command in the preceding section

· The /drush and /sites/all/drush directories within the current Drupal installation.

· All the enabled modules in the current Drupal installation.

Now, let's change directory into our sample Drupal project and run it again. Notice that we defined our command at testhooks_drush_command() to use 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, which means that we don't want to bootstrap Drupal at all:

$ cd /home/juampy/projects/drupal

$ drush --show-invoke testhooks

Available drush_invoke() hooks for testhooks: [ok]

drush_testhooks_pre_validate

drush_archive_testhooks_pre_validate

drush_browse_testhooks_pre_validate

drush_registry_rebuild_testhooks_pre_validate

drush_testcommand_testhooks_pre_validate

drush_updatepath_testhooks_pre_validate

...

Drush did not look for command implementations at Drupal project's installed modules as our command does not need it. For commands that do not need to bootstrap a Drupal site, this is a performance boost as Drush does not spend time doing it before running our command. However, if your command might benefit from having a Drupal project bootstrapped, then you can set the bootstrap setting to DRUSH_BOOTSTRAP_MAX, which attempts to bootstrap a Drupal project if it is available. We will now update our command definition at /home/juampy/.drush/testhooks.drush.inc and then run it again within our Drupal project to verify that it now looks into the installed modules for command hook implementations. Here is the command with the bootstrap setting changed:

/**

* Implements hook_drush_command().

*/

function testhooks_drush_command() {

$items = array();

$items['testhooks'] = array(

'description' => 'Dummy command to test command invocations.',

// No bootstrap at all.

'bootstrap' => DRUSH_BOOTSTRAP_MAX,

);

return $items;

}

Here is the output when we run the command:

$ cd /home/juampy/projects/drupal

$ drush --show-invoke testhooks

Available drush_invoke() hooks for testhooks: [ok]

drush_testhooks_pre_validate

drush_archive_testhooks_pre_validate

drush_browse_testhooks_pre_validate

drush_ctools_testhooks_pre_validate

drush_features_testhooks_pre_validate

drush_newsfetcher_testhooks_pre_validate

There you are! Now, Drush is also looking for command hook implementations at contributed (ctools, features) and custom modules (newsfetcher) in our Drupal project. If we implement any of these functions, they will be called by Drush. We will dive even deeper into Drush's bootstrap phases in the following section.

Inspecting the bootstrapping process

When Drush is called, it goes over a set of bootstrap steps that are very similar to how Drupal bootstraps on a web request. Drush commands might require minimum bootstrap phase to run. Here is a simplified list of each of Drush's bootstrap steps based on the documentation at drush topic docs-bootstrap:

1. DRUSH_BOOTSTRAP_DRUSH: This is the minimum bootstrap phase. It just loads Drush configuration and core files.

2. DRUSH_BOOTSTRAP_DRUPAL_ROOT: This checks whether there is a valid Drupal's root directory available. It is useful for commands that deal with a whole Drupal installation and not a specific site at the sites directory.

3. DRUSH_BOOTSTRAP_DRUPAL_SITE: This will load Drush's configuration of a specific site within the sites directory of a Drupal project, but it won't load settings.php.

4. DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION: This loads the site's settings.php file.

5. DRUSH_BOOTSTRAP_DRUPAL_DATABASE: This connects to the site's database, so database queries against a Drupal project can be made from this phase onwards.

6. DRUSH_BOOTSTRAP_DRUPAL_FULL: This loads all the available APIs in the Drupal project.

7. DRUSH_BOOTSTRAP_DRUPAL_LOGIN: This logs in as a given user defined by the --user option. The default value is to use the anonymous user.

8. DRUSH_BOOTSTRAP_MAX: This will try to bootstrap Drupal as far as possible, but it does not require a Drupal project to be available.

The default phase, if none is set, when defining a command at hook_drush_command() is DRUSH_BOOTSTRAP_DRUPAL_LOGIN. We used the last one (DRUSH_BOOTSTRAP_MAX) at our testhooks custom command in order to execute it both with and without the context of a Drupal project.

Drush's --debug option can also provide useful information regarding how far Drush reached in the bootstrap process. Here is a sample command output:

$ cd /home/juampy/projects/drupal

$ drush --debug testhooks

We started by changing the directory into our Drupal project and then ran our sample command with the --debug option to see how the bootstrap process works. Here is the output step by step:

Drush bootstrap phase : _drush_bootstrap_drush() [bootstrap]

Loading drushrc "/home/juampy/.drush/drushrc.php" into

"home.drush" scope. [bootstrap]

Loading drushrc

"sites/all/drush/drushrc.php" into "drupal" scope. [bootstrap]

Step 1 (DRUSH_BOOTSTRAP_DRUSH) is completed. Drush has been bootstrapped and it has loaded all the configuration files that it found available: one at the .drush directory under our home path and the other at the current Drupal project where we are. Let's move on to step 2:

Drush bootstrap phase :

_drush_bootstrap_drupal_root() [bootstrap]

Initialized Drupal 7.29-dev root directory at

/home/juampy/projects/drupal [notice]

The DRUSH_BOOTSTRAP_DRUPAL_ROOT phase is completed. We now know that we are within a Drupal project and can access its root directory with drush_get_context('DRUSH_DRUPAL_ROOT') from our command if we need to. Let's move on to the next phase:

Drush bootstrap phase :

_drush_bootstrap_drupal_site() [bootstrap]

Initialized Drupal site default at sites/default [notice]

The DRUSH_BOOTSTRAP_DRUPAL_SITE phase is completed. We can now gain access to the directory of the selected site under the sites directory with drush_get_context('DRUSH_SELECTED_DRUPAL_SITE_CONF_PATH'). Here is the output for the next phase:

Drush bootstrap phase :

_drush_bootstrap_drupal_configuration() [bootstrap]

The DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION phase has completed loading the settings.php file located at sites/default within our Drupal project. Let's move on to the next step:

Drush bootstrap phase :

_drush_bootstrap_drupal_database() [bootstrap]

Successfully connected to the Drupal database. [bootstrap]

The DRUSH_BOOTSTRAP_DRUPAL_DATABASE phase is completed and now we can query the database in our command using Drupal's database APIs:

Drush bootstrap phase :

_drush_bootstrap_drupal_full() [bootstrap]

In DRUSH_BOOTSTRAP_DRUPAL_FULL, all of the available APIs in our Drupal project are loaded and our command can make use of them if needed:

Drush bootstrap phase :

_drush_bootstrap_drupal_login() [bootstrap]

Successfully logged into Drupal as (uid=0) [bootstrap]

We did not provide a user with the --user option when we ran our command, so Drush used the special user with uid as 0 (the anonymous user) on the DRUSH_BOOTSTRAP_DRUPAL_LOGIN phase. If you need a specific user to run a command (for example, when your code is creating content), consider adding the --user option to your command with the user ID that you need:

Found command: testhooks (commandfile=testhooks) [bootstrap]

Calling hook drush_testhooks [debug]

Returned from hook drush_testhooks [debug]

Command dispatch complete [notice]

Timer Cum (sec) Count Avg (msec)

page 0.308 1 308.06

Peak memory usage was 26.38 MB [memory]

Our command was executed and Drush finished the process. Note that there was no log entry for DRUSH_BOOTSTRAP_MAX as this is not a phase, but an order for Drush to bootstrap as far as possible.

Inspecting hook and function implementations

The Devel module (https://www.drupal.org/project/devel) has a couple of commands that are extremely useful when either looking for hook implementations or locating functions within a Drupal installation. We will see them in action in the following sections.

Browsing and navigating hook implementations

The fn- hook command lists all modules implementing a given hook name. This command comes in very handy when you want to implement a hook, but want to check before whether any other modules implement it and what do they do.

Let's take hook_cron() as an example. In Chapter 3, Running and Monitoring Tasks in Drupal Projects, we spoke about the importance of extracting some tasks out of hook_cron() and moved them into a custom Drush command so that they could run on their own process and scheduling. Let's go to our sample Drupal project and run the command to see which modules implement hook_cron(). We will assume that the Devel module is already downloaded and installed:

$ cd /home/juampy/projects/drupal

$ drush fn-hook cron

Enter the number of the hook implementation you wish to view.

[0] : Cancel

[1] : ctools

[2] : dblog

[3] : feeds

[4] : field

[5] : job_scheduler

[6] : node

[7] : node_revision_delete

[8] : search

[9] : system

[10] : update

We can see in the preceding output that the fn-hook command lists all modules implementing hook_cron() sorted by module weight and filename, so you can get an idea of the order in which these callbacks will be executed. The output is listed as a select list where each module has an option number; Drush waits for us to enter a value in the command line. We will choose option 3 (the Feeds module) and hit Enter:

3

// file: /home/juampy/projects/drupal/sites/all/modules/contrib/feeds/feeds.module, lines 48-63

/**

* Implements hook_cron().

*/

function feeds_cron() {

if ($importers = feeds_reschedule()) {

foreach ($importers as $id) {

feeds_importer($id)->schedule();

$rows = db_query("SELECT feed_nid FROM {feeds_source} WHERE id = :id", array(':id' => $id));

foreach ($rows as $row) {

feeds_source($id, $row->feed_nid)->schedule();

}

}

feeds_reschedule(FALSE);

}

// Expire old log entries.

db_delete('feeds_log')

->condition('request_time', REQUEST_TIME - 604800, '<')

->execute();

}

Drush printed the Feed module's hook_cron() implementation, plus a heading with its file location and start and end lines. How cool is that?

Viewing source code of a function or method

The second handy command that comes with the Devel module to quickly view a particular piece of code is fn-view. It is specially helpful when you remember a function name, but not where it is defined. This command accepts a function or class method and prints its contents and location, if found. Here is an example where we print the contents of the drupal_debug() function, a debugging function provided by the Devel module that prints a variable into a temporary logfile:

$ drush fn-view drupal_debug

// file: /home/juampy/projects/drupal/sites/all/modules/contrib/devel/devel.module, lines 1788-1797

/**

* Logs a variable to a drupal_debug.txt in the site's temp directory.

*

* @param mixed $data

* The variable to log to the drupal_debug.txt log file.

* @param string $label

* (optional) If set, a label to output before $data in the log file.

*

* @return void|false

* Empty if successful, FALSE if the log file could not be written.

*

* @see dd()

* @see http://drupal.org/node/314112

*/

function drupal_debug($data, $label = NULL) {

$out = ($label ? $label . ': ' : '') . print_r($data, TRUE) . "\n";

// The temp directory does vary across multiple simpletest instances.

$file = file_directory_temp() . '/drupal_debug.txt';

if (file_put_contents($file, $out, FILE_APPEND) === FALSE) {

drupal_set_message(t('Devel was unable to write to %file.', array('%file' => $file)), 'error');

return FALSE;

}

}

We can also view class methods with fn-view. Here, we are viewing the contents of the render() method of the views_handler_field class, which is the base class to define custom fields in the Views module:

$ drush fn-view views_handler_field::render

// file: /home/juampy/projects/drupal/sites/all/modules/contrib/views/handlers/views_handler_field.inc, lines 1021-1024

function render($values) {

$value = $this->get_value($values);

return $this->sanitize_value($value);

}

These two methods are very useful to inspect code quickly. Keep in mind that both of them have their limitations: fn-hook cannot list all the available hooks in a Drupal project and instead expects you to provide the hook to search for, while fn-view can only print functions that are loaded by Drupal automatically either by the .info or .module files.

Summary

In this chapter, we covered many tools to help you write safe code and make the most of Drush's APIs. We started by discovering how input data is processed by Drush and how we can alter its behavior to fit our needs. Custom validation can also be implemented in a hook so that it runs its checks before the actual command.

Preparing our commands for unexpected errors is key in any project. Drush's rollback mechanism gives us a chance to take action if a command fails, so we can make any required cleanup and logging. We saw how our update path script can benefit from this mechanism in order to become more robust.

For times when we are writing a custom command, being aware of which Drush hooks are available and when they are executed is useful in order to split the logic in the most appropriate way. The --show-invoke option provides detailed information about each of the callbacks that Drush attempts to call during a command execution.

Whenever we use a command, Drush goes through a bootstrap process in a way that mimics Drupal's bootstrap. Understanding each of the different phases is vital in order to decide which APIs are available for a given command's callback. We saw that we can inspect Drush's bootstrap process on the fly with the --debug option.

Finally, we wrapped up the chapter with some usage examples of the Devel module's fn-hook and fn-view commands, which can be helpful to navigate through hook implementations and functions.

In the next chapter, we will discover one of the killer features of Drush: managing local and remote sites using site aliases.