Chris@13: tokenPairs = $this->tokenize($input); Chris@13: } Chris@13: Chris@13: /** Chris@13: * {@inheritdoc} Chris@13: * Chris@13: * @throws \InvalidArgumentException if $definition has CodeArgument before the final argument position Chris@13: */ Chris@13: public function bind(InputDefinition $definition) Chris@13: { Chris@13: $hasCodeArgument = false; Chris@13: Chris@13: if ($definition->getArgumentCount() > 0) { Chris@13: $args = $definition->getArguments(); Chris@17: $lastArg = \array_pop($args); Chris@13: foreach ($args as $arg) { Chris@13: if ($arg instanceof CodeArgument) { Chris@17: $msg = \sprintf('Unexpected CodeArgument before the final position: %s', $arg->getName()); Chris@13: throw new \InvalidArgumentException($msg); Chris@13: } Chris@13: } Chris@13: Chris@13: if ($lastArg instanceof CodeArgument) { Chris@13: $hasCodeArgument = true; Chris@13: } Chris@13: } Chris@13: Chris@13: $this->hasCodeArgument = $hasCodeArgument; Chris@13: Chris@13: return parent::bind($definition); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Tokenizes a string. Chris@13: * Chris@13: * The version of this on StringInput is good, but doesn't handle code Chris@13: * arguments if they're at all complicated. This does :) Chris@13: * Chris@13: * @param string $input The input to tokenize Chris@13: * Chris@13: * @return array An array of token/rest pairs Chris@13: * Chris@13: * @throws \InvalidArgumentException When unable to parse input (should never happen) Chris@13: */ Chris@13: private function tokenize($input) Chris@13: { Chris@13: $tokens = []; Chris@17: $length = \strlen($input); Chris@13: $cursor = 0; Chris@13: while ($cursor < $length) { Chris@17: if (\preg_match('/\s+/A', $input, $match, null, $cursor)) { Chris@17: } elseif (\preg_match('/([^="\'\s]+?)(=?)(' . StringInput::REGEX_QUOTED_STRING . '+)/A', $input, $match, null, $cursor)) { Chris@13: $tokens[] = [ Chris@17: $match[1] . $match[2] . \stripcslashes(\str_replace(['"\'', '\'"', '\'\'', '""'], '', \substr($match[3], 1, \strlen($match[3]) - 2))), Chris@17: \stripcslashes(\substr($input, $cursor)), Chris@13: ]; Chris@17: } elseif (\preg_match('/' . StringInput::REGEX_QUOTED_STRING . '/A', $input, $match, null, $cursor)) { Chris@13: $tokens[] = [ Chris@17: \stripcslashes(\substr($match[0], 1, \strlen($match[0]) - 2)), Chris@17: \stripcslashes(\substr($input, $cursor)), Chris@13: ]; Chris@17: } elseif (\preg_match('/' . StringInput::REGEX_STRING . '/A', $input, $match, null, $cursor)) { Chris@13: $tokens[] = [ Chris@17: \stripcslashes($match[1]), Chris@17: \stripcslashes(\substr($input, $cursor)), Chris@13: ]; Chris@13: } else { Chris@13: // should never happen Chris@16: // @codeCoverageIgnoreStart Chris@17: throw new \InvalidArgumentException(\sprintf('Unable to parse input near "... %s ..."', \substr($input, $cursor, 10))); Chris@16: // @codeCoverageIgnoreEnd Chris@13: } Chris@13: Chris@17: $cursor += \strlen($match[0]); Chris@13: } Chris@13: Chris@13: return $tokens; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Same as parent, but with some bonus handling for code arguments. Chris@13: */ Chris@13: protected function parse() Chris@13: { Chris@13: $parseOptions = true; Chris@13: $this->parsed = $this->tokenPairs; Chris@17: while (null !== $tokenPair = \array_shift($this->parsed)) { Chris@13: // token is what you'd expect. rest is the remainder of the input Chris@13: // string, including token, and will be used if this is a code arg. Chris@13: list($token, $rest) = $tokenPair; Chris@13: Chris@13: if ($parseOptions && '' === $token) { Chris@13: $this->parseShellArgument($token, $rest); Chris@13: } elseif ($parseOptions && '--' === $token) { Chris@13: $parseOptions = false; Chris@17: } elseif ($parseOptions && 0 === \strpos($token, '--')) { Chris@13: $this->parseLongOption($token); Chris@13: } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { Chris@13: $this->parseShortOption($token); Chris@13: } else { Chris@13: $this->parseShellArgument($token, $rest); Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Parses an argument, with bonus handling for code arguments. Chris@13: * Chris@13: * @param string $token The current token Chris@13: * @param string $rest The remaining unparsed input, including the current token Chris@13: * Chris@13: * @throws \RuntimeException When too many arguments are given Chris@13: */ Chris@13: private function parseShellArgument($token, $rest) Chris@13: { Chris@17: $c = \count($this->arguments); Chris@13: Chris@13: // if input is expecting another argument, add it Chris@13: if ($this->definition->hasArgument($c)) { Chris@13: $arg = $this->definition->getArgument($c); Chris@13: Chris@13: if ($arg instanceof CodeArgument) { Chris@13: // When we find a code argument, we're done parsing. Add the Chris@13: // remaining input to the current argument and call it a day. Chris@13: $this->parsed = []; Chris@13: $this->arguments[$arg->getName()] = $rest; Chris@13: } else { Chris@13: $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; Chris@13: } Chris@13: Chris@13: return; Chris@13: } Chris@13: Chris@16: // (copypasta) Chris@16: // Chris@16: // @codeCoverageIgnoreStart Chris@16: Chris@13: // if last argument isArray(), append token to last argument Chris@13: if ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { Chris@13: $arg = $this->definition->getArgument($c - 1); Chris@13: $this->arguments[$arg->getName()][] = $token; Chris@13: Chris@13: return; Chris@13: } Chris@13: Chris@13: // unexpected argument Chris@13: $all = $this->definition->getArguments(); Chris@17: if (\count($all)) { Chris@17: throw new \RuntimeException(\sprintf('Too many arguments, expected arguments "%s".', \implode('" "', \array_keys($all)))); Chris@13: } Chris@13: Chris@17: throw new \RuntimeException(\sprintf('No arguments expected, got "%s".', $token)); Chris@16: // @codeCoverageIgnoreEnd Chris@13: } Chris@13: Chris@13: // Everything below this is copypasta from ArgvInput private methods Chris@16: // @codeCoverageIgnoreStart Chris@13: Chris@13: /** Chris@13: * Parses a short option. Chris@13: * Chris@13: * @param string $token The current token Chris@13: */ Chris@13: private function parseShortOption($token) Chris@13: { Chris@17: $name = \substr($token, 1); Chris@13: Chris@17: if (\strlen($name) > 1) { Chris@13: if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { Chris@13: // an option with a value (with no space) Chris@17: $this->addShortOption($name[0], \substr($name, 1)); Chris@13: } else { Chris@13: $this->parseShortOptionSet($name); Chris@13: } Chris@13: } else { Chris@13: $this->addShortOption($name, null); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Parses a short option set. Chris@13: * Chris@13: * @param string $name The current token Chris@13: * Chris@13: * @throws \RuntimeException When option given doesn't exist Chris@13: */ Chris@13: private function parseShortOptionSet($name) Chris@13: { Chris@17: $len = \strlen($name); Chris@13: for ($i = 0; $i < $len; $i++) { Chris@13: if (!$this->definition->hasShortcut($name[$i])) { Chris@17: throw new \RuntimeException(\sprintf('The "-%s" option does not exist.', $name[$i])); Chris@13: } Chris@13: Chris@13: $option = $this->definition->getOptionForShortcut($name[$i]); Chris@13: if ($option->acceptValue()) { Chris@17: $this->addLongOption($option->getName(), $i === $len - 1 ? null : \substr($name, $i + 1)); Chris@13: Chris@13: break; Chris@13: } else { Chris@13: $this->addLongOption($option->getName(), null); Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Parses a long option. Chris@13: * Chris@13: * @param string $token The current token Chris@13: */ Chris@13: private function parseLongOption($token) Chris@13: { Chris@17: $name = \substr($token, 2); Chris@13: Chris@17: if (false !== $pos = \strpos($name, '=')) { Chris@17: if (0 === \strlen($value = \substr($name, $pos + 1))) { Chris@13: // if no value after "=" then substr() returns "" since php7 only, false before Chris@13: // see http://php.net/manual/fr/migration70.incompatible.php#119151 Chris@13: if (PHP_VERSION_ID < 70000 && false === $value) { Chris@13: $value = ''; Chris@13: } Chris@17: \array_unshift($this->parsed, [$value, null]); Chris@13: } Chris@17: $this->addLongOption(\substr($name, 0, $pos), $value); Chris@13: } else { Chris@13: $this->addLongOption($name, null); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Adds a short option value. Chris@13: * Chris@13: * @param string $shortcut The short option key Chris@13: * @param mixed $value The value for the option Chris@13: * Chris@13: * @throws \RuntimeException When option given doesn't exist Chris@13: */ Chris@13: private function addShortOption($shortcut, $value) Chris@13: { Chris@13: if (!$this->definition->hasShortcut($shortcut)) { Chris@17: throw new \RuntimeException(\sprintf('The "-%s" option does not exist.', $shortcut)); Chris@13: } Chris@13: Chris@13: $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Adds a long option value. Chris@13: * Chris@13: * @param string $name The long option key Chris@13: * @param mixed $value The value for the option Chris@13: * Chris@13: * @throws \RuntimeException When option given doesn't exist Chris@13: */ Chris@13: private function addLongOption($name, $value) Chris@13: { Chris@13: if (!$this->definition->hasOption($name)) { Chris@17: throw new \RuntimeException(\sprintf('The "--%s" option does not exist.', $name)); Chris@13: } Chris@13: Chris@13: $option = $this->definition->getOption($name); Chris@13: Chris@13: if (null !== $value && !$option->acceptValue()) { Chris@17: throw new \RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name)); Chris@13: } Chris@13: Chris@17: if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) { Chris@13: // if option accepts an optional or mandatory argument Chris@13: // let's see if there is one provided Chris@17: $next = \array_shift($this->parsed); Chris@13: $nextToken = $next[0]; Chris@17: if ((isset($nextToken[0]) && '-' !== $nextToken[0]) || \in_array($nextToken, ['', null], true)) { Chris@13: $value = $nextToken; Chris@13: } else { Chris@17: \array_unshift($this->parsed, $next); Chris@13: } Chris@13: } Chris@13: Chris@13: if (null === $value) { Chris@13: if ($option->isValueRequired()) { Chris@17: throw new \RuntimeException(\sprintf('The "--%s" option requires a value.', $name)); Chris@13: } Chris@13: Chris@13: if (!$option->isArray() && !$option->isValueOptional()) { Chris@13: $value = true; Chris@13: } Chris@13: } Chris@13: Chris@13: if ($option->isArray()) { Chris@13: $this->options[$name][] = $value; Chris@13: } else { Chris@13: $this->options[$name] = $value; Chris@13: } Chris@13: } Chris@16: Chris@16: // @codeCoverageIgnoreEnd Chris@13: }