From 2e989b40d6daa3321eadecaa2a527b75235ace19 Mon Sep 17 00:00:00 2001 From: Andy Postnikov Date: Sat, 9 Oct 2021 03:30:03 +0300 Subject: [PATCH] Patch drush 8.4.8 for PHP 8.1 and make it slimer --- .gitignore | 2 + Makefile | 6 + drush8/files/commands/core/config.drush.inc | 1070 ++++++++++ drush8/files/includes/batch.inc | 100 + drush8/files/includes/command.inc | 2017 +++++++++++++++++++ drush8/slim-drush.php | 126 ++ drush8/slim-drush.sh | 48 + php8/Dockerfile | 6 +- php81/Dockerfile | 6 +- 9 files changed, 3377 insertions(+), 4 deletions(-) create mode 100644 drush8/files/commands/core/config.drush.inc create mode 100644 drush8/files/includes/batch.inc create mode 100644 drush8/files/includes/command.inc create mode 100644 drush8/slim-drush.php create mode 100755 drush8/slim-drush.sh diff --git a/.gitignore b/.gitignore index 5ec1083..4248be2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ drupal db .idea +drush.phar +drush.phar.bak diff --git a/Makefile b/Makefile index c367fe6..d4d30cf 100644 --- a/Makefile +++ b/Makefile @@ -23,3 +23,9 @@ build: push: @echo "Pushing images for tags: $(TAGS)" set -e; for i in $(TAGS); do printf "\nPushing $(NAME):$$i \n\n"; docker push $(NAME):$$i; done + +drush: +# docker run --rm -u $(shell id -u):$(shell id -g) -v $(CURDIR)/drush8:/srv $(NAME):81 time sh slim-drush.sh + docker run --rm -u $(shell id -u):$(shell id -g) -v $(CURDIR)/drush8:/srv $(NAME):81 time php -dphar.readonly=0 slim-drush.php + cp drush8/drush.phar php8 + cp drush8/drush.phar php81 diff --git a/drush8/files/commands/core/config.drush.inc b/drush8/files/commands/core/config.drush.inc new file mode 100644 index 0000000..3f2b199 --- /dev/null +++ b/drush8/files/commands/core/config.drush.inc @@ -0,0 +1,1070 @@ + array('config')); + $items['config-get'] = array( + 'description' => 'Display a config value, or a whole configuration object.', + 'arguments' => array( + 'config-name' => 'The config object name, for example "system.site".', + 'key' => 'The config key, for example "page.front". Optional.', + ), + 'required-arguments' => 1, + 'options' => array( + 'source' => array( + 'description' => 'The config storage source to read. Additional labels may be defined in settings.php', + 'example-value' => 'sync', + 'value' => 'required', + ), + 'include-overridden' => array( + 'description' => 'Include overridden values.', + ) + ), + 'examples' => array( + 'drush config-get system.site' => 'Displays the system.site config.', + 'drush config-get system.site page.front' => 'gets system.site:page.front value.', + ), + 'outputformat' => array( + 'default' => 'yaml', + 'pipe-format' => 'var_export', + ), + 'aliases' => array('cget', 'config:get'), + 'core' => array('8+'), + ); + + $items['config-set'] = array( + 'description' => 'Set config value directly. Does not perform a config import.', + 'arguments' => array( + 'config-name' => 'The config object name, for example "system.site".', + 'key' => 'The config key, for example "page.front".', + 'value' => 'The value to assign to the config key. Use \'-\' to read from STDIN.', + ), + 'options' => array( + 'format' => array( + 'description' => 'Format to parse the object. Use "string" for string (default), and "yaml" for YAML.', + 'example-value' => 'yaml', + 'value' => 'required', + ), + // A convenient way to pass a multiline value within a backend request. + 'value' => array( + 'description' => 'The value to assign to the config key (if any).', + 'hidden' => TRUE, + ), + ), + 'examples' => array( + 'drush config-set system.site page.front node' => 'Sets system.site:page.front to "node".', + ), + 'aliases' => array('cset', 'config:set'), + 'core' => array('8+'), + ); + + $items['config-export'] = array( + 'description' => 'Export configuration to a directory.', + 'core' => array('8+'), + 'aliases' => array('cex', 'config:export'), + 'arguments' => array( + 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'", + ), + 'options' => array( + 'add' => 'Run `git add -p` after exporting. This lets you choose which config changes to sync for commit.', + 'commit' => 'Run `git add -A` and `git commit` after exporting. This commits everything that was exported without prompting.', + 'message' => 'Commit comment for the exported configuration. Optional; may only be used with --commit or --push.', + 'push' => 'Run `git push` after committing. Implies --commit.', + 'remote' => array( + 'description' => 'The remote git branch to use to push changes. Defaults to "origin".', + 'example-value' => 'origin', + ), + 'branch' => array( + 'description' => 'Make commit on provided working branch. Ignored if used without --commit or --push.', + 'example-value' => 'branchname', + ), + 'destination' => 'An arbitrary directory that should receive the exported files. An alternative to label argument.', + ), + 'examples' => array( + 'drush config-export --destination' => 'Export configuration; Save files in a backup directory named config-export.', + ), + ); + + $items['config-import'] = array( + 'description' => 'Import config from a config directory.', + 'arguments' => array( + 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'", + ), + 'options' => array( + 'preview' => array( + 'description' => 'Format for displaying proposed changes. Recognized values: list, diff. Defaults to list.', + 'example-value' => 'list', + ), + 'source' => array( + 'description' => 'An arbitrary directory that holds the configuration files. An alternative to label argument', + ), + 'partial' => array( + 'description' => 'Allows for partial config imports from the source directory. Only updates and new configs will be processed with this flag (missing configs will not be deleted).', + ), + ), + 'core' => array('8+'), + 'examples' => array( + 'drush config-import --partial' => 'Import configuration; do not remove missing configuration.', + ), + 'aliases' => array('cim', 'config:import'), + ); + + $items['config-list'] = array( + 'description' => 'List config names by prefix.', + 'core' => array('8+'), + 'aliases' => array('cli', 'config:list'), + 'arguments' => array( + 'prefix' => 'The config prefix. For example, "system". No prefix will return all names in the system.', + ), + 'examples' => array( + 'drush config-list system' => 'Return a list of all system config names.', + 'drush config-list "image.style"' => 'Return a list of all image styles.', + 'drush config-list --format="json"' => 'Return all config names as json.', + ), + 'outputformat' => array( + 'default' => 'list', + 'pipe-format' => 'var_export', + 'output-data-type' => 'format-list', + ), + ); + + $items['config-edit'] = $deps + array( + 'description' => 'Open a config file in a text editor. Edits are imported into active configuration after closing editor.', + 'core' => array('8+'), + 'aliases' => array('cedit', 'config:edit'), + 'arguments' => array( + 'config-name' => 'The config object name, for example "system.site".', + ), + 'global-options' => array('editor', 'bg'), + 'allow-additional-options' => array('config-import'), + 'examples' => array( + 'drush config-edit image.style.large' => 'Edit the image style configurations.', + 'drush config-edit' => 'Choose a config file to edit.', + 'drush config-edit --choice=2' => 'Edit the second file in the choice list.', + 'drush --bg config-edit image.style.large' => 'Return to shell prompt as soon as the editor window opens.', + ), + ); + + $items['config-delete'] = array( + 'description' => 'Delete a configuration object.', + 'core' => array('8+'), + 'aliases' => array('cdel', 'config:delete'), + 'arguments' => array( + 'config-name' => 'The config object name, for example "system.site".', + 'key' => 'A config key to clear, for example "page.front".', + ), + 'required-arguments' => 1, + ); + + $items['config-pull'] = array( + 'description' => 'Export and transfer config from one environment to another.', + // 'core' => array('8+'), Operates on remote sites so not possible to declare this locally. + 'drush dependencies' => array('config', 'core'), // core-rsync, core-execute. + 'bootstrap' => DRUSH_BOOTSTRAP_NONE, + 'aliases' => array('cpull', 'config:pull'), + 'arguments' => array( + 'source' => 'A site-alias or the name of a subdirectory within /sites whose config you want to copy from.', + 'target' => 'A site-alias or the name of a subdirectory within /sites whose config you want to replace.', + ), + 'required-arguments' => TRUE, + 'allow-additional-options' => array(), // Most options from config-export and core-rsync unusable. + 'examples' => array( + 'drush config-pull @prod @stage' => "Export config from @prod and transfer to @stage.", + 'drush config-pull @prod @self --label=vcs' => "Export config from @prod and transfer to the 'vcs' config directory of current site.", + ), + 'options' => array( + 'safe' => 'Validate that there are no git uncommitted changes before proceeding', + 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'", + 'runner' => 'Where to run the rsync command; defaults to the local site. Can also be "source" or "destination".', + ), + 'topics' => array('docs-aliases', 'docs-config-exporting'), + ); + + return $items; +} + +/** + * Implements hook_drush_help_alter(). + */ +function config_drush_help_alter(&$command) { + // Hide additional-options which are for internal use only. + if ($command['command'] == 'config-edit') { + $command['options']['source']['hidden'] = TRUE; + $command['options']['partial']['hidden'] = TRUE; + } +} + +/** + * Config list command callback + * + * @param string $prefix + * The config prefix to retrieve, or empty to return all. + */ +function drush_config_list($prefix = '') { + $names = \Drupal::configFactory()->listAll($prefix); + + if (empty($names)) { + // Just in case there is no config. + if (!$prefix) { + return drush_set_error(dt('No config storage names found.')); + } + else { + return drush_set_error(dt('No config storage names found matching @prefix', array('@prefix' => $prefix))); + } + } + + return $names; +} + +/** + * Config get command callback. + * + * @param $config_name + * The config name. + * @param $key + * The config key. + */ +function drush_config_get($config_name, $key = NULL) { + if (!isset($key)) { + return drush_config_get_object($config_name); + } + else { + return drush_config_get_value($config_name, $key); + } +} + +/** + * Config delete command callback. + * + * @param $config_name + * The config name. + * @param $key + * A config key to clear, for example "page.front". + */ +function drush_config_delete($config_name, $key = null) { + $config =\Drupal::service('config.factory')->getEditable($config_name); + if ($config->isNew()) { + return drush_set_error('DRUSH_CONFIG_ERROR', dt('Configuration name not recognized. Use config-list to see all names.')); + } + else { + if ($key) { + if ($config->get($key) === null) { + return drush_set_error('DRUSH_CONFIG_ERROR', dt('Configuration key !key not found.', array('!key' => $key))); + } + $config->clear($key)->save(); + } + else { + $config->delete(); + } + } +} + +/** + * Config set command callback. + * + * @param $config_name + * The config name. + * @param $key + * The config key. + * @param $data + * The data to save to config. + */ +function drush_config_set($config_name, $key = NULL, $data = NULL) { + // This hidden option is a convenient way to pass a value without passing a key. + $data = drush_get_option('value', $data); + + if (!isset($data)) { + return drush_set_error('DRUSH_CONFIG_ERROR', dt('No config value specified.')); + } + + $config = \Drupal::configFactory()->getEditable($config_name); + // Check to see if config key already exists. + if ($config->get($key) === NULL) { + $new_key = TRUE; + } + else { + $new_key = FALSE; + } + + // Special flag indicating that the value has been passed via STDIN. + if ($data === '-') { + $data = stream_get_contents(STDIN); + } + + // Now, we parse the value. + switch (drush_get_option('format', 'string')) { + case 'yaml': + $parser = new Parser(); + $data = $parser->parse($data, TRUE); + } + + if (is_array($data) && drush_confirm(dt('Do you want to update or set multiple keys on !name config.', array('!name' => $config_name)))) { + foreach ($data as $key => $value) { + $config->set($key, $value); + } + return $config->save(); + } + else { + $confirmed = FALSE; + if ($config->isNew() && drush_confirm(dt('!name config does not exist. Do you want to create a new config object?', array('!name' => $config_name)))) { + $confirmed = TRUE; + } + elseif ($new_key && drush_confirm(dt('!key key does not exist in !name config. Do you want to create a new config key?', array('!key' => $key, '!name' => $config_name)))) { + $confirmed = TRUE; + } + elseif (drush_confirm(dt('Do you want to update !key key in !name config?', array('!key' => $key, '!name' => $config_name)))) { + $confirmed = TRUE; + } + if ($confirmed && !drush_get_context('DRUSH_SIMULATE')) { + return $config->set($key, $data)->save(); + } + } +} + +/* + * If provided $destination is not TRUE and not empty, make sure it is writable. + */ +function drush_config_export_validate() { + $destination = drush_get_option('destination'); + if ($destination === TRUE) { + // We create a dir in command callback. No need to validate. + return; + } + + if (!empty($destination)) { + $additional = array(); + $values = drush_sitealias_evaluate_path($destination, $additional, TRUE); + if (!isset($values['path'])) { + return drush_set_error('config_export_target', 'The destination directory could not be evaluated.'); + } + $destination = $values['path']; + drush_set_option('destination', $destination); + if (!file_exists($destination)) { + $parent = dirname($destination); + if (!is_dir($parent)) { + return drush_set_error('config_export_target', 'The destination parent directory does not exist.'); + } + if (!is_writable($parent)) { + return drush_set_error('config_export_target', 'The destination parent directory is not writable.'); + } + } + else { + if (!is_dir($destination)) { + return drush_set_error('config_export_target', 'The destination is not a directory.'); + } + if (!is_writable($destination)) { + return drush_set_error('config_export_target', 'The destination directory is not writable.'); + } + } + } +} + +/** + * Command callback: Export config to specified directory (usually sync). + */ +function drush_config_export($destination = NULL) { + global $config_directories; + + // Determine which target directory to use. + if ($target = drush_get_option('destination')) { + if ($target === TRUE) { + // User did not pass a specific value for --destination. Make one. + /** @var drush_version_control_backup $backup */ + $backup = drush_include_engine('version_control', 'backup'); + $destination_dir = $backup->prepare_backup_dir('config-export'); + } + else { + $destination_dir = $target; + // It is important to be able to specify a destination directory that + // does not exist yet, for exporting on remote systems + drush_mkdir($destination_dir); + } + } + elseif ($config_directories === NULL) { + // For Drupal 8.8+ see https://www.drupal.org/node/3018145 change. + $destination_dir = Settings::get('config_sync_directory'); + } + else { + $choices = drush_map_assoc(array_keys($config_directories)); + unset($choices[CONFIG_ACTIVE_DIRECTORY]); + if (!isset($destination) && count($choices) >= 2) { + $destination = drush_choice($choices, 'Choose a destination.'); + if (empty($destination)) { + return drush_user_abort(); + } + } + elseif (!isset($destination)) { + $destination = CONFIG_SYNC_DIRECTORY; + } + $destination_dir = drush_config_get_config_directory($destination); + } + + // Prepare a new branch, if applicable + $remote = drush_get_option('push', FALSE); + $original_branch = FALSE; + $branch = FALSE; + if ($remote) { + // Get the branch that we're on at the moment + $result = drush_shell_cd_and_exec($destination_dir, 'git rev-parse --abbrev-ref HEAD'); + if (!$result) { + return drush_set_error('DRUSH_CONFIG_EXPORT_NO_GIT', dt("The drush config-export command requires that the selected configuration directory !dir be under git revision control when using --commit or --push options.", array('!dir' => $destination_dir))); + } + $output = drush_shell_exec_output(); + $original_branch = $output[0]; + $branch = drush_get_option('branch', FALSE); + if (!$branch) { + $branch = $original_branch; + } + if ($branch != $original_branch) { + // Switch to the working branch; create it if it does not exist. + // We do NOT want to use -B here, as we do NOT want to reset the + // branch if it already exists. + $result = drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $branch); + if (!$result) { + $result = drush_shell_cd_and_exec($destination_dir, 'git checkout -b %s', $branch); + } + } + } + + // Do the actual config export operation + $result = _drush_config_export($destination, $destination_dir, $branch); + + // Regardless of the result of the export, reset to our original branch. + if ($branch != $original_branch) { + drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $original_branch); + } + + return $result; +} + +function _drush_config_export($destination, $destination_dir, $branch) { + if (!defined('CONFIG_SYNC_DIRECTORY')) { + // For Drupal 8.8+ see https://www.drupal.org/node/3018145 change. + define('CONFIG_SYNC_DIRECTORY', Settings::get('config_sync_directory')); + } + $commit = drush_get_option('commit'); + $comment = drush_get_option('message', 'Exported configuration.'); + if (count(glob($destination_dir . '/*')) > 0) { + // Retrieve a list of differences between the active and target configuration (if any). + if ($destination == CONFIG_SYNC_DIRECTORY) { + $target_storage = \Drupal::service('config.storage.sync'); + } + else { + $target_storage = new FileStorage($destination_dir); + } + /** @var \Drupal\Core\Config\StorageInterface $active_storage */ + $active_storage = \Drupal::service('config.storage'); + $comparison_source = $active_storage; + + $config_comparer = new StorageComparer($comparison_source, $target_storage, \Drupal::service('config.manager')); + if (!$config_comparer->createChangelist()->hasChanges()) { + return drush_log(dt('The active configuration is identical to the configuration in the export directory (!target).', array('!target' => $destination_dir)), LogLevel::OK); + } + + drush_print("Differences of the active config to the export directory:\n"); + $change_list = array(); + foreach ($config_comparer->getAllCollectionNames() as $collection) { + $change_list[$collection] = $config_comparer->getChangelist(NULL, $collection); + } + // Print a table with changes in color, then re-generate again without + // color to place in the commit comment. + _drush_print_config_changes_table($change_list); + $tbl = _drush_format_config_changes_table($change_list); + $output = $tbl->getTable(); + if (!stristr(PHP_OS, 'WIN')) { + $output = str_replace("\r\n", PHP_EOL, $output); + } + $comment .= "\n\n$output"; + + if (!$commit && !drush_confirm(dt('The .yml files in your export directory (!target) will be deleted and replaced with the active config.', array('!target' => $destination_dir)))) { + return drush_user_abort(); + } + // Only delete .yml files, and not .htaccess or .git. + $target_storage->deleteAll(); + } + + // Write all .yml files. + $source_storage = \Drupal::service('config.storage'); + if ($destination == CONFIG_SYNC_DIRECTORY) { + $destination_storage = \Drupal::service('config.storage.sync'); + } + else { + $destination_storage = new FileStorage($destination_dir); + } + + foreach ($source_storage->listAll() as $name) { + $destination_storage->write($name, $source_storage->read($name)); + } + + // Export configuration collections. + foreach (\Drupal::service('config.storage')->getAllCollectionNames() as $collection) { + $source_storage = $source_storage->createCollection($collection); + $destination_storage = $destination_storage->createCollection($collection); + foreach ($source_storage->listAll() as $name) { + $destination_storage->write($name, $source_storage->read($name)); + } + } + + drush_log(dt('Configuration successfully exported to !target.', array('!target' => $destination_dir)), LogLevel::SUCCESS); + drush_backend_set_result($destination_dir); + + // Commit and push, or add exported configuration if requested. + $remote = drush_get_option('push', FALSE); + if ($commit || $remote) { + // There must be changed files at the destination dir; if there are not, then + // we will skip the commit-and-push step + $result = drush_shell_cd_and_exec($destination_dir, 'git status --porcelain .'); + if (!$result) { + return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git status` failed.")); + } + $uncommitted_changes = drush_shell_exec_output(); + if (!empty($uncommitted_changes)) { + $result = drush_shell_cd_and_exec($destination_dir, 'git add -A .'); + if (!$result) { + return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git add -A` failed.")); + } + $comment_file = drush_save_data_to_temp_file($comment); + $result = drush_shell_cd_and_exec($destination_dir, 'git commit --file=%s', $comment_file); + if (!$result) { + return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git commit` failed. Output:\n\n!output", array('!output' => implode("\n", drush_shell_exec_output())))); + } + if ($remote) { + // Remote might be FALSE, if --push was not specified, or + // it might be TRUE if --push was not given a value. + if (!is_string($remote)) { + $remote = 'origin'; + } + $result = drush_shell_cd_and_exec($destination_dir, 'git push --set-upstream %s %s', $remote, $branch); + if (!$result) { + return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git push` failed.")); + } + } + } + } + elseif (drush_get_option('add')) { + drush_shell_exec_interactive('git add -p %s', $destination_dir); + } + + $values = array( + 'destination' => $destination_dir, + ); + return $values; +} + +function drush_config_import_validate() { + drush_include_engine('drupal', 'environment'); + if (drush_get_option('partial') && !drush_module_exists('config')) { + return drush_set_error('config_import_partial', 'Enable the config module in order to use the --partial option.'); + } + if ($source = drush_get_option('source')) { + if (!file_exists($source)) { + return drush_set_error('config_import_target', 'The source directory does not exist.'); + } + if (!is_dir($source)) { + return drush_set_error('config_import_target', 'The source is not a directory.'); + } + } +} + +/** + * Command callback. Import from specified config directory (defaults to sync). + */ +function drush_config_import($source = NULL) { + global $config_directories; + if (!defined('CONFIG_SYNC_DIRECTORY')) { + // For Drupal 8.8+ see https://www.drupal.org/node/3018145 change. + define('CONFIG_SYNC_DIRECTORY', Settings::get('config_sync_directory')); + } + + // Determine source directory. + if ($target = drush_get_option('source')) { + $source_dir = $target; + } + else { + $source = CONFIG_SYNC_DIRECTORY; + if (!empty($config_directories) && defined('CONFIG_ACTIVE_DIRECTORY')) { + $choices = drush_map_assoc(array_keys($config_directories)); + unset($choices[CONFIG_ACTIVE_DIRECTORY]); + if (!isset($source) && count($choices) >= 2) { + $source= drush_choice($choices, 'Choose a source.'); + if (empty($source)) { + return drush_user_abort(); + } + } + elseif (!isset($source)) { + $source = CONFIG_SYNC_DIRECTORY; + } + } + $source_dir = drush_config_get_config_directory($source); + } + + if ($source == CONFIG_SYNC_DIRECTORY) { + $source_storage = \Drupal::service('config.storage.sync'); + } + else { + $source_storage = new FileStorage($source_dir); + } + + // Determine $source_storage in partial and non-partial cases. + /** @var \Drupal\Core\Config\StorageInterface $active_storage */ + $active_storage = \Drupal::service('config.storage'); + if (drush_get_option('partial')) { + $replacement_storage = new StorageReplaceDataWrapper($active_storage); + foreach ($source_storage->listAll() as $name) { + $data = $source_storage->read($name); + $replacement_storage->replaceData($name, $data); + } + $source_storage = $replacement_storage; + } + + /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ + $config_manager = \Drupal::service('config.manager'); + $storage_comparer = new StorageComparer($source_storage, $active_storage, $config_manager); + + if (!$storage_comparer->createChangelist()->hasChanges()) { + return drush_log(dt('There are no changes to import.'), LogLevel::OK); + } + + if (drush_get_option('preview', 'list') == 'list') { + $change_list = array(); + foreach ($storage_comparer->getAllCollectionNames() as $collection) { + $change_list[$collection] = $storage_comparer->getChangelist(NULL, $collection); + } + _drush_print_config_changes_table($change_list); + } + else { + // Copy active storage to the temporary directory. + $temp_dir = drush_tempdir(); + $temp_storage = new FileStorage($temp_dir); + $source_dir_storage = new FileStorage($source_dir); + foreach ($source_dir_storage->listAll() as $name) { + if ($data = $active_storage->read($name)) { + $temp_storage->write($name, $data); + } + } + drush_shell_exec('diff -x %s -u %s %s', '*.git', $temp_dir, $source_dir); + $output = drush_shell_exec_output(); + drush_print(implode("\n", $output)); + } + + if (drush_confirm(dt('Import the listed configuration changes?'))) { + return drush_op('_drush_config_import', $storage_comparer); + } +} + +// Copied from submitForm() at /core/modules/config/src/Form/ConfigSync.php +function _drush_config_import(StorageComparer $storage_comparer) { + $config_importer = _drush_create_config_importer($storage_comparer); + if ($config_importer->alreadyImporting()) { + drush_log('Another request may be synchronizing configuration already.', LogLevel::WARNING); + } + else{ + try { + // This is the contents of \Drupal\Core\Config\ConfigImporter::import. + // Copied here so we can log progress. + if ($config_importer->hasUnprocessedConfigurationChanges()) { + $sync_steps = $config_importer->initialize(); + foreach ($sync_steps as $step) { + $context = array(); + do { + $config_importer->doSyncStep($step, $context); + if (isset($context['message'])) { + drush_log(str_replace('Synchronizing', 'Synchronized', (string)$context['message']), LogLevel::OK); + } + } while ($context['finished'] < 1); + } + } + if ($config_importer->getErrors()) { + throw new \Drupal\Core\Config\ConfigException('Errors occurred during import'); + } + else { + drush_log('The configuration was imported successfully.', LogLevel::SUCCESS); + } + } + catch (ConfigException $e) { + // Return a negative result for UI purposes. We do not differentiate + // between an actual synchronization error and a failed lock, because + // concurrent synchronizations are an edge-case happening only when + // multiple developers or site builders attempt to do it without + // coordinating. + $message = 'The import failed due for the following reasons:' . "\n"; + $message .= implode("\n", $config_importer->getErrors()); + + watchdog_exception('config_import', $e); + return drush_set_error('config_import_fail', $message); + } + } +} + +function _drush_create_config_importer(StorageComparer $storage_comparer) { + // Drupal 9 ConfigImporter + if (drush_drupal_major_version() >= 9) { + return new ConfigImporter( + $storage_comparer, + \Drupal::service('event_dispatcher'), + \Drupal::service('config.manager'), + \Drupal::lock(), + \Drupal::service('config.typed'), + \Drupal::moduleHandler(), + \Drupal::service('module_installer'), + \Drupal::service('theme_handler'), + \Drupal::service('string_translation'), + \Drupal::service('extension.list.module') + ); + } + // Drupal 8 ConfigImporter + return new ConfigImporter( + $storage_comparer, + \Drupal::service('event_dispatcher'), + \Drupal::service('config.manager'), + \Drupal::lock(), + \Drupal::service('config.typed'), + \Drupal::moduleHandler(), + \Drupal::service('module_installer'), + \Drupal::service('theme_handler'), + \Drupal::service('string_translation') + ); +} + +/** + * Edit command callback. + */ +function drush_config_edit($config_name = '') { + // Identify and validate input. + if ($config_name) { + $config = \Drupal::configFactory()->get($config_name); + if ($config->isNew()) { + return drush_set_error(dt('Config !name does not exist', array('!name' => $config_name))); + } + } + else { + $config_names = \Drupal::configFactory()->listAll(); + $choice = drush_choice($config_names, 'Choose a configuration.'); + if (empty($choice)) { + return drush_user_abort(); + } + else { + $config_name = $config_names[$choice]; + $config = \Drupal::configFactory()->get($config_name); + } + } + + $active_storage = $config->getStorage(); + $contents = $active_storage->read($config_name); + + // Write tmp YAML file for editing + $temp_dir = drush_tempdir(); + $temp_storage = new FileStorage($temp_dir); + $temp_storage->write($config_name, $contents); + + $exec = drush_get_editor(); + drush_shell_exec_interactive($exec, $temp_storage->getFilePath($config_name)); + + // Perform import operation if user did not immediately exit editor. + if (!drush_get_option('bg', FALSE)) { + $options = drush_redispatch_get_options() + array('partial' => TRUE, 'source' => $temp_dir); + $backend_options = array('interactive' => TRUE); + return (bool) drush_invoke_process('@self', 'config-import', array(), $options, $backend_options); + } +} + +/** + * Config pull validate callback + * + */ +function drush_config_pull_validate($source, $destination) { + if (drush_get_option('safe')) { + $return = drush_invoke_process($destination, 'core-execute', array('git diff --quiet'), array('escape' => 0)); + if ($return['error_status']) { + return drush_set_error('DRUSH_GIT_DIRTY', 'There are uncommitted changes in your git working copy.'); + } + } +} + +/** + * Config pull command callback + * + * @param string $label + * The config label which receives the transferred files. + */ +function drush_config_pull($source, $destination) { + // @todo drush_redispatch_get_options() assumes you will execute same command. Not good. + $global_options = drush_redispatch_get_options() + array( + 'strict' => 0, + ); + + // @todo If either call is made interactive, we don't get an $return['object'] back. + $backend_options = array('interactive' => FALSE); + if (drush_get_context('DRUSH_SIMULATE')) { + $backend_options['backend-simulate'] = TRUE; + } + + $export_options = array( + // Use the standard backup directory on Destination. + 'destination' => TRUE, + ); + drush_log(dt('Starting to export configuration on Target.'), LogLevel::OK); + $return = drush_invoke_process($source, 'config-export', array(), $global_options + $export_options, $backend_options); + if ($return === FALSE || $return['error_status']) { + return drush_set_error('DRUSH_CONFIG_PULL_EXPORT_FAILED', dt('Config-export failed.')); + } + else { + // Trailing slash assures that transfer files and not the containing dir. + $export_path = $return['object'] . '/'; + } + + $rsync_options = array( + 'remove-source-files' => TRUE, + 'delete' => TRUE, + 'exclude-paths' => '.htaccess', + 'yes' => TRUE, // No need to prompt as destination is always the target config directory. + ); + $label = drush_get_option('label', 'sync'); + $runner = drush_get_runner($source, $destination, drush_get_option('runner', FALSE)); + drush_log(dt('Starting to rsync configuration files from !source to !dest.', array('!source' => $source, '!dest' => $destination)), LogLevel::OK); + // This comment applies similarly to sql-sync's use of core-rsync. + // Since core-rsync is a strict-handling command and drush_invoke_process() puts options at end, we can't send along cli options to rsync. + // Alternatively, add options like --ssh-options to a site alias (usually on the machine that initiates the sql-sync). + $return = drush_invoke_process($runner, 'core-rsync', array("$source:$export_path", "$destination:%config-$label"), $rsync_options); + if ($return['error_status']) { + return drush_set_error('DRUSH_CONFIG_PULL_RSYNC_FAILED', dt('Config-pull rsync failed.')); + } + + drush_backend_set_result($return['object']); +} + +/** + * Show and return a config object + * + * @param $config_name + * The config object name. + */ +function drush_config_get_object($config_name) { + $source = drush_get_option('source', 'active'); + $include_overridden = drush_get_option('include-overridden', FALSE); + + if ($include_overridden) { + // Displaying overrides only applies to active storage. + $config = \Drupal::config($config_name); + $data = $config->get(); + } + elseif ($source == 'active') { + $config = \Drupal::service('config.storage'); + $data = $config->read($config_name); + } + elseif ($source == 'sync') { + $config = \Drupal::service('config.storage.sync'); + $data = $config->read($config_name); + } + else { + return drush_set_error(dt('Unknown value !value for config source.', array('!value' => $source))); + } + + if ($data === FALSE) { + return drush_set_error(dt('Config !name does not exist in !source configuration.', array('!name' => $config_name, '!source' => $source))); + } + if (empty($data)) { + drush_log(dt('Config !name exists but has no data.', array('!name' => $config_name)), LogLevel::NOTICE); + return; + } + return $data; +} + +/** + * Show and return a value from config system. + * + * @param $config_name + * The config name. + * @param $key + * The config key. + */ +function drush_config_get_value($config_name, $key) { + $data = drush_config_get_object($config_name); + $parts = explode('.', $key); + if (count($parts) == 1) { + $value = isset($data[$key]) ? $data[$key] : NULL; + } + else { + $value = NestedArray::getValue($data, $parts, $key_exists); + $value = $key_exists ? $value : NULL; + } + + $returns[$config_name . ':' . $key] = $value; + + if ($value === NULL) { + return drush_set_error('DRUSH_CONFIG_ERROR', dt('No matching key found in !name config.', array('!name' => $config_name))); + } + else { + return $returns; + } +} + +/** + * Print a table of config changes. + * + * @param array $config_changes + * An array of changes keyed by collection. + */ +function _drush_format_config_changes_table(array $config_changes, $use_color = FALSE) { + if (!$use_color) { + $red = "%s"; + $yellow = "%s"; + $green = "%s"; + } + else { + $red = "\033[31;40m\033[1m%s\033[0m"; + $yellow = "\033[1;33;40m\033[1m%s\033[0m"; + $green = "\033[1;32;40m\033[1m%s\033[0m"; + } + + $rows = array(); + $rows[] = array('Collection', 'Config', 'Operation'); + foreach ($config_changes as $collection => $changes) { + foreach ($changes as $change => $configs) { + switch ($change) { + case 'delete': + $colour = $red; + break; + case 'update': + $colour = $yellow; + break; + case 'create': + $colour = $green; + break; + default: + $colour = "%s"; + break; + } + foreach($configs as $config) { + $rows[] = array( + $collection, + $config, + sprintf($colour, $change) + ); + } + } + } + $tbl = _drush_format_table($rows); + return $tbl; +} + +/** + * Print a table of config changes. + * + * @param array $config_changes + * An array of changes keyed by collection. + */ +function _drush_print_config_changes_table(array $config_changes) { + $tbl = _drush_format_config_changes_table($config_changes, !drush_get_context('DRUSH_NOCOLOR')); + + $output = $tbl->getTable(); + if (!stristr(PHP_OS, 'WIN')) { + $output = str_replace("\r\n", PHP_EOL, $output); + } + + drush_print(rtrim($output)); + return $tbl; +} + +/** + * Command argument complete callback. + */ +function config_config_get_complete() { + return _drush_config_names_complete(); +} + +/** + * Command argument complete callback. + */ +function config_config_set_complete() { + return _drush_config_names_complete(); +} + +/** + * Command argument complete callback. + */ +function config_config_view_complete() { + return _drush_config_names_complete(); +} + +/** + * Command argument complete callback. + */ +function config_config_edit_complete() { + return _drush_config_names_complete(); +} + +/** + * Command argument complete callback. + */ +function config_config_import_complete() { + return _drush_config_directories_complete(); +} + +/** + * Command argument complete callback. + */ +function config_config_export_complete() { + return _drush_config_directories_complete(); +} + +/** + * Command argument complete callback. + */ +function config_config_pull_complete() { + return array('values' => array_keys(_drush_sitealias_all_list())); +} + +/** + * Helper function for command argument complete callback. + * + * @return + * Array of available config directories. + */ +function _drush_config_directories_complete() { + drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); + global $config_directories; + return array('values' => array_keys($config_directories)); +} + +/** + * Helper function for command argument complete callback. + * + * @return + * Array of available config names. + */ +function _drush_config_names_complete() { + drush_bootstrap_max(); + return array('values' => $storage = \Drupal::service('config.storage')->listAll()); +} diff --git a/drush8/files/includes/batch.inc b/drush8/files/includes/batch.inc new file mode 100644 index 0000000..62dd673 --- /dev/null +++ b/drush8/files/includes/batch.inc @@ -0,0 +1,100 @@ +getCurrentUserAsSingle()->id(); + + drush_include_engine('drupal', 'batch'); + _drush_backend_batch_process($command, $args, $options); +} + +/** + * Process sets from the specified batch. + * + * This function is called by the worker process that is spawned by the + * drush_backend_batch_process function. + * + * The command called needs to call this function after it's special bootstrap + * requirements have been taken care of. + * + * @param int $id + * The batch ID of the batch being processed. + */ +function drush_batch_command($id) { + include_once(DRUSH_DRUPAL_CORE . '/includes/batch.inc'); + drush_include_engine('drupal', 'batch'); + _drush_batch_command($id); +} diff --git a/drush8/files/includes/command.inc b/drush8/files/includes/command.inc new file mode 100644 index 0000000..c06858f --- /dev/null +++ b/drush8/files/includes/command.inc @@ -0,0 +1,2017 @@ + $command))); + } +} + +/** + * 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. Use '@self' to run on the current site. + * @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. + * @param mixed $backend_options + * TRUE - integrate errors + * FALSE - do not integrate errors + * array - @see drush_backend_invoke_concurrent + * There are also several options that _only_ work when set in + * this parameter. They include: + * 'invoke-multiple' + * If $site_alias_record represents a single site, then 'invoke-multiple' + * will cause the _same_ command with the _same_ arguments and options + * to be invoked concurrently (e.g. for running concurrent batch processes). + * 'concurrency' + * Limits the number of concurrent processes that will run at the same time. + * Defaults to '4'. + * 'override-simulated' + * Forces the command to run, even in 'simulated' mode. Useful for + * commands that do not change any state on the machine, e.g. to fetch + * database information for sql-sync via sql-conf. + * 'interactive' + * Overrides the backend invoke process to run commands interactively. + * 'fork' + * Overrides the backend invoke process to run non blocking commands in + * the background. Forks a new process by adding a '&' at the end of the + * command. The calling process does not receive any output from the child + * process. The fork option is used to spawn a process that outlives its + * parent. + * + * @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() + * + * Do not change the signature of this function! drush_invoke_process + * is one of the key Drush APIs. See http://drupal.org/node/1152908 + */ +function drush_invoke_process($site_alias_record, $command_name, $commandline_args = array(), $commandline_options = array(), $backend_options = TRUE) { + if (is_array($site_alias_record) && array_key_exists('site-list', $site_alias_record)) { + list($site_alias_records, $not_found) = drush_sitealias_resolve_sitespecs($site_alias_record['site-list']); + if (!empty($not_found)) { + drush_log(dt("Not found: @list", array("@list" => implode(', ', $not_found))), LogLevel::WARNING); + return FALSE; + } + $site_alias_records = drush_sitealias_simplify_names($site_alias_records); + foreach ($site_alias_records as $alias_name => $alias_record) { + $invocations[] = array( + 'site' => $alias_record, + 'command' => $command_name, + 'args' => $commandline_args, + ); + } + } + else { + $invocations[] = array( + 'site' => $site_alias_record, + 'command' => $command_name, + 'args' => $commandline_args); + $invoke_multiple = drush_get_option_override($backend_options, 'invoke-multiple', 0); + if ($invoke_multiple) { + $invocations = array_fill(0, $invoke_multiple, $invocations[0]); + } + } + return drush_backend_invoke_concurrent($invocations, $commandline_options, $backend_options); +} + +/** + * 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(), + * or a string containing the name of the command record from + * drush_get_commands() to call. + * @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); + } + + // Merge in the options added by hooks. We need this + // for validation, but this $command is just going to + // get thrown away, so we'll have to do this again later. + annotationcommand_adapter_add_hook_options($command); + + // Add command-specific options, if applicable. + drush_command_default_options($command); + + // Test to see if any of the options in the 'cli' context + // are not represented in the command structure. + if ((_drush_verify_cli_options($command) === FALSE) || (_drush_verify_cli_arguments($command) === FALSE)) { + return FALSE; + } + + // Give command files an opportunity to alter the command record + drush_command_invoke_all_ref('drush_command_alter', $command); + + // Include and validate command engines. + if (drush_load_command_engines($command) === FALSE) { + return FALSE; + } + + // Do tilde expansion immediately prior to execution, + // so that tildes are passed through unchanged for + // remote commands and other redispatches. + drush_preflight_tilde_expansion($command); + + // Get the arguments for this command. Add the options + // on the end if this is that kind of command. + $args = $command['arguments']; + + // Call the callback function of the active command. + $return = call_user_func_array($command['callback'], $args); + } + + // Add a final log entry, just so a timestamp appears. + drush_log(dt('Command dispatch complete'), LogLevel::NOTICE); + + return $return; +} + +/** + * 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'; + if (drush_get_option('show-invoke')) { + drush_log(dt("Calling global init hook: !func", array('!name' => $name, '!func' => $func . '()')), LogLevel::BOOTSTRAP); + } + call_user_func_array($func, $args); + _drush_log_drupal_messages(); + } + + if (!drush_get_error()) { + $result = _drush_invoke_hooks($command, $args); + } + + if (!drush_get_error()) { + foreach (drush_command_implements('drush_exit') as $name) { + $func = $name . '_drush_exit'; + if (drush_get_option('show-invoke')) { + drush_log(dt("Calling global exit hook: !func", array('!name' => $name, '!func' => $func . '()')), LogLevel::BOOTSTRAP); + } + call_user_func_array($func, $args); + _drush_log_drupal_messages(); + } + } +} + +/** + * Invoke Drush API calls, including all hooks. + * + * This is an internal function; it is called from drush_dispatch via + * drush_command, but only if the command does not specify a 'callback' + * function. If a callback function is specified, it will be called + * instead of drush_command + _drush_invoke_hooks. + * + * Executes the specified command with the specified arguments on the + * currently bootstrapped site using the current option contexts. + * Note that _drush_invoke_hooks 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. + * + * Call the correct hook for all the modules that implement it. + * Additionally, the ability to rollback when an error has been encountered is also provided. + * If at any point during execution, the drush_get_error() function returns anything but 0, + * drush_invoke() will trigger $hook_rollback for each of the hooks that implement it, + * in reverse order from how they were executed. Rollbacks are also triggered any + * time a hook function returns FALSE. + * + * This function will also trigger pre_$hook and post_$hook variants of the hook + * and its rollbacks automatically. + * + * HOW DRUSH HOOK FUNCTIONS ARE NAMED: + * + * The name of the hook is composed from the name of the command and the name of + * the command file that the command definition is declared in. The general + * form for the hook filename is: + * + * drush_COMMANDFILE_COMMANDNAME + * + * In many cases, drush commands that are functionally part of a common collection + * of similar commands will all be declared in the same file, and every command + * defined in that file will start with the same command prefix. For example, the + * command file "pm.drush.inc" defines commands such as "pm-enable" and "pm-disable". + * In the case of "pm-enable", the command file is "pm", and and command name is + * "pm-enable". When the command name starts with the same sequence of characters + * as the command file, then the repeated sequence is dropped; thus, the command + * hook for "pm-enable" is "drush_pm_enable", not "drush_pm_pm_enable". + * + * There is also a special Drupal-version-specific naming convention that may + * be used. To hide a commandfile from all versions of Drupal except for the + * specific one named, add a ".dVERSION" after the command prefix. For example, + * the file "views.d8.drush.inc" defines a "views" commandfile that will only + * load with Drupal 8. This feature is not necessary and should not be used + * in contrib modules (any extension with a ".module" file), since these modules + * are already version-specific. + * + * @param command + * The drush command to execute. + * @param args + * An array of arguments to the command OR a single non-array argument. + * @return + * The return value will be passed along to the caller if --backend option is + * present. A boolean FALSE indicates failure and rollback will be intitated. + * + * This function should not be called directly. + * @see drush_invoke() and @see drush_invoke_process() + */ +function _drush_invoke_hooks($command, $args) { + $return = null; + // If someone passed a standalone arg, convert it to a single-element array + if (!is_array($args)) { + $args = array($args); + } + // Include the external command file used by this command, if there is one. + drush_command_include($command['command-hook']); + // Generate the base name for the hook by converting all + // dashes in the command name to underscores. + $hook = str_replace("-", "_", $command['command-hook']); + + // Call the hook init function, if it exists. + // If a command needs to bootstrap, it is advisable + // to do so in _init; otherwise, new commandfiles + // will miss out on participating in any stage that + // has passed or started at the time it was discovered. + $func = 'drush_' . $hook . '_init'; + if (function_exists($func)) { + drush_log(dt("Calling drush command init function: !func", array('!func' => $func)), LogLevel::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['command-hook'])), LogLevel::ERROR); + return FALSE; + } + } + + // We will adapt and call as many of the annotated command hooks as we can. + // The following command hooks are not supported in Drush 8.x: + // - Command Event: not called (requires CommandEvent object) + // - Option: Equivalent functionality supported in annotationcommand_adapter.inc + // - Interact: not called (We don't use SymfonyStyle in 8.x at the moment) + // - Status: not called - probably not needed? + // - Extract not called - probably not needed? + // The hooks that are called include: + // - Pre-initialize, initialize and post-initialize + // - Pre-validate and validate + // - Pre-command, command and post-command + // - Pre-process, process and post-process + // - Pre-alter, alter and post-alter + + $names = annotationcommand_adapter_command_names($command); + // Merge in the options added by hooks (again) + annotationcommand_adapter_add_hook_options($command); + $annotationData = $command['annotations']; + + $input = new DrushInputAdapter($args, annotationcommand_adapter_get_options($command), $command['command']); + $output = new DrushOutputAdapter(); + $commandData = new CommandData( + $annotationData, + $input, + $output, + false, + false + ); + + annotationcommand_adapter_call_initialize($names, $commandData); + + $rollback = FALSE; + $completed = array(); + $available_rollbacks = array(); + $all_available_hooks = array(); + + // Iterate through the different hook variations + $variations = array( + 'pre_validate' => $hook . "_pre_validate", + 'validate' => $hook . "_validate", + 'pre_command' => "pre_$hook", + 'command' => $hook, + 'post_command' => "post_$hook" + ); + foreach ($variations as $hook_phase => $var_hook) { + + $adapterHookFunction = 'annotationcommand_adapter_call_hook_' . $hook_phase; + $adapterHookFunction($names, $commandData, $return); + + // 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(); + + // Make a list of function callbacks to call. If + // there is a 'primary function' mentioned, make sure + // that it appears first in the list, but only if + // we are running the main hook ("$hook"). After that, + // make sure that any callback associated with this commandfile + // executes before any other hooks defined in any other + // commandfiles. + $callback_list = array(); + if (($var_hook == $hook) && ($command['primary function'])) { + $callback_list[$command['primary function']] = $list[$command['commandfile']]; + } + else { + $primary_func = ($command['commandfile'] . "_" == substr($var_hook . "_",0,strlen($command['commandfile']) + 1)) ? sprintf("drush_%s", $var_hook) : sprintf("drush_%s_%s", $command['commandfile'], $var_hook); + $callback_list[$primary_func] = $list[$command['commandfile']]; + } + // We've got the callback for the primary function in the + // callback list; now add all of the other callback functions. + unset($list[$command['commandfile']]); + foreach ($list as $commandfile => $filename) { + $func = sprintf("drush_%s_%s", $commandfile, $var_hook); + $callback_list[$func] = $filename; + } + // Run all of the functions available for this variation + $accumulated_result = NULL; + foreach ($callback_list as $func => $filename) { + if (function_exists($func)) { + $all_available_hooks[] = $func . ' [* Defined in ' . $filename . ']'; + $available_rollbacks[] = $func . '_rollback'; + $completed[] = $func; + drush_log(dt("Calling hook !hook", array('!hook' => $func)), LogLevel::DEBUG); + try { + $result = call_user_func_array($func, $args); + drush_log(dt("Returned from hook !hook", array('!hook' => $func)), LogLevel::DEBUG); + } + catch (Exception $e) { + drush_set_error('DRUSH_EXECUTION_EXCEPTION', (string) $e); + } + // If there is an error, break out of the foreach + // $variations and foreach $callback_list + if (drush_get_error() || ($result === FALSE)) { + $rollback = TRUE; + break 2; + } + // If result values are arrays, then combine them all together. + // Later results overwrite earlier results. + if (isset($result) && is_array($accumulated_result) && is_array($result)) { + $accumulated_result = array_merge($accumulated_result, $result); + } + else { + $accumulated_result = $result; + } + _drush_log_drupal_messages(); + } + else { + $all_available_hooks[] = $func; + } + } + // Process the result value from the 'main' callback hook only. + if ($var_hook == $hook) { + $return = $accumulated_result; + if (isset($return)) { + annotationcommand_adapter_call_hook_process_and_alter($names, $commandData, $return); + drush_handle_command_output($command, $return); + } + } + } + + // If no hook functions were found, print a warning. + if (empty($completed)) { + $default_command_hook = sprintf("drush_%s_%s", $command['commandfile'], $hook); + if (($command['commandfile'] . "_" == substr($hook . "_",0,strlen($command['commandfile'])+ 1))) { + $default_command_hook = sprintf("drush_%s", $hook); + } + $dt_args = array( + '!command' => $command['command-hook'], + '!default_func' => $default_command_hook, + ); + $message = "No hook functions were found for !command. The primary hook function is !default_func(). Please implement this function. Run with --show-invoke to see all available hooks."; + $return = drush_set_error('DRUSH_FUNCTION_NOT_FOUND', dt($message, $dt_args)); + } + if (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['command-hook'], '!available' => "\n" . implode("\n", $all_available_hooks))), LogLevel::OK); + } + if (drush_get_option('show-invoke') && !empty($available_rollbacks)) { + drush_log(dt("Available rollback hooks for !command: !rollback", array('!command' => $command['command-hook'], '!rollback' => "\n" . implode("\n", $available_rollbacks))), LogLevel::OK); + } + + // Something went wrong, we need to undo. + if ($rollback) { + if (drush_get_option('confirm-rollback', FALSE)) { + // Optionally ask for confirmation, --yes and --no are ignored from here on as we are about to finish this process. + drush_set_context('DRUSH_AFFIRMATIVE', FALSE); + drush_set_context('DRUSH_NEGATIVE', FALSE); + $rollback = drush_confirm(dt('Do you want to rollback? (manual cleanup might be required otherwise)')); + } + + 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)), LogLevel::DEBUG); + } + } + } + $return = FALSE; + } + + if (isset($return)) { + return $return; + } +} + +/** + * Convert the structured output array provided from the Drush + * command into formatted output. Output is only printed for commands + * that define 'default-format' &/or 'default-pipe-format'; all + * other commands are expected to do their own output. + */ +function drush_handle_command_output($command, $structured_output) { + // If the hook already called drush_backend_set_result, + // then return that value. If it did not, then the return + // value from the hook will be the value returned from + // this routine. + $return = drush_backend_get_result(); + if (empty($return)) { + drush_backend_set_result($structured_output); + } + // We skip empty strings and empty arrays, but note that 'empty' + // returns TRUE for the integer value '0', but we do want to print that. + // Only handle output here if the command defined an output format + // engine. If no engine was declared, then we presume that the command + // handled its own output. + if ((!empty($structured_output) || ($structured_output === 0))) { + // If the command specifies a default pipe format and + // returned a result, then output the formatted output when + // in --pipe mode. + $formatter = drush_get_outputformat(); + if (!$formatter && is_string($structured_output)) { + $formatter = drush_load_engine('outputformat', 'string'); + } + if ($formatter) { + if ($formatter === TRUE) { + return drush_set_error(dt('No outputformat class defined for !format', array('!format' => $format))); + } + if ((!empty($command['engines']['outputformat'])) && (!in_array($formatter->engine, $command['engines']['outputformat']['usable']))) { + return $formatter->format_error(dt("The command '!command' does not produce output in a structure usable by this output format.", array('!command' => $command['command']))); + } + // Add any user-specified options to the metadata passed to the formatter. + $metadata = array(); + $metadata['strict'] = drush_get_option('strict', FALSE); + if (isset($formatter->engine_config['options'])) { + $machine_parsable = $formatter->engine_config['engine-info']['machine-parsable']; + if (drush_get_option('full', FALSE)) { + if (isset($formatter->engine_config['fields-full'])) { + $formatter->engine_config['fields-default'] = $formatter->engine_config['fields-full']; + } + else { + $formatter->engine_config['fields-default'] = array_keys($formatter->engine_config['field-labels']); + } + } + elseif ((drush_get_context('DRUSH_PIPE') || $machine_parsable) && isset($formatter->engine_config['fields-pipe'])) { + $formatter->engine_config['fields-default'] = $formatter->engine_config['fields-pipe']; + } + + // Determine the --format, and options relevant for that format. + foreach ($formatter->engine_config['options'] as $option => $option_info) { + $default_value = isset($formatter->engine_config[$option . '-default']) ? $formatter->engine_config[$option . '-default'] : FALSE; + if (($default_value === FALSE) && array_key_exists('default', $option_info)) { + $default_value = $option_info['default']; + } + if (isset($option_info['list'])) { + $user_specified_value = drush_get_option_list($option, $default_value); + } + else { + $user_specified_value = drush_get_option($option, $default_value); + } + if ($user_specified_value !== FALSE) { + if (array_key_exists('key', $option_info)) { + $option = $option_info['key']; + } + $metadata[$option] =$user_specified_value; + } + } + } + if (isset($metadata['fields']) && !empty($metadata['fields'])) { + if (isset($formatter->engine_config['field-labels'])) { + $formatter->engine_config['field-labels'] = drush_select_fields($formatter->engine_config['field-labels'], $metadata['fields'], $metadata['strict']); + } + } + $output = $formatter->process($structured_output, $metadata); + if (drush_get_context('DRUSH_PIPE')) { + drush_print_pipe($output); + } + else { + drush_print($output); + } + } + } +} + +/** + * Fail with an error if the user specified options on the + * command line that are not documented in the current command + * record. Also verify that required options are present. + */ +function _drush_verify_cli_options($command) { + + // Start out with just the options in the current command record. + $options = _drush_get_command_options($command); + // Skip all tests if the command is marked to allow anything. + // Also skip backend commands, which may have options on the commandline + // that were inherited from the calling command. + if (($command['allow-additional-options'] === TRUE)) { + return TRUE; + } + // If 'allow-additional-options' contains a list of command names, + // then union together all of the options from all of the commands. + if (is_array($command['allow-additional-options'])) { + $implemented = drush_get_commands(); + foreach ($command['allow-additional-options'] as $subcommand_name) { + if (array_key_exists($subcommand_name, $implemented)) { + $options = array_merge($options, _drush_get_command_options($implemented[$subcommand_name])); + } + } + } + // Also add in global options + $options = array_merge($options, drush_get_global_options()); + + // Add a placeholder option so that backend requests originating from prior versions of Drush are valid. + $options += array('invoke' => ''); + + // Now we will figure out which options in the cli context + // are not represented in our options list. + $cli_options = array_keys(drush_get_context('cli')); + $allowed_options = _drush_flatten_options($options); + $allowed_options = drush_append_negation_options($allowed_options); + $disallowed_options = array_diff($cli_options, $allowed_options); + if (!empty($disallowed_options)) { + $unknown = count($disallowed_options) > 1 ? dt('Unknown options') : dt('Unknown option'); + if (drush_get_option('strict', TRUE)) { + $msg = dt("@unknown: --@options. See `drush help @command` for available options. To suppress this error, add the option --strict=0.", array('@unknown' => $unknown, '@options' => implode(', --', $disallowed_options), '@command' => $command['command'])); + return drush_set_error('DRUSH_UNKNOWN_OPTION', $msg); + } + } + + // Next check to see if all required options were specified, + // and if all specified options with required values have values. + $missing_required_options = array(); + $options_missing_required_values = array(); + foreach ($command['options'] as $option_name => $option) { + if (is_array($option) && !empty($option['required']) && drush_get_option($option_name, NULL) === NULL) { + $missing_required_options[] = $option_name; + } + // Note that drush_get_option() will return TRUE if an option + // was specified without a value (--option), as opposed to + // the string "1" is --option=1 was used. + elseif (is_array($option) && !empty($option['value']) && ($option['value'] == 'required') && drush_get_option($option_name, NULL) === TRUE) { + $options_missing_required_values[] = $option_name; + } + } + if (!empty($missing_required_options) || !empty($options_missing_required_values)) { + $missing_message = ''; + if (!empty($missing_required_options)) { + $missing = count($missing_required_options) > 1 ? dt('Missing required options') : dt('Missing required option'); + $missing_message = dt("@missing: --@options.", array('@missing' => $missing, '@options' => implode(', --', $missing_required_options))); + } + if (!empty($options_missing_required_values)) { + if (!empty($missing_message)) { + $missing_message .= " "; + } + $missing = count($options_missing_required_values) > 1 ? dt('Options used without providing required values') : dt('Option used without a value where one was required'); + $missing_message .= dt("@missing: --@options.", array('@missing' => $missing, '@options' => implode(', --', $options_missing_required_values))); + } + return drush_set_error(dt("!message See `drush help @command` for information on usage.", array('!message' => $missing_message, '@command' => $command['command']))); + } + return TRUE; +} + +function drush_append_negation_options($allowed_options) { + $new_allowed = $allowed_options; + foreach ($allowed_options as $option) { + $new_allowed[] = 'no-' . $option; + } + return $new_allowed; +} + +function _drush_verify_cli_arguments($command) { + // Check to see if all of the required arguments + // are specified. + if ($command['required-arguments']) { + $required_arg_count = $command['required-arguments']; + if ($required_arg_count === TRUE) { + $required_arg_count = count($command['argument-description']); + } + + if (count($command['arguments']) < $required_arg_count) { + $missing = $required_arg_count > 1 ? dt('Missing required arguments') : dt('Missing required argument'); + $required = array_slice(array_keys($command['argument-description']), 0, $required_arg_count); + + return drush_set_error(dt("@missing: @required. See `drush help @command` for information on usage.", array( + '@missing' => $missing, + '@required' => implode(", ", $required), + '@command' => $command['command'], + ))); + } + } + return TRUE; +} + +/** + * Return the list of all of the options for the given + * command record by merging the 'options' and 'sub-options' + * records. + */ +function _drush_get_command_options($command) { + drush_command_invoke_all_ref('drush_help_alter', $command); + $options = $command['options']; + foreach ($command['sub-options'] as $group => $suboptions) { + $options = array_merge($options, $suboptions); + } + return $options; +} + +/** + * Return the list of all of the options for the given + * command record including options provided by engines and additional-options. + */ +function drush_get_command_options_extended($command) { + drush_merge_engine_data($command); + + // Start out with just the options in the current command record. + $options = _drush_get_command_options($command); + // If 'allow-additional-options' contains a list of command names, + // then union together all of the options from all of the commands. + if (is_array($command['allow-additional-options'])) { + $implemented = drush_get_commands(); + foreach ($command['allow-additional-options'] as $subcommand_name) { + if (array_key_exists($subcommand_name, $implemented)) { + $options = array_merge($options, _drush_get_command_options($implemented[$subcommand_name])); + } + } + } + return $options; +} + +/** + * Return the array keys of $options, plus any 'short-form' + * representations that may appear in the option's value. + */ +function _drush_flatten_options($options) { + $flattened_options = array(); + + foreach($options as $key => $value) { + // engine sections start with 'package-handler=git_drupalorg', + // or something similar. Get rid of everything from the = onward. + if (($eq_pos = strpos($key, '=')) !== FALSE) { + $key = substr($key, 0, $eq_pos); + } + $flattened_options[] = $key; + if (is_array($value)) { + if (array_key_exists('short-form', $value)) { + $flattened_options[] = $value['short-form']; + } + } + } + return $flattened_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 drush_invoke_process. + * + * @return + * An associative array of option key => value pairs. + */ +function drush_redispatch_get_options() { + $options = array(); + + // Add in command-specific and alias options, but for global options only. + $options_soup = drush_get_context('specific') + drush_get_context('alias'); + $global_option_list = drush_get_global_options(FALSE); + foreach ($options_soup as $key => $value) { + if (array_key_exists($key, $global_option_list)) { + $options[$key] = $value; + } + } + + // Local php settings should not override sitealias settings. + $cli_context = drush_get_context('cli'); + unset($cli_context['php'], $cli_context['php-options']); + // Pass along CLI parameters, as higher priority. + $options = $cli_context + $options; + + $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 (drush_get_command_options_extended($command) as $key => $value) { + $value = drush_get_option($key); + if (isset($value)) { + $options[$key] = $value; + } + } + } + // 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'); + $command_args = NULL; + $global_options = array(); + $target_alias_name = NULL; + // It would be nice if commandfiles could somehow extend this list, + // but it is not possible. We need to parse args before we find commandfiles, + // because the specified options may affect how commandfiles are located. + // Therefore, commandfiles are loaded too late to affect arg parsing. + // There are only a limited number of short options anyway; drush reserves + // all for use by drush core. + 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(); + + $verbosity = 0; + for ($i = 1; $i < count($args); $i++) { + $opt = $args[$i]; + // We set $command_args to NULL until the first argument that is not + // an alias is found (the command); we put everything that follows + // into $command_args. + if (is_array($command_args)) { + $command_args[] = $opt; + } + // Is the arg an option (starting with '-')? + if (!empty($opt) && $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++) { + $opt_char = substr($opt, $j, 1); + $options[$opt_char] = TRUE; + if ($opt_char == 'v') { + ++$verbosity; + } + } + } + // 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))) { + // Raising errors for missing option values should be handled by the + // bootstrap or specific command, so we no longer do this here. + $options[$opt] = $args[$i + 1]; + $i++; + } + else { + $options[$opt] = TRUE; + } + } + } + // If it's not an option, it's a command. + else { + $arguments[] = $opt; + // Once we find the first argument, record the command args and global options + if (!is_array($command_args)) { + // Remember whether we set $target_alias_name on a previous iteration, + // then record the $target_alias_name iff this arguement references a valid site alias. + $already_set_target = is_string($target_alias_name); + if (!$already_set_target && drush_sitealias_valid_alias_format($opt)) { + $target_alias_name = $opt; + } + // If an alias record was set on a previous iteration, then this + // argument must be the command name. If we set the target alias + // record on this iteration, then this is not the command name. + // If we've found the command name, then save $options in $global_options + // (all options that came before the command name), and initialize + // $command_args to an array so that we will begin storing all args + // and options that follow the command name in $command_args. + if ($already_set_target || (!is_string($target_alias_name))) { + $command_args = array(); + $global_options = $options; + } + } + } + } + if ($verbosity >= 3) { + $options['debug'] = TRUE; + } + // If no arguments are specified, then the command will + // be either 'help' or 'version' (the latter if --version is specified) + // @todo: it would be handy if one could do `drush @remote st --help` and + // have that show help for st. Today, that shows --help for help command! + if (!count($arguments)) { + if (array_key_exists('version', $options)) { + $arguments = array('version'); + } + else { + $arguments = array('help'); + } + } + if (is_array($command_args)) { + drush_set_context('DRUSH_COMMAND_ARGS', $command_args); + } + drush_set_context('DRUSH_GLOBAL_CLI_OPTIONS', $global_options); + + // Handle the "@shift" alias, if present + drush_process_bootstrap_to_first_arg($arguments); + + drush_set_arguments($arguments); + drush_set_config_special_contexts($options); + drush_set_context('cli', $options); + return $arguments; +} + +/** + * 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_has_bash()) { + // The drush.launcher script may add --php or --php-options at the + // head of the argument list; skip past those. + $base_arg_number = 1; + while (isset($args[$base_arg_number]) && substr($args[$base_arg_number], 0, 5) == '--php') { + ++$base_arg_number; + } + if (!isset($args[$base_arg_number])) { + // No arguments passed. + return; + } + elseif (_drush_is_drush_shebang_script($args[$base_arg_number])) { + // 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, $base_arg_number, 0, array('php-script')); + drush_set_context('DRUSH_SHEBANG_SCRIPT', TRUE); + } + elseif (((strpos($args[$base_arg_number], ' ') !== FALSE) || (!ctype_alnum($args[$base_arg_number][0]))) && (_drush_is_drush_shebang_script($args[$base_arg_number + 1]))) { + // 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[$base_arg_number]); + $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, $base_arg_number, $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 (count($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($reset = FALSE) { + static $commands = array(); + + if ($reset) { + $commands = array(); + return; + } + elseif ($commands) { + return $commands; + } + + $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); + + // Add engine data. + drush_merge_engine_data($command); + + // Translate command. + drush_command_translate($command); + + // If the command callback is not 'drush_command', then + // copy the callback function to an alternate element + // of the command array that will be called when Drush + // calls the command function hooks. Then, set the + // callback to drush_command so that the function hooks + // will be called. + if (($command['callback'] != 'drush_command') && $command['invoke hooks']) { + $command['primary function'] = $command['callback']; + $command['callback'] = 'drush_command'; + } + + $commands[$key] = $command; + } + } + } + $commands = array_merge($commands, annotationcommand_adapter_commands()); + foreach ($commands as $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; + } + } + } + return $commands; +} + +/** + * Organize commands into categories. Used by help listing and core-cli. + * + * @param array $commands + * A commands array as per drush_get_commands(). + * + * @return array $command_categories + * A categorized associative array of commands. + */ +function drush_commands_categorize($commands) { + $command_categories = array(); + $category_map = array(); + foreach ($commands as $key => $candidate) { + if ((!array_key_exists('is_alias', $candidate) || !$candidate['is_alias']) && !$candidate['hidden']) { + $category = $candidate['category']; + // If we have decided to remap a category, remap every command + if (array_key_exists($category, $category_map)) { + $category = $category_map[$category]; + } + if (!array_key_exists($category, $command_categories)) { + $title = drush_command_invoke_all('drush_help', "meta:$category:title"); + $alternate_title = ''; + if (!$title) { + // If there is no title, then check to see if the + // command file is stored in a folder with the same + // name as some other command file (e.g. 'core') that + // defines a title. + $alternate = basename($candidate['path']); + $alternate_title = drush_command_invoke_all('drush_help', "meta:$alternate:title"); + } + if (!empty($alternate_title)) { + $category_map[$category] = $alternate; + $category = $alternate; + $title = $alternate_title; + } + $command_categories[$category]['title'] = empty($title) ? '' : $title[0]; + $summary = drush_command_invoke_all('drush_help', "meta:$category:summary"); + if ($summary) { + $command_categories[$category]['summary'] = $summary[0]; + } + } + $candidate['category'] = $category; + $command_categories[$category]['commands'][$key] = $candidate; + } + } + + // Make sure that 'core' is always first in the list + $core_category = array('core' => $command_categories['core']); + unset($command_categories['core']); + + // Post-process the categories that have no title. + // Any that have fewer than 4 commands go into a section called "other". + $processed_categories = array(); + $misc_categories = array(); + $other_commands = array(); + $other_categories = array(); + foreach ($command_categories as $key => $info) { + if (empty($info['title'])) { + $one_category = $key; + if (count($info['commands']) < 4) { + $other_commands = array_merge($other_commands, $info['commands']); + $other_categories[] = $one_category; + } + else { + $info['title'] = dt("All commands in !category", array('!category' => $key)); + $misc_categories[$one_category] = $info; + } + } + else { + $processed_categories[$key] = $info; + } + } + $other_category = array(); + if (!empty($other_categories)) { + $other_category[implode(',', $other_categories)] = array('title' => dt("Other commands"), 'commands' => $other_commands); + } + asort($processed_categories); + asort($misc_categories); + $command_categories = array_merge($core_category, $processed_categories, $misc_categories, $other_category); + + // If the user specified --sort, then merge all of the remaining + // categories together + if (drush_get_option('sort', FALSE)) { + $combined_commands = array(); + foreach ($command_categories as $key => $info) { + $combined_commands = array_merge($combined_commands, $info['commands']); + } + $command_categories = array('all' => array('commands' => $combined_commands, 'title' => dt("Commands:"))); + } + + return $command_categories; +} + +function drush_command_defaults($key, $commandfile, $path) { + $defaults = array( + 'command' => $key, + 'command-hook' => $key, + 'invoke hooks' => TRUE, + 'callback arguments' => array(), + 'commandfile' => $commandfile, + 'path' => dirname($path), + 'engines' => array(), // Helpful for drush_show_help(). + 'callback' => 'drush_command', + 'primary function' => FALSE, + 'description' => NULL, + 'sections' => array( + 'examples' => 'Examples', + 'arguments' => 'Arguments', + 'options' => 'Options', + ), + 'arguments' => array(), + 'required-arguments' => FALSE, + 'options' => array(), + 'sub-options' => array(), + 'allow-additional-options' => FALSE, + 'global-options' => array(), + 'examples' => array(), + 'aliases' => array(), + 'core' => array(), + 'scope' => 'site', + 'drush dependencies' => array(), + 'handle-remote-commands' => FALSE, + 'remote-tty' => FALSE, + 'strict-option-handling' => FALSE, + 'tilde-expansion' => TRUE, + 'bootstrap_errors' => array(), + 'topics' => array(), + 'hidden' => FALSE, + 'category' => $commandfile, + 'add-options-to-arguments' => FALSE, + 'consolidation-output-formatters' => FALSE, + 'annotated-command-callback' => '', + 'annotations' => new AnnotationData(['command' => $key]), + ); + // We end up here, setting the defaults for a command, when + // called from drush_get_global_options(). Early in the Drush + // bootstrap, there will be no bootstrap object, because we + // need to get the list of global options when loading config + // files, and config files are loaded before the bootstrap object + // is created. In this early stage, we just use the core global + // options list. Later, the bootstrap object can also provide + // additional defaults if needed. The bootstrap command defaults + // will be merged into the command object again just before + // running it in bootstrap_and_dispatch(). + if ($bootstrap = drush_get_bootstrap_object()) { + $defaults = array_merge($defaults, $bootstrap->command_defaults()); + } + return $defaults; +} + +/** + * Translates description and other keys of a command definition. + * + * @param $command + * A command definition. + */ +function drush_command_translate(&$command) { + $command['description'] = _drush_command_translate($command['description']); + $keys = array('arguments', 'options', 'examples', 'sections'); + foreach ($keys as $key) { + foreach ($command[$key] as $k => $v) { + if (is_array($v)) { + $v['description'] = _drush_command_translate($v['description']); + } + else { + $v = _drush_command_translate($v); + } + $command[$key][$k] = $v; + } + } +} + +/** + * Helper function for drush_command_translate(). + * + * @param $source + * String or array. + */ +function _drush_command_translate($source) { + return is_array($source) ? call_user_func_array('dt', $source) : dt($source); +} + +/** + * 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. + * - callback: the function to run. Usually, this is 'drush_command', which + * will determine the primary hook for the function automatically. Only + * specify a callback function if you need many commands to call the same + * function (e.g. drush_print_file). + * - invoke hooks: If TRUE (the default), Drush will invoke all of the pre and + * post hooks for this command. Set to FALSE to suppress hooks. This setting + * is ignored unless the command 'callback' is also set. + * - primary function: Drush will copy the 'callback' parameter here if + * necessary. This value should not be set explicitly; use 'callback' instead. + * - description: description of the command. + * - arguments: an array of arguments that are understood by the command. for help texts. + * - required-arguments: The minimum number of arguments that are required, or TRUE if all are required. + * - options: an array of options that are understood by the command. for help texts. + * - global-options: a list of options from the set of Drush global options (@see: + * drush_get_global_options()) that relate to this command. The help for these + * options will be included in the help output for this command. + * - 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. + * - handle-remote-commands: set to TRUE if `drush @remote mycommand` should be executed + * locally rather than remotely dispatched. When this mode is set, the target site + * can be obtained via: + * drush_get_context('DRUSH_TARGET_SITE_ALIAS') + * - remote-tty: set to TRUE if Drush should force ssh to allocate a pseudo-tty + * when this command is being called remotely. Important for interactive commands. + * Remote commands that allocate a psedo-tty always print "Connection closed..." when done. + * - strict-option-handling: set to TRUE if drush should strictly separate local command + * cli options from the global options. Usually, drush allows global cli options and + * command cli options to be interspersed freely on the commandline. For commands where + * this flag is set, options are separated, with global options comming before the + * command names, and command options coming after, like so: + * drush --global-options command --command-options + * In this mode, the command options are no longer available via drush_get_option(); + * instead, they can be retrieved via: + * $args = drush_get_original_cli_args_and_options(); + * $args = drush_get_context('DRUSH_COMMAND_ARGS', array()); + * In this case, $args will contain the command args and options literally, exactly as they + * were entered on the command line, and in the same order as they appeared. + * - 'outputformat': declares the data format to be used to render the + * command result. In addition to the output format engine options + * listed below, each output format type can take additional metadata + * items that control the way that the output is rendered. See the + * comment in each particular output format class for information. The + * Drush core output format engines can be found in commands/core/outputformat. + * - 'default': The default type to render output as. If declared, the + * command should not print any output on its own, but instead should + * return a data structure (usually an associative array) that can + * be rendered by the output type selected. + * - 'pipe-format': When the command is executed in --pipe mode, the + * command output will be rendered by the format specified by the + * pipe-format item instead of the default format. Note that in + * either event, the user may specify the format to use via the + * --format command-line option. + * - 'formatted-filter': specifies a function callback that will be + * used to filter the command result if the selected output formatter + * is NOT declared to be machine-parsable. "table" is an example of + * an output format that is not machine-parsable. + * - 'parsable-filter': function callback that will be used to filter the + * command result if the selected output formatter is declared to be + * machine-parsable. "var_export" is an example of an output format that + * is machine-parsable. + * - 'output-data-type': An identifier representing the data structure that + * the command returns. @see outputformat_drush_engine_outputformat() for + * a description of the supported values. + * - 'field-labels': A mapping from machine name to human-readable name + * for all of the fields in a table-format command result. All + * possible field names should appear in this list. + * - 'fields-default': A list of the machine names of the fields that + * should be displayed by default in tables. + * - 'private-fields': A list of any fields that contain sensitive + * information, such as passwords. By default, Drush will hide private + * fields before printing the results to the console, but will include + * them in backend invoke results. Use --show-passwords to display. + * - 'column-widths': A mapping from field machine name to the column width + * that should be used in table output. Drush will automatically + * calculate the width of any field not listed here based on the length + * of the data items in it. + * - engines: declares information on Drush engines the command will load. + * Available engines can vary by command type. + * + * @return bool|array + * A command definition. + */ +function drush_parse_command() { + $args = drush_get_arguments(); + $command = FALSE; + + // Get a list of all implemented commands. + $implemented = drush_get_commands(); + if (!empty($args) && 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('help')) { + $arguments = array($command['command']); + $command = $implemented['helpsingle']; + $command['arguments'] = $arguments; + $command['allow-additional-options'] = TRUE; + } + 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()) { + // Drush overloads $command['arguments']; save the argument description + if (!isset($command['argument-description'])) { + $command['argument-description'] = $command['arguments']; + } + // 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; +} + +/** + * 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(); + $modules = drush_command_implements($hook); + if ($hook != 'drush_invoke_alter') { + // Allow modules to control the order of hook invocations + drush_command_invoke_all_ref('drush_invoke_alter', $modules, $hook); + } + foreach ($modules 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]); +} + +/** + * @param string + * name of command or command alias. + * + * @return string + * Primary name of command. + */ +function drush_command_normalize_name($command_name) { + $commands = drush_get_commands(); + return isset($commands[$command_name]) ? $commands[$command_name]['command'] : $command_name; +} + +/** + * 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. + * - /drush and sites/all/drush in current Drupal site. + * - Folders belonging to enabled modules in the current Drupal site. + * + * 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 commandfiles_cache()->get(); +} + +function _drush_find_commandfiles($phase, $phase_max = FALSE) { + drush_log(dt("Find command files for phase !phase (max=!max)", array('!phase' => $phase, '!max' => (string)$phase_max)), LogLevel::DEBUG); + if ($bootstrap = drush_get_bootstrap_object()) { + $searchpath = $bootstrap->commandfile_searchpaths($phase, $phase_max); + _drush_add_commandfiles($searchpath, $phase); + annotationcommand_adapter_discover($searchpath, $phase, $phase_max); + } +} + +function _drush_add_commandfiles($searchpath, $phase = NULL, $reset = FALSE) { + static $evaluated = array(); + $needs_sort = FALSE; + + if (count($searchpath)) { + if (!$reset) { + // Assemble a cid specific to the bootstrap phase and searchpaths. + // Bump $cf_version when making a change to a dev version of Drush + // that invalidates the commandfile cache. + $cf_version = 8; + $cid = drush_get_cid('commandfiles-' . $phase, array(), array_merge($searchpath, array($cf_version))); + $command_cache = drush_cache_get($cid); + if (isset($command_cache->data)) { + $cached_list = $command_cache->data; + // If we want to temporarily ignore modules via 'ignored-modules', + // then we need to take these out of the cache as well. + foreach (drush_get_option_list('ignored-modules') as $ignored) { + unset($cached_list[$ignored]); + } + } + } + + // Build a list of all of the modules to attempt to load. + // Start with any modules deferred from a previous phase. + $list = commandfiles_cache()->deferred(); + if (isset($cached_list)) { + $list = array_merge($list, $cached_list); + } + else { + // Scan for drush command files; add to list for consideration if found. + foreach (array_unique($searchpath) as $path) { + if (is_dir($path)) { + $nomask = array_merge(drush_filename_blacklist(), drush_get_option_list('ignored-modules')); + $dmv = DRUSH_MAJOR_VERSION; + $files = drush_scan_directory($path, "/\.drush($dmv|)\.inc$/", $nomask); + foreach ($files as $filename => $info) { + $module = basename($filename); + $module = preg_replace('/\.*drush[0-9]*\.inc/', '', $module); + // Only try to bootstrap modules that we have never seen before. + if (!array_key_exists($module, $evaluated) && file_exists($filename)) { + $evaluated[$module] = TRUE; + $list[$module] = Path::canonicalize($filename); + } + } + } + } + if (isset($cid)) { + drush_cache_set($cid, $list); + } + } + // Check to see if the commandfile is valid for this version of Drupal + // and is still present on filesystem (in case of cached commandfile list). + foreach ($list as $module => $filename) { + // Only try to require if the file exists. If not, a file from the + // command file cache may not be available anymore, in which case + // we rebuild the cache for this phase. + if (file_exists($filename)) { + // Avoid realpath() here as Drush commandfiles can have phar:// locations. + $load_command = commandfiles_cache()->add($filename); + if ($load_command) { + $needs_sort = TRUE; + } + } + elseif (!$reset) { + _drush_add_commandfiles($searchpath, $phase, TRUE); + $needs_sort = FALSE; + } + } + + if ($needs_sort) { + commandfiles_cache()->sort(); + } + } +} + +/** + * Substrings to ignore during commandfile and site alias searching. + */ +function drush_filename_blacklist() { + $blacklist = array('.', '..', 'drush_make', 'examples', 'tests', 'disabled', 'gitcache', 'cache'); + for ($v=4; $v<=(DRUSH_MAJOR_VERSION)+3; ++$v) { + if ($v != DRUSH_MAJOR_VERSION) { + $blacklist[] = 'drush' . $v; + } + } + $blacklist = array_merge($blacklist, drush_get_option_list('exclude')); + return $blacklist; +} + +/** + * 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)), LogLevel::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) { + $command_default_options = drush_get_context('command-specific'); + drush_command_set_command_specific($command_default_options, $command); +} + +function drush_sitealias_command_default_options($site_record, $prefix, $command = NULL) { + if (isset($site_record) && array_key_exists($prefix . 'command-specific', $site_record)) { + drush_command_set_command_specific($site_record[$prefix . 'command-specific'], $command); + } + return FALSE; +} + +function drush_command_set_command_specific_options($prefix, $command = NULL) { + $command_default_options = drush_get_option($prefix . 'command-specific', array()); + drush_command_set_command_specific($command_default_options, $command); +} + +function drush_command_set_command_specific($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_default_options, $command['command']); + if (isset($command['aliases']) && count($command['aliases'])) { + foreach ($command['aliases'] as $alias) { + $options_were_set += _drush_command_set_default_options($command_default_options, $alias); + } + } + // If we set or cleared any options, go back and re-bootstrap any global + // options such as -y and -v. + if (!empty($options_were_set)) { + _drush_preflight_global_options(); + } + // If the command uses strict option handling, back out any global + // options that were set. + if ($command['strict-option-handling']) { + $global_options = drush_get_global_options(); + foreach ($options_were_set as $key) { + if (array_key_exists($key, $global_options)) { + if (!array_key_exists('context', $global_options[$key])) { + $strict_options_warning =& drush_get_context('DRUSH_STRICT_OPTIONS_WARNING', array()); + if (!array_key_exists($key, $strict_options_warning)) { + drush_log(dt("Global option --!option not supported in command-specific options for command !command due to a limitation in strict option handling.", array('!option' => $key, '!command' => $command['command'])), LogLevel::WARNING); + $strict_options_warning[$key] = TRUE; + } + } + drush_unset_option($key, 'specific'); + } + } + } + } +} + +function _drush_command_set_default_options($command_default_options, $command) { + $options_were_set = array(); + 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[] = $key; + } + } + } + return $options_were_set; +} + +/** + * Return all of the command-specific options defined in the given + * options set for the specified command name. Note that it is valid + * to use the command name alias rather than the primary command name, + * both in the parameter to this function, and in the options set. + */ +function drush_command_get_command_specific_options($options, $command_name, $prefix = '') { + $result = array(); + $command_name = drush_command_normalize_name($command_name); + if (isset($options[$prefix . 'command-specific'])) { + foreach ($options[$prefix . 'command-specific'] as $options_for_command => $values) { + if ($command_name == drush_command_normalize_name($options_for_command)) { + $result = array_merge($result, $values); + } + } + } + return $result; +} + +/** + * Return the original cli args and options, exactly as they + * appeared on the command line, and in the same order. + * Any command-specific options that were set will also + * appear in this list, appended at the very end. + * + * The args and options returned are raw, and must be + * escaped as necessary before use. + */ +function drush_get_original_cli_args_and_options($command = NULL) { + $args = drush_get_context('DRUSH_COMMAND_ARGS', array()); + $command_specific_options = drush_get_context('specific'); + if ($command == NULL) { + $command = drush_get_command(); + } + $command_options = ($command == NULL) ? array() : _drush_get_command_options($command); + foreach ($command_specific_options as $key => $value) { + if (!array_key_exists($key, $command_options)) { + if (($value === TRUE) || (!isset($value))) { + $args[] = "--$key"; + } + else { + $args[] = "--$key=$value"; + } + } + } + return $args; +} + +/** + * 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); +} + +/** + * 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 to invoke drush from a more functional Drupal environment to run this command.', array('!command' => $command['command'])); +} + +/** + * 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_COMMANDFILE_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. Handles + * explicit version numbers and 'plus' numbers like 7+ (compatible with 7,8 ...). + * + * @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) { + $major = drush_drupal_major_version(); + if (!$core = $command['core']) { + return TRUE; + } + foreach ($core as $compat) { + if ($compat == $major) { + return TRUE; + } + elseif (substr($compat, -1) == '+' && $major >= substr($compat, 0, strlen($compat)-1)) { + 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)); +} + +/** + * Check if a shell alias exists for current request. If so, re-route to + * core-execute and pass alias value along with rest of CLI arguments. + */ +function drush_shell_alias_replace($target_site_alias) { + $escape = TRUE; + $args = drush_get_arguments(); + $argv = drush_get_context('argv'); + $first = current($args); + // @todo drush_get_option is awkward here. + $shell_aliases = drush_get_context('shell-aliases', array()); + if (isset($shell_aliases[$first])) { + // Shell alias found for first argument in the request. + $alias_value = $shell_aliases[$first]; + if (!is_array($alias_value)) { + // Shell aliases can have embedded variables such as {{@target}} and {{%root}} + // that are replaced with the name of the target site alias, or the value of a + // path alias defined in the target site alias record. We only support replacements + // when the alias value is a string; if it is already broken out into an array, + // then the values therein are used literally. + $alias_variables = array( '{{@target}}' => '@none' ); + if ($target_site_alias) { + $alias_variables = array( '{{@target}}' => $target_site_alias ); + $target = drush_sitealias_get_record($target_site_alias); + foreach ($target as $key => $value) { + if (!is_array($value)) { + $alias_variables['{{' . $key . '}}'] = $value; + } + } + if (array_key_exists('path-aliases', $target)) { + foreach ($target['path-aliases'] as $key => $value) { + // n.b. $key will contain something like "%root" or "%files". + $alias_variables['{{' . $key . '}}'] = $value; + } + } + } + $alias_value = str_replace(array_keys($alias_variables), array_values($alias_variables), $alias_value); + // Check for unmatched replacements + $matches = array(); + $match_result = preg_match('/{{[%@#]*[a-z0-9.]*}}/', $alias_value, $matches); + if ($match_result) { + $unmatched_replacements = implode(', ', $matches); + $unmatched_replacements = preg_replace('/[{}]/', '', $unmatched_replacements); + return drush_set_error('DRUSH_SHELL_ALIAS_UNMATCHED_REPLACEMENTS', dt('The shell alias @alias-name uses replacements "@unmatched". You must use this command with a site alias (e.g. `drush @myalias @alias-name ...`) that defines all of these variables.', array('@alias-name' => $first, '@unmatched' => $unmatched_replacements))); + } + if (substr($alias_value, 0, 1) == '!') { + $alias_value = ltrim($alias_value, '!'); + $alias_value = array('core-execute', $alias_value); + $escape = FALSE; + } + else { + // Respect quoting. See http://stackoverflow.com/questions/2202435/php-ex + $alias_value = str_getcsv($alias_value, ' '); + } + } + drush_log(dt('Shell alias found: !key => !value', array('!key' => $first, '!value' => implode(' ', $alias_value))), LogLevel::DEBUG); + $pos = array_search($first, $argv); + $number = 1; + if ($target_site_alias && ($argv[$pos - 1] == $target_site_alias)) { + --$pos; + ++$number; + } + array_splice($argv, $pos, $number, $alias_value); + if (!$escape) { + drush_set_option('escape', FALSE); + } + drush_set_context('argv', $argv); + drush_parse_args(); + _drush_preflight_global_options(); + } +} + +function commandfiles_cache() { + static $commandfiles_cache = NULL; + + if (!isset($commandfiles_cache)) { + $commandfiles_cache = new Drush\Command\Commandfiles(); + } + return $commandfiles_cache; +} diff --git a/drush8/slim-drush.php b/drush8/slim-drush.php new file mode 100644 index 0000000..52d697d --- /dev/null +++ b/drush8/slim-drush.php @@ -0,0 +1,126 @@ +#!/usr/bin/env php +isFile()) { + $file = substr($info->getPathname(), strlen($patched_files) + 1); + $replace[] = $file; + } +} + +foreach ($replace as $file) { + my_replace($file); +} + +my_msg("Patched $drush is ready for use"); + +$cleanup = [ + 'docs', + 'examples', + 'misc/windrush_build', + 'vendor/doctrine', + 'vendor/phpdocumentor', + 'vendor/phpspec', + 'vendor/phpunit', + 'vendor/sebastian', +]; + +$delete = []; + +$offset = strlen($drush) + 1; +$handle = new \Phar($drush); +/** @var \SplFileInfo $item */ +foreach (new \RecursiveIteratorIterator($handle) as $item) { + if ($item->isFile()) { + $file = $item->getPathname(); + $file = substr($file, strpos($file, "$drush/") + $offset); + foreach ($cleanup as $dir) { + if (str_starts_with($file, $dir)) { + $delete[] = $file; + } + } + } +} + +if (!$delete) { + my_msg("Nothing to clean-up"); + exit(0); +} + +foreach ($delete as $file) { + my_delete($file); +} + + +function my_msg($msg) { + echo $msg . PHP_EOL; +} + +function my_fail($msg) { + my_msg($msg); + die(1); +} + +function my_replace($file) { + global $drush, $patched_files, $hash; + $phar = "phar://$drush/$file"; + $old = file_get_contents($phar); + $old_hash = hash($hash, $old); + $new = file_get_contents("$patched_files/$file"); + $new_hash = hash($hash, $new); + if ($old_hash !== $new_hash) { + my_msg("Patching $file - $hash checksums old and new"); + my_msg($old_hash); + my_msg($new_hash); + $r = file_put_contents($phar, $new); + if ($r) my_msg("Patched $file"); + else my_fail("Failed to patch $file"); + } + else my_msg("File $file already patched"); +} + +function my_delete($file) { + global $drush; + $phar = "phar://$drush/$file"; + my_msg("Deleting file $file"); + if (file_exists($phar)) { + unlink($phar); + } +} diff --git a/drush8/slim-drush.sh b/drush8/slim-drush.sh new file mode 100755 index 0000000..c510cac --- /dev/null +++ b/drush8/slim-drush.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env sh + +DRUSH=drush.phar +BACKUP=$DRUSH.bak +PHAR=/usr/bin/phar81 + + +_download() { +[ ! -e $DRUSH ] && curl -L https://github.com/drush-ops/drush/releases/download/8.4.8/drush.phar -o drush.phar +cp -n $DRUSH $BACKUP +php -dphar.readonly=0 $PHAR add -f $DRUSH files/ +} + +_cleanup() { +_dir=$1 +_grep=$2 +echo "processing dir: $_dir" +list=$($PHAR list -f $DRUSH | grep -e $DRUSH/$_grep | sed "s|.*$DRUSH/$_dir/||" ) +for _file in $list; do + +echo "deleting file: $_dir/$_file" + +php -dphar.readonly=0 $PHAR delete -f $DRUSH -e $_dir/$_file +done +} + +_download + +# remove unused dirs +cleanup=' +docs +examples +misc/windrush_build/assets +misc/windrush_build +' +for _dir in $cleanup; do _cleanup $_dir "$_dir/" ; done + +# remove unused vendor +cleanup=' +doctrine +phpdocumentor +phpspec +phpunit +sebastian +' +for _dir in $cleanup; do _cleanup vendor/$_dir "vendor/$_dir.*\.php$" ; done + +#php -dphar.readonly=0 $PHAR compress -c auto -f $DRUSH diff --git a/php8/Dockerfile b/php8/Dockerfile index e6e23a8..430e479 100644 --- a/php8/Dockerfile +++ b/php8/Dockerfile @@ -87,10 +87,12 @@ RUN set -e \ --2 \ && ln -s composer2 /usr/bin/composer \ && php -r "unlink('composer-setup.php');" \ - && php -r "copy('https://github.com/drush-ops/drush/releases/download/$DRUSH_VERSION/drush.phar', '/usr/bin/drush');" \ - && chmod +x /usr/bin/drush && /usr/bin/drush version && rm -fr /root/.drush \ +# && php -r "copy('https://github.com/drush-ops/drush/releases/download/$DRUSH_VERSION/drush.phar', '/usr/bin/drush');" \ +# && chmod +x /usr/bin/drush && /usr/bin/drush version && rm -fr /root/.drush \ && rm -fr /var/cache/apk/* +COPY --chmod=0755 drush.phar /usr/bin/drush + COPY php.ini /etc/php8/conf.d/xx-drupal.ini WORKDIR /srv diff --git a/php81/Dockerfile b/php81/Dockerfile index 09852ed..d64864a 100644 --- a/php81/Dockerfile +++ b/php81/Dockerfile @@ -87,10 +87,12 @@ RUN set -e \ --2 \ && ln -s composer2 /usr/bin/composer \ && php -r "unlink('composer-setup.php');" \ - && php -r "copy('https://github.com/drush-ops/drush/releases/download/$DRUSH_VERSION/drush.phar', '/usr/bin/drush');" \ - && chmod +x /usr/bin/drush && /usr/bin/drush version && rm -fr /root/.drush \ +# && php -r "copy('https://github.com/drush-ops/drush/releases/download/$DRUSH_VERSION/drush.phar', '/usr/bin/drush');" \ +# && chmod +x /usr/bin/drush && /usr/bin/drush version && rm -fr /root/.drush \ && rm -fr /var/cache/apk/* +COPY --chmod=0755 drush.phar /usr/bin/drush + COPY php.ini /etc/php81/conf.d/xx-drupal.ini WORKDIR /srv