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