. The shell completion scripts should call * "drush complete ", where is the full command line, which we take * as input and use to produce a list of possible completions for the * current/next word, separated by newlines. Typically, when multiple * completions are returned the shell will display them to the user in a concise * format - but when a single completion is returned it will autocomplete. * * We provide completion for site aliases, commands, shell aliases, options, * engines and arguments. Displaying all of these when the last word has no * characters yet is not useful, as there are too many items. Instead we filter * the possible completions based on position, in a similar way to git. * For example: * - We only display site aliases and commands if one is not already present. * - We only display options if the user has already entered a hyphen. * - We only display global options before a command is entered, and we only * display command specific options after the command (Drush itself does not * care about option placement, but this approach keeps things more concise). * * Below is typical output of complete in different situations. Tokens in square * brackets are optional, and [word] will filter available options that start * with the same characters, or display all listed options if empty. * drush --[word] : Output global options * drush [word] : Output site aliases, sites, commands and shell aliases * drush [@alias] [word] : Output commands * drush [@alias] command [word] : Output command specific arguments * drush [@alias] command --[word] : Output command specific options * * Because the purpose of autocompletion is to make the command line more * efficient for users we need to respond quickly with the list of completions. * To do this, we call drush_complete() early in the Drush bootstrap, and * implement a simple caching system. * * To generate the list of completions, we set up the Drush environment as if * the command was called on it's own, parse the command using the standard * Drush functions, bootstrap the site (if any) and collect available * completions from various sources. Because this can be somewhat slow, we cache * the results. The cache strategy aims to balance accuracy and responsiveness: * - We cache per site, if a site is available. * - We generate (and cache) everything except arguments at the same time, so * subsequent completions on the site don't need any bootstrap. * - We generate and cache arguments on-demand, since these can often be * expensive to generate. Arguments are also cached per-site. * * For argument completions, commandfiles can implement * COMMANDFILE_COMMAND_complete() returning an array containing a key 'values' * containing an array of all possible argument completions for that command. * For example, return array('values' => array('aardvark', 'aardwolf')) offers * the words 'aardvark' and 'aardwolf', or will complete to 'aardwolf' if the * letters 'aardw' are already present. Since command arguments are cached, * commandfiles can bootstrap a site or perform other somewhat time consuming * activities to retrieve the list of possible arguments. Commands can also * clear the cache (or just the "arguments" cache for their command) when the * completion results have likely changed - see drush_complete_cache_clear(). * * Commandfiles can also return a special optional element in their array with * the key 'files' that contains an array of patterns/flags for the glob() * function. These are used to produce file and directory completions (the * results of these are not cached, since this is a fast operation). * See http://php.net/glob for details of valid patterns and flags. * For example the following will complete the command arguments on all * directories, as well as files ending in tar.gz: * return array( * 'files' => array( * 'directories' => array( * 'pattern' => '*', * 'flags' => GLOB_ONLYDIR, * ), * 'tar' => array( * 'pattern' => '*.tar.gz', * ), * ), * ); * * To check completion results without needing to actually trigger shell * completion, you can call this manually using a command like: * * drush --early=includes/complete.inc [--complete-debug] drush [@alias] [command]... * * If you want to simulate the results of pressing tab after a space (i.e. * and empty last word, include '' on the end of your command: * * drush --early=includes/complete.inc [--complete-debug] drush '' */ /** * Produce autocomplete output. * * Determine position (is there a site-alias or command set, and are we trying * to complete an option). Then produce a list of completions for the last word * and output them separated by newlines. */ function drush_early_complete() { // We use a distinct --complete-debug option to avoid unwanted debug messages // being printed when users use this option for other purposes in the command // they are trying to complete. drush_set_option(LogLevel::DEBUG, FALSE); if (drush_get_option('complete-debug', FALSE)) { drush_set_context('DRUSH_DEBUG', TRUE); } // Set up as if we were running the command, and attempt to parse. $argv = drush_complete_process_argv(); if ($alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS')) { $set_sitealias_name = $alias; $set_sitealias = drush_sitealias_get_record($alias); } // Arguments have now had site-aliases and options removed, so we take the // first item as our command. We need to know if the command is valid, so that // we know if we are supposed to complete an in-progress command name, or // arguments for a command. We do this by checking against our per-site cache // of command names (which will only bootstrap if the cache needs to be // regenerated), rather than drush_parse_command() which always requires a // site bootstrap. $arguments = drush_get_arguments(); $set_command_name = NULL; if (isset($arguments[0]) && in_array($arguments[0] . ' ', drush_complete_get('command-names'))) { $set_command_name = $arguments[0]; } // We unset the command if it is "help" but that is not explicitly found in // args, since Drush sets the command to "help" if no command is specified, // which prevents completion of global options. if ($set_command_name == 'help' && !array_search('help', $argv)) { $set_command_name = NULL; } // Determine the word we are trying to complete, and if it is an option. $last_word = end($argv); $word_is_option = FALSE; if (!empty($last_word) && $last_word[0] == '-') { $word_is_option = TRUE; $last_word = ltrim($last_word, '-'); } $completions = array(); if (!$set_command_name) { // We have no command yet. if ($word_is_option) { // Include global option completions. $completions += drush_hyphenate_options(drush_complete_match($last_word, drush_complete_get('options'))); } else { if (empty($set_sitealias_name)) { // Include site alias completions. $completions += drush_complete_match($last_word, drush_complete_get('site-aliases')); } // Include command completions. $completions += drush_complete_match($last_word, drush_complete_get('command-names')); } } else { if ($last_word == $set_command_name) { // The user just typed a valid command name, but we still do command // completion, as there may be other commands that start with the detected // command (e.g. "make" is a valid command, but so is "make-test"). // If there is only the single matching command, this will include in the // completion list so they get a space inserted, confirming it is valid. $completions += drush_complete_match($last_word, drush_complete_get('command-names')); } else if ($word_is_option) { // Include command option completions. $completions += drush_hyphenate_options(drush_complete_match($last_word, drush_complete_get('options', $set_command_name))); } else { // Include command argument completions. $argument_completion = drush_complete_get('arguments', $set_command_name); if (isset($argument_completion['values'])) { $completions += drush_complete_match($last_word, $argument_completion['values']); } if (isset($argument_completion['files'])) { $completions += drush_complete_match_file($last_word, $argument_completion['files']); } } } if (!empty($completions)) { sort($completions); return implode("\n", $completions); } return TRUE; } /** * This function resets the raw arguments so that Drush can parse the command as * if it was run directly. The shell complete command passes the * full command line as an argument, and the --early and --complete-debug * options have to come before that, and the "drush" bash script will add a * --php option on the end, so we end up with something like this: * * /path/to/drush.php --early=includes/complete.inc [--complete-debug] drush [@alias] [command]... --php=/usr/bin/php * * Note that "drush" occurs twice, and also that the second occurrence could be * an alias, so we can't easily use it as to detect the start of the actual * command. Hence our approach is to remove the initial "drush" and then any * options directly following that - what remains is then the command we need * to complete - i.e.: * * drush [@alias] [command]... * * Note that if completion is initiated following a space an empty argument is * added to argv. So in that case argv looks something like this: * array ( * '0' => '/path/to/drush.php', * '1' => '--early=includes/complete.inc', * '2' => 'drush', * '3' => 'topic', * '4' => '', * '5' => '--php=/usr/bin/php', * ); * * @return $args * Array of arguments (argv), excluding the initial command and options * associated with the complete call. * array ( * '0' => 'drush', * '1' => 'topic', * '2' => '', * ); */ function drush_complete_process_argv() { $argv = drush_get_context('argv'); // Remove the first argument, which will be the "drush" command. array_shift($argv); while (substr($arg = array_shift($argv), 0, 2) == '--') { // We remove all options, until we get to a non option, which // marks the start of the actual command we are trying to complete. } // Replace the initial argument. array_unshift($argv, $arg); // Remove the --php option at the end if exists (added by the "drush" shell // script that is called when completion is requested). if (substr(end($argv), 0, 6) == '--php=') { array_pop($argv); } drush_set_context('argv', $argv); drush_set_command(NULL); // Reparse arguments, site alias, and command. drush_parse_args(); // Ensure the base environment is configured, so tests look in the correct // places. _drush_preflight_base_environment(); // Check for and record any site alias. drush_sitealias_check_arg(); drush_sitealias_check_site_env(); // We might have just changed our root--run drush_select_bootstrap_class() again. $bootstrap = drush_select_bootstrap_class(); // Return the new argv for easy reference. return $argv; } /** * Retrieves the appropriate list of candidate completions, then filters this * list using the last word that we are trying to complete. * * @param string $last_word * The last word in the argument list (i.e. the subject of completion). * @param array $values * Array of possible completion values to filter. * * @return array * Array of candidate completions that start with the same characters as the * last word. If the last word is empty, return all candidates. */ function drush_complete_match($last_word, $values) { // Using preg_grep appears to be faster that strpos with array_filter/loop. return preg_grep('/^' . preg_quote($last_word, '/') . '/', $values); } /** * Retrieves the appropriate list of candidate file/directory completions, * filtered by the last word that we are trying to complete. * * @param string $last_word * The last word in the argument list (i.e. the subject of completion). * @param array $files * Array of file specs, each with a pattern and flags subarray. * * @return array * Array of candidate file/directory completions that start with the same * characters as the last word. If the last word is empty, return all * candidates. */ function drush_complete_match_file($last_word, $files) { $return = array(); if ($last_word[0] == '~') { // Complete does not do tilde expansion, so we do it here. // We shell out (unquoted) to expand the tilde. drush_shell_exec('echo ' . $last_word); return drush_shell_exec_output(); } $dir = ''; if (substr($last_word, -1) == '/' && is_dir($last_word)) { // If we exactly match a trailing directory, then we use that as the base // for the listing. We only do this if a trailing slash is present, since at // this stage it is still possible there are other directories that start // with this string. $dir = $last_word; } else { // Otherwise we discard the last part of the path (this is matched against // the list later), and use that as our base. $dir = dirname($last_word); if (empty($dir) || $dir == '.' && $last_word != '.' && substr($last_word, 0, 2) != './') { // We are looking at the current working directory, so unless the user is // actually specifying a leading dot we leave the path empty. $dir = ''; } else { // In all other cases we need to add a trailing slash. $dir .= '/'; } } foreach ($files as $spec) { // We always include GLOB_MARK, as an easy way to detect directories. $flags = GLOB_MARK; if (isset($spec['flags'])) { $flags = $spec['flags'] | GLOB_MARK; } $listing = glob($dir . $spec['pattern'], $flags); $return = array_merge($return, drush_complete_match($last_word, $listing)); } // If we are returning a single item (which will become part of the final // command), we need to use the full path, and we need to escape it // appropriately. if (count($return) == 1) { // Escape common shell metacharacters (we don't use escapeshellarg as it // single quotes everything, even when unnecessary). $item = array_pop($return); $item = preg_replace('/[ |&;()<>]/', "\\\\$0", $item); if (substr($item, -1) !== '/') { // Insert a space after files, since the argument is complete. $item = $item . ' '; } $return = array($item); } else { $firstchar = TRUE; if ($last_word[0] == '/') { // If we are working with absolute paths, we need to check if the first // character of all the completions matches. If it does, then we pass a // full path for each match, so the shell completes as far as it can, // matching the behaviour with relative paths. $pos = strlen($last_word); foreach ($return as $id => $item) { if ($item[$pos] !== $return[0][$pos]) { $firstchar = FALSE; continue; } } } foreach ($return as $id => $item) { // For directories we leave the path alone. $slash_pos = strpos($last_word, '/'); if ($slash_pos === 0 && $firstchar) { // With absolute paths where completions share initial characters, we // pass in a resolved path. $return[$id] = realpath($item); } else if ($slash_pos !== FALSE && $dir != './') { // For files, we pass only the file name, ignoring the false match when // the user is using a single dot relative path. $return[$id] = basename($item); } } } return $return; } /** * Simple helper function to ensure options are properly hyphenated before we * return them to the user (we match against the non-hyphenated versions * internally). * * @param array $options * Array of unhyphenated option names. * * @return array * Array of hyphenated option names. */ function drush_hyphenate_options($options) { foreach ($options as $key => $option) { $options[$key] = '--' . ltrim($option, '--'); } return $options; } /** * Retrieves from cache, or generates a listing of completion candidates of a * specific type (and optionally, command). * * @param string $type * String indicating type of completions to return. * See drush_complete_rebuild() for possible keys. * @param string $command * An optional command name if command specific completion is needed. * * @return array * List of candidate completions. */ function drush_complete_get($type, $command = NULL) { static $complete; if (empty($command)) { // Quick return if we already have a complete static cache. if (!empty($complete[$type])) { return $complete[$type]; } // Retrieve global items from a non-command specific cache, or rebuild cache // if needed. $cache = drush_cache_get(drush_complete_cache_cid($type), 'complete'); if (isset($cache->data)) { return $cache->data; } $complete = drush_complete_rebuild(); return $complete[$type]; } // Retrieve items from a command specific cache. $cache = drush_cache_get(drush_complete_cache_cid($type, $command), 'complete'); if (isset($cache->data)) { return $cache->data; } // Build argument cache - built only on demand. if ($type == 'arguments') { return drush_complete_rebuild_arguments($command); } // Rebuild cache of general command specific items. if (empty($complete)) { $complete = drush_complete_rebuild(); } if (!empty($complete['commands'][$command][$type])) { return $complete['commands'][$command][$type]; } return array(); } /** * Rebuild and cache completions for everything except command arguments. * * @return array * Structured array of completion types, commands and candidate completions. */ function drush_complete_rebuild() { $complete = array(); // Bootstrap to the site level (if possible) - commands may need to check // the bootstrap level, and perhaps bootstrap higher in extraordinary cases. drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); $commands = drush_get_commands(); foreach ($commands as $command_name => $command) { // Add command options and suboptions. $options = array_keys($command['options']); foreach ($command['sub-options'] as $option => $sub_options) { $options = array_merge($options, array_keys($sub_options)); } $complete['commands'][$command_name]['options'] = $options; } // We treat shell aliases as commands for the purposes of completion. $complete['command-names'] = array_merge(array_keys($commands), array_keys(drush_get_context('shell-aliases', array()))); $site_aliases = _drush_sitealias_all_list(); // TODO: Figure out where this dummy @0 alias is introduced. unset($site_aliases['@0']); $complete['site-aliases'] = array_keys($site_aliases); $complete['options'] = array_keys(drush_get_global_options()); // We add a space following all completes. Eventually there may be some // items (e.g. options that we know need values) where we don't add a space. array_walk_recursive($complete, 'drush_complete_trailing_space'); drush_complete_cache_set($complete); return $complete; } /** * Helper callback function that adds a trailing space to completes in an array. */ function drush_complete_trailing_space(&$item, $key) { if (!is_array($item)) { $item = (string)$item . ' '; } } /** * Rebuild and cache completions for command arguments. * * @param string $command * A specific command to retrieve and cache arguments for. * * @return array * Structured array of candidate completion arguments, keyed by the command. */ function drush_complete_rebuild_arguments($command) { // Bootstrap to the site level (if possible) - commands may need to check // the bootstrap level, and perhaps bootstrap higher in extraordinary cases. drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_SITE); $commands = drush_get_commands(); $command_info = $commands[$command]; if ($callback = $command_info['annotated-command-callback']) { list($classname, $method) = $callback; $commandInfo = new CommandInfo($classname, $method); if ($callable = $commandInfo->getAnnotation('complete')) { $result = call_user_func($callable); } } else { $hook = str_replace("-", "_", $command_info['command-hook']); $result = drush_command_invoke_all($hook . '_complete'); } if (isset($result['values'])) { // We add a space following all completes. Eventually there may be some // items (e.g. comma separated arguments) where we don't add a space. array_walk($result['values'], 'drush_complete_trailing_space'); } $complete = array( 'commands' => array( $command => array( 'arguments' => $result, ) ) ); drush_complete_cache_set($complete); return $complete['commands'][$command]['arguments']; } /** * Stores caches for completions. * * @param $complete * A structured array of completions, keyed by type, including a 'commands' * type that contains all commands with command specific completions keyed by * type. The array does not need to include all types - used by * drush_complete_rebuild_arguments(). */ function drush_complete_cache_set($complete) { foreach ($complete as $type => $values) { if ($type == 'commands') { foreach ($values as $command_name => $command) { foreach ($command as $command_type => $command_values) { drush_cache_set(drush_complete_cache_cid($command_type, $command_name), $command_values, 'complete', DRUSH_CACHE_TEMPORARY); } } } else { drush_cache_set(drush_complete_cache_cid($type), $values, 'complete', DRUSH_CACHE_TEMPORARY); } } } /** * Generate a cache id. * * @param $type * The completion type. * @param $command * The command name (optional), if completions are command specific. * * @return string * Cache id. */ function drush_complete_cache_cid($type, $command = NULL) { // For per-site caches, we include the site root and uri/path in the cache id // hash. These are quick to determine, and prevents a bootstrap to site just // to get a validated root and URI. Because these are not validated, there is // the possibility of cache misses/ but they should be rare, since sites are // normally referred to the same way (e.g. a site alias, or using the current // directory), at least within a single command completion session. // We also static cache them, since we may get differing results after // bootstrap, which prevents the caches from being found on the next call. static $root, $site; if (empty($root)) { $root = drush_get_option(array('r', 'root'), drush_locate_root()); $site = drush_get_option(array('l', 'uri'), drush_site_path()); } return drush_get_cid('complete', array(), array($type, $command, $root, $site)); }