Parser.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. <?php
  2. /**
  3. * Hoa
  4. *
  5. *
  6. * @license
  7. *
  8. * New BSD License
  9. *
  10. * Copyright © 2007-2017, Hoa community. All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions are met:
  14. * * Redistributions of source code must retain the above copyright
  15. * notice, this list of conditions and the following disclaimer.
  16. * * Redistributions in binary form must reproduce the above copyright
  17. * notice, this list of conditions and the following disclaimer in the
  18. * documentation and/or other materials provided with the distribution.
  19. * * Neither the name of the Hoa nor the names of its contributors may be
  20. * used to endorse or promote products derived from this software without
  21. * specific prior written permission.
  22. *
  23. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  24. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  25. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  26. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
  27. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  28. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  29. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  30. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  31. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  32. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33. * POSSIBILITY OF SUCH DAMAGE.
  34. */
  35. namespace Hoa\Console;
  36. /**
  37. * Class \Hoa\Console\Parser.
  38. *
  39. * This class parses a command line.
  40. * See the parse() method to get more informations about command-line
  41. * vocabulary, patterns, limitations, etc.
  42. *
  43. * @copyright Copyright © 2007-2017 Hoa community
  44. * @license New BSD License
  45. */
  46. class Parser
  47. {
  48. /**
  49. * If long value is not enabled, -abc is equivalent to -a -b -c, else -abc
  50. * is equivalent to --abc.
  51. *
  52. * @var \Hoa\Console\Parser
  53. */
  54. protected $_longonly = false;
  55. /**
  56. * The parsed result in three categories : command, input, and switch.
  57. *
  58. * @var array
  59. */
  60. protected $_parsed = null;
  61. /**
  62. * Parse a command.
  63. * Some explanations :
  64. * 1. Command :
  65. * $ cmd is the command : cmd ;
  66. * $ "cmd sub" is the command : cmd sub ;
  67. * $ cmd\ sub is the command : cmd sub.
  68. *
  69. * 2. Short option :
  70. * $ … -s is a short option ;
  71. * $ … -abc is equivalent to -a -b -c if and only if $longonly is set
  72. * to false, else (set to true) -abc is equivalent to --abc.
  73. *
  74. * 3. Long option :
  75. * $ … --long is a long option ;
  76. * $ … --lo-ng is a long option.
  77. * $ etc.
  78. *
  79. * 4. Boolean switch or flag :
  80. * $ … -s is a boolean switch, -s is set to true ;
  81. * $ … --long is a boolean switch, --long is set to true ;
  82. * $ … -s -s and --long --long
  83. * are boolean switches, -s and --long are set to false ;
  84. * $ … -aa are boolean switches, -a is set to false, if and only if
  85. * the $longonly is set to false, else --aa is set to true.
  86. *
  87. * 5. Valued switch :
  88. * x should be s, -long, abc etc.
  89. * All the following examples are valued switches, where -x is set to the
  90. * specified value.
  91. * $ … -x=value : value ;
  92. * $ … -x=va\ lue : va lue ;
  93. * $ … -x="va lue" : va lue ;
  94. * $ … -x="va l\"ue" : va l"ue ;
  95. * $ … -x value : value ;
  96. * $ … -x va\ lue : va lue ;
  97. * $ … -x "value" : value ;
  98. * $ … -x "va lue" : va lue ;
  99. * $ … -x va\ l"ue : va l"ue ;
  100. * $ … -x 'va "l"ue' : va "l"ue ;
  101. * $ etc. (we did not written all cases, but the philosophy is here).
  102. * Two type of quotes are supported : double quotes ("), and simple
  103. * quotes (').
  104. * We got very particulary cases :
  105. * $ … -x=-value : -value ;
  106. * $ … -x "-value" : -value ;
  107. * $ … -x \-value : -value ;
  108. * $ … -x -value : two switches, -x and -value are set to true ;
  109. * $ … -x=-7 : -7, a negative number.
  110. * And if we have more than one valued switch, the value is overwritted :
  111. * $ … -x a -x b : b.
  112. * Maybe, it should produce an array, like the special valued switch (see
  113. * the point 6. please).
  114. *
  115. * 6. Special valued switch :
  116. * Some valued switch can have a list, or an interval in value ;
  117. * e.g. -x=a,b,c, or -x=1:7 etc.
  118. * This class gives the value as it is, i.e. no manipulation or treatment
  119. * is made.
  120. * $ … -x=a,b,c : a,b,c (and no array('a', 'b', 'c')) ;
  121. * $ etc.
  122. * These manipulations should be made by the user no ? The
  123. * self::parseSpecialValue() is written for that.
  124. *
  125. * 7. Input :
  126. * The regular expression sets a value as much as possible to each
  127. * switch (option). If a switch does not take a value (see the
  128. * \Hoa\Console\GetOption::NO_ARGUMENT constant), the value will be
  129. * transfered to the input stack. But this action is not made in this
  130. * class, only in the \Hoa\Console\GetOption class, because this class
  131. * does not have the options profile. We got the transferSwitchToInput()
  132. * method, that is called in the GetOption class.
  133. * So :
  134. * $ cmd -x input the input is the -x value ;
  135. * $ cmd -x -- input the input is a real input, not a value ;
  136. * $ cmd -x value input -x is set to value, and the input is a real
  137. * input ;
  138. * $ cmd -x value -- input equivalent to -x value input ;
  139. * $ … -a b i -c d ii -a is set to b, -c to d, and we got two
  140. * inputs : i and ii.
  141. *
  142. * Warning : if the command was reconstitued from the $_SERVER variable, all
  143. * these cases are not sure to work, because the command was already
  144. * interpreted/parsed by an other parser (Shell, DOS etc.), and maybe they
  145. * remove some character, or some particular case. But if we give the
  146. * command manually — i.e. without any reconstitution —, all these cases
  147. * will work :).
  148. *
  149. * @param string $command Command to parse.
  150. * @return void
  151. */
  152. public function parse($command)
  153. {
  154. unset($this->_parsed);
  155. $this->_parsed = [
  156. 'input' => [],
  157. 'switch' => []
  158. ];
  159. /**
  160. * Here we go …
  161. *
  162. * #
  163. * (?:
  164. * (?<b>--?[^=\s]+)
  165. * (?:
  166. * (?:(=)|(\s))
  167. * (?<!\\\)(?:("|\')|)
  168. * (?<s>(?(3)[^-]|).*?)
  169. * (?(4)
  170. * (?<!\\\)\4
  171. * |
  172. * (?(2)
  173. * (?<!\\\)\s
  174. * |
  175. * (?:(?:(?<!\\\)\s)|$)
  176. * )
  177. * )
  178. * )?
  179. * )
  180. * |
  181. * (?:
  182. * (?<!\\\)(?:("|\')|)
  183. * (?<i>.*?)
  184. * (?(6)
  185. * (?<!\\\)\6
  186. * |
  187. * (?:(?:(?<!\\\)\s)|$)
  188. * )
  189. * )
  190. * #xSsm
  191. *
  192. * Nice isn't it :-D ?
  193. *
  194. * Note : this regular expression likes to capture empty array (near
  195. * <i>), why?
  196. */
  197. $regex = '#(?:(?<b>--?[^=\s]+)(?:(?:(=)|(\s))(?<!\\\)(?:("|\')|)(?<s>(?(3)[^-]|).*?)(?(4)(?<!\\\)\4|(?(2)(?<!\\\)\s|(?:(?:(?<!\\\)\s)|$))))?)|(?:(?<!\\\)(?:("|\')|)(?<i>.*?)(?(6)(?<!\\\)\6|(?:(?:(?<!\\\)\s)|$)))#Ssm';
  198. preg_match_all($regex, $command, $matches, PREG_SET_ORDER);
  199. for ($i = 0, $max = count($matches); $i < $max; ++$i) {
  200. $match = $matches[$i];
  201. if (isset($match['i']) &&
  202. ('0' === $match['i'] || !empty($match['i']))) {
  203. $this->addInput($match);
  204. } elseif (!isset($match['i']) && !isset($match['s'])) {
  205. if (isset($matches[$i + 1])) {
  206. $nextMatch = $matches[$i + 1];
  207. if (!empty($nextMatch['i']) &&
  208. '=' === $nextMatch['i'][0]) {
  209. ++$i;
  210. $match[2] = '=';
  211. $match[3] =
  212. $match[4] = null;
  213. $match['s'] =
  214. $match[5] = substr($nextMatch[7], 1);
  215. $this->addValuedSwitch($match);
  216. continue;
  217. }
  218. }
  219. $this->addBoolSwitch($match);
  220. } elseif (!isset($match['i']) && isset($match['s'])) {
  221. $this->addValuedSwitch($match);
  222. }
  223. }
  224. return;
  225. }
  226. /**
  227. * Add an input.
  228. *
  229. * @param array $input Intput.
  230. * @return void
  231. */
  232. protected function addInput(array $input)
  233. {
  234. $handle = $input['i'];
  235. if (!empty($input[6])) {
  236. $handle = str_replace('\\' . $input[6], $input[6], $handle);
  237. } else {
  238. $handle = str_replace('\\ ', ' ', $handle);
  239. }
  240. $this->_parsed['input'][] = $handle;
  241. return;
  242. }
  243. /**
  244. * Add a boolean switch.
  245. *
  246. * @param array $switch Switch.
  247. * @return void
  248. */
  249. protected function addBoolSwitch(array $switch)
  250. {
  251. $this->addSwitch($switch['b'], true);
  252. return;
  253. }
  254. /**
  255. * Add a valued switch.
  256. *
  257. * @param array $switch Switch.
  258. * @return void
  259. */
  260. protected function addValuedSwitch(array $switch)
  261. {
  262. $this->addSwitch($switch['b'], $switch['s'], $switch[4]);
  263. return;
  264. }
  265. /**
  266. * Add a switch.
  267. *
  268. * @param string $name Switch name.
  269. * @param string $value Switch value.
  270. * @param string $escape Character to escape.
  271. * @return void
  272. */
  273. protected function addSwitch($name, $value, $escape = null)
  274. {
  275. if (substr($name, 0, 2) == '--') {
  276. return $this->addSwitch(substr($name, 2), $value, $escape);
  277. }
  278. if (substr($name, 0, 1) == '-') {
  279. if (true === $this->getLongOnly()) {
  280. return $this->addSwitch('-' . $name, $value, $escape);
  281. }
  282. foreach (str_split(substr($name, 1)) as $foo => $switch) {
  283. $this->addSwitch($switch, $value, $escape);
  284. }
  285. return;
  286. }
  287. if (null !== $escape) {
  288. $escape = '' == $escape ? ' ' : $escape;
  289. if (is_string($value)) {
  290. $value = str_replace('\\' . $escape, $escape, $value);
  291. }
  292. } elseif (is_string($value)) {
  293. $value = str_replace('\\ ', ' ', $value);
  294. }
  295. if (isset($this->_parsed['switch'][$name])) {
  296. if (is_bool($this->_parsed['switch'][$name])) {
  297. $value = !$this->_parsed['switch'][$name];
  298. } else {
  299. $value = [$this->_parsed['switch'][$name], $value];
  300. }
  301. }
  302. if (empty($name)) {
  303. return $this->addInput([6 => null, 'i' => $value]);
  304. }
  305. $this->_parsed['switch'][$name] = $value;
  306. return;
  307. }
  308. /**
  309. * Transfer a switch value in the input stack.
  310. *
  311. * @param string $name The switch name.
  312. * @param string $value The switch value.
  313. * @return void
  314. */
  315. public function transferSwitchToInput($name, &$value)
  316. {
  317. if (!isset($this->_parsed['switch'][$name])) {
  318. return;
  319. }
  320. $this->_parsed['input'][] = $this->_parsed['switch'][$name];
  321. $value = true;
  322. unset($this->_parsed['switch'][$name]);
  323. return;
  324. }
  325. /**
  326. * Get all inputs.
  327. *
  328. * @return array
  329. */
  330. public function getInputs()
  331. {
  332. return $this->_parsed['input'];
  333. }
  334. /**
  335. * Distribute inputs in variable (like the list() function, but without
  336. * error).
  337. *
  338. * @param string $a First input.
  339. * @param string $b Second input.
  340. * @param string $c Third input.
  341. * @param ... ... ...
  342. * @param string $z 26th input.
  343. * @return void
  344. */
  345. public function listInputs(
  346. &$a,
  347. &$b = null,
  348. &$c = null,
  349. &$d = null,
  350. &$e = null,
  351. &$f = null,
  352. &$g = null,
  353. &$h = null,
  354. &$i = null,
  355. &$j = null,
  356. &$k = null,
  357. &$l = null,
  358. &$m = null,
  359. &$n = null,
  360. &$o = null,
  361. &$p = null,
  362. &$q = null,
  363. &$r = null,
  364. &$s = null,
  365. &$t = null,
  366. &$u = null,
  367. &$v = null,
  368. &$w = null,
  369. &$x = null,
  370. &$y = null,
  371. &$z = null
  372. ) {
  373. $inputs = $this->getInputs();
  374. $i = 'a';
  375. $ii = -1;
  376. while (isset($inputs[++$ii]) && $i <= 'z') {
  377. ${$i++} = $inputs[$ii];
  378. }
  379. return;
  380. }
  381. /**
  382. * Get all switches.
  383. *
  384. * @return array
  385. */
  386. public function getSwitches()
  387. {
  388. return $this->_parsed['switch'];
  389. }
  390. /**
  391. * Parse a special value, i.e. with comma and intervals.
  392. *
  393. * @param string $value The value to parse.
  394. * @param array $keywords Value of keywords.
  395. * @return array
  396. * @throws \Hoa\Console\Exception
  397. * @todo Could be ameliorate with a ":" explode, and some eval.
  398. * Check if operands are integer.
  399. */
  400. public function parseSpecialValue($value, array $keywords = [])
  401. {
  402. $out = [];
  403. foreach (explode(',', $value) as $key => $subvalue) {
  404. $subvalue = str_replace(
  405. array_keys($keywords),
  406. array_values($keywords),
  407. $subvalue
  408. );
  409. if (0 !== preg_match('#^(-?[0-9]+):(-?[0-9]+)$#', $subvalue, $matches)) {
  410. if (0 > $matches[1] && 0 > $matches[2]) {
  411. throw new Exception(
  412. 'Cannot give two negative numbers, given %s.',
  413. 0,
  414. $subvalue
  415. );
  416. }
  417. array_shift($matches);
  418. $max = max($matches);
  419. $min = min($matches);
  420. if (0 > $max || 0 > $min) {
  421. if (0 > $max - $min) {
  422. throw new Exception(
  423. 'The difference between operands must be ' .
  424. 'positive.',
  425. 1
  426. );
  427. }
  428. $min = $max + $min;
  429. }
  430. $out = array_merge(range($min, $max), $out);
  431. } else {
  432. $out[] = $subvalue;
  433. }
  434. }
  435. return $out;
  436. }
  437. /**
  438. * Set the long-only parameter.
  439. *
  440. * @param bool $longonly The long-only value.
  441. * @return bool
  442. */
  443. public function setLongOnly($longonly = false)
  444. {
  445. $old = $this->_longonly;
  446. $this->_longonly = $longonly;
  447. return $old;
  448. }
  449. /**
  450. * Get the long-only value.
  451. *
  452. * @return bool
  453. */
  454. public function getLongOnly()
  455. {
  456. return $this->_longonly;
  457. }
  458. }