$func)), 'bootstrap'); call_user_func_array($func, $args); _drush_log_drupal_messages(); if (drush_get_error()) { drush_log(dt('The command @command could not be initialized.', array('@command' => $command)), 'error'); return FALSE; } } $rollback = FALSE; $completed = array(); $available_rollbacks = array(); $all_available_hooks = array(); // Iterate through the different hook variations $variations = array($hook . "_validate", "pre_$hook", $hook, "post_$hook"); foreach ($variations as $var_hook) { // Get the list of command files. // We re-fetch the list every time through // the loop in case one of the hook function // does something that will add additional // commandfiles to the list (i.e. bootstrapping // to a higher phase will do this). $list = drush_commandfile_list(); $functions = array(); // Run all of the functions available for this variation foreach ($list as $commandfile => $filename) { $oldfunc = sprintf("drush_%s_%s", $commandfile, $var_hook); $func = str_replace('drush_' . $commandfile . '_' . $commandfile, 'drush_' . $commandfile, $oldfunc); if (($oldfunc != $func) && (function_exists($oldfunc))) { drush_log(dt("The drush command hook naming conventions have changed; the function !oldfunc must be renamed to !func. The old function will be called, but this will be removed shortly.", array('!oldfunc' => $oldfunc, '!func' => $func)), "error"); // TEMPORARY: Allow the function to be called by its old name. $functions[] = $oldfunc; } if (function_exists($func)) { $functions[] = $func; $all_available_hooks[] = $func . ' [*]'; $available_rollbacks[] = $func . '_rollback'; $completed[] = $func; $result = call_user_func_array($func, $args); // Only the 'main' callback can send data to backend. if ($var_hook == $hook) { drush_backend_set_result($result); } _drush_log_drupal_messages(); if (drush_get_error() || ($result === FALSE)) { $rollback = TRUE; // break out of the foreach variations and foreach list break 2; } } else { $all_available_hooks[] = $func; } } } // If no hook functions were found, print a warning. if (empty($all_available_hooks)) { drush_log(dt("No hook functions were found for !command.", array('!command' => $command)), 'warning'); drush_log(dt("Available drush_invoke() hooks for !command: !available", array('!command' => $command, '!available' => "\n" . implode("\n", $all_available_hooks))), 'warning'); } elseif (drush_get_option('show-invoke')) { // We show all available hooks up to and including the one that failed (or all, if there were no failures) drush_log(dt("Available drush_invoke() hooks for !command: !available", array('!command' => $command, '!available' => "\n" . implode("\n", $all_available_hooks))), 'ok'); } if (drush_get_option('show-invoke')) { drush_log(dt("Available rollback hooks for !command: !rollback", array('!command' => $command, '!rollback' => "\n" . implode("\n", $available_rollbacks))), 'ok'); } // something went wrong, we need to undo if ($rollback) { foreach (array_reverse($completed) as $func) { $rb_func = $func . '_rollback'; if (function_exists($rb_func)) { call_user_func_array($rb_func, $args); _drush_log_drupal_messages(); drush_log(dt("Changes made in !func have been rolled back.", array('!func' => $func)), 'rollback'); } } } return !$rollback; } /** * Given a command record, dispatch it as if it were * the original command. Executes in the currently * bootstrapped site using the current option contexts. * Note that drush_dispatch will not bootstrap any further than the * current command has already bootstrapped; therefore, you should only invoke * commands that have the same (or lower) bootstrap requirements. * * @param command * A full $command such as returned by drush_get_commands(). * @param arguments * An array of argument values. * * @see drush_topic_docs_topic(). */ function drush_dispatch($command, $arguments = array()) { drush_set_command($command); $return = FALSE; if ($command) { // Add arguments, if this has not already been done. // (If the command was fetched from drush_parse_command, // then you cannot provide arguments to drush_dispatch.) if (empty($command['arguments'])) { _drush_prepare_command($command, $arguments); } // Add command-specific options, if applicable drush_command_default_options($command); // Print a warning if someone tries to use a deprecated alias. if (isset($command['deprecated'])) { drush_log(dt('Warning: The command name "!deprecated" is deprecated. Please use a recommended form instead (!recommended).', array('!deprecated' => $command['deprecated-name'], '!recommended' => implode(',', array_merge(array($command['command']), $command['aliases'])))), 'warning'); } // Print a warning if a command callback function is misnamed if (isset($command['callback-required-prefix'])) { drush_log(dt('Warning: The command callback function !callback has a deprecated name. It must begin with !requiredprefix. Skipping hook functions.', array('!callback' => $command['callback'], '!requiredprefix' => $command['callback-required-prefix']))); } // Call the callback function of the active command. // TODO: If we make the required prefix actually required rather than just emitting a // warning, then this could become a direct call to drush_command (all commands with // the required prefix will now go through drush_command + drush_invoke). $return = call_user_func_array($command['callback'], $command['arguments']); } // Add a final log entry, just so a timestamp appears. drush_log(dt('Command dispatch complete'), 'notice'); return $return; } /** * Invoke a command in a new process. * * @param command_name * The drush command to execute. * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative * array containing the results of the API call. * @see drush_backend_get_result() */ function drush_invoke_process($command_name) { $args = func_get_args(); array_shift($args); return drush_invoke_process_args($command_name, $args); } /** * Invoke a command in a new process. * * @param command_name * The drush command to execute. * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative * array containing the results of the API call. * @see drush_backend_get_result() */ function drush_invoke_process_args($command_name, $commandline_args, $commandline_options = array()) { return drush_backend_invoke_args($command_name, $commandline_args, $commandline_options); } /** * Invoke a command in a new process, targeting the site specified by * the provided site alias record. * * @param array $site_alias_record * The site record to execute the command on. * @param string $command_name * The command to invoke. * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative * array containing the results of the API call. * @see drush_backend_get_result() */ function drush_invoke_sitealias($site_alias_record, $command_name) { $args = func_get_args(); array_shift($args); array_shift($args); return drush_invoke_sitealias_args($site_alias_record, $command_name, $args); } /** * Invoke a command in a new process, targeting the site specified by * the provided site alias record. * * @param array $site_alias_record * The site record to execute the command on. * @param string $command_name * The command to invoke. * @param array $commandline_args * The arguments to pass to the command. * @param array $commandline_options * The options (e.g. --select) to provide to the command. * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative * array containing the results of the API call. * @see drush_backend_get_result() */ function drush_invoke_sitealias_args($site_alias_record, $command_name, $commandline_args, $commandline_options = array()) { // If the first parameter is not a site alias record, // then presume it is an alias name, and try to look up // the alias record. if (!is_array($site_alias_record)) { $site_alias_record = drush_sitealias_get_record($site_alias_record); } return drush_do_site_command($site_alias_record, $command_name, $commandline_args, $commandline_options); } /** * Get the options that were passed to the current command. * * This function returns an array that contains all of the options * that are appropriate for forwarding along to one of the drush_invoke_*_args * functions. * * @return * An associative array of option key => value pairs. */ function drush_redispatch_get_options() { // Start off by taking everything from the site alias and command line // ('cli' context) $options = array_merge(drush_get_context('alias'), drush_get_context('cli')); $options = array_diff_key($options, array_flip(drush_sitealias_site_selection_keys())); unset($options['command-specific']); unset($options['path-aliases']); // If we can parse the current command, then examine all contexts // in order for any option that is directly related to the current command $command = drush_parse_command(); if (is_array($command)) { foreach ($command['options'] as $key => $value) { // Strip leading -- $key = ltrim($key, '-'); $value = drush_get_option($key); if (isset($value)) { $options[$key] = $value; } } } // 'php', if needed, will be included in DRUSH_COMMAND. If DRUSH_COMMAND // is not used (e.g. when calling a remote instance of drush), then --php // should not be passed along. unset($options['php']); // If --bootstrap-to-first-arg is specified, do not // pass it along to remote commands. unset($options['bootstrap-to-first-arg']); return $options; } /** * @} End of "defgroup dispatching". */ /** * @file * The drush command engine. * * Since drush can be invoked independently of a proper Drupal * installation and commands may operate across sites, a distinct * command engine is needed. * * It mimics the Drupal module engine in order to economize on * concepts and to make developing commands as familiar as possible * to traditional Drupal module developers. */ /** * Parse console arguments. */ function drush_parse_args() { $args = drush_get_context('argv'); // TODO: commandfiles should be able to extend this list. static $arg_opts = array('c', 'u', 'r', 'l', 'i'); // Check to see if we were executed via a "#!/usr/bin/env drush" script drush_adjust_args_if_shebang_script($args); // Now process the command line arguments. We will divide them // into options (starting with a '-') and arguments. $arguments = $options = array(); for ($i = 1; $i < count($args); $i++) { $opt = $args[$i]; // Is the arg an option (starting with '-')? if ($opt{0} == "-" && strlen($opt) != 1) { // Do we have multiple options behind one '-'? if (strlen($opt) > 2 && $opt{1} != "-") { // Each char becomes a key of its own. for ($j = 1; $j < strlen($opt); $j++) { $options[substr($opt, $j, 1)] = true; } } // Do we have a longopt (starting with '--')? elseif ($opt{1} == "-") { if ($pos = strpos($opt, '=')) { $options[substr($opt, 2, $pos - 2)] = substr($opt, $pos + 1); } else { $options[substr($opt, 2)] = true; } } else { $opt = substr($opt, 1); // Check if the current opt is in $arg_opts (= has to be followed by an argument). if ((in_array($opt, $arg_opts))) { if (($args[$i+1] == NULL) || ($args[$i+1] == "") || ($args[$i + 1]{0} == "-")) { drush_set_error('DRUSH_INVALID_INPUT', "Invalid input: -$opt needs to be followed by an argument."); } $options[$opt] = $args[$i + 1]; $i++; } else { $options[$opt] = true; } } } // If it's not an option, it's a command. else { $arguments[] = $opt; } } // If no arguments are specified, then the command will // be either 'help' or 'version' (the later if --version is specified) if (!sizeof($arguments)) { if (array_key_exists('version', $options)) { $arguments = array('version'); } else { $arguments = array('help'); } } // Handle the "@shift" alias, if present drush_process_bootstrap_to_first_arg($arguments); drush_set_arguments($arguments); drush_set_context('cli', $options); } /** * Pop an argument off of drush's argument list */ function drush_shift() { $arguments = drush_get_arguments(); $result = NULL; if (!empty($arguments)) { // The php-script command uses the DRUSH_SHIFT_SKIP // context to cause drush_shift to skip the 'php-script' // command and the script path argument when it is // called from the user script. $skip_count = drush_get_context('DRUSH_SHIFT_SKIP'); if (is_numeric($skip_count)) { for ($i = 0; $i < $skip_count; $i++) { array_shift($arguments); } $skip_count = drush_set_context('DRUSH_SHIFT_SKIP', 0); } $result = array_shift($arguments); drush_set_arguments($arguments); } return $result; } /** * Special checking for "shebang" script handling. * * If there is a file 'script.php' that begins like so: * #!/path/to/drush * Then $args will be: * /path/to/drush /path/to/script userArg1 userArg2 ... * If it instead starts like this: * #!/path/to/drush --flag php-script * Then $args will be: * /path/to/drush "--flag php-script" /path/to/script userArg1 userArg2 ... * (Note that execve does not split the parameters from * the shebang line on whitespace; see http://en.wikipedia.org/wiki/Shebang_%28Unix%29) * When drush is called via one of the "shebang" lines above, * the first or second parameter will be the full path * to the "shebang" script file -- and if the path to the * script is in the second position, then we will expect that * the argument in the first position must begin with a * '@' (alias) or '-' (flag). Under ordinary circumstances, * we do not expect that the drush command must come before * any argument that is the full path to a file. We use * this assumption to detect "shebang" script execution. */ function drush_adjust_args_if_shebang_script(&$args) { if (_drush_is_drush_shebang_script($args[1])) { // If $args[1] is a drush "shebang" script, we will insert // the option "--bootstrap-to-first-arg" and the command // "php-script" at the beginning of @args, so the command // line args become: // /path/to/drush --bootstrap-to-first-arg php-script /path/to/script userArg1 userArg2 ... drush_set_option('bootstrap-to-first-arg', TRUE); array_splice($args, 1, 0, array('php-script')); drush_set_context('DRUSH_SHEBANG_SCRIPT', TRUE); } elseif (((strpos($args[1], ' ') !== FALSE) || (!ctype_alnum($args[1][0]))) && (_drush_is_drush_shebang_script($args[2]))) { // If $args[2] is a drush "shebang" script, we will insert // the space-exploded $arg[1] in place of $arg[1], so the // command line args become: // /path/to/drush scriptArg1 scriptArg2 ... /path/to/script userArg1 userArg2 ... // If none of the script arguments look like a drush command, // then we will insert "php-script" as the default command to // execute. $script_args = explode(' ', $args[1]); $has_command = FALSE; foreach ($script_args as $script_arg) { if (preg_match("/^[a-z][a-z0-9-]*$/",$script_arg)) { $has_command = TRUE; } } if (!$has_command) { $script_args[] = 'php-script'; } array_splice($args, 1, 1, $script_args); drush_set_context('DRUSH_SHEBANG_SCRIPT', TRUE); } } /** * Process the --bootstrap-to-first-arg option, if it is present. * * This option checks to see if the first user-provided argument is an alias * or site specification; if it is, it will be shifted into the first argument * position, where it will specify the site to bootstrap. The result of this * is that if your shebang line looks like this: * * #!/path/to/drush --bootstrap-to-first-arg php-script * * Then when you run that script, you can optionally provide an alias such * as @dev as the first argument (e.g. $ ./mydrushscript.php @dev scriptarg1 * scriptarg2). Since this is the behavior that one would usually want, * it is default behavior for a canonical script. That is, a script * with a simple shebang line, like so: * * #!/path/to/drush * * will implicitly have "--bootstrap-to-first-arg" and "php-script" prepended, and will therefore * behave exactly like the first example. To write a script that does not * use --bootstrap-to-first-arg, then the drush command or at least one flag must be explicitly * included, like so: * * #!/path/to/drush php-script */ function drush_process_bootstrap_to_first_arg(&$arguments) { if (drush_get_option('bootstrap-to-first-arg', FALSE)) { $shift_alias_pos = 1 + (drush_get_context('DRUSH_SHEBANG_SCRIPT') === TRUE); if (sizeof($arguments) >= $shift_alias_pos) { $shifted_alias = $arguments[$shift_alias_pos]; $alias_record = drush_sitealias_get_record($shifted_alias); if (!empty($alias_record)) { // Move the alias we shifted from its current position // in the argument list to the front of the list array_splice($arguments, $shift_alias_pos, 1); array_unshift($arguments, $shifted_alias); } } } } /** * Get a list of all implemented commands. * This invokes hook_drush_command(). * * @return * Associative array of currently active command descriptors. * */ function drush_get_commands() { $commands = $available_commands = array(); $list = drush_commandfile_list(); foreach ($list as $commandfile => $path) { if (drush_command_hook($commandfile, 'drush_command')) { $function = $commandfile . '_drush_command'; $result = $function(); foreach ((array)$result as $key => $command) { // Add some defaults and normalize the command descriptor $command += drush_command_defaults($key, $commandfile, $path); // If command callback is correctly named, then fix // up the command entry so that drush_invoke will be // called. if ($command['callback'] != 'drush_command') { $required_command_prefix = 'drush_' . $commandfile . '_'; if ((substr($command['callback'], 0, strlen($required_command_prefix)) == $required_command_prefix)) { $command['command-hook'] = substr($command['callback'], strlen('drush_')); $command['callback'] = 'drush_command'; } else { $command['callback-required-prefix'] = $required_command_prefix; } } $commands[$key] = $command; // For every alias, make a copy of the command and store it in the command list // using the alias as a key if (isset($command['aliases']) && count($command['aliases'])) { foreach ($command['aliases'] as $alias) { $commands[$alias] = $command; $commands[$alias]['is_alias'] = TRUE; } } // Do the same operation on the deprecated aliases. if (isset($command['deprecated-aliases']) && count($command['deprecated-aliases'])) { foreach ($command['deprecated-aliases'] as $alias) { $commands[$alias] = $command; $commands[$alias]['is_alias'] = TRUE; $commands[$alias]['deprecated'] = TRUE; $commands[$alias]['deprecated-name'] = $alias; } } } } } return drush_set_context('DRUSH_COMMANDS', $commands); } function drush_command_defaults($key, $commandfile, $path) { return array( 'command' => $key, 'command-hook' => $key, 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_LOGIN, 'callback arguments' => array(), 'commandfile' => $commandfile, 'path' => dirname($path), 'engines' => array(), // Helpful for drush_show_help(). 'callback' => 'drush_command', 'description' => NULL, 'sections' => array( 'examples' => 'Examples', 'arguments' => 'Arguments', 'options' => 'Options', ), 'arguments' => array(), 'options' => array(), 'examples' => array(), 'aliases' => array(), 'deprecated-aliases' => array(), 'core' => array(), 'scope' => 'site', 'drupal dependencies' => array(), 'drush dependencies' => array(), 'bootstrap_errors' => array(), 'topics' => array(), 'hidden' => FALSE, ); } /** * Matches a commands array, as returned by drush_get_arguments, with the * current command table. * * Note that not all commands may be discoverable at the point-of-call, * since Drupal modules can ship commands as well, and they are * not available until after bootstrapping. * * drush_parse_command returns a normalized command descriptor, which * is an associative array. Some of its entries are: * - callback arguments: an array of arguments to pass to the calback. * - description: description of the command. * - arguments: an array of arguments that are understood by the command. for help texts. * - options: an array of options that are understood by the command. for help texts. * - examples: an array of examples that are understood by the command. for help texts. * - scope: one of 'system', 'project', 'site'. * - bootstrap: drupal bootstrap level (depends on Drupal major version). -1=no_bootstrap. * - core: Drupal major version required. * - drupal dependencies: drupal modules required for this command. * - drush dependencies: other drush command files required for this command. * */ function drush_parse_command() { $args = drush_get_arguments(); $command = FALSE; // Get a list of all implemented commands. $implemented = drush_get_commands(); if (isset($implemented[$args[0]])) { $command = $implemented[$args[0]]; $arguments = array_slice($args, 1); } // We have found a command that matches. Set the appropriate values. if ($command) { // Special case. Force help command if --help option was specified. if (drush_get_option(array('h', 'help'))) { $arguments = array($command['command']); $command = $implemented['help']; $command['arguments'] = $arguments; } else { _drush_prepare_command($command, $arguments); } drush_set_command($command); } return $command; } /* * Called by drush_parse_command. If a command is dispatched * directly by drush_dispatch, then drush_dispatch will call * this function. */ function _drush_prepare_command(&$command, $arguments = array()) { // Merge specified callback arguments, which precede the arguments passed on the command line. if (isset($command['callback arguments']) && is_array($command['callback arguments'])) { $arguments = array_merge($command['callback arguments'], $arguments); } $command['arguments'] = $arguments; } /** * Entry point for commands into the drush_invoke API * * If a command does not have a callback specified, this function will be called. * * This function will trigger $hook_drush_init, then if no errors occur, * it will call drush_invoke() with the command that was dispatch. * * If no errors have occured, it will run $hook_drush_exit. */ function drush_command() { $args = func_get_args(); $command = drush_get_command(); foreach (drush_command_implements("drush_init") as $name) { $func = $name . '_drush_init'; drush_log(dt("Initializing drush commandfile: !name", array('!name' => $name)), 'bootstrap'); call_user_func_array($func, $args); _drush_log_drupal_messages(); } if (!drush_get_error()) { call_user_func_array('drush_invoke', array_merge(array($command['command-hook']), $args)); } if (!drush_get_error()) { foreach (drush_command_implements('drush_exit') as $name) { $func = $name . '_drush_exit'; call_user_func_array($func, $args); _drush_log_drupal_messages(); } } } /** * Invoke a hook in all available command files that implement it. * * @see drush_command_invoke_all_ref() * * @param $hook * The name of the hook to invoke. * @param ... * Arguments to pass to the hook. * @return * An array of return values of the hook implementations. If commands return * arrays from their implementations, those are merged into one array. */ function drush_command_invoke_all() { $args = func_get_args(); if (count($args) == 1) { $args[] = NULL; } $reference_value = $args[1]; $args[1] = &$reference_value; return call_user_func_array('drush_command_invoke_all_ref', $args); } /** * A drush_command_invoke_all() that wants the first parameter to be passed by reference. * * @see drush_command_invoke_all() */ function drush_command_invoke_all_ref($hook, &$reference_parameter) { $args = func_get_args(); array_shift($args); // Insure that call_user_func_array can alter first parameter $args[0] = &$reference_parameter; $return = array(); foreach (drush_command_implements($hook) as $module) { $function = $module .'_'. $hook; $result = call_user_func_array($function, $args); if (isset($result) && is_array($result)) { $return = array_merge_recursive($return, $result); } else if (isset($result)) { $return[] = $result; } } return $return; } /** * Determine which command files are implementing a hook. * * @param $hook * The name of the hook (e.g. "drush_help" or "drush_command"). * * @return * An array with the names of the command files which are implementing this hook. */ function drush_command_implements($hook) { $implementations[$hook] = array(); $list = drush_commandfile_list(); foreach ($list as $commandfile => $file) { if (drush_command_hook($commandfile, $hook)) { $implementations[$hook][] = $commandfile; } } return (array)$implementations[$hook]; } /** * @param string * name of command to check. * * @return boolean * TRUE if the given command has an implementation. */ function drush_is_command($command) { $commands = drush_get_commands(); return isset($commands[$command]); } /** * Collect a list of all available drush command files. * * Scans the following paths for drush command files: * * - The "/path/to/drush/commands" folder. * - Folders listed in the 'include' option (see example.drushrc.php). * - The system-wide drush commands folder, e.g. /usr/share/drush/commands * - The ".drush" folder in the user's HOME folder. * - All modules in the current Drupal installation whether they are enabled or * not. Commands implementing hook_drush_load() in MODULE.drush.load.inc with * a return value FALSE will not be loaded. * * A drush command file is a file that matches "*.drush.inc". * * @see drush_scan_directory() * * @return * An associative array whose keys and values are the names of all available * command files. */ function drush_commandfile_list() { return drush_get_context('DRUSH_COMMAND_FILES', array()); } function _drush_find_commandfiles($phase, $phase_max = FALSE) { if (!$phase_max) { $phase_max = $phase; } $searchpath = array(); switch ($phase) { case DRUSH_BOOTSTRAP_DRUSH: // Core commands shipping with drush $searchpath[] = realpath(dirname(__FILE__) . '/../commands/'); // User commands, specified by 'include' option if ($include = drush_get_option(array('i', 'include'), FALSE)) { foreach (explode(PATH_SEPARATOR, $include) as $path) { $searchpath[] = $path; } } // System commands, residing in $SHARE_PREFIX/share/drush/commands $share_path = drush_get_context('SHARE_PREFIX', '/usr') . '/share/drush/commands'; if (is_dir($share_path)) { $searchpath[] = $share_path; } // User commands, residing in ~/.drush if (!is_null(drush_server_home())) { $searchpath[] = drush_server_home() . '/.drush'; } break; case DRUSH_BOOTSTRAP_DRUPAL_SITE: // If we are going to stop bootstrapping at the site, then // we will quickly add all commandfiles that we can find for // any module associated with the site, whether it is enabled // or not. If we are, however, going to continue on to bootstrap // all the way to DRUSH_BOOTSTRAP_DRUPAL_FULL, then we will // instead wait for that phase, which will more carefully add // only those drush command files that are associated with // enabled modules. if ($phase_max < DRUSH_BOOTSTRAP_DRUPAL_FULL) { $searchpath[] = conf_path() . '/modules'; // Too early for variable_get('install_profile', 'default'); Just use default. $searchpath[] = "profiles/default/modules"; // Add all module paths, even disabled modules. Prefer speed over accuracy. $searchpath[] = 'sites/all/modules'; } break; case DRUSH_BOOTSTRAP_DRUPAL_FULL: // Add enabled module paths. Since we are bootstrapped, // we can use the Drupal API. foreach (module_list() as $module) { $filename = drupal_get_filename('module', $module); $searchpath[] = dirname($filename); } break; } _drush_add_commandfiles($searchpath, $phase); } function _drush_add_commandfiles($searchpath, $phase = NULL) { $cache =& drush_get_context('DRUSH_COMMAND_FILES', array()); static $evaluated = array(); static $deferred = array(); if (sizeof($searchpath)) { // Build a list of all of the modules to attempt to load. // Start with any modules deferred from a previous phase. $list = $deferred; // Scan for drush command files; add to list for consideration if found. foreach (array_unique($searchpath) as $path) { if (is_dir($path)) { $files = drush_scan_directory($path, '/\.drush\.inc$/', array('.', '..', 'examples')); foreach ($files as $filename => $info) { $module = basename($filename, '.drush.inc'); // Only try to bootstrap modules that we have never seen before, or that we // have tried to load but did not due to an unmet _drush_load() requirement. if (!array_key_exists($module, $evaluated) && file_exists($filename)) { $evaluated[$module] = TRUE; $list[$module] = $filename; } } } } // Check each file in the consideration list; if there is // a modulename_drush_load() function in modulename.drush.load.inc, // then call it to determine if this file should be loaded. foreach ($list as $module => $filename) { $load_command = TRUE; $load_test_inc = dirname($filename) . "/" . $module . ".drush.load.inc"; if (file_exists($load_test_inc)) { include_once($load_test_inc); $load_test_func = $module . "_drush_load"; if (function_exists($load_test_func)) { $load_command = $load_test_func($phase); } } if ($load_command) { require_once realpath($filename); unset($deferred[$module]); } else { unset($list[$module]); // Signal that we should try again on // the next bootstrap phase. We set // the flag to the filename of the first // module we find so that only that one // will be retried. $deferred[$module] = $filename; } } if (sizeof($list)) { $cache = array_merge($cache, $list); ksort($cache); } } } /** * Conditionally include files based on the command used. * * Steps through each of the currently loaded commandfiles and * loads an optional commandfile based on the key. * * When a command such as 'pm-enable' is called, this * function will find all 'enable.pm.inc' files that * are present in each of the commandfile directories. */ function drush_command_include($command) { $include_files = drush_command_get_includes($command); foreach($include_files as $filename => $commandfile) { drush_log(dt('Including !filename', array('!filename' => $filename)), 'bootstrap'); include_once($filename); } } function drush_command_get_includes($command) { $include_files = array(); $parts = explode('-', $command); $command = implode(".", array_reverse($parts)); $commandfiles = drush_commandfile_list(); $options = array(); foreach ($commandfiles as $commandfile => $file) { $filename = sprintf("%s/%s.inc", dirname($file), $command); if (file_exists($filename)) { $include_files[$filename] = $commandfile; } } return $include_files; } /** * Conditionally include default options based on the command used. */ function drush_command_default_options($command = NULL) { if (!$command) { $command = drush_get_command(); } if ($command) { // Look for command-specific options for this command // keyed both on the command's primary name, and on each // of its aliases. $options_were_set = _drush_command_set_default_options($command['command']); if (isset($command['aliases']) && count($command['aliases'])) { foreach ($command['aliases'] as $alias) { if (_drush_command_set_default_options($alias) === TRUE) { $options_were_set = TRUE; } } } // If we set or cleared any options, go back and re-bootstrap any global // options such as -y and -v. if ($options_were_set) { _drush_bootstrap_global_options(); } } } function _drush_command_set_default_options($command) { $options_were_set = FALSE; $command_default_options = drush_get_context('command-specific'); if (array_key_exists($command, $command_default_options)) { foreach ($command_default_options[$command] as $key => $value) { // We set command-specific options in their own context // that is higher precedence than the various config file // context, but lower than command-line options. if (!drush_get_option('no-' . $key, FALSE)) { drush_set_option($key, $value, 'specific'); $options_were_set = TRUE; } } } return $options_were_set; } /** * Determine whether a command file implements a hook. * * @param $module * The name of the module (without the .module extension). * @param $hook * The name of the hook (e.g. "help" or "menu"). * @return * TRUE if the the hook is implemented. */ function drush_command_hook($commandfile, $hook) { return function_exists($commandfile .'_'. $hook); } /** * Finds all files that match a given mask in a given directory. * Directories and files beginning with a period are excluded; this * prevents hidden files and directories (such as SVN working directories * and GIT repositories) from being scanned. * * @param $dir * The base directory for the scan, without trailing slash. * @param $mask * The regular expression of the files to find. * @param $nomask * An array of files/directories to ignore. * @param $callback * The callback function to call for each match. * @param $recurse_max_depth * When TRUE, the directory scan will recurse the entire tree * starting at the provided directory. When FALSE, only files * in the provided directory are returned. Integer values * limit the depth of the traversal, with zero being treated * identically to FALSE, and 1 limiting the traversal to the * provided directory and its immediate children only, and so on. * @param $key * The key to be used for the returned array of files. Possible * values are "filename", for the path starting with $dir, * "basename", for the basename of the file, and "name" for the name * of the file without an extension. * @param $min_depth * Minimum depth of directories to return files from. * @param $include_dot_files * If TRUE, files that begin with a '.' will be returned if they * match the provided mask. If FALSE, files that begin with a '.' * will not be returned, even if they match the provided mask. * @param $depth * Current depth of recursion. This parameter is only used internally and should not be passed. * * @return * An associative array (keyed on the provided key) of objects with * "path", "basename", and "name" members corresponding to the * matching files. */ function drush_scan_directory($dir, $mask, $nomask = array('.', '..', 'CVS'), $callback = 0, $recurse_max_depth = TRUE, $key = 'filename', $min_depth = 0, $include_dot_files = FALSE, $depth = 0) { $key = (in_array($key, array('filename', 'basename', 'name')) ? $key : 'filename'); $files = array(); if (is_string($dir) && is_dir($dir) && $handle = opendir($dir)) { while (FALSE !== ($file = readdir($handle))) { if (!in_array($file, $nomask) && (($include_dot_files && (!preg_match("/\.\+/",$file))) || ($file[0] != '.'))) { if (is_dir("$dir/$file") && (($recurse_max_depth === TRUE) || ($depth < $recurse_max_depth))) { // Give priority to files in this folder by merging them in after any subdirectory files. $files = array_merge(drush_scan_directory("$dir/$file", $mask, $nomask, $callback, $recurse_max_depth, $key, $min_depth, $include_dot_files, $depth + 1), $files); } elseif ($depth >= $min_depth && preg_match($mask, $file)) { // Always use this match over anything already set in $files with the same $$key. $filename = "$dir/$file"; $basename = basename($file); $name = substr($basename, 0, strrpos($basename, '.')); $files[$$key] = new stdClass(); $files[$$key]->filename = $filename; $files[$$key]->basename = $basename; $files[$$key]->name = $name; if ($callback) { drush_op($callback, $filename); } } } } closedir($handle); } return $files; } /** * Check that a command is valid for the current bootstrap phase. * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * * @return * TRUE if command is valid. */ function drush_enforce_requirement_bootstrap_phase(&$command) { $valid = array(); $current_phase = drush_get_context('DRUSH_BOOTSTRAP_PHASE'); if ($command['bootstrap'] <= $current_phase) { return TRUE; } // TODO: provide description text for each bootstrap level so we can give // the user something more helpful and specific here. $command['bootstrap_errors']['DRUSH_COMMAND_INSUFFICIENT_BOOTSTRAP'] = dt('Command !command needs a higher bootstrap level to run - you will need invoke drush from a more functional Drupal environment to run this command.', array('!command' => $command['command'])); } /** * Check that a command has its declared dependencies available or have no * dependencies. * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * * @return * TRUE if command is valid. */ function drush_enforce_requirement_drupal_dependencies(&$command) { // If the command bootstrap is DRUSH_BOOTSTRAP_MAX, then we will // allow the requirements to pass if we have not successfully // bootstrapped Drupal. The combination of DRUSH_BOOTSTRAP_MAX // and 'drupal dependencies' indicates that the drush command // will use the dependent modules only if they are available. if ($command['bootstrap'] == DRUSH_BOOTSTRAP_MAX) { // If we have not bootstrapped, then let the dependencies pass; // if we have bootstrapped, then enforce them. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') < DRUSH_BOOTSTRAP_DRUPAL_FULL) { return TRUE; } } // If there are no drupal dependencies, then do nothing if (!empty($command['drupal dependencies'])) { foreach ($command['drupal dependencies'] as $dependency) { if(!function_exists('module_exists') || !module_exists($dependency)) { $command['bootstrap_errors']['DRUSH_COMMAND_DEPENDENCY_ERROR'] = dt('Command !command needs the following modules installed/enabled to run: !dependencies.', array('!command' => $command['command'], '!dependencies' => implode(', ', $command['drupal dependencies']))); return FALSE; } } } return TRUE; } /** * Check that a command has its declared drush dependencies available or have no * dependencies. Drush dependencies are helpful when a command is invoking * another command, or implementing its API. * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * @return * TRUE if dependencies are met. */ function drush_enforce_requirement_drush_dependencies(&$command) { // If there are no drush dependencies, then do nothing. if (!empty($command['drush dependencies'])) { $commandfiles = drush_commandfile_list(); foreach ($command['drush dependencies'] as $dependency) { if (!isset($commandfiles[$dependency])) { $dt_args = array( '!command' => $command['command'], '!dependency' => "$dependency.drush.inc", ); $command['bootstrap_errors']['DRUSH_COMMAND_DEPENDENCY_ERROR'] = dt('Command !command needs the following drush command file to run: !dependency.', $dt_args); return FALSE; } } } return TRUE; } /** * Check that a command is valid for the current major version of core. * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * * @return * TRUE if command is valid. */ function drush_enforce_requirement_core(&$command) { $core = $command['core']; if (empty($core) || in_array(drush_drupal_major_version(), $core)) { return TRUE; } $versions = array_pop($core); if (!empty($core)) { $versions = implode(', ', $core) . dt(' or ') . $versions; } $command['bootstrap_errors']['DRUSH_COMMAND_CORE_VERSION_ERROR'] = dt('Command !command requires Drupal core version !versions to run.', array('!command' => $command['command'], '!versions' => $versions)); }