I've been working on several custom Drush commands recently, and found that I could clean up the control flow if I could exit from a method, instead of returning from the function body. This would prevent many repetitive control flow structures in my code. Here is an example of a check I performed many times
$quiet = drush_get_option('quiet');
if (empty($fileUris)) {
if (!$quiet) {
drush_print('No files were found to process');
}
return;
}
What I wanted instead was
exitIfEmpty($fileUris, 'No files were found to process');
Of course, I could not return from the caller inside my function, so I needed some way to exit. But I found a few roadblocks.
Can't use exit() or die()
Drush uses register_shutdown_function as a way to control the execution flow of a custom Drush command body, and calling exit() or die() directly will produce an error and display this annoying message on the CLI, which I was trying to avoid
Drush command terminated abnormally due to an unrecoverable error. [error]
GOTO is discouraged
I thought (briefly) that I could potentially use GOTO
as a control flow structure in a pinch, but it is highly discouraged, and I tend to agree it should only be used in the direst of spaghetti code circumstances, if ever at all. I don't think I've ever really run into a circumstance yet where GOTO
was the only choice I had, except when I was working on a project once to migrate some legacy Visual Basic code that was riddled with them.
I have to wonder, though, why the PHP folks would introduce GOTO
in version 5.3 after it was not available for all versions prior? Was there a strong outcry from somewhere?
Refactoring would be time-consuming and not portable
I considered modularizing all my code into a class, but then I wouldn't have a solution that could work for those small one-off utilities that makes custom Drush commands so easy to implement.
Once I did a little more digging, I found a clean way to exit without errors.
The code review
I looked a little deeper into the source code of Drush itself and found what I was looking for in preflight.inc
I started by searching the code for that annoying "Drush command terminated abnormally..." error described earlier.
The method drush_shutdown
is registered as a shutdown function. There's a check for Drush context values, which give the first clue of what to work with
function drush_shutdown() {
...
if (!drush_get_context('DRUSH_EXECUTION_COMPLETED', FALSE) ...
...
// We did not reach the end of the drush_main function,
// this generally means somewhere in the code a call to exit(),
// was made. We catch this, so that we can trigger an error in
// those cases.
$php_error_message = "\n" . dt('Error: !message in !file, line !line', array('!message' => $error['message'], '!file' => $error['file'], '!line' => $error['line']));
drush_set_error("DRUSH_NOT_COMPLETED", dt("Drush command terminated abnormally due to an unrecoverable error.!message", array('!message' => $php_error_message)));
...
The second parameter of drush_get_context
is an optional default value, so if the value of DRUSH_EXECUTION_COMPLETED
is not set, it's assumed to be false. And here we see where the annoying error is set.
The last shutdown function in the chain, drush_return_status
, is looking for another context value which, if not set, prompts a look at any Drush errors that may have been caught
function drush_return_status() {
// If a specific exit code was set, then use it.
$exit_code = drush_get_context('DRUSH_EXIT_CODE');
if (empty($exit_code)) {
$exit_code = (drush_get_error()) ? DRUSH_FRAMEWORK_ERROR : DRUSH_SUCCESS;
}
exit($exit_code);
}
If we want to be safe, better to set DRUSH_EXIT_CODE
using the provided constant DRUSH_SUCCESS
The final Drush clean exit method
To safely exit with no Drush errors, we need to set the context values for DRUSH_EXECUTION_COMPLETED
and DRUSH_EXIT_CODE
. This can be done anywhere within the Drush method body.
drush_set_context('DRUSH_EXECUTION_COMPLETED', TRUE);
drush_set_context('DRUSH_EXIT_CODE', DRUSH_SUCCESS);
exit(0);
I'd optionally like to set a friendly information message in some cases.
The final method allows for this, and the exitClean
method is still highly portable
/**
* Clean Drush exit
*/
function exitClean() {
drush_set_context('DRUSH_EXECUTION_COMPLETED', TRUE);
drush_set_context('DRUSH_EXIT_CODE', DRUSH_SUCCESS);
exit(0);
}
/**
* Tests subject for emptiness, and exits if true
*
* @param mixed $subject
* @param null $message
*/
function exitIfEmpty($subject, $message=null) {
$quiet = drush_get_option('quiet');
if (empty($subject)) {
if ($message) {
if (!$quiet) {
drush_print($message);
}
}
exitClean();
}
}