2 namespace Drush\Preflight;
4 use Consolidation\Config\Config;
5 use Consolidation\Config\ConfigInterface;
7 use Drush\Symfony\DrushArgvInput;
8 use Drush\Utils\StringUtils;
9 use Drush\Symfony\LessStrictArgvInput;
12 * Storage for arguments preprocessed during preflight.
14 * Holds @sitealias, if present, and a limited number of global options.
16 * TODO: The methods here with >~3 lines of logic could be refactored into a couple
17 * of different classes e.g. a helper to convert preflight args to configuration,
18 * and another to prepare the input object.
20 class PreflightArgs extends Config implements PreflightArgsInterface
23 * @var array $args Remaining arguments not handled by the preprocessor
28 * @var string $homeDir Path to directory to use when replacing ~ in paths
35 public function homeDir()
37 return $this->homeDir;
41 * @param string $homeDir
43 public function setHomeDir($homeDir)
45 $this->homeDir = $homeDir;
48 const DRUSH_CONFIG_PATH_NAMESPACE = 'drush.paths';
49 const DRUSH_RUNTIME_CONTEXT_NAMESPACE = 'runtime.contxt';
50 const ALIAS = 'alias';
51 const ALIAS_PATH = 'alias-path';
52 const COMMAND_PATH = 'include';
53 const CONFIG_PATH = 'config';
54 const COVERAGE_FILE = 'coverage-file';
55 const LOCAL = 'local';
58 const SIMULATE = 'simulate';
59 const BACKEND = 'backend';
60 const STRICT = 'strict';
61 const DEBUG = 'preflight-debug';
64 * PreflightArgs constructor
66 * @param array $data Initial data (not usually used)
68 public function __construct($data = [])
70 parent::__construct($data + [self::STRICT => true]);
76 public function optionsWithValues()
79 '-r=' => 'setSelectedSite',
80 '--root=' => 'setSelectedSite',
81 '--debug' => 'setDebug',
87 '-c=' => 'addConfigPath',
88 '--config=' => 'addConfigPath',
89 '--alias-path=' => 'addAliasPath',
90 '--include=' => 'addCommandPath',
91 '--local' => 'setLocal',
92 '--simulate' => 'setSimulate',
93 '-s' => 'setSimulate',
94 '--backend' => 'setBackend',
95 '--drush-coverage=' => 'setCoverageFile',
96 '--strict=' => 'setStrict',
97 '--help' => 'adjustHelpOption',
98 '-h' => 'adjustHelpOption',
103 * If the user enters '--help' or '-h', thrown that
104 * option away and add a 'help' command to the beginning
105 * of the argument list.
107 public function adjustHelpOption()
109 $drushPath = array_shift($this->args);
110 array_unshift($this->args, $drushPath, 'help');
114 * Map of option key to the corresponding config key to store the
115 * preflight option in.
117 protected function optionConfigMap()
120 self::SIMULATE => \Robo\Config\Config::SIMULATE,
121 self::BACKEND => self::BACKEND,
122 self::LOCAL => self::DRUSH_RUNTIME_CONTEXT_NAMESPACE . '.' . self::LOCAL,
127 * Map of path option keys to the corresponding config key to store the
128 * preflight option in.
130 protected function optionConfigPathMap()
133 self::ALIAS_PATH => self::DRUSH_CONFIG_PATH_NAMESPACE . '.' . self::ALIAS_PATH,
134 self::CONFIG_PATH => self::DRUSH_CONFIG_PATH_NAMESPACE . '.' . self::CONFIG_PATH,
135 self::COMMAND_PATH => self::DRUSH_CONFIG_PATH_NAMESPACE . '.' . self::COMMAND_PATH,
142 * @see Environment::exportConfigData(), which also exports information to config.
144 public function applyToConfig(ConfigInterface $config)
146 // Copy the relevant preflight options to the applicable configuration namespace
147 foreach ($this->optionConfigMap() as $option_key => $config_key) {
148 $config->set($config_key, $this->get($option_key));
150 // Merging as they are lists.
151 foreach ($this->optionConfigPathMap() as $option_key => $config_key) {
152 $cli_paths = $this->get($option_key, []);
153 $config_paths = (array) $config->get($config_key, []);
154 $merged_paths = array_merge($cli_paths, $config_paths);
155 $config->set($config_key, $merged_paths);
156 $this->set($option_key, $merged_paths);
159 // Store the runtime arguments and options (sans the runtime context items)
160 // in runtime.argv et. al.
161 $config->set('runtime.argv', $this->args());
162 $config->set('runtime.options', $this->getOptionNameList($this->args()));
168 public function args()
176 public function applicationPath()
178 return reset($this->args);
184 public function addArg($arg)
186 $this->args[] = $arg;
193 public function passArgs($args)
195 $this->args = array_merge($this->args, $args);
202 public function alias()
204 return $this->get(self::ALIAS);
210 public function hasAlias()
212 return $this->has(self::ALIAS);
218 public function setAlias($alias)
220 // Treat `drush @self ...` as if an alias had not been used at all.
221 if ($alias == '@self') {
224 return $this->set(self::ALIAS, $alias);
228 * Get the selected site. Here, the default will typically be the cwd.
230 public function selectedSite($default = false)
232 return $this->get(self::ROOT, $default);
235 public function setDebug($value)
237 $this->set(self::DEBUG, $value);
238 $this->addArg('-vvv');
242 * Set the selected site.
244 public function setSelectedSite($root)
246 return $this->set(self::ROOT, StringUtils::replaceTilde($root, $this->homeDir()));
250 * Get the selected uri
252 public function uri($default = false)
254 return $this->get(self::URI, $default);
260 public function setUri($uri)
262 return $this->set(self::URI, $uri);
266 * Get the config path where drush.yml files may be found
268 public function configPaths()
270 return $this->get(self::CONFIG_PATH, []);
274 * Add another location where drush.yml files may be found
276 * @param string $path
278 public function addConfigPath($path)
280 $paths = $this->configPaths();
281 $paths[] = StringUtils::replaceTilde($path, $this->homeDir());
282 return $this->set(self::CONFIG_PATH, $paths);
286 * Add multiple additional locations where drush.yml files may be found.
288 * @param string[] $configPaths
290 public function mergeConfigPaths($configPaths)
292 $paths = $this->configPaths();
293 $merged_paths = array_merge($paths, $configPaths);
294 return $this->set(self::CONFIG_PATH, $merged_paths);
298 * Get the alias paths where drush site.site.yml files may be found
300 public function aliasPaths()
302 return $this->get(self::ALIAS_PATH, []);
306 * Set one more path where aliases may be found.
308 * @param string $path
310 public function addAliasPath($path)
312 $paths = $this->aliasPaths();
313 $paths[] = StringUtils::replaceTilde($path, $this->homeDir());
314 return $this->set(self::ALIAS_PATH, $paths);
318 * Add multiple additional locations for alias paths.
320 * @param string $aliasPaths
322 public function mergeAliasPaths($aliasPaths)
324 $paths = $this->aliasPaths();
325 $merged_paths = array_merge($paths, $aliasPaths);
326 return $this->set(self::ALIAS_PATH, $merged_paths);
330 * Get the path where Drush commandfiles e.g. FooCommands.php may be found.
332 public function commandPaths()
334 return $this->get(self::COMMAND_PATH, []);
338 * Add one more path where commandfiles might be found.
340 * @param string $path
342 public function addCommandPath($path)
344 $paths = $this->commandPaths();
345 $paths[] = StringUtils::replaceTilde($path, $this->homeDir());
346 return $this->set(self::COMMAND_PATH, $paths);
350 * Add multiple paths where commandfiles might be found.
352 * @param $commanPaths
354 public function mergeCommandPaths($commandPaths)
356 $paths = $this->commandPaths();
357 $merged_paths = array_merge($paths, $commandPaths);
358 return $this->set(self::COMMAND_PATH, $merged_paths);
362 * Determine whether Drush is in "local" mode
364 public function isLocal()
366 return $this->get(self::LOCAL);
372 * @param bool $isLocal
374 public function setLocal($isLocal)
376 return $this->set(self::LOCAL, $isLocal);
380 * Determine whether Drush is in "simulated" mode.
382 public function isSimulated()
384 return $this->get(self::SIMULATE);
390 * @param bool $simulated
392 public function setSimulate($simulate)
394 return $this->set(self::SIMULATE, $simulate);
398 * Determine whether Drush was placed in simulated mode.
400 public function isBackend()
402 return $this->get(self::BACKEND);
408 * @param bool $backend
410 public function setBackend($backend)
412 return $this->set(self::BACKEND, $backend);
416 * Get the path to the coverage file.
418 public function coverageFile()
420 return $this->get(self::COVERAGE_FILE);
424 * Set the coverage file path.
428 public function setCoverageFile($coverageFile)
430 return $this->set(self::COVERAGE_FILE, StringUtils::replaceTilde($coverageFile, $this->homeDir()));
434 * Determine whether Drush is in "strict" mode or not.
436 public function isStrict()
438 return $this->get(self::STRICT);
444 * @param bool $strict
446 public function setStrict($strict)
448 return $this->set(self::STRICT, $strict);
452 * Search through the provided argv list, and return
453 * just the option name of any item that is an option.
455 * @param array $argv e.g. ['foo', '--bar=baz', 'boz']
456 * @return string[] e.g. ['bar']
458 protected function getOptionNameList($argv)
463 // Ignore configuration definitions
464 if (substr($item, 0, 2) == '-D') {
467 // Regular expression matches:
468 // ^-+ # anything that begins with one or more '-'
469 // ([^= ]*) # any number of characters up to the first = or space
470 if (preg_match('#^-+([^= ]*)#', $item, $matches)) {
480 * Create a Symfony Input object.
482 public function createInput()
484 // In strict mode (the default), create an ArgvInput. When
485 // strict mode is disabled, create a more forgiving input object.
486 if ($this->isStrict() && !$this->isBackend()) {
487 return new DrushArgvInput($this->args());
490 // If in backend mode, read additional options from stdin.
491 // TODO: Maybe reading stdin options should be the responsibility of some
492 // backend manager class? Could be called from preflight and injected here.
493 $input = new LessStrictArgvInput($this->args());
494 $input->injectAdditionalOptions($this->readStdinOptions());
500 * Read options fron STDIN during POST requests.
502 * This function will read any text from the STDIN pipe,
503 * and attempts to generate an associative array if valid
507 * An associative array of options, if successfull. Otherwise an empty array.
509 protected function readStdinOptions()
511 // If we move this method to a backend manager, then testing for
512 // backend mode will be the responsibility of the caller.
513 if (!$this->isBackend()) {
517 $fp = fopen('php://stdin', 'r');
518 // Windows workaround: we cannot count on stream_get_contents to
519 // return if STDIN is reading from the keyboard. We will therefore
520 // check to see if there are already characters waiting on the
521 // stream (as there always should be, if this is a backend call),
522 // and if there are not, then we will exit.
523 // This code prevents drush from hanging forever when called with
524 // --backend from the commandline; however, overall it is still
525 // a futile effort, as it does not seem that backend invoke can
526 // successfully write data to that this function can read,
527 // so the argument list and command always come out empty. :(
528 // Perhaps stream_get_contents is the problem, and we should use
529 // the technique described here:
530 // http://bugs.php.net/bug.php?id=30154
531 // n.b. the code in that issue passes '0' for the timeout in stream_select
532 // in a loop, which is not recommended.
533 // Note that the following DOES work:
534 // drush ev 'print(json_encode(array("test" => "XYZZY")));' | drush status --backend
535 // So, redirecting input is okay, it is just the proc_open that is a problem.
536 if (drush_is_windows()) {
537 // Note that stream_select uses reference parameters, so we need variables (can't pass a constant NULL)
541 // Question: might we need to wait a bit for STDIN to be ready,
542 // even if the process that called us immediately writes our parameters?
543 // Passing '100' for the timeout here causes us to hang indefinitely
544 // when called from the shell.
545 $changed_streams = stream_select($read, $write, $except, 0);
546 // Return on error or no changed streams (0).
547 // Oh, according to http://php.net/manual/en/function.stream-select.php,
548 // stream_select will return FALSE for streams returned by proc_open.
549 // That is not applicable to us, is it? Our stream is connected to a stream
550 // created by proc_open, but is not a stream returned by proc_open.
551 if ($changed_streams < 1) {
555 stream_set_blocking($fp, false);
556 $string = stream_get_contents($fp);
559 return json_decode($string, true);