--- /dev/null
+<?php
+namespace Drush\Preflight;
+
+use Consolidation\Config\Config;
+use Consolidation\Config\ConfigInterface;
+
+use Drush\Symfony\DrushArgvInput;
+use Drush\Utils\StringUtils;
+use Drush\Symfony\LessStrictArgvInput;
+
+/**
+ * Storage for arguments preprocessed during preflight.
+ *
+ * Holds @sitealias, if present, and a limited number of global options.
+ *
+ * TODO: The methods here with >~3 lines of logic could be refactored into a couple
+ * of different classes e.g. a helper to convert preflight args to configuration,
+ * and another to prepare the input object.
+ */
+class PreflightArgs extends Config implements PreflightArgsInterface
+{
+ /**
+ * @var array $args Remaining arguments not handled by the preprocessor
+ */
+ protected $args;
+
+ /**
+ * @var string $homeDir Path to directory to use when replacing ~ in paths
+ */
+ protected $homeDir;
+
+ /**
+ * @return string
+ */
+ public function homeDir()
+ {
+ return $this->homeDir;
+ }
+
+ /**
+ * @param string $homeDir
+ */
+ public function setHomeDir($homeDir)
+ {
+ $this->homeDir = $homeDir;
+ }
+
+ const DRUSH_CONFIG_PATH_NAMESPACE = 'drush.paths';
+ const DRUSH_RUNTIME_CONTEXT_NAMESPACE = 'runtime.contxt';
+ const ALIAS = 'alias';
+ const ALIAS_PATH = 'alias-path';
+ const COMMAND_PATH = 'include';
+ const CONFIG_PATH = 'config';
+ const COVERAGE_FILE = 'coverage-file';
+ const LOCAL = 'local';
+ const ROOT = 'root';
+ const URI = 'uri';
+ const SIMULATE = 'simulate';
+ const BACKEND = 'backend';
+ const STRICT = 'strict';
+ const DEBUG = 'preflight-debug';
+
+ /**
+ * PreflightArgs constructor
+ *
+ * @param array $data Initial data (not usually used)
+ */
+ public function __construct($data = [])
+ {
+ parent::__construct($data + [self::STRICT => true]);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function optionsWithValues()
+ {
+ return [
+ '-r=' => 'setSelectedSite',
+ '--root=' => 'setSelectedSite',
+ '--debug' => 'setDebug',
+ '-d' => 'setDebug',
+ '-vv' => 'setDebug',
+ '-vvv' => 'setDebug',
+ '-l=' => 'setUri',
+ '--uri=' => 'setUri',
+ '-c=' => 'addConfigPath',
+ '--config=' => 'addConfigPath',
+ '--alias-path=' => 'addAliasPath',
+ '--include=' => 'addCommandPath',
+ '--local' => 'setLocal',
+ '--simulate' => 'setSimulate',
+ '-s' => 'setSimulate',
+ '--backend' => 'setBackend',
+ '--drush-coverage=' => 'setCoverageFile',
+ '--strict=' => 'setStrict',
+ '--help' => 'adjustHelpOption',
+ '-h' => 'adjustHelpOption',
+ ];
+ }
+
+ /**
+ * If the user enters '--help' or '-h', thrown that
+ * option away and add a 'help' command to the beginning
+ * of the argument list.
+ */
+ public function adjustHelpOption()
+ {
+ $drushPath = array_shift($this->args);
+ array_unshift($this->args, $drushPath, 'help');
+ }
+
+ /**
+ * Map of option key to the corresponding config key to store the
+ * preflight option in.
+ */
+ protected function optionConfigMap()
+ {
+ return [
+ self::SIMULATE => \Robo\Config\Config::SIMULATE,
+ self::BACKEND => self::BACKEND,
+ self::LOCAL => self::DRUSH_RUNTIME_CONTEXT_NAMESPACE . '.' . self::LOCAL,
+ ];
+ }
+
+ /**
+ * Map of path option keys to the corresponding config key to store the
+ * preflight option in.
+ */
+ protected function optionConfigPathMap()
+ {
+ return [
+ self::ALIAS_PATH => self::DRUSH_CONFIG_PATH_NAMESPACE . '.' . self::ALIAS_PATH,
+ self::CONFIG_PATH => self::DRUSH_CONFIG_PATH_NAMESPACE . '.' . self::CONFIG_PATH,
+ self::COMMAND_PATH => self::DRUSH_CONFIG_PATH_NAMESPACE . '.' . self::COMMAND_PATH,
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @see Environment::exportConfigData(), which also exports information to config.
+ */
+ public function applyToConfig(ConfigInterface $config)
+ {
+ // Copy the relevant preflight options to the applicable configuration namespace
+ foreach ($this->optionConfigMap() as $option_key => $config_key) {
+ $config->set($config_key, $this->get($option_key));
+ }
+ // Merging as they are lists.
+ foreach ($this->optionConfigPathMap() as $option_key => $config_key) {
+ $cli_paths = $this->get($option_key, []);
+ $config_paths = (array) $config->get($config_key, []);
+ $merged_paths = array_merge($cli_paths, $config_paths);
+ $config->set($config_key, $merged_paths);
+ $this->set($option_key, $merged_paths);
+ }
+
+ // Store the runtime arguments and options (sans the runtime context items)
+ // in runtime.argv et. al.
+ $config->set('runtime.argv', $this->args());
+ $config->set('runtime.options', $this->getOptionNameList($this->args()));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function args()
+ {
+ return $this->args;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applicationPath()
+ {
+ return reset($this->args);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function addArg($arg)
+ {
+ $this->args[] = $arg;
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function passArgs($args)
+ {
+ $this->args = array_merge($this->args, $args);
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function alias()
+ {
+ return $this->get(self::ALIAS);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hasAlias()
+ {
+ return $this->has(self::ALIAS);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setAlias($alias)
+ {
+ // Treat `drush @self ...` as if an alias had not been used at all.
+ if ($alias == '@self') {
+ $alias = '';
+ }
+ return $this->set(self::ALIAS, $alias);
+ }
+
+ /**
+ * Get the selected site. Here, the default will typically be the cwd.
+ */
+ public function selectedSite($default = false)
+ {
+ return $this->get(self::ROOT, $default);
+ }
+
+ public function setDebug($value)
+ {
+ $this->set(self::DEBUG, $value);
+ $this->addArg('-vvv');
+ }
+
+ /**
+ * Set the selected site.
+ */
+ public function setSelectedSite($root)
+ {
+ return $this->set(self::ROOT, StringUtils::replaceTilde($root, $this->homeDir()));
+ }
+
+ /**
+ * Get the selected uri
+ */
+ public function uri($default = false)
+ {
+ return $this->get(self::URI, $default);
+ }
+
+ /**
+ * Set the uri option
+ */
+ public function setUri($uri)
+ {
+ return $this->set(self::URI, $uri);
+ }
+
+ /**
+ * Get the config path where drush.yml files may be found
+ */
+ public function configPaths()
+ {
+ return $this->get(self::CONFIG_PATH, []);
+ }
+
+ /**
+ * Add another location where drush.yml files may be found
+ *
+ * @param string $path
+ */
+ public function addConfigPath($path)
+ {
+ $paths = $this->configPaths();
+ $paths[] = StringUtils::replaceTilde($path, $this->homeDir());
+ return $this->set(self::CONFIG_PATH, $paths);
+ }
+
+ /**
+ * Add multiple additional locations where drush.yml files may be found.
+ *
+ * @param string[] $configPaths
+ */
+ public function mergeConfigPaths($configPaths)
+ {
+ $paths = $this->configPaths();
+ $merged_paths = array_merge($paths, $configPaths);
+ return $this->set(self::CONFIG_PATH, $merged_paths);
+ }
+
+ /**
+ * Get the alias paths where drush site.site.yml files may be found
+ */
+ public function aliasPaths()
+ {
+ return $this->get(self::ALIAS_PATH, []);
+ }
+
+ /**
+ * Set one more path where aliases may be found.
+ *
+ * @param string $path
+ */
+ public function addAliasPath($path)
+ {
+ $paths = $this->aliasPaths();
+ $paths[] = StringUtils::replaceTilde($path, $this->homeDir());
+ return $this->set(self::ALIAS_PATH, $paths);
+ }
+
+ /**
+ * Add multiple additional locations for alias paths.
+ *
+ * @param string $aliasPaths
+ */
+ public function mergeAliasPaths($aliasPaths)
+ {
+ $paths = $this->aliasPaths();
+ $merged_paths = array_merge($paths, $aliasPaths);
+ return $this->set(self::ALIAS_PATH, $merged_paths);
+ }
+
+ /**
+ * Get the path where Drush commandfiles e.g. FooCommands.php may be found.
+ */
+ public function commandPaths()
+ {
+ return $this->get(self::COMMAND_PATH, []);
+ }
+
+ /**
+ * Add one more path where commandfiles might be found.
+ *
+ * @param string $path
+ */
+ public function addCommandPath($path)
+ {
+ $paths = $this->commandPaths();
+ $paths[] = StringUtils::replaceTilde($path, $this->homeDir());
+ return $this->set(self::COMMAND_PATH, $paths);
+ }
+
+ /**
+ * Add multiple paths where commandfiles might be found.
+ *
+ * @param $commanPaths
+ */
+ public function mergeCommandPaths($commandPaths)
+ {
+ $paths = $this->commandPaths();
+ $merged_paths = array_merge($paths, $commandPaths);
+ return $this->set(self::COMMAND_PATH, $merged_paths);
+ }
+
+ /**
+ * Determine whether Drush is in "local" mode
+ */
+ public function isLocal()
+ {
+ return $this->get(self::LOCAL);
+ }
+
+ /**
+ * Set local mode
+ *
+ * @param bool $isLocal
+ */
+ public function setLocal($isLocal)
+ {
+ return $this->set(self::LOCAL, $isLocal);
+ }
+
+ /**
+ * Determine whether Drush is in "simulated" mode.
+ */
+ public function isSimulated()
+ {
+ return $this->get(self::SIMULATE);
+ }
+
+ /**
+ * Set simulated mode
+ *
+ * @param bool $simulated
+ */
+ public function setSimulate($simulate)
+ {
+ return $this->set(self::SIMULATE, $simulate);
+ }
+
+ /**
+ * Determine whether Drush was placed in simulated mode.
+ */
+ public function isBackend()
+ {
+ return $this->get(self::BACKEND);
+ }
+
+ /**
+ * Set backend mode
+ *
+ * @param bool $backend
+ */
+ public function setBackend($backend)
+ {
+ return $this->set(self::BACKEND, $backend);
+ }
+
+ /**
+ * Get the path to the coverage file.
+ */
+ public function coverageFile()
+ {
+ return $this->get(self::COVERAGE_FILE);
+ }
+
+ /**
+ * Set the coverage file path.
+ *
+ * @param string
+ */
+ public function setCoverageFile($coverageFile)
+ {
+ return $this->set(self::COVERAGE_FILE, StringUtils::replaceTilde($coverageFile, $this->homeDir()));
+ }
+
+ /**
+ * Determine whether Drush is in "strict" mode or not.
+ */
+ public function isStrict()
+ {
+ return $this->get(self::STRICT);
+ }
+
+ /**
+ * Set strict mode.
+ *
+ * @param bool $strict
+ */
+ public function setStrict($strict)
+ {
+ return $this->set(self::STRICT, $strict);
+ }
+
+ /**
+ * Search through the provided argv list, and return
+ * just the option name of any item that is an option.
+ *
+ * @param array $argv e.g. ['foo', '--bar=baz', 'boz']
+ * @return string[] e.g. ['bar']
+ */
+ protected function getOptionNameList($argv)
+ {
+ return array_filter(
+ array_map(
+ function ($item) {
+ // Ignore configuration definitions
+ if (substr($item, 0, 2) == '-D') {
+ return null;
+ }
+ // Regular expression matches:
+ // ^-+ # anything that begins with one or more '-'
+ // ([^= ]*) # any number of characters up to the first = or space
+ if (preg_match('#^-+([^= ]*)#', $item, $matches)) {
+ return $matches[1];
+ }
+ },
+ $argv
+ )
+ );
+ }
+
+ /**
+ * Create a Symfony Input object.
+ */
+ public function createInput()
+ {
+ // In strict mode (the default), create an ArgvInput. When
+ // strict mode is disabled, create a more forgiving input object.
+ if ($this->isStrict() && !$this->isBackend()) {
+ return new DrushArgvInput($this->args());
+ }
+
+ // If in backend mode, read additional options from stdin.
+ // TODO: Maybe reading stdin options should be the responsibility of some
+ // backend manager class? Could be called from preflight and injected here.
+ $input = new LessStrictArgvInput($this->args());
+ $input->injectAdditionalOptions($this->readStdinOptions());
+
+ return $input;
+ }
+
+ /**
+ * Read options fron STDIN during POST requests.
+ *
+ * This function will read any text from the STDIN pipe,
+ * and attempts to generate an associative array if valid
+ * JSON was received.
+ *
+ * @return
+ * An associative array of options, if successfull. Otherwise an empty array.
+ */
+ protected function readStdinOptions()
+ {
+ // If we move this method to a backend manager, then testing for
+ // backend mode will be the responsibility of the caller.
+ if (!$this->isBackend()) {
+ return [];
+ }
+
+ $fp = fopen('php://stdin', 'r');
+ // Windows workaround: we cannot count on stream_get_contents to
+ // return if STDIN is reading from the keyboard. We will therefore
+ // check to see if there are already characters waiting on the
+ // stream (as there always should be, if this is a backend call),
+ // and if there are not, then we will exit.
+ // This code prevents drush from hanging forever when called with
+ // --backend from the commandline; however, overall it is still
+ // a futile effort, as it does not seem that backend invoke can
+ // successfully write data to that this function can read,
+ // so the argument list and command always come out empty. :(
+ // Perhaps stream_get_contents is the problem, and we should use
+ // the technique described here:
+ // http://bugs.php.net/bug.php?id=30154
+ // n.b. the code in that issue passes '0' for the timeout in stream_select
+ // in a loop, which is not recommended.
+ // Note that the following DOES work:
+ // drush ev 'print(json_encode(array("test" => "XYZZY")));' | drush status --backend
+ // So, redirecting input is okay, it is just the proc_open that is a problem.
+ if (drush_is_windows()) {
+ // Note that stream_select uses reference parameters, so we need variables (can't pass a constant NULL)
+ $read = [$fp];
+ $write = null;
+ $except = null;
+ // Question: might we need to wait a bit for STDIN to be ready,
+ // even if the process that called us immediately writes our parameters?
+ // Passing '100' for the timeout here causes us to hang indefinitely
+ // when called from the shell.
+ $changed_streams = stream_select($read, $write, $except, 0);
+ // Return on error or no changed streams (0).
+ // Oh, according to http://php.net/manual/en/function.stream-select.php,
+ // stream_select will return FALSE for streams returned by proc_open.
+ // That is not applicable to us, is it? Our stream is connected to a stream
+ // created by proc_open, but is not a stream returned by proc_open.
+ if ($changed_streams < 1) {
+ return [];
+ }
+ }
+ stream_set_blocking($fp, false);
+ $string = stream_get_contents($fp);
+ fclose($fp);
+ if (trim($string)) {
+ return json_decode($string, true);
+ }
+ return [];
+ }
+}