Parser.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. * @param list<string> $argv
  36. * @param list<string> $longOptions
  37. *
  38. * @throws AmbiguousOptionException
  39. * @throws OptionDoesNotAllowArgumentException
  40. * @throws RequiredOptionArgumentMissingException
  41. * @throws UnknownOptionException
  42. *
  43. * @return array{0: list<array{0: non-empty-string, 1: ?non-empty-string}>, 1: list<non-empty-string>}
  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 !== null) {
  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 === null)) {
  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. * @param list<array{0: non-empty-string, 1: ?non-empty-string}> $options
  95. * @param list<string> $argv
  96. *
  97. * @throws RequiredOptionArgumentMissingException
  98. */
  99. private function parseShortOption(string $argument, string $shortOptions, array &$options, array &$argv): void
  100. {
  101. $argumentLength = strlen($argument);
  102. for ($i = 0; $i < $argumentLength; $i++) {
  103. $option = $argument[$i];
  104. $optionArgument = null;
  105. if ($argument[$i] === ':' || ($spec = strstr($shortOptions, $option)) === false) {
  106. throw new UnknownOptionException('-' . $option);
  107. }
  108. if (strlen($spec) > 1 && $spec[1] === ':') {
  109. if ($i + 1 < $argumentLength) {
  110. $options[] = [$option, substr($argument, $i + 1)];
  111. break;
  112. }
  113. if (!(strlen($spec) > 2 && $spec[2] === ':')) {
  114. $optionArgument = current($argv);
  115. if ($optionArgument === false) {
  116. throw new RequiredOptionArgumentMissingException('-' . $option);
  117. }
  118. assert(is_string($optionArgument));
  119. next($argv);
  120. }
  121. }
  122. $options[] = [$option, $optionArgument];
  123. }
  124. }
  125. /**
  126. * @param list<string> $longOptions
  127. * @param list<array{0: non-empty-string, 1: ?non-empty-string}> $options
  128. * @param list<string> $argv
  129. *
  130. * @throws AmbiguousOptionException
  131. * @throws OptionDoesNotAllowArgumentException
  132. * @throws RequiredOptionArgumentMissingException
  133. * @throws UnknownOptionException
  134. */
  135. private function parseLongOption(string $argument, array $longOptions, array &$options, array &$argv): void
  136. {
  137. $count = count($longOptions);
  138. $list = explode('=', $argument);
  139. $option = $list[0];
  140. $optionArgument = null;
  141. if (count($list) > 1) {
  142. $optionArgument = $list[1];
  143. }
  144. $optionLength = strlen($option);
  145. foreach ($longOptions as $i => $longOption) {
  146. $opt_start = substr($longOption, 0, $optionLength);
  147. if ($opt_start !== $option) {
  148. continue;
  149. }
  150. $opt_rest = substr($longOption, $optionLength);
  151. if ($opt_rest !== '' && $i + 1 < $count && $option[0] !== '=' && str_starts_with($longOptions[$i + 1], $option)) {
  152. throw new AmbiguousOptionException('--' . $option);
  153. }
  154. if (str_ends_with($longOption, '=')) {
  155. if (!str_ends_with($longOption, '==') && !strlen((string) $optionArgument)) {
  156. if (false === $optionArgument = current($argv)) {
  157. throw new RequiredOptionArgumentMissingException('--' . $option);
  158. }
  159. next($argv);
  160. }
  161. } elseif ($optionArgument !== null) {
  162. throw new OptionDoesNotAllowArgumentException('--' . $option);
  163. }
  164. $fullOption = '--' . preg_replace('/={1,2}$/', '', $longOption);
  165. $options[] = [$fullOption, $optionArgument];
  166. return;
  167. }
  168. throw new UnknownOptionException('--' . $option);
  169. }
  170. }