diff --git a/box.json.dist b/box.json.dist index 37d7069a26..2a50be0d26 100644 --- a/box.json.dist +++ b/box.json.dist @@ -3,7 +3,7 @@ "chmod": "0755", "compactors": [], "directories": ["commands", "docs", "examples", "includes", "lib", "misc"], - "files": ["drush.api.php", "drush.complete.sh", "drush.info", "drush.launcher", "drush.php", "drush-services.yml", "drush_logo-black.png", "README.md"], + "files": ["drush.api.php", "drush.complete.sh", "drush.info", "drush.launcher", "drush.php", "drush-services.yml", "drush_logo-black.png", "README.md"], "finder": [ { "name": "*.php", diff --git a/commands/core/help.drush.inc b/commands/core/help.drush.inc index e8c22e63f8..55dbc0ba8d 100644 --- a/commands/core/help.drush.inc +++ b/commands/core/help.drush.inc @@ -100,7 +100,8 @@ function drush_core_help($name = '') { } // For speed, only bootstrap up to DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION. - drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); + drush_bootstrap_max(); + drush_get_commands(true); $implemented = drush_get_commands(); ksort($implemented); $command_categories = drush_commands_categorize($implemented); diff --git a/commands/core/helpsingle.drush.inc b/commands/core/helpsingle.drush.inc index ee26b78f93..7c6bee4efc 100644 --- a/commands/core/helpsingle.drush.inc +++ b/commands/core/helpsingle.drush.inc @@ -41,12 +41,19 @@ function drush_core_helpsingle($commandstring) { if (!array_key_exists($commandstring, $commands)) { // If the command cannot be found, then bootstrap so that // additional commands will be brought in. - // For speed, only bootstrap up to DRUSH_BOOTSTRAP_DRUPAL_SITE. - drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_SITE); + // TODO: We need to do a full bootstrap in order to find module service + // commands. We only need to do this for Drupal 8, though; 7 and earlier + // can stop at DRUSH_BOOTSTRAP_DRUPAL_SITE. Perhaps we could use command + // caching to avoid bootstrapping, if we have collected the commands for + // this site once already. + drush_bootstrap_max(); $commands = drush_get_commands(); } if (array_key_exists($commandstring, $commands)) { $command = $commands[$commandstring]; + + annotationcommand_adapter_add_hook_options($command); + drush_print_help($command); return TRUE; } diff --git a/commands/core/site_install.drush.inc b/commands/core/site_install.drush.inc index e562169c7c..8818d31a54 100644 --- a/commands/core/site_install.drush.inc +++ b/commands/core/site_install.drush.inc @@ -225,7 +225,7 @@ function drush_core_site_install($profile = NULL) { drush_include_engine('drupal', 'site_install'); drush_core_site_install_version($profile, $form_options); - // Post installation run the configuration import. + // Post installation, run the configuration import. if ($config = drush_get_option('config-dir')) { // Set the destination site UUID to match the source UUID, to bypass a core fail-safe. $source_storage = new FileStorage($config); diff --git a/commands/make/make.drush.inc b/commands/make/make.drush.inc index 240f3c8a34..b0bedd760f 100644 --- a/commands/make/make.drush.inc +++ b/commands/make/make.drush.inc @@ -155,7 +155,7 @@ function make_drush_command() { 'makefile' => 'Filename of the makefile to convert.', ), 'options' => array( - 'format' => 'The format to which the make file should be converted. Accepted values include make, composer, and yml.' + 'format' => 'The format to which the make file should be converted. Accepted values include make, composer, and yml.', ), 'required-arguments' => TRUE, 'examples' => array( diff --git a/composer.json b/composer.json index ecfa711cf6..88b717032d 100644 --- a/composer.json +++ b/composer.json @@ -27,24 +27,20 @@ "drush.php", "drush.complete.sh" ], - "config": { - "platform": { - "php": "5.6.0" - } - }, "require": { "php": ">=5.6.0", "psr/log": "~1.0", "psy/psysh": "~0.6", - "symfony/yaml": "~2.3|~3.0", - "symfony/var-dumper": "~2.7|~3.0", "league/container": "~2", "consolidation/robo": "dev-master", + "symfony/config": "~2.2", "consolidation/annotated-command": "~2", "consolidation/output-formatters": "~2", - "symfony/console": "2.7.*", - "symfony/event-dispatcher": "2.7.*", - "symfony/config": "~2.2", + "symfony/yaml": "~2.3", + "symfony/var-dumper": "~2.7", + "symfony/console": "~2.7", + "symfony/event-dispatcher": "~2.7", + "symfony/finder": "~2.7", "pear/console_table": "~1.3.0", "phpdocumentor/reflection-docblock": "^2.0", "webmozart/path-util": "~2" diff --git a/composer.lock b/composer.lock index 8ee752373f..55414d6d3c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,25 +4,25 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "6438a4c09743cb2cf614b223a83c10f4", - "content-hash": "01ca732d8e43306efe4bcfc98597f420", + "hash": "a2bbac66cd879f4984bddcf258b12bc1", + "content-hash": "9aa160dd928fd5edcecf0eaa5a223dfd", "packages": [ { "name": "consolidation/annotated-command", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "2e178edadfa186806bc7a296fa1dc4e9587c7d64" + "reference": "2a6ef0b39ed904dabefd796eeaf5f8feeaa881c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/2e178edadfa186806bc7a296fa1dc4e9587c7d64", - "reference": "2e178edadfa186806bc7a296fa1dc4e9587c7d64", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/2a6ef0b39ed904dabefd796eeaf5f8feeaa881c4", + "reference": "2a6ef0b39ed904dabefd796eeaf5f8feeaa881c4", "shasum": "" }, "require": { - "consolidation/output-formatters": "^2.0.0-rc1", + "consolidation/output-formatters": "~2", "php": ">=5.4.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", "psr/log": "~1.0", @@ -57,7 +57,7 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2016-10-01 15:03:43" + "time": "2016-10-05 04:09:14" }, { "name": "consolidation/log", @@ -108,16 +108,16 @@ }, { "name": "consolidation/output-formatters", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "42fedb697d47e4c3657238e48d1098d5c18e7286" + "reference": "8bce15438a97afba5dcf036a71d961977b64fa3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/42fedb697d47e4c3657238e48d1098d5c18e7286", - "reference": "42fedb697d47e4c3657238e48d1098d5c18e7286", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/8bce15438a97afba5dcf036a71d961977b64fa3e", + "reference": "8bce15438a97afba5dcf036a71d961977b64fa3e", "shasum": "" }, "require": { @@ -152,7 +152,7 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2016-10-01 14:58:50" + "time": "2016-10-05 04:05:17" }, { "name": "consolidation/robo", @@ -160,12 +160,12 @@ "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "7c81132c536d6f5ec026ecc110ad0d92005198e8" + "reference": "32f349ed83e53892935004e507878fbdd360e2f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/7c81132c536d6f5ec026ecc110ad0d92005198e8", - "reference": "7c81132c536d6f5ec026ecc110ad0d92005198e8", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/32f349ed83e53892935004e507878fbdd360e2f2", + "reference": "32f349ed83e53892935004e507878fbdd360e2f2", "shasum": "" }, "require": { @@ -226,7 +226,7 @@ } ], "description": "Modern task runner", - "time": "2016-10-01 15:20:49" + "time": "2016-10-05 16:47:33" }, { "name": "container-interop/container-interop", @@ -715,16 +715,16 @@ }, { "name": "symfony/config", - "version": "v2.8.11", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "005bf10c156335ede2e89fb9a9ee10a0b742bc84" + "reference": "f8b1922bbda9d2ac86aecd649399040bce849fde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/005bf10c156335ede2e89fb9a9ee10a0b742bc84", - "reference": "005bf10c156335ede2e89fb9a9ee10a0b742bc84", + "url": "https://api.github.com/repos/symfony/config/zipball/f8b1922bbda9d2ac86aecd649399040bce849fde", + "reference": "f8b1922bbda9d2ac86aecd649399040bce849fde", "shasum": "" }, "require": { @@ -764,29 +764,31 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2016-08-16 14:56:08" + "time": "2016-09-14 20:31:12" }, { "name": "symfony/console", - "version": "v2.7.18", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cc3d188345b84d27a931db0969b0ff36272f451c" + "reference": "d7a5a88178f94dcc29531ea4028ea614e35452d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cc3d188345b84d27a931db0969b0ff36272f451c", - "reference": "cc3d188345b84d27a931db0969b0ff36272f451c", + "url": "https://api.github.com/repos/symfony/console/zipball/d7a5a88178f94dcc29531ea4028ea614e35452d4", + "reference": "d7a5a88178f94dcc29531ea4028ea614e35452d4", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/debug": "~2.7,>=2.7.2|~3.0.0", + "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1", - "symfony/process": "~2.1" + "symfony/event-dispatcher": "~2.1|~3.0.0", + "symfony/process": "~2.1|~3.0.0" }, "suggest": { "psr/log": "For using the console logger", @@ -796,7 +798,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -823,20 +825,77 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-09-06 07:26:07" + "time": "2016-09-28 00:10:16" + }, + { + "name": "symfony/debug", + "version": "v3.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a", + "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2016-07-30 07:22:48" }, { "name": "symfony/event-dispatcher", - "version": "v2.7.18", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "2b096845cbe49f4616dc90494e1a68d48e9bba23" + "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2b096845cbe49f4616dc90494e1a68d48e9bba23", - "reference": "2b096845cbe49f4616dc90494e1a68d48e9bba23", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/889983a79a043dfda68f38c38b6dba092dd49cd8", + "reference": "889983a79a043dfda68f38c38b6dba092dd49cd8", "shasum": "" }, "require": { @@ -844,10 +903,10 @@ }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.0,>=2.0.5", - "symfony/dependency-injection": "~2.6", - "symfony/expression-language": "~2.6", - "symfony/stopwatch": "~2.3" + "symfony/config": "~2.0,>=2.0.5|~3.0.0", + "symfony/dependency-injection": "~2.6|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/stopwatch": "~2.3|~3.0.0" }, "suggest": { "symfony/dependency-injection": "", @@ -856,7 +915,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -883,7 +942,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-08-09 09:00:18" + "time": "2016-07-28 16:56:28" }, { "name": "symfony/filesystem", @@ -936,25 +995,25 @@ }, { "name": "symfony/finder", - "version": "v3.1.4", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e568ef1784f447a0e54dcb6f6de30b9747b0f577" + "reference": "bc24c8f5674c6f6841f2856b70e5d60784be5691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e568ef1784f447a0e54dcb6f6de30b9747b0f577", - "reference": "e568ef1784f447a0e54dcb6f6de30b9747b0f577", + "url": "https://api.github.com/repos/symfony/finder/zipball/bc24c8f5674c6f6841f2856b70e5d60784be5691", + "reference": "bc24c8f5674c6f6841f2856b70e5d60784be5691", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": ">=5.3.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -981,7 +1040,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-08-26 12:04:02" + "time": "2016-09-28 00:10:16" }, { "name": "symfony/polyfill-mbstring", @@ -1044,16 +1103,16 @@ }, { "name": "symfony/process", - "version": "v2.7.18", + "version": "v2.7.19", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "86be324a1898603789765790c6e2288a505f0ead" + "reference": "e134ba500a16bead3a059a087b652182f4f85481" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/86be324a1898603789765790c6e2288a505f0ead", - "reference": "86be324a1898603789765790c6e2288a505f0ead", + "url": "https://api.github.com/repos/symfony/process/zipball/e134ba500a16bead3a059a087b652182f4f85481", + "reference": "e134ba500a16bead3a059a087b652182f4f85481", "shasum": "" }, "require": { @@ -1089,24 +1148,24 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-09-06 07:26:07" + "time": "2016-09-29 02:20:21" }, { "name": "symfony/var-dumper", - "version": "v3.1.4", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "62ee73706c421654a4c840028954510277f7dfc8" + "reference": "08761341cc4a3b2d9d9578364f7c655b178254aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/62ee73706c421654a4c840028954510277f7dfc8", - "reference": "62ee73706c421654a4c840028954510277f7dfc8", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/08761341cc4a3b2d9d9578364f7c655b178254aa", + "reference": "08761341cc4a3b2d9d9578364f7c655b178254aa", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": ">=5.3.9", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { @@ -1118,7 +1177,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -1152,29 +1211,29 @@ "debug", "dump" ], - "time": "2016-08-31 09:05:42" + "time": "2016-09-29 14:06:15" }, { "name": "symfony/yaml", - "version": "v3.1.4", + "version": "v2.8.12", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d" + "reference": "e7540734bad981fe59f8ef14b6fc194ae9df8d9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f291ed25eb1435bddbe8a96caaef16469c2a092d", - "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e7540734bad981fe59f8ef14b6fc194ae9df8d9c", + "reference": "e7540734bad981fe59f8ef14b6fc194ae9df8d9c", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": ">=5.3.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -1201,7 +1260,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-09-02 02:12:52" + "time": "2016-09-02 01:57:56" }, { "name": "webmozart/assert", @@ -2171,8 +2230,5 @@ "platform": { "php": ">=5.6.0" }, - "platform-dev": [], - "platform-overrides": { - "php": "5.6.0" - } + "platform-dev": [] } diff --git a/includes/annotationcommand_adapter.inc b/includes/annotationcommand_adapter.inc new file mode 100644 index 0000000000..c8b2f891cc --- /dev/null +++ b/includes/annotationcommand_adapter.inc @@ -0,0 +1,796 @@ +commandProcessor(); +} + +/** + * Fetch the formatter manager from the command processor + * + * @return FormatterManager + */ +function annotatedcomand_adapter_get_formatter() { + $commandProcessor = annotationcommand_adapter_get_processor(); + return $commandProcessor->formatterManager(); +} + +/** + * Callback function called by HookManager::EXTRACT_OUTPUT to set + * the backend result. + */ +function annotatedcomand_adapter_backend_result($structured_data) { + $return = drush_backend_get_result(); + if (empty($return)) { + drush_backend_set_result($structured_data); + } +} + +/** + * Return the cached commands built by annotationcommand_adapter_discover. + * @see drush_get_commands() + */ +function annotationcommand_adapter_commands() { + $annotation_commandfiles = drush_get_context('DRUSH_ANNOTATED_COMMANDFILES'); + // Remove any entry in the commandfiles list from an ignored module. + $ignored = implode('|', drush_get_option_list('ignored-modules')); + $regex = "#/(modules|themes|profiles)(/|/.*/)($ignored)/#"; + foreach ($annotation_commandfiles as $key => $path) { + if (preg_match($regex, $path)) { + unset($annotation_commandfiles[$key]); + } + } + $commands = annotationcommand_adapter_get_commands($annotation_commandfiles); + $module_service_commands = drush_get_context('DRUSH_MODULE_SERVICE_COMMANDS'); + return array_merge($commands, $module_service_commands); +} + +/** + * Search for annotation commands at the provided search path. + * @see _drush_find_commandfiles() + */ +function annotationcommand_adapter_discover($searchpath, $phase = false, $phase_max = false) { + if (empty($searchpath)) { + return; + } + if (($phase >= DRUSH_BOOTSTRAP_DRUPAL_SITE) && (drush_drupal_major_version() >= 8)) { + return; + } + $annotation_commandfiles = []; + // 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 = 1; + $cid = drush_get_cid('annotationfiles-' . $phase, array(), array_merge($searchpath, array($cf_version))); + $command_cache = drush_cache_get($cid); + if (isset($command_cache->data)) { + $annotation_commandfiles = $command_cache->data; + } + else { + // Check to see if this is the Drush searchpath for instances where we are + // NOT going to do a full bootstrap (e.g. when running a help command) + if (($phase == DRUSH_BOOTSTRAP_DRUPAL_SITE) && ($phase_max < DRUSH_BOOTSTRAP_DRUPAL_FULL)) { + $searchpath = annotationcommand_adapter_refine_searchpaths($searchpath); + } + $discovery = annotationcommand_adapter_get_discovery(); + $annotation_commandfiles = $discovery->discoverNamespaced($searchpath, '\Drupal'); + drush_cache_set($cid, $annotation_commandfiles); + } + drush_set_context( + 'DRUSH_ANNOTATED_COMMANDFILES', + array_merge( + drush_get_context('DRUSH_ANNOTATED_COMMANDFILES'), + $annotation_commandfiles + ) + ); +} + +/** + * This function is set as the $command['callback'] for Symfony Console commands + * e.g. those provided by Drupal 8 modules. When the DRUSH_SYMFONY environment + * variable is set, these will be called via Symfony's Application::run() method. + * Otherwise, the legacy Drush command dispatcher will be used for all commands. + * + * @return bolean false if command failed (expect drush_set_error was called in this case) + */ +function annotationcommand_adapter_run_console_command() { + $args = func_get_args(); + $command = drush_get_command(); + + $console_command = $command['drush-console-command']; + // TODO: Build an appropriate input object + $input = annotationcommand_adapter_build_input($console_command, $args); + $output = new ConsoleOutput(); + $result = $console_command->run($input, $output); + + return $result; +} + +/** + * TODO: This could probably just be a DrushInputAdapter now. + */ +function annotationcommand_adapter_build_input($console_command, $userArgs) { + $args = []; + $defaultOptions = []; + $definition = $console_command->getDefinition(); + $inputArguments = $definition->getArguments(); + foreach ($inputArguments as $key => $inputOption) { + $value = array_shift($userArgs); + if (!isset($value)) { + $value = $inputOption->getDefault(); + } + $args[$key] = $value; + } + $inputOptions = $definition->getOptions(); + foreach ($inputOptions as $option => $inputOption) { + $defaultOptions[$option] = $inputOption->getDefault(); + } + foreach ($defaultOptions as $option => $value) { + $args["--$option"] = drush_get_option($option, $value); + } + // TODO: Need to add global options. Note that ArrayInput is validated. + $input = new ArrayInput($args, $definition); + return $input; +} + +/** + * Collect all of the options defined in every relevant context, and + * merge them together to form the options array. + * + * @return array + */ +function annotationcommand_adapter_get_options($command) { + $default_options = isset($command['consolidation-option-defaults']) ? $command['consolidation-option-defaults'] : []; + $options = drush_redispatch_get_options() + $default_options; + + $options += drush_get_merged_options(); + + return $options; +} + +/** + * This function is set as the $command['callback'] for commands that have + * been converted to annotated commands. When the DRUSH_SYMFONY environment + * variable is set, these will be called via Symfony's Application::run() method. + * Otherwise, the legacy Drush command dispatcher will be used for all commands. + * + * @return bolean false if command failed (expect drush_set_error was called in this case) + */ +function annotationcommand_adapter_process_command() { + $userArgs = func_get_args(); + $commandprocessor = annotationcommand_adapter_get_processor(); + $command = drush_get_command(); + annotationcommand_adapter_add_hook_options($command); + $args = []; + foreach ($command['consolidation-arg-defaults'] as $key => $default) { + $value = array_shift($userArgs); + if (!isset($value)) { + $value = $default; + } + $args[$key] = $value; + } + + $input = new DrushInputAdapter($args, annotationcommand_adapter_get_options($command), $command['command']); + $output = new DrushOutputAdapter(); + $annotationData = $command['annotations']; + $commandData = new CommandData( + $annotationData, + $input, + $output + ); + $commandData->setIncludeOptionsInArgs($command['add-options-to-arguments']); + $names = annotationcommand_adapter_command_names($command); + + // n.b.: backend result is set by a post-alter hook. + $result = $commandprocessor->process( + $output, + $names, + $command['annotated-command-callback'], + $commandData + ); + + return $result; +} + +/** + * Internal function called by annotationcommand_adapter_commands, which + * is called by drush_get_commands(). + * + * @param array $annotation_commandfiles path => class mapping + * + * @return object[] + */ +function annotationcommand_adapter_get_commands($annotation_commandfiles) { + $commands = []; + // This will give us a list containing something akin to: + // 'modules/default_content/src/CliTools/DefaultContentCommands.php' => + // '\\Drupal\\default_content\\CliTools\\DefaultContentCommands', + foreach ($annotation_commandfiles as $commandfile_path => $commandfile_class) { + if (file_exists($commandfile_path)) { + $commandhandler = annotationcommand_adapter_create_commandfile_instance($commandfile_path, $commandfile_class); + $commands_for_this_commandhandler = annotationcommand_adapter_get_commands_for_commandhandler($commandhandler, $commandfile_path); + $commands = array_merge($commands, $commands_for_this_commandhandler); + } + } + return $commands; +} + +/** + * Create and cache a commandfile instance. + * + * @param string $commandfile_path Path to the commandfile implementation + * @param string $commandfile_class Namespace and class of the commandfile object + * + * @return object + */ +function annotationcommand_adapter_create_commandfile_instance($commandfile_path, $commandfile_class) { + $runner = \Drush::runner(); + $app = \Drush::service('application'); + $cache =& drush_get_context('DRUSH_ANNOTATION_COMMANDFILE_INSTANCES'); + if (!isset($cache[$commandfile_path])) { + include_once $commandfile_path; + $commandhandler = $runner->registerCommandClass($app, $commandfile_class); + $cache[$commandfile_path] = $commandhandler; + } + return $cache[$commandfile_path]; +} + +/** + * TODO: document + */ +function annotationcommand_adapter_cache_module_console_commands($console_command, $commandfile_path = null) { + if (!isset($commandfile_path)) { + $class = new \ReflectionClass($console_command); + $commandfile_path = $class->getFileName(); + } + $module_service_commands = drush_get_context('DRUSH_MODULE_SERVICE_COMMANDS'); + $commands = annotationcommand_adapter_get_command_for_console_command($console_command, $commandfile_path); + drush_set_context('DRUSH_MODULE_SERVICE_COMMANDS', array_merge($commands, $module_service_commands)); +} + +/** + * TODO: document + */ +function annotationcommand_adapter_cache_module_service_commands($commandhandler, $commandfile_path = null) { + if (!isset($commandfile_path)) { + $class = new \ReflectionClass($commandhandler); + $commandfile_path = $class->getFileName(); + } + $module_service_commands = drush_get_context('DRUSH_MODULE_SERVICE_COMMANDS'); + $commands = annotationcommand_adapter_get_commands_for_commandhandler($commandhandler, $commandfile_path); + drush_set_context('DRUSH_MODULE_SERVICE_COMMANDS', array_merge($commands, $module_service_commands)); +} + +/** + * Convert a Symfony Console command into a Drush $command record + * + * @param Symfony\Component\Console\Command\Command $console_command The Symfony Console command to convert + * @param string $commandfile_path Path to console command file + * + * @return array Drush $command record + */ +function annotationcommand_adapter_get_command_for_console_command($console_command, $commandfile_path) { + $commands = []; + $commandfile = basename($commandfile_path, '.php'); + $factory = \Drush::commandFactory(); + $inputDefinition = $console_command->getDefinition(); + $inputArguments = $inputDefinition->getArguments(); + $inputOptions = $inputDefinition->getOptions(); + $aliases = $console_command->getAliases(); + $command_name = strtolower($console_command->getName()); + $standard_alias = str_replace(':', '-', $command_name); + if ($command_name != $standard_alias) { + $aliases[] = $standard_alias; + } + $command = [ + 'name' => $command_name, + 'callback' => 'annotationcommand_adapter_run_console_command', + 'drush-console-command' => $console_command, + 'commandfile' => $commandfile, + 'category' => $commandfile, + 'options' => [], + 'arguments' => [], + 'description' => $console_command->getDescription(), + 'examples' => $console_command->getUsages(), + 'aliases' => $aliases, + ]; + foreach ($inputArguments as $arg => $inputArg) { + $command['arguments'][$arg] = $inputArg->getDescription(); + } + $command['required-arguments'] = $inputDefinition->getArgumentRequiredCount(); + foreach ($inputOptions as $option => $inputOption) { + $description = $inputOption->getDescription(); + $default = $inputOption->getDefault(); + $command['options'][$option] = ['description' => $description]; + if (!empty($default)) { + $command['options'][$option]['example-value'] = $default; + } + } + $command += drush_command_defaults($command_name, $commandfile, $commandfile_path); + $commands[$command_name] = $command; + return $commands; +} + +/** + * Convert an annotated command command handler object into a Drush $command record. + * + * @param object $commandhandler Command handler object + * @param string $commandfile_path + * @param boolean $includeAllPublicMethods TODO remove this, make it 'false' always + * + * @return array Drush $command record + */ +function annotationcommand_adapter_get_commands_for_commandhandler($commandhandler, $commandfile_path) { + $cache =& drush_get_context('DRUSH_ANNOTATION_COMMANDS_FOR_COMMANDFILE'); + if (isset($cache[$commandfile_path])) { + return $cache[$commandfile_path]; + } + $factory = \Drush::commandFactory(); + $commands = []; + $commandfile = basename($commandfile_path, '.php'); + + $commandinfo_list = $factory->getCommandInfoListFromClass($commandhandler); + + foreach ($commandinfo_list as $commandinfo) { + $factory->registerCommandHook($commandinfo, $commandhandler); + // Skip anything that is not a command + if (!AnnotatedCommandFactory::isCommandMethod($commandinfo, false)) { + continue; + } + + $aliases = $commandinfo->getAliases(); + $command_name = strtolower($commandinfo->getName()); + $standard_alias = str_replace(':', '-', $command_name); + if ($command_name != $standard_alias) { + $aliases[] = $standard_alias; + } + $handle_remote_commands = $commandinfo->getAnnotation('handle-remote-commands') == 'true'; + // If there is no 'bootstrap' annotation, default to NONE. + $bootstrap = DRUSH_BOOTSTRAP_NONE; + if ($bootstrap = $commandinfo->getAnnotation('bootstrap')) { + $bootstrap = constant($bootstrap); + } + $command = [ + 'name' => $command_name, + //'callback' => [$commandhandler, $commandinfo->getMethodName()], + 'callback' => 'annotationcommand_adapter_process_command', + 'annotated-command-callback' => [$commandhandler, $commandinfo->getMethodName()], + 'commandfile' => $commandfile, + 'category' => $commandfile, + 'options' => [], + 'arguments' => [], + 'description' => $commandinfo->getDescription(), + 'examples' => $commandinfo->getExampleUsages(), + 'bootstrap' => $bootstrap, + 'handle-remote-commands' => $handle_remote_commands, + 'aliases' => $aliases, + 'add-options-to-arguments' => TRUE, + 'consolidation-output-formatters' => TRUE, + 'consolidation-option-defaults' => $commandinfo->options()->getValues(), + 'consolidation-arg-defaults' => $commandinfo->arguments()->getValues(), + ]; + $required_arguments = 0; + foreach ($commandinfo->arguments()->getValues() as $arg => $default) { + $command['arguments'][$arg] = $commandinfo->arguments()->getDescription($arg); + if (!isset($default)) { + ++$required_arguments; + } + } + $command['required-arguments'] = $required_arguments; + foreach ($commandinfo->options()->getValues() as $option => $default) { + $description = $commandinfo->options()->getDescription($option); + $command['options'][$option] = ['description' => $description]; + if (!empty($default)) { + $command['options'][$option]['example-value'] = $default; + } + $fn = 'annotationcommand_adapter_alter_option_description_' . $option; + if (function_exists($fn)) { + $command['options'][$option] = $fn($command['options'][$option], $commandinfo, $default); + } + } + $command['annotations'] = $commandinfo->getAnnotations(); + // If the command has a '@return' annotation, then + // remember information we will need to use the output formatter. + $returnType = $commandinfo->getReturnType(); + if (isset($returnType)) { + $command['return-type'] = $returnType; + } + if ($commandinfo->getAnnotation('allow-additional-options') !== null) { + $command['allow-additional-options'] = TRUE; + } + $command += drush_command_defaults($command_name, $commandfile, $commandfile_path); + $commands[$command_name] = $command; + } + $cache[$commandfile_path] = $commands; + return $commands; +} + +/** + * Modify a $command record, adding option definitions defined by any + * command hook. + * + * @param array $command Drush command record to modify + */ +function annotationcommand_adapter_add_hook_options(&$command) +{ + // Get options added by hooks. We postpone doing this until the + // last minute rather than doing it when processing commandfiles + // so that we do not need to worry about what order we process the + // commandfiles in -- we can load extensions late, and still have + // the extension hook a core command, or have an early-loaded global + // extension hook a late-loaded extension (e.g. attached to a module). + $names = annotationcommand_adapter_command_names($command); + $names[] = '*'; // we are missing annotations here; maybe we just don't support that? (TODO later, maybe) + $factory = \Drush::commandFactory(); + $extraOptions = $factory->hookManager()->getHookOptions($names); + foreach ($extraOptions as $commandinfo) { + if (!isset($command['consolidation-option-defaults'])) { + $command['consolidation-option-defaults'] = array(); + } + $command['consolidation-option-defaults'] += $commandinfo->options()->getValues(); + foreach ($commandinfo->options()->getValues() as $option => $default) { + $description = $commandinfo->options()->getDescription($option); + $command['options'][$option] = ['description' => $description]; + if (!empty($default)) { + $command['options'][$option]['example-value'] = $default; + } + $fn = 'annotationcommand_adapter_alter_option_description_' . $option; + if (function_exists($fn)) { + $command['options'][$option] = $fn($command['options'][$option], $commandinfo, $default); + } + } + } +} + +/** + * Build all of the name variants for a Drush $command record + * + * @param array $command Drush command record + * + * @return string[] + */ +function annotationcommand_adapter_command_names($command) +{ + $names = array_merge( + [$command['command']], + $command['aliases'] + ); + if (!empty($command['annotated-command-callback'])) { + $commandHandler = $command['annotated-command-callback'][0]; + $reflectionClass = new \ReflectionClass($commandHandler); + $commandFileClass = $reflectionClass->getName(); + $names[] = $commandFileClass; + } + return $names; +} + +/** + * Convert from an old-style Drush initialize hook into annotated-command hooks. + * @see _drush_invoke_hooks(). + * + * @param string[] $names All of the applicable names for the command being hooked + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + */ +function annotationcommand_adapter_call_initialize($names, CommandData $commandData) +{ + $factory = \Drush::commandFactory(); + $hookManager = $factory->hookManager(); + + $hooks = $hookManager->getHooks( + $names, + [ + HookManager::PRE_INITIALIZE, + HookManager::INITIALIZE, + HookManager::POST_INITIALIZE, + ], + $commandData->annotationData() + ); + + foreach ((array)$hooks as $hook) { + if (!is_object($hook)) { + $hook($commandData->input(), $commandData->annotationData()); + } + } +} + +/** + * Convert from an old-style Drush pre-validate hook into annotated-command hooks. + * @see _drush_invoke_hooks(). + * + * @param string[] $names All of the applicable names for the command being hooked + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * + * @return boolean|object + */ +function annotationcommand_adapter_call_hook_pre_validate($names, CommandData $commandData) +{ + return annotationcommand_adapter_call_validate_interface( + $names, + [ + HookManager::PRE_ARGUMENT_VALIDATOR, + ], + $commandData + ); +} + +/** + * Convert from an old-style Drush validate hook into annotated-command hooks. + * @see _drush_invoke_hooks(). + * + * @param string[] $names All of the applicable names for the command being hooked + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * + * @return boolean|object + */ +function annotationcommand_adapter_call_hook_validate($names, CommandData $commandData) +{ + return annotationcommand_adapter_call_validate_interface( + $names, + [ + HookManager::ARGUMENT_VALIDATOR, + ], + $commandData + ); +} + +/** + * Convert from an old-style Drush pre-command hook into annotated-command hooks. + * @see _drush_invoke_hooks(). + * + * @param string[] $names All of the applicable names for the command being hooked + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * + * @return boolean|object + */ +function annotationcommand_adapter_call_hook_pre_command($names, CommandData $commandData) +{ + return annotationcommand_adapter_call_validate_interface( + $names, + [ + HookManager::PRE_COMMAND_HOOK, + ], + $commandData + ); +} + +/** + * Convert from an old-style Drush 'command' hook into annotated-command hooks. + * @see _drush_invoke_hooks(). + * + * @param string[] $names All of the applicable names for the command being hooked + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * + * @return boolean|object + */ +function annotationcommand_adapter_call_hook_command($names, CommandData $commandData) +{ + return annotationcommand_adapter_call_validate_interface( + $names, + [ + HookManager::COMMAND_HOOK, + ], + $commandData + ); +} + +/** + * Convert from an old-style Drush post-command hook into annotated-command hooks. + * @see _drush_invoke_hooks(). + * + * @param string[] $names All of the applicable names for the command being hooked + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * @param mixed $return The return value of the command being executed + * + * @return mixed The altered command return value + */ +function annotationcommand_adapter_call_hook_post_command($names, CommandData $commandData, $return) +{ + return annotationcommand_adapter_call_process_interface( + $names, + [ + HookManager::POST_COMMAND_HOOK, + ], + $commandData, + $return + ); +} + +/** + * After the primary Drush command hook is called, call all of the annotated-command + * process and alter hooks. + * @see _drush_invoke_hooks(). + * + * @param string[] $names All of the applicable names for the command being hooked + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * @param mixed $return The return value of the command being executed + * + * @return mixed The altered command return value + */ +function annotationcommand_adapter_call_hook_process_and_alter($names, $commandData, $return) +{ + return annotationcommand_adapter_call_process_interface( + $names, + [ + HookManager::PRE_PROCESS_RESULT, + HookManager::PROCESS_RESULT, + HookManager::POST_PROCESS_RESULT, + HookManager::PRE_ALTER_RESULT, + HookManager::ALTER_RESULT, + HookManager::POST_ALTER_RESULT, + ], + $commandData, + $return + ); +} + +/** + * Given a list of hooks that conform to the interface ProcessResultInterface, + * call them and return the result. + * + * @param string[] $names All of the applicable names for the command being hooked + * @param string[] $hooks All of the HookManager hooks that should be called + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * @param mixed $return The return value of the command being executed + * + * @return mixed The altered command return value + */ +function annotationcommand_adapter_call_process_interface($names, $hooks, CommandData $commandData, $return) +{ + $factory = \Drush::commandFactory(); + $hookManager = $factory->hookManager(); + + $hooks = $hookManager->getHooks($names, $hooks, $commandData->annotationData()); + + foreach ((array)$hooks as $hook) { + // @todo: $hook might be a ProcessResultInterface object. Support those? + if (is_object($hook)) { + continue; + } + $result = $hook($return, $commandData); + if (isset($result)) { + $return = $result; + } + } + return $return; +} + +/** + * Given a list of hooks that conform to the interface ValidatorInterface, + * call them and return the result. + * + * @param string[] $names All of the applicable names for the command being hooked + * @param string[] $hooks All of the HookManager hooks that should be called + * @param CommandData $commandData All of the parameter data associated with the + * current command invokation, including the InputInterface, OutputInterface + * and AnnotationData + * + * @return boolean|object + */ +function annotationcommand_adapter_call_validate_interface($names, $hooks, CommandData $commandData) +{ + $factory = \Drush::commandFactory(); + $hookManager = $factory->hookManager(); + $annotationData = $commandData->annotationData(); + + $hooks = $hookManager->getHooks($names, $hooks, $annotationData); + + foreach ((array)$hooks as $hook) { + // @todo: $hook might be a ValidatorInterface object. Support those? + if (is_object($hook)) { + continue; + } + $validated = $hook($commandData); + // TODO: if $validated is a CommandError, maybe the best thing to do is 'return drush_set_error()'? + if (is_object($validated) || ($validated === false)) { + return $validated; + } + } + return true; +} + +/** + * TODO: Document + */ +function annotationcommand_adapter_alter_option_description_format($option_help, $commandinfo, $default) { + $formatterManager = annotatedcomand_adapter_get_formatter(); + $return_type = $commandinfo->getReturnType(); + if (!empty($return_type)) { + $available_formats = $formatterManager->validFormats($return_type); + $option_help['description'] = dt('Select output format. Available: !formats.', array('!formats' => implode(', ', $available_formats))); + if (!empty($default)) { + $option_help['description'] .= dt(' Default is !default.', array('!default' => $default)); + } + } + return $option_help; +} + +/** + * TODO: Document + */ +function annotationcommand_adapter_alter_option_description_fields($option_help, $commandinfo, $default) { + $formatOptions = new FormatterOptions($commandinfo->getAnnotations()->getArrayCopy()); + $field_labels = $formatOptions->get(FormatterOptions::FIELD_LABELS); + $default_fields = $formatOptions->get(FormatterOptions::DEFAULT_FIELDS, [], array_keys($field_labels)); + $available_fields = array_keys($field_labels); + $option_help['example-value'] = implode(', ', $default_fields); + $option_help['description'] = dt('Fields to output. All available fields are: !available.', array('!available' => implode(', ', $available_fields))); + return $option_help; +} + +/** + * In some circumstances, Drush just does a deep search for any *.drush.inc + * file, so that it can find all commands, in enabled and disabled modules alike, + * for the purpose of displaying the help text for that command. + */ +function annotationcommand_adapter_refine_searchpaths($searchpath) { + $result = []; + foreach ($searchpath as $path) { + $max_depth = TRUE; + $pattern = '/.*\.info$/'; + if (drush_drupal_major_version() > 7) { + $pattern = '/.*\.info.yml$/'; + } + $locations = drush_scan_directory($path, $pattern, ['.', '..'], false, $max_depth); + + // Search for any directory that might be a module or theme (contains + // a *.info or a *.info.yml file) + foreach ($locations as $key => $info) { + $result[dirname($key)] = true; + } + } + return array_keys($result); +} diff --git a/includes/command.inc b/includes/command.inc index e886694866..a82b2e38aa 100644 --- a/includes/command.inc +++ b/includes/command.inc @@ -2,6 +2,10 @@ use Drush\Log\LogLevel; use Webmozart\PathUtil\Path; +use Consolidation\AnnotatedCommand\AnnotationData; +use Drush\Command\DrushInputAdapter; +use Drush\Command\DrushOutputAdapter; +use Consolidation\AnnotatedCommand\CommandData; /** * @defgroup dispatching Command dispatching functions. @@ -160,6 +164,11 @@ function drush_dispatch($command, $arguments = array()) { _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); @@ -182,8 +191,12 @@ function drush_dispatch($command, $arguments = array()) { // 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'], $command['arguments']); + $return = call_user_func_array($command['callback'], $args); } // Add a final log entry, just so a timestamp appears. @@ -291,6 +304,7 @@ function drush_command() { * @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); @@ -317,14 +331,55 @@ function _drush_invoke_hooks($command, $args) { } } + // We will adapt and call as many of the annotated command hooks as we can. + // The following command hooks are not presently supported in the legacy dispatcher: + // - 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($hook . "_pre_validate", $hook . "_validate", "pre_$hook", $hook, "post_$hook"); - foreach ($variations as $var_hook) { + $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 @@ -394,6 +449,7 @@ function _drush_invoke_hooks($command, $args) { 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); } } @@ -541,6 +597,7 @@ function drush_handle_command_output($command, $structured_output) { * 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. @@ -1057,18 +1114,20 @@ function drush_get_commands($reset = FALSE) { } $commands[$key] = $command; - // For every alias, make a copy of the command and store it in the command list - // using the alias as a key - if (isset($command['aliases']) && count($command['aliases'])) { - foreach ($command['aliases'] as $alias) { - $commands[$alias] = $command; - $commands[$alias]['is_alias'] = TRUE; - } - } } } } - + $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; } @@ -1201,6 +1260,10 @@ function drush_command_defaults($key, $commandfile, $path) { '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 @@ -1518,6 +1581,7 @@ function _drush_find_commandfiles($phase, $phase_max = FALSE) { if ($bootstrap = \Drush::bootstrap()) { $searchpath = $bootstrap->commandfile_searchpaths($phase, $phase_max); _drush_add_commandfiles($searchpath, $phase); + annotationcommand_adapter_discover($searchpath, $phase, $phase_max); } } diff --git a/includes/complete.inc b/includes/complete.inc index 189a27d0ed..ef893bc521 100644 --- a/includes/complete.inc +++ b/includes/complete.inc @@ -1,6 +1,7 @@ terminate(); } drush_postflight(); + if (is_object($return)) { + $return = 0; + } // How strict are we? If we are very strict, turn 'ok' into 'error' // if there are any warnings in the log. @@ -160,6 +163,7 @@ function drush_preflight_prepare() { require_once DRUSH_BASE_PATH . '/includes/startup.inc'; require_once DRUSH_BASE_PATH . '/includes/bootstrap.inc'; require_once DRUSH_BASE_PATH . '/includes/environment.inc'; + require_once DRUSH_BASE_PATH . '/includes/annotationcommand_adapter.inc'; require_once DRUSH_BASE_PATH . '/includes/command.inc'; require_once DRUSH_BASE_PATH . '/includes/drush.inc'; require_once DRUSH_BASE_PATH . '/includes/engines.inc'; @@ -265,7 +269,7 @@ function drush_init_dependency_injection_container($input = null, $output = null ->withMethodCall('setLogOutputStyler', ['logStyler']); // Override Robo's formatter manager with our own - // Placeholder: not sure that we'll use this. + // @todo not sure that we'll use this. Maybe remove it. $container->share('formatterManager', \Drush\Formatters\DrushFormatterManager::class) ->withMethodCall('addDefaultFormatters', []) ->withMethodCall('addDefaultSimplifiers', []); @@ -295,6 +299,11 @@ function drush_init_dependency_injection_container($input = null, $output = null // Add our own callback to the hook manager $hookManager = $container->get('hookManager'); $hookManager->addOutputExtractor(new Drush\Backend\BackendResultSetter()); + // @todo: do we need both backend result setters? The one below should be removed at some point. + $hookManager->add('annotatedcomand_adapter_backend_result', HookManager::EXTRACT_OUTPUT); + + $factory = $container->get('commandFactory'); + $factory->setIncludeAllPublicMethods(false); // It is necessary to set the dispatcher when using configureContainer $eventDispatcher = $container->get('eventDispatcher'); @@ -743,6 +752,12 @@ function _drush_find_commandfiles_drush() { // @todo the zero parameter is a bit weird here. It's $phase. _drush_add_commandfiles($searchpath, 0); + + // Also discover Drush's own annotation commands. + $discovery = annotationcommand_adapter_get_discovery(); + $discovery->addSearchLocation('CommandFiles')->setSearchPattern('#.*(Commands|CommandFile).php$#'); + $annotation_commandfiles = $discovery->discover(DRUSH_BASE_PATH . '/lib/Drush', '\Drush'); + drush_set_context('DRUSH_ANNOTATED_COMMANDFILES', $annotation_commandfiles); } /** diff --git a/lib/Drush.php b/lib/Drush.php index d95d305dc4..c31df76265 100644 --- a/lib/Drush.php +++ b/lib/Drush.php @@ -52,6 +52,13 @@ class Drush { */ protected static $container; + /** + * The Robo Runner -- manages and constructs all commandfile classes + * + * @var \Robo\Runner + */ + protected static $runner; + /** * Return the current Drush version. * @@ -125,6 +132,18 @@ public static function hasContainer() { return static::$container !== NULL; } + /** + * Return the Robo runner. + * + * @return \Robo\Runner + */ + public static function runner() { + if (!isset(static::$runner)) { + static::$runner = new \Robo\Runner(); + } + return static::$runner; + } + /** * Retrieves a service from the container. * @@ -156,6 +175,15 @@ public static function hasService($id) { return static::hasContainer() && static::getContainer()->has($id); } + /** + * Return command factory + * + * @return \Consolidation\AnnotatedCommand\AnnotatedCommandFactory + */ + public static function commandFactory() { + return static::service('commandFactory'); + } + /** * Return the Drush logger object. * diff --git a/lib/Drush/Boot/DrupalBoot8.php b/lib/Drush/Boot/DrupalBoot8.php index 36b869da38..7feb23a1d0 100644 --- a/lib/Drush/Boot/DrupalBoot8.php +++ b/lib/Drush/Boot/DrupalBoot8.php @@ -168,7 +168,7 @@ function bootstrap_drupal_full() { foreach ($serviceCommandlist->getCommandList() as $command) { if (!$this->commandIgnored($command, $ignored_modules)) { drush_log(dt('Add a command: !name', ['!name' => $command->getName()]), LogLevel::DEBUG); - drush_add_command_to_application(\Drush::getContainer(), $command); + annotationcommand_adapter_cache_module_console_commands($command); } } // Do the same thing with the annotation commands. @@ -176,7 +176,7 @@ function bootstrap_drupal_full() { foreach ($serviceCommandlist->getCommandList() as $commandhandler) { if (!$this->commandIgnored($commandhandler, $ignored_modules)) { drush_log(dt('Add a commandhandler: !name', ['!name' => get_class($commandhandler)]), LogLevel::DEBUG); - drush_create_commands_from_command_instance(\Drush::getContainer(), $commandhandler); + annotationcommand_adapter_cache_module_service_commands($commandhandler); } } } diff --git a/lib/Drush/Command/DrushInputAdapter.php b/lib/Drush/Command/DrushInputAdapter.php new file mode 100644 index 0000000000..29126f19a0 --- /dev/null +++ b/lib/Drush/Command/DrushInputAdapter.php @@ -0,0 +1,186 @@ +arguments = $arguments; + $this->options = $options; + + // If a command name is provided as a parameter, then push + // it onto the front of the arguments list as a service + if ($command) { + $this->arguments = array_merge( + [ 'command' => $command ], + $this->arguments + ); + } + // Is it interactive, or is it not interactive? + // Call drush_get_option() here if value not passed in? + $this->interactive = $interactive; + } + + /** + * {@inheritdoc} + */ + public function getFirstArgument() + { + return reset($arguments); + } + + /** + * {@inheritdoc} + */ + public function hasParameterOption($values) + { + $values = (array) $values; + + foreach ($values as $value) { + if (array_key_exists($value, $this->options)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getParameterOption($values, $default = false) + { + $values = (array) $values; + + foreach ($values as $value) { + if (array_key_exists($value, $this->options)) { + return $this->getOption($value); + } + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function bind(InputDefinition $definition) + { + // no-op: this class exists to avoid validation + } + + /** + * {@inheritdoc} + */ + public function validate() + { + // no-op: this class exists to avoid validation + } + + /** + * {@inheritdoc} + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * {@inheritdoc} + */ + public function getArgument($name) + { + // TODO: better to throw if an argument that does not exist is requested? + return isset($this->arguments[$name]) ? $this->arguments[$name] : ''; + } + + /** + * {@inheritdoc} + */ + public function setArgument($name, $value) + { + $this->arguments[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function hasArgument($name) + { + return isset($this->arguments[$name]); + } + + /** + * {@inheritdoc} + */ + public function getOptions() + { + return $this->options; + } + + /** + * {@inheritdoc} + */ + public function getOption($name) + { + return $this->options[$name]; + } + + /** + * {@inheritdoc} + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * {@inheritdoc} + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * {@inheritdoc} + */ + public function setInteractive($interactive) + { + $this->interactive = $interactive; + } +} diff --git a/lib/Drush/Command/DrushOutputAdapter.php b/lib/Drush/Command/DrushOutputAdapter.php new file mode 100644 index 0000000000..5d2101bf06 --- /dev/null +++ b/lib/Drush/Command/DrushOutputAdapter.php @@ -0,0 +1,25 @@ +title;' @@ -24,6 +25,8 @@ class EvalCommandFile * @usage php-eval "node_access_rebuild();" * Rebuild node access permissions. * @default-format var_export + * @bootstrap DRUSH_BOOTSTRAP_MAX + * @allow-additional-options true */ public function phpEval($php, $options = [ diff --git a/lib/Drush/CommandFiles/ExampleCommandFile.php b/lib/Drush/CommandFiles/ExampleCommandFile.php index ec764dd6f4..561cef8b54 100644 --- a/lib/Drush/CommandFiles/ExampleCommandFile.php +++ b/lib/Drush/CommandFiles/ExampleCommandFile.php @@ -17,6 +17,7 @@ class ExampleCommandFile /** * Demonstrate Robo formatters. Default format is 'table'. * + * @command example-table * @field-labels * first: I * second: II @@ -44,7 +45,7 @@ public function exampleTable($options = ['format' => 'table', 'fields' => '']) /** * Demonstrate an alter hook with an option * - * @hook alter example:table + * @hook alter example-table * @option french Add a row with French numbers. * @usage example:formatters --french */ diff --git a/lib/Drush/CommandFiles/InitCommandFile.php b/lib/Drush/CommandFiles/InitCommandFile.php index edc84c1766..7968465ff8 100644 --- a/lib/Drush/CommandFiles/InitCommandFile.php +++ b/lib/Drush/CommandFiles/InitCommandFile.php @@ -13,6 +13,8 @@ class InitCommandFile extends \Robo\Tasks /** * Initialize local Drush configuration * + * @command core-init + * * Enrich the bash startup file with completion and aliases. * Copy .drushrc file to ~/.drush * @@ -23,7 +25,7 @@ class InitCommandFile extends \Robo\Tasks * file, even if it is already in the \$PATH. Use --no-add-path to skip * updating .bashrc with the Drush \$PATH. Default is to update .bashrc * only if Drush is not already in the \$PATH. - * @aliases core-init, init + * @aliases init * @usage core-init --edit * Enrich Bash and open drush config file in editor. * @usage core-init --edit --bg diff --git a/lib/Drush/CommandFiles/core/BrowseCommands.php b/lib/Drush/CommandFiles/core/BrowseCommands.php index b990ab6c4f..62baa837f9 100644 --- a/lib/Drush/CommandFiles/core/BrowseCommands.php +++ b/lib/Drush/CommandFiles/core/BrowseCommands.php @@ -6,6 +6,7 @@ class BrowseCommands { /** * Display a link to a given path or open link in a browser. * + * @command browse * @todo Document new @handle-remote-commands and @bootstrap annotations. * * @param string|null $path Path to open. If omitted, the site front page will be opened. @@ -29,7 +30,7 @@ public function browse($path = '', $options = ['browser' => NULL]) { $alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS'); if (drush_sitealias_is_remote_site($alias)) { $site_record = drush_sitealias_get_record($alias); - $return = drush_invoke_process($site_record, 'browse', func_get_args(), drush_redispatch_get_options(), array('integrate' => TRUE)); + $return = drush_invoke_process($site_record, 'browse', [$path], drush_redispatch_get_options(), array('integrate' => TRUE)); if ($return['error_status']) { return drush_set_error('Unable to execute browse command on remote alias.'); } @@ -56,4 +57,4 @@ public function browse($path = '', $options = ['browser' => NULL]) { static function complete() { return ['values' => ['admin', 'admin/content', 'admin/reports', 'admin/structure', 'admin/people', 'admin/modules', 'admin/config']]; } -} \ No newline at end of file +} diff --git a/lib/Drush/CommandFiles/core/DrupliconCommands.php b/lib/Drush/CommandFiles/core/DrupliconCommands.php index 4e58fff794..c2bfde5cb8 100644 --- a/lib/Drush/CommandFiles/core/DrupliconCommands.php +++ b/lib/Drush/CommandFiles/core/DrupliconCommands.php @@ -27,8 +27,8 @@ public function druplicon($result, CommandData $commandData) { if ($commandName == 'helpsingle') { return; } - drush_log(dt('Displaying Druplicon for "!command" command.', array('!command' => $commandName))); if ($commandData->input()->getOption('druplicon')) { + drush_log(dt('Displaying Druplicon for "!command" command.', array('!command' => $commandName))); $misc_dir = DRUSH_BASE_PATH . '/misc'; if (drush_get_context('DRUSH_NOCOLOR')) { $content = file_get_contents($misc_dir . '/druplicon-no_color.txt'); @@ -36,6 +36,7 @@ public function druplicon($result, CommandData $commandData) { else { $content = file_get_contents($misc_dir . '/druplicon-color.txt'); } + // @todo: `$commandData->output->writeln($content)` after $output hooked up to backend invoke drush_print($content); } } diff --git a/lib/Drush/Sql/SqlBase.php b/lib/Drush/Sql/SqlBase.php index 8d5da32307..29be578435 100644 --- a/lib/Drush/Sql/SqlBase.php +++ b/lib/Drush/Sql/SqlBase.php @@ -247,7 +247,8 @@ public function createdb_sql($dbname, $quoted = FALSE) {} * Create a new database. * * @param boolean $quoted - * Quote the database name. + * Quote the database name. Mysql uses backticks to quote which can cause problems + * in a Windows shell. Set TRUE if the CREATE is not running on the bash command line. * @return boolean * True if successful, FALSE otherwise. */ diff --git a/lib/Drush/UpdateService/Project.php b/lib/Drush/UpdateService/Project.php index c83e5b5819..0d17b1bf30 100644 --- a/lib/Drush/UpdateService/Project.php +++ b/lib/Drush/UpdateService/Project.php @@ -305,7 +305,7 @@ public function getInfo() { * * @return array|bool */ - private static function getBestRelease(array $releases) { + public static function getBestRelease(array $releases) { if (empty($releases)) { return FALSE; } diff --git a/tests/annotatedCommandTest.php b/tests/annotatedCommandTest.php index 73afe0f9bc..47a6e83276 100644 --- a/tests/annotatedCommandTest.php +++ b/tests/annotatedCommandTest.php @@ -19,21 +19,35 @@ public function testExecute() { // Copy the 'woot' module over to the Drupal site we just set up. $this->setupModulesForTests($root); + // These are not good asserts, but for the purposes of isolation.... + $targetDir = $root . DIRECTORY_SEPARATOR . $this->drupalSitewideDirectory() . '/modules/woot'; + if (UNISH_DRUPAL_MAJOR_VERSION == 8) { + $commandFile = $targetDir . "/src/Command/WootCommands.php"; + } else { + $commandFile = $targetDir . "/Command/WootCommands.php"; + } + $this->assertFileExists(dirname(dirname(dirname($commandFile)))); + $this->assertFileExists(dirname(dirname($commandFile))); + $this->assertFileExists(dirname($commandFile)); + $this->assertFileExists($commandFile); + // Enable out module. This will also clear the commandfile cache. $this->drush('pm-enable', array('woot'), $options); + // In theory this is not necessary, but this test keeps failing. + $this->drush('cc', array('drush'), $options); + // drush woot --help $this->drush('woot', array(), $options + ['help' => NULL]); $output = $this->getOutput(); $this->assertContains('Woot mightily.', $output); - // TODO: Symfony Console does not print alias info like this - // $this->assertContains('Aliases: wt', $output); - // drush help woot. TODO: drush help does not find annotated commands yet -// $this->drush('help', array('woot'), $options); -// $output = $this->getOutput(); -// $this->assertContains('Woot mightily.', $output); + $this->assertContains('Aliases: wt', $output); + // drush help woot + $this->drush('help', array('woot'), $options); + $output = $this->getOutput(); + $this->assertContains('Woot mightily.', $output); // drush woot $this->drush('woot', array(), $options); @@ -43,20 +57,17 @@ public function testExecute() { // drush my-cat --help $this->drush('my-cat', array(), $options + ['help' => NULL]); $output = $this->getOutput(); - // TODO: the command description does not appear in the help text yet - //$this->assertContains('This is the my-cat command', $output); + $this->assertContains('This is the my-cat command', $output); $this->assertContains('bet alpha --flip', $output); $this->assertContains('The first parameter', $output); $this->assertContains('The other parameter', $output); $this->assertContains('Whether or not the second parameter', $output); - // TODO: Symfony Console does not print alias info like this - // $this->assertContains('Aliases: c', $output); + $this->assertContains('Aliases: c', $output); // drush help my-cat - // TODO: help cannot find annotated commands yet - //$this->drush('help', array('my-cat'), $options); - //$output = $this->getOutput(); - //$this->assertContains('This is the my-cat command', $output); + $this->drush('help', array('my-cat'), $options); + $output = $this->getOutput(); + $this->assertContains('This is the my-cat command', $output); // drush my-cat bet alpha --flip $this->drush('my-cat', array('bet', 'alpha'), $options + ['flip' => NULL]); @@ -112,17 +123,14 @@ public function testExecute() { // drush try-formatters --help $this->drush('try-formatters', array(), $options + ['help' => NULL], NULL, NULL, self::EXIT_SUCCESS); $output = $this->getOutput(); - // TODO: Command description does not appear in help text yet - //$this->assertContains('Demonstrate formatters', $output); + $this->assertContains('Demonstrate formatters', $output); $this->assertContains('try:formatters --fields=first,third', $output); $this->assertContains('try:formatters --fields=III,II', $output); - // TODO: Help for formats and fields not available yet - //$this->assertContains('--fields=', $output); - //$this->assertContains('Fields to output. All available', $output); - //$this->assertContains('--format=', $output); - //$this->assertContains('Select output format. Available:', $output); - // TODO: Symfony Console does not print alias info like this - // $this->assertContains('Aliases: try-formatters', $output); + $this->assertContains('--fields=', $output); + $this->assertContains('Fields to output. All available', $output); + $this->assertContains('--format=
', $output); + $this->assertContains('Select output format. Available:', $output); + $this->assertContains('Aliases: try-formatters', $output); // If we are running Drupal version 8 or later, then also check to // see if the demo:greet and annotated:greet commands are available. diff --git a/tests/drushScriptTest.php b/tests/drushScriptTest.php index f65e921d02..17daa2887b 100644 --- a/tests/drushScriptTest.php +++ b/tests/drushScriptTest.php @@ -66,9 +66,9 @@ public function testDrushFinder() { $drush_location = $this->getDrushLocation(array('root' => $this->webroot())); $this->assertEquals(realpath($drush_root . '/drush.php'), realpath($drush_location)); // Test to see if --local was added - $result = $this->drush('ev', array('return drush_get_option("local");'), array('root' => $this->webroot())); + $result = $this->drush('ev', array('var_export(drush_get_option("local"));'), array('root' => $this->webroot())); $output = $this->getOutput(); - $this->assertEquals("TRUE", $output); + $this->assertEquals("true", $output); // Get rid of the symlink and site-local Drush we created $this->remove_site_local_drush($drush_base); diff --git a/tests/resources/modules/d6/woot/Command/WootCommands.php b/tests/resources/modules/d6/woot/Command/WootCommands.php new file mode 100644 index 0000000000..770fbe2bcd --- /dev/null +++ b/tests/resources/modules/d6/woot/Command/WootCommands.php @@ -0,0 +1,66 @@ + false]) + { + if ($options['flip']) { + return "{$two}{$one}"; + } + return "{$one}{$two}"; + } + + /** + * Demonstrate formatters. Default format is 'table'. + * + * @field-labels + * first: I + * second: II + * third: III + * @usage try:formatters --format=yaml + * @usage try:formatters --format=csv + * @usage try:formatters --fields=first,third + * @usage try:formatters --fields=III,II + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields + */ + public function tryFormatters($options = ['format' => 'table', 'fields' => '']) + { + $outputData = [ + 'en' => [ 'first' => 'One', 'second' => 'Two', 'third' => 'Three' ], + 'de' => [ 'first' => 'Eins', 'second' => 'Zwei', 'third' => 'Drei' ], + 'jp' => [ 'first' => 'Ichi', 'second' => 'Ni', 'third' => 'San' ], + 'es' => [ 'first' => 'Uno', 'second' => 'Dos', 'third' => 'Tres' ], + ]; + return new RowsOfFields($outputData); + } +} diff --git a/tests/resources/modules/d6/woot/woot.info b/tests/resources/modules/d6/woot/woot.info new file mode 100644 index 0000000000..45f4a3ccb2 --- /dev/null +++ b/tests/resources/modules/d6/woot/woot.info @@ -0,0 +1,4 @@ +name = woot +description = Woot Mightily +core = 6.x +files[] = woot.module diff --git a/tests/resources/modules/d6/woot/woot.module b/tests/resources/modules/d6/woot/woot.module new file mode 100644 index 0000000000..f51b34a5be --- /dev/null +++ b/tests/resources/modules/d6/woot/woot.module @@ -0,0 +1,22 @@ + 'Woot', + 'description' => 'Woot mightily.', + 'page callback' => 'woot_page', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +function woot_page() { + return array('#markup' => 'Woot!'); +} diff --git a/tests/resources/modules/d7/woot/Command/WootCommands.php b/tests/resources/modules/d7/woot/Command/WootCommands.php index 0f2cdfe882..b923d651b9 100644 --- a/tests/resources/modules/d7/woot/Command/WootCommands.php +++ b/tests/resources/modules/d7/woot/Command/WootCommands.php @@ -12,6 +12,7 @@ class WootCommands /** * Woot mightily. * + * @command woot * @aliases wt */ public function woot() @@ -25,6 +26,7 @@ public function woot() * This command will concatinate two parameters. If the --flip flag * is provided, then the result is the concatination of two and one. * + * @command my-cat * @param string $one The first parameter. * @param string $two The other parameter. * @option boolean $flip Whether or not the second parameter should come first in the result. @@ -43,6 +45,7 @@ public function myCat($one, $two = '', $options = ['flip' => false]) /** * Demonstrate formatters. Default format is 'table'. * + * @command try:formatters * @field-labels * first: I * second: II @@ -51,7 +54,7 @@ public function myCat($one, $two = '', $options = ['flip' => false]) * @usage try:formatters --format=csv * @usage try:formatters --fields=first,third * @usage try:formatters --fields=III,II - * @return Consolidation\OutputFormatters\StructuredData\RowsOfFields + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields */ public function tryFormatters($options = ['format' => 'table', 'fields' => '']) { diff --git a/tests/resources/modules/d8/woot/src/Command/WootCommands.php b/tests/resources/modules/d8/woot/src/Command/WootCommands.php index 0f2cdfe882..b923d651b9 100644 --- a/tests/resources/modules/d8/woot/src/Command/WootCommands.php +++ b/tests/resources/modules/d8/woot/src/Command/WootCommands.php @@ -12,6 +12,7 @@ class WootCommands /** * Woot mightily. * + * @command woot * @aliases wt */ public function woot() @@ -25,6 +26,7 @@ public function woot() * This command will concatinate two parameters. If the --flip flag * is provided, then the result is the concatination of two and one. * + * @command my-cat * @param string $one The first parameter. * @param string $two The other parameter. * @option boolean $flip Whether or not the second parameter should come first in the result. @@ -43,6 +45,7 @@ public function myCat($one, $two = '', $options = ['flip' => false]) /** * Demonstrate formatters. Default format is 'table'. * + * @command try:formatters * @field-labels * first: I * second: II @@ -51,7 +54,7 @@ public function myCat($one, $two = '', $options = ['flip' => false]) * @usage try:formatters --format=csv * @usage try:formatters --fields=first,third * @usage try:formatters --fields=III,II - * @return Consolidation\OutputFormatters\StructuredData\RowsOfFields + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields */ public function tryFormatters($options = ['format' => 'table', 'fields' => '']) { diff --git a/tests/userTest.php b/tests/userTest.php index 1cdd30614a..b17a3b4087 100644 --- a/tests/userTest.php +++ b/tests/userTest.php @@ -74,7 +74,7 @@ function testUserPassword() { } $this->drush('php-eval', array($eval), $this->options()); $output = $this->getOutput(); - $this->assertEquals("'2'", $output, 'User can login with new password.'); + $this->assertEquals("2", $output, 'User can login with new password.'); } function testUserLogin() {