Parser.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of sebastian/cli-parser.
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace SebastianBergmann\CliParser;
  11. use function array_map;
  12. use function array_merge;
  13. use function array_shift;
  14. use function array_slice;
  15. use function assert;
  16. use function count;
  17. use function current;
  18. use function explode;
  19. use function is_array;
  20. use function is_int;
  21. use function is_string;
  22. use function key;
  23. use function next;
  24. use function preg_replace;
  25. use function reset;
  26. use function sort;
  27. use function str_ends_with;
  28. use function str_starts_with;
  29. use function strlen;
  30. use function strstr;
  31. use function substr;
  32. final class Parser
  33. {
  34. /**
  35. * @psalm-param list<string> $argv
  36. * @psalm-param list<string> $longOptions
  37. *
  38. * @psalm-return array{0: array, 1: array}
  39. *
  40. * @throws AmbiguousOptionException
  41. * @throws OptionDoesNotAllowArgumentException
  42. * @throws RequiredOptionArgumentMissingException
  43. * @throws UnknownOptionException
  44. */
  45. public function parse(array $argv, string $shortOptions, ?array $longOptions = null): array
  46. {
  47. if (empty($argv)) {
  48. return [[], []];
  49. }
  50. $options = [];
  51. $nonOptions = [];
  52. if ($longOptions) {
  53. sort($longOptions);
  54. }
  55. if (isset($argv[0][0]) && $argv[0][0] !== '-') {
  56. array_shift($argv);
  57. }
  58. reset($argv);
  59. $argv = array_map('trim', $argv);
  60. while (false !== $arg = current($argv)) {
  61. $i = key($argv);
  62. assert(is_int($i));
  63. next($argv);
  64. if ($arg === '') {
  65. continue;
  66. }
  67. if ($arg === '--') {
  68. $nonOptions = array_merge($nonOptions, array_slice($argv, $i + 1));
  69. break;
  70. }
  71. if ($arg[0] !== '-' || (strlen($arg) > 1 && $arg[1] === '-' && !$longOptions)) {
  72. $nonOptions[] = $arg;
  73. continue;
  74. }
  75. if (strlen($arg) > 1 && $arg[1] === '-' && is_array($longOptions)) {
  76. $this->parseLongOption(
  77. substr($arg, 2),
  78. $longOptions,
  79. $options,
  80. $argv,
  81. );
  82. continue;
  83. }
  84. $this->parseShortOption(
  85. substr($arg, 1),
  86. $shortOptions,
  87. $options,
  88. $argv,
  89. );
  90. }
  91. return [$options, $nonOptions];
  92. }
  93. /**
  94. * @throws RequiredOptionArgumentMissingException
  95. */
  96. private function parseShortOption(string $argument, string $shortOptions, array &$options, array &$argv): void
  97. {
  98. $argumentLength = strlen($argument);
  99. for ($i = 0; $i < $argumentLength; $i++) {
  100. $option = $argument[$i];
  101. $optionArgument = null;
  102. if ($argument[$i] === ':' || ($spec = strstr($shortOptions, $option)) === false) {
  103. throw new UnknownOptionException('-' . $option);
  104. }
  105. if (strlen($spec) > 1 && $spec[1] === ':') {
  106. if ($i + 1 < $argumentLength) {
  107. $options[] = [$option, substr($argument, $i + 1)];
  108. break;
  109. }
  110. if (!(strlen($spec) > 2 && $spec[2] === ':')) {
  111. $optionArgument = current($argv);
  112. if (!$optionArgument) {
  113. throw new RequiredOptionArgumentMissingException('-' . $option);
  114. }
  115. assert(is_string($optionArgument));
  116. next($argv);
  117. }
  118. }
  119. $options[] = [$option, $optionArgument];
  120. }
  121. }
  122. /**
  123. * @psalm-param list<string> $longOptions
  124. *
  125. * @throws AmbiguousOptionException
  126. * @throws OptionDoesNotAllowArgumentException
  127. * @throws RequiredOptionArgumentMissingException
  128. * @throws UnknownOptionException
  129. */
  130. private function parseLongOption(string $argument, array $longOptions, array &$options, array &$argv): void
  131. {
  132. $count = count($longOptions);
  133. $list = explode('=', $argument);
  134. $option = $list[0];
  135. $optionArgument = null;
  136. if (count($list) > 1) {
  137. $optionArgument = $list[1];
  138. }
  139. $optionLength = strlen($option);
  140. foreach ($longOptions as $i => $longOption) {
  141. $opt_start = substr($longOption, 0, $optionLength);
  142. if ($opt_start !== $option) {
  143. continue;
  144. }
  145. $opt_rest = substr($longOption, $optionLength);
  146. if ($opt_rest !== '' && $i + 1 < $count && $option[0] !== '=' && str_starts_with($longOptions[$i + 1], $option)) {
  147. throw new AmbiguousOptionException('--' . $option);
  148. }
  149. if (str_ends_with($longOption, '=')) {
  150. if (!str_ends_with($longOption, '==') && !strlen((string) $optionArgument)) {
  151. if (false === $optionArgument = current($argv)) {
  152. throw new RequiredOptionArgumentMissingException('--' . $option);
  153. }
  154. next($argv);
  155. }
  156. } elseif ($optionArgument) {
  157. throw new OptionDoesNotAllowArgumentException('--' . $option);
  158. }
  159. $fullOption = '--' . preg_replace('/={1,2}$/', '', $longOption);
  160. $options[] = [$fullOption, $optionArgument];
  161. return;
  162. }
  163. throw new UnknownOptionException('--' . $option);
  164. }
  165. }