ConsoleOptionParserTest.php 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 2.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Console;
  17. use Cake\Console\ConsoleInputArgument;
  18. use Cake\Console\ConsoleInputOption;
  19. use Cake\Console\ConsoleInputSubcommand;
  20. use Cake\Console\ConsoleIo;
  21. use Cake\Console\ConsoleOptionParser;
  22. use Cake\Console\Exception\ConsoleException;
  23. use Cake\Console\Exception\MissingOptionException;
  24. use Cake\Console\TestSuite\StubConsoleInput;
  25. use Cake\Console\TestSuite\StubConsoleOutput;
  26. use Cake\TestSuite\TestCase;
  27. use LogicException;
  28. /**
  29. * ConsoleOptionParserTest
  30. */
  31. class ConsoleOptionParserTest extends TestCase
  32. {
  33. /**
  34. * @var \Cake\Console\ConsoleIo
  35. */
  36. private $io;
  37. public function setUp(): void
  38. {
  39. parent::setUp();
  40. $this->io = new ConsoleIo(new StubConsoleOutput(), new StubConsoleOutput(), new StubConsoleInput([]));
  41. }
  42. /**
  43. * test setting the console description
  44. */
  45. public function testDescription(): void
  46. {
  47. $parser = new ConsoleOptionParser('test', false);
  48. $result = $parser->setDescription('A test');
  49. $this->assertEquals($parser, $result, 'Setting description is not chainable');
  50. $this->assertSame('A test', $parser->getDescription(), 'getting value is wrong.');
  51. $result = $parser->setDescription(['A test', 'something']);
  52. $this->assertSame("A test\nsomething", $parser->getDescription(), 'getting value is wrong.');
  53. }
  54. /**
  55. * test setting and getting the console epilog
  56. */
  57. public function testEpilog(): void
  58. {
  59. $parser = new ConsoleOptionParser('test', false);
  60. $result = $parser->setEpilog('A test');
  61. $this->assertEquals($parser, $result, 'Setting epilog is not chainable');
  62. $this->assertSame('A test', $parser->getEpilog(), 'getting value is wrong.');
  63. $result = $parser->setEpilog(['A test', 'something']);
  64. $this->assertSame("A test\nsomething", $parser->getEpilog(), 'getting value is wrong.');
  65. }
  66. /**
  67. * test adding an option returns self.
  68. */
  69. public function testAddOptionReturnSelf(): void
  70. {
  71. $parser = new ConsoleOptionParser('test', false);
  72. $result = $parser->addOption('test');
  73. $this->assertEquals($parser, $result, 'Did not return $this from addOption');
  74. }
  75. /**
  76. * test removing an option
  77. */
  78. public function testRemoveOption(): void
  79. {
  80. $parser = new ConsoleOptionParser('test', false);
  81. $result = $parser->addOption('test')
  82. ->removeOption('test')
  83. ->removeOption('help');
  84. $this->assertSame($parser, $result, 'Did not return $this from removeOption');
  85. $this->assertEquals([], $result->options());
  86. }
  87. /**
  88. * test adding an option and using the long value for parsing.
  89. */
  90. public function testAddOptionLong(): void
  91. {
  92. $parser = new ConsoleOptionParser('test', false);
  93. $parser->addOption('test', [
  94. 'short' => 't',
  95. ]);
  96. $result = $parser->parse(['--test', 'value'], $this->io);
  97. $this->assertEquals(['test' => 'value', 'help' => false], $result[0], 'Long parameter did not parse out');
  98. }
  99. /**
  100. * test adding an option with a zero value
  101. */
  102. public function testAddOptionZero(): void
  103. {
  104. $parser = new ConsoleOptionParser('test', false);
  105. $parser->addOption('count', []);
  106. $result = $parser->parse(['--count', '0'], $this->io);
  107. $this->assertEquals(['count' => '0', 'help' => false], $result[0], 'Zero parameter did not parse out');
  108. }
  109. /**
  110. * test addOption with an object.
  111. */
  112. public function testAddOptionObject(): void
  113. {
  114. $parser = new ConsoleOptionParser('test', false);
  115. $parser->addOption(new ConsoleInputOption('test', 't'));
  116. $result = $parser->parse(['--test=value'], $this->io);
  117. $this->assertEquals(['test' => 'value', 'help' => false], $result[0], 'Long parameter did not parse out');
  118. }
  119. /**
  120. * test adding an option and using the long value for parsing.
  121. */
  122. public function testAddOptionLongEquals(): void
  123. {
  124. $parser = new ConsoleOptionParser('test', false);
  125. $parser->addOption('test', [
  126. 'short' => 't',
  127. ]);
  128. $result = $parser->parse(['--test=value'], $this->io);
  129. $this->assertEquals(['test' => 'value', 'help' => false], $result[0], 'Long parameter did not parse out');
  130. }
  131. /**
  132. * test adding an option and using the default.
  133. */
  134. public function testAddOptionDefault(): void
  135. {
  136. $parser = new ConsoleOptionParser('test', false);
  137. $parser
  138. ->addOption('test', [
  139. 'default' => 'default value',
  140. ])
  141. ->addOption('no-default', []);
  142. $result = $parser->parse(['--test'], $this->io);
  143. $this->assertSame(
  144. ['test' => 'default value', 'help' => false],
  145. $result[0],
  146. 'Default value did not parse out'
  147. );
  148. $parser = new ConsoleOptionParser('test', false);
  149. $parser->addOption('test', [
  150. 'default' => 'default value',
  151. ]);
  152. $result = $parser->parse([], $this->io);
  153. $this->assertEquals(['test' => 'default value', 'help' => false], $result[0], 'Default value did not parse out');
  154. }
  155. /**
  156. * test adding an option and using the short value for parsing.
  157. */
  158. public function testAddOptionShort(): void
  159. {
  160. $parser = new ConsoleOptionParser('test', false);
  161. $parser->addOption('test', [
  162. 'short' => 't',
  163. ]);
  164. $result = $parser->parse(['-t', 'value'], $this->io);
  165. $this->assertEquals(['test' => 'value', 'help' => false], $result[0], 'Short parameter did not parse out');
  166. }
  167. /**
  168. * test adding an option and using the short value for parsing.
  169. */
  170. public function testAddOptionWithMultipleShort(): void
  171. {
  172. $parser = new ConsoleOptionParser('test', false);
  173. $parser->addOption('source', [
  174. 'multiple' => true,
  175. 'short' => 's',
  176. ]);
  177. $result = $parser->parse(['-s', 'mysql', '-s', 'postgres'], $this->io);
  178. $this->assertEquals(
  179. [
  180. 'source' => ['mysql', 'postgres'],
  181. 'help' => false,
  182. ],
  183. $result[0],
  184. 'Short parameter did not parse out'
  185. );
  186. }
  187. /**
  188. * Test that adding an option using a two letter short value causes an exception.
  189. * As they will not parse correctly.
  190. */
  191. public function testAddOptionShortOneLetter(): void
  192. {
  193. $this->expectException(ConsoleException::class);
  194. $parser = new ConsoleOptionParser('test', false);
  195. $parser->addOption('test', ['short' => 'te']);
  196. }
  197. /**
  198. * test adding and using boolean options.
  199. */
  200. public function testAddOptionBoolean(): void
  201. {
  202. $parser = new ConsoleOptionParser('test', false);
  203. $parser->addOption('test', [
  204. 'boolean' => true,
  205. ]);
  206. $result = $parser->parse(['--test', 'value'], $this->io);
  207. $expected = [['test' => true, 'help' => false], ['value']];
  208. $this->assertEquals($expected, $result);
  209. $result = $parser->parse(['value'], $this->io);
  210. $expected = [['test' => false, 'help' => false], ['value']];
  211. $this->assertEquals($expected, $result);
  212. }
  213. /**
  214. * test adding an multiple shorts.
  215. */
  216. public function testAddOptionMultipleShort(): void
  217. {
  218. $parser = new ConsoleOptionParser('test', false);
  219. $parser->addOption('test', ['short' => 't', 'boolean' => true])
  220. ->addOption('file', ['short' => 'f', 'boolean' => true])
  221. ->addOption('output', ['short' => 'o', 'boolean' => true]);
  222. $result = $parser->parse(['-o', '-t', '-f'], $this->io);
  223. $expected = ['file' => true, 'test' => true, 'output' => true, 'help' => false];
  224. $this->assertEquals($expected, $result[0], 'Short parameter did not parse out');
  225. $result = $parser->parse(['-otf'], $this->io);
  226. $this->assertEquals($expected, $result[0], 'Short parameter did not parse out');
  227. }
  228. /**
  229. * test multiple options at once.
  230. */
  231. public function testAddOptionMultipleOptions(): void
  232. {
  233. $parser = new ConsoleOptionParser('test', false);
  234. $parser->addOption('test')
  235. ->addOption('connection')
  236. ->addOption('table', ['short' => 't', 'default' => true]);
  237. $result = $parser->parse(['--test', 'value', '-t', '--connection', 'postgres'], $this->io);
  238. $expected = ['test' => 'value', 'table' => true, 'connection' => 'postgres', 'help' => false];
  239. $this->assertEquals($expected, $result[0], 'multiple options did not parse');
  240. }
  241. /**
  242. * test adding an option that accepts multiple values.
  243. */
  244. public function testAddOptionWithMultiple(): void
  245. {
  246. $parser = new ConsoleOptionParser('test', false);
  247. $parser->addOption('source', ['short' => 's', 'multiple' => true]);
  248. $result = $parser->parse(['--source', 'mysql', '-s', 'postgres'], $this->io);
  249. $expected = [
  250. 'source' => [
  251. 'mysql',
  252. 'postgres',
  253. ],
  254. 'help' => false,
  255. ];
  256. $this->assertEquals($expected, $result[0], 'options with multiple values did not parse');
  257. }
  258. /**
  259. * test adding multiple options, including one that accepts multiple values.
  260. */
  261. public function testAddOptionMultipleOptionsWithMultiple(): void
  262. {
  263. $parser = new ConsoleOptionParser('test', false);
  264. $parser
  265. ->addOption('source', ['multiple' => true])
  266. ->addOption('name')
  267. ->addOption('export', ['boolean' => true]);
  268. $result = $parser->parse(['--export', '--source', 'mysql', '--name', 'annual-report', '--source', 'postgres'], $this->io);
  269. $expected = [
  270. 'export' => true,
  271. 'source' => [
  272. 'mysql',
  273. 'postgres',
  274. ],
  275. 'name' => 'annual-report',
  276. 'help' => false,
  277. ];
  278. $this->assertEquals($expected, $result[0], 'options with multiple values did not parse');
  279. }
  280. /**
  281. * test adding a required option with a default.
  282. */
  283. public function testAddOptionRequiredDefaultValue(): void
  284. {
  285. $parser = new ConsoleOptionParser('test', false);
  286. $parser
  287. ->addOption('test', [
  288. 'default' => 'default value',
  289. 'required' => true,
  290. ])
  291. ->addOption('no-default', [
  292. 'required' => true,
  293. ]);
  294. $result = $parser->parse(['--test', '--no-default', 'value'], $this->io);
  295. $this->assertSame(
  296. ['test' => 'default value', 'no-default' => 'value', 'help' => false],
  297. $result[0]
  298. );
  299. $result = $parser->parse(['--no-default', 'value'], $this->io);
  300. $this->assertSame(
  301. ['no-default' => 'value', 'help' => false, 'test' => 'default value'],
  302. $result[0]
  303. );
  304. }
  305. /**
  306. * test adding a required option that is missing.
  307. */
  308. public function testAddOptionRequiredMissing(): void
  309. {
  310. $parser = new ConsoleOptionParser('test', false);
  311. $parser
  312. ->addOption('test', [
  313. 'default' => 'default value',
  314. 'required' => true,
  315. ])
  316. ->addOption('no-default', [
  317. 'required' => true,
  318. ]);
  319. $this->expectException(ConsoleException::class);
  320. $parser->parse(['--test'], $this->io);
  321. }
  322. /**
  323. * test adding an option and prompting and optional options
  324. */
  325. public function testAddOptionWithPromptNoIo(): void
  326. {
  327. $parser = new ConsoleOptionParser('test', false);
  328. $parser->addOption('color', [
  329. 'prompt' => 'What is your favorite?',
  330. ]);
  331. $this->expectException(ConsoleException::class);
  332. $parser->parse([]);
  333. }
  334. /**
  335. * test adding an option and prompting and optional options
  336. */
  337. public function testAddOptionWithPrompt(): void
  338. {
  339. $parser = new ConsoleOptionParser('test', false);
  340. $parser->addOption('color', [
  341. 'prompt' => 'What is your favorite?',
  342. ]);
  343. $out = new StubConsoleOutput();
  344. $io = new ConsoleIo($out, new StubConsoleOutput(), new StubConsoleInput(['red']));
  345. $result = $parser->parse([], $io);
  346. $this->assertEquals(['color' => 'red', 'help' => false], $result[0]);
  347. $messages = $out->messages();
  348. $this->assertCount(1, $messages);
  349. $expected = "<question>What is your favorite?</question>\n> ";
  350. $this->assertEquals($expected, $messages[0]);
  351. }
  352. /**
  353. * test adding an option and default values
  354. */
  355. public function testAddOptionWithPromptAndDefault(): void
  356. {
  357. $parser = new ConsoleOptionParser('test', false);
  358. $this->expectException(ConsoleException::class);
  359. $this->expectExceptionMessage('You cannot set both `prompt` and `default`');
  360. $parser->addOption('color', [
  361. 'prompt' => 'What is your favorite?',
  362. 'default' => 'blue',
  363. ]);
  364. }
  365. /**
  366. * test adding an option and prompting with cli data
  367. */
  368. public function testAddOptionWithPromptAndProvidedValue(): void
  369. {
  370. $parser = new ConsoleOptionParser('test', false);
  371. $parser->addOption('color', [
  372. 'prompt' => 'What is your favorite?',
  373. ]);
  374. $out = new StubConsoleOutput();
  375. $io = new ConsoleIo($out, new StubConsoleOutput(), new StubConsoleInput([]));
  376. $result = $parser->parse(['--color', 'blue'], $io);
  377. $this->assertEquals(['color' => 'blue', 'help' => false], $result[0]);
  378. $this->assertCount(0, $out->messages());
  379. }
  380. /**
  381. * test adding an option and prompting and required options
  382. */
  383. public function testAddOptionWithPromptAndRequired(): void
  384. {
  385. $parser = new ConsoleOptionParser('test', false);
  386. $parser->addOption('color', [
  387. 'required' => true,
  388. 'prompt' => 'What is your favorite?',
  389. ]);
  390. $out = new StubConsoleOutput();
  391. $io = new ConsoleIo($out, new StubConsoleOutput(), new StubConsoleInput(['red']));
  392. $result = $parser->parse([], $io);
  393. $this->assertEquals(['color' => 'red', 'help' => false], $result[0]);
  394. $messages = $out->messages();
  395. $this->assertCount(1, $messages);
  396. $expected = "<question>What is your favorite?</question>\n> ";
  397. $this->assertEquals($expected, $messages[0]);
  398. }
  399. /**
  400. * test adding an option and prompting for additional values.
  401. */
  402. public function testAddOptionWithPromptAndOptions(): void
  403. {
  404. $parser = new ConsoleOptionParser('test', false);
  405. $parser->addOption('color', [
  406. 'required' => true,
  407. 'prompt' => 'What is your favorite?',
  408. 'choices' => ['red', 'green', 'blue'],
  409. ]);
  410. $out = new StubConsoleOutput();
  411. $io = new ConsoleIo($out, new StubConsoleOutput(), new StubConsoleInput(['purple', 'red']));
  412. $result = $parser->parse([], $io);
  413. $this->assertEquals(['color' => 'red', 'help' => false], $result[0]);
  414. $messages = $out->messages();
  415. $this->assertCount(2, $messages);
  416. $expected = "<question>What is your favorite?</question> (red/green/blue) \n> ";
  417. $this->assertEquals($expected, $messages[0]);
  418. $this->assertEquals($expected, $messages[1]);
  419. }
  420. /**
  421. * Test adding multiple options.
  422. */
  423. public function testAddOptions(): void
  424. {
  425. $parser = new ConsoleOptionParser('something', false);
  426. $result = $parser->addOptions([
  427. 'name' => ['help' => 'The name'],
  428. 'other' => ['help' => 'The other arg'],
  429. ]);
  430. $this->assertEquals($parser, $result, 'addOptions is not chainable.');
  431. $result = $parser->options();
  432. $this->assertCount(3, $result, 'Not enough options');
  433. }
  434. /**
  435. * test that boolean options work
  436. */
  437. public function testOptionWithBooleanParam(): void
  438. {
  439. $parser = new ConsoleOptionParser('test', false);
  440. $parser->addOption('no-commit', ['boolean' => true])
  441. ->addOption('table', ['short' => 't']);
  442. $result = $parser->parse(['--table', 'posts', '--no-commit', 'arg1', 'arg2'], $this->io);
  443. $expected = [['table' => 'posts', 'no-commit' => true, 'help' => false], ['arg1', 'arg2']];
  444. $this->assertEquals($expected, $result, 'Boolean option did not parse correctly.');
  445. }
  446. /**
  447. * test parsing options that do not exist.
  448. */
  449. public function testOptionThatDoesNotExist(): void
  450. {
  451. $parser = new ConsoleOptionParser('test', false);
  452. $parser->addOption('no-commit', ['boolean' => true]);
  453. try {
  454. $parser->parse(['--he', 'other'], $this->io);
  455. } catch (MissingOptionException $e) {
  456. $this->assertStringContainsString(
  457. "Unknown option `he`.\n" .
  458. "Did you mean: `help`?\n" .
  459. "\n" .
  460. "Other valid choices:\n" .
  461. "\n" .
  462. '- help',
  463. $e->getFullMessage()
  464. );
  465. }
  466. }
  467. /**
  468. * test parsing short options that do not exist.
  469. */
  470. public function testShortOptionThatDoesNotExist(): void
  471. {
  472. $parser = new ConsoleOptionParser('test', false);
  473. $parser->addOption('no-commit', ['boolean' => true, 'short' => 'n']);
  474. $parser->addOption('construct', ['boolean' => true]);
  475. $parser->addOption('clear', ['boolean' => true, 'short' => 'c']);
  476. try {
  477. $parser->parse(['-f'], $this->io);
  478. } catch (MissingOptionException $e) {
  479. $this->assertStringContainsString('Unknown short option `f`.', $e->getFullMessage());
  480. }
  481. }
  482. /**
  483. * test that options with choices enforce them.
  484. */
  485. public function testOptionWithChoices(): void
  486. {
  487. $this->expectException(ConsoleException::class);
  488. $parser = new ConsoleOptionParser('test', false);
  489. $parser->addOption('name', ['choices' => ['mark', 'jose']]);
  490. $result = $parser->parse(['--name', 'mark'], $this->io);
  491. $expected = ['name' => 'mark', 'help' => false];
  492. $this->assertEquals($expected, $result[0], 'Got the correct value.');
  493. $result = $parser->parse(['--name', 'jimmy'], $this->io);
  494. }
  495. /**
  496. * Ensure that option values can start with -
  497. */
  498. public function testOptionWithValueStartingWithMinus(): void
  499. {
  500. $parser = new ConsoleOptionParser('test', false);
  501. $parser->addOption('name')
  502. ->addOption('age');
  503. $result = $parser->parse(['--name', '-foo', '--age', 'old'], $this->io);
  504. $expected = ['name' => '-foo', 'age' => 'old', 'help' => false];
  505. $this->assertEquals($expected, $result[0], 'Option values starting with "-" are broken.');
  506. }
  507. /**
  508. * test positional argument parsing.
  509. */
  510. public function testAddArgument(): void
  511. {
  512. $parser = new ConsoleOptionParser('test', false);
  513. $result = $parser->addArgument('name', ['help' => 'An argument']);
  514. $this->assertEquals($parser, $result, 'Should return this');
  515. }
  516. /**
  517. * Add arguments that were once considered the same
  518. */
  519. public function testAddArgumentDuplicate(): void
  520. {
  521. $parser = new ConsoleOptionParser('test', false);
  522. $parser
  523. ->addArgument('first', ['help' => 'An argument', 'choices' => [1, 2]])
  524. ->addArgument('second', ['help' => 'An argument', 'choices' => [1, 2]]);
  525. $args = $parser->arguments();
  526. $this->assertCount(2, $args);
  527. $this->assertEquals('first', $args[0]->name());
  528. $this->assertEquals('second', $args[1]->name());
  529. }
  530. /**
  531. * test addOption with an object.
  532. */
  533. public function testAddArgumentObject(): void
  534. {
  535. $parser = new ConsoleOptionParser('test', false);
  536. $parser->addArgument(new ConsoleInputArgument('test'));
  537. $result = $parser->arguments();
  538. $this->assertCount(1, $result);
  539. $this->assertSame('test', $result[0]->name());
  540. }
  541. /**
  542. * Test adding arguments out of order.
  543. */
  544. public function testAddArgumentOutOfOrder(): void
  545. {
  546. $parser = new ConsoleOptionParser('test', false);
  547. $parser->addArgument('name', ['index' => 1, 'help' => 'first argument'])
  548. ->addArgument('bag', ['index' => 2, 'help' => 'second argument'])
  549. ->addArgument('other', ['index' => 0, 'help' => 'Zeroth argument']);
  550. $result = $parser->arguments();
  551. $this->assertCount(3, $result);
  552. $this->assertSame('other', $result[0]->name());
  553. $this->assertSame('name', $result[1]->name());
  554. $this->assertSame('bag', $result[2]->name());
  555. $this->assertSame([0, 1, 2], array_keys($result));
  556. $this->assertEquals(
  557. ['other', 'name', 'bag'],
  558. $parser->argumentNames()
  559. );
  560. }
  561. /**
  562. * test overwriting positional arguments.
  563. */
  564. public function testPositionalArgOverwrite(): void
  565. {
  566. $parser = new ConsoleOptionParser('test', false);
  567. $parser->addArgument('name', ['help' => 'An argument'])
  568. ->addArgument('other', ['index' => 0]);
  569. $result = $parser->arguments();
  570. $this->assertCount(1, $result, 'Overwrite did not occur');
  571. }
  572. /**
  573. * test parsing arguments.
  574. */
  575. public function testParseArgumentTooMany(): void
  576. {
  577. $this->expectException(ConsoleException::class);
  578. $parser = new ConsoleOptionParser('test', false);
  579. $parser->addArgument('name', ['help' => 'An argument'])
  580. ->addArgument('other');
  581. $expected = ['one', 'two'];
  582. $result = $parser->parse($expected, $this->io);
  583. $this->assertEquals($expected, $result[1], 'Arguments are not as expected');
  584. $result = $parser->parse(['one', 'two', 'three'], $this->io);
  585. }
  586. /**
  587. * test parsing arguments with 0 value.
  588. */
  589. public function testParseArgumentZero(): void
  590. {
  591. $parser = new ConsoleOptionParser('test', false);
  592. $expected = ['one', 'two', 0, 'after', 'zero'];
  593. $result = $parser->parse($expected, $this->io);
  594. $this->assertEquals($expected, $result[1], 'Arguments are not as expected');
  595. }
  596. /**
  597. * test that when there are not enough arguments an exception is raised
  598. */
  599. public function testPositionalArgNotEnough(): void
  600. {
  601. $this->expectException(ConsoleException::class);
  602. $parser = new ConsoleOptionParser('test', false);
  603. $parser->addArgument('name', ['required' => true])
  604. ->addArgument('other', ['required' => true]);
  605. $parser->parse(['one'], $this->io);
  606. }
  607. /**
  608. * test that when there are required arguments after optional ones an exception is raised
  609. */
  610. public function testPositionalArgRequiredAfterOptional(): void
  611. {
  612. $this->expectException(LogicException::class);
  613. $parser = new ConsoleOptionParser('test');
  614. $parser->addArgument('name', ['required' => false])
  615. ->addArgument('other', ['required' => true]);
  616. }
  617. /**
  618. * test that arguments with choices enforce them.
  619. */
  620. public function testPositionalArgWithChoices(): void
  621. {
  622. $this->expectException(ConsoleException::class);
  623. $parser = new ConsoleOptionParser('test', false);
  624. $parser->addArgument('name', ['choices' => ['mark', 'jose']])
  625. ->addArgument('alias', ['choices' => ['cowboy', 'samurai']])
  626. ->addArgument('weapon', ['choices' => ['gun', 'sword']]);
  627. $result = $parser->parse(['mark', 'samurai', 'sword'], $this->io);
  628. $expected = ['mark', 'samurai', 'sword'];
  629. $this->assertEquals($expected, $result[1], 'Got the correct value.');
  630. $result = $parser->parse(['jose', 'coder'], $this->io);
  631. }
  632. /**
  633. * Test adding multiple arguments.
  634. */
  635. public function testAddArguments(): void
  636. {
  637. $parser = new ConsoleOptionParser('test', false);
  638. $result = $parser->addArguments([
  639. 'name' => ['help' => 'The name'],
  640. 'other' => ['help' => 'The other arg'],
  641. ]);
  642. $this->assertEquals($parser, $result, 'addArguments is not chainable.');
  643. $result = $parser->arguments();
  644. $this->assertCount(2, $result, 'Not enough arguments');
  645. }
  646. /**
  647. * test setting a subcommand up.
  648. */
  649. public function testSubcommand(): void
  650. {
  651. $parser = new ConsoleOptionParser('test', false);
  652. $result = $parser->addSubcommand('initdb', [
  653. 'help' => 'Initialize the database',
  654. ]);
  655. $this->assertEquals($parser, $result, 'Adding a subcommand is not chainable');
  656. }
  657. /**
  658. * Tests setting a subcommand up for a Shell method `initMyDb`.
  659. */
  660. public function testSubcommandCamelBacked(): void
  661. {
  662. $parser = new ConsoleOptionParser('test', false);
  663. $result = $parser->addSubcommand('initMyDb', [
  664. 'help' => 'Initialize the database',
  665. ]);
  666. $subcommands = array_keys($result->subcommands());
  667. $this->assertEquals(['init_my_db'], $subcommands, 'Adding a subcommand does not work with camel backed method names.');
  668. }
  669. /**
  670. * Test addSubcommand inherits options as no
  671. * parser is created.
  672. */
  673. public function testAddSubcommandInheritOptions(): void
  674. {
  675. $parser = new ConsoleOptionParser('test', false);
  676. $parser->addSubcommand('build', [
  677. 'help' => 'Build things',
  678. ])->addOption('connection', [
  679. 'short' => 'c',
  680. 'default' => 'default',
  681. ])->addArgument('name', ['required' => false]);
  682. $result = $parser->parse(['build'], $this->io);
  683. $this->assertSame('default', $result[0]['connection']);
  684. $result = $parser->subcommands();
  685. $this->assertArrayHasKey('build', $result);
  686. $this->assertNull($result['build']->parser(), 'No parser should be created');
  687. }
  688. /**
  689. * test addSubcommand with an object.
  690. */
  691. public function testAddSubcommandObject(): void
  692. {
  693. $parser = new ConsoleOptionParser('test', false);
  694. $parser->addSubcommand(new ConsoleInputSubcommand('test'));
  695. $result = $parser->subcommands();
  696. $this->assertCount(1, $result);
  697. $this->assertSame('test', $result['test']->name());
  698. }
  699. /**
  700. * test addSubcommand without sorting applied.
  701. */
  702. public function testAddSubcommandSort(): void
  703. {
  704. $parser = new ConsoleOptionParser('test', false);
  705. $this->assertTrue($parser->isSubcommandSortEnabled());
  706. $parser->enableSubcommandSort(false);
  707. $this->assertFalse($parser->isSubcommandSortEnabled());
  708. $parser->addSubcommand(new ConsoleInputSubcommand('betaTest'), []);
  709. $parser->addSubcommand(new ConsoleInputSubcommand('alphaTest'), []);
  710. $result = $parser->subcommands();
  711. $this->assertCount(2, $result);
  712. $firstResult = key($result);
  713. $this->assertSame('betaTest', $firstResult);
  714. }
  715. /**
  716. * test removeSubcommand with an object.
  717. */
  718. public function testRemoveSubcommand(): void
  719. {
  720. $parser = new ConsoleOptionParser('test', false);
  721. $parser->addSubcommand(new ConsoleInputSubcommand('test'));
  722. $result = $parser->subcommands();
  723. $this->assertCount(1, $result);
  724. $parser->removeSubcommand('test');
  725. $result = $parser->subcommands();
  726. $this->assertCount(0, $result, 'Remove a subcommand does not work');
  727. }
  728. /**
  729. * test adding multiple subcommands
  730. */
  731. public function testAddSubcommands(): void
  732. {
  733. $parser = new ConsoleOptionParser('test', false);
  734. $result = $parser->addSubcommands([
  735. 'initdb' => ['help' => 'Initialize the database'],
  736. 'create' => ['help' => 'Create something'],
  737. ]);
  738. $this->assertEquals($parser, $result, 'Adding a subcommands is not chainable');
  739. $result = $parser->subcommands();
  740. $this->assertCount(2, $result, 'Not enough subcommands');
  741. }
  742. /**
  743. * test that no exception is triggered for required arguments when help is being generated
  744. */
  745. public function testHelpNoExceptionForRequiredArgumentsWhenGettingHelp(): void
  746. {
  747. $parser = new ConsoleOptionParser('mycommand', false);
  748. $parser->addOption('test', ['help' => 'A test option.'])
  749. ->addArgument('model', ['help' => 'The model to make.', 'required' => true]);
  750. $result = $parser->parse(['--help'], $this->io);
  751. $this->assertTrue($result[0]['help']);
  752. }
  753. /**
  754. * test that no exception is triggered for required options when help is being generated
  755. */
  756. public function testHelpNoExceptionForRequiredOptionsWhenGettingHelp(): void
  757. {
  758. $parser = new ConsoleOptionParser('mycommand', false);
  759. $parser->addOption('test', ['help' => 'A test option.'])
  760. ->addOption('model', ['help' => 'The model to make.', 'required' => true]);
  761. $result = $parser->parse(['--help']);
  762. $this->assertTrue($result[0]['help']);
  763. }
  764. /**
  765. * test that help() with a command param shows the help for a subcommand
  766. */
  767. public function testHelpSubcommandHelp(): void
  768. {
  769. $subParser = new ConsoleOptionParser('method', false);
  770. $subParser->addOption('connection', ['help' => 'Db connection.']);
  771. $subParser->addOption('zero', ['short' => '0', 'help' => 'Zero.']);
  772. $parser = new ConsoleOptionParser('mycommand', false);
  773. $parser->addSubcommand('method', [
  774. 'help' => 'This is another command',
  775. 'parser' => $subParser,
  776. ])
  777. ->addOption('test', ['help' => 'A test option.']);
  778. $result = $parser->help('method');
  779. $expected = <<<TEXT
  780. This is another command
  781. <info>Usage:</info>
  782. cake mycommand method [--connection] [-h] [-0]
  783. <info>Options:</info>
  784. --connection Db connection.
  785. --help, -h Display this help.
  786. --zero, -0 Zero.
  787. TEXT;
  788. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  789. }
  790. /**
  791. * Test addSubcommand inherits options as no
  792. * parser is created.
  793. */
  794. public function testHelpSubcommandInheritOptions(): void
  795. {
  796. $parser = new ConsoleOptionParser('mycommand', false);
  797. $parser->addSubcommand('build', [
  798. 'help' => 'Build things.',
  799. ])->addSubcommand('destroy', [
  800. 'help' => 'Destroy things.',
  801. ])->addOption('connection', [
  802. 'help' => 'Db connection.',
  803. 'short' => 'c',
  804. ])->addArgument('name', ['required' => false]);
  805. $result = $parser->help('build');
  806. $expected = <<<TEXT
  807. Build things.
  808. <info>Usage:</info>
  809. cake mycommand build [-c] [-h] [-q] [-v] [<name>]
  810. <info>Options:</info>
  811. --connection, -c Db connection.
  812. --help, -h Display this help.
  813. --quiet, -q Enable quiet output.
  814. --verbose, -v Enable verbose output.
  815. <info>Arguments:</info>
  816. name <comment>(optional)</comment>
  817. TEXT;
  818. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  819. }
  820. /**
  821. * test that help() with a command param shows the help for a subcommand
  822. */
  823. public function testHelpSubcommandHelpArray(): void
  824. {
  825. $subParser = [
  826. 'options' => [
  827. 'foo' => [
  828. 'short' => 'f',
  829. 'help' => 'Foo.',
  830. 'boolean' => true,
  831. ],
  832. ],
  833. ];
  834. $parser = new ConsoleOptionParser('mycommand', false);
  835. $parser->addSubcommand('method', [
  836. 'help' => 'This is a subcommand',
  837. 'parser' => $subParser,
  838. ])
  839. ->setRootName('tool')
  840. ->addOption('test', ['help' => 'A test option.']);
  841. $result = $parser->help('method');
  842. $expected = <<<TEXT
  843. This is a subcommand
  844. <info>Usage:</info>
  845. tool mycommand method [-f] [-h] [-q] [-v]
  846. <info>Options:</info>
  847. --foo, -f Foo.
  848. --help, -h Display this help.
  849. --quiet, -q Enable quiet output.
  850. --verbose, -v Enable verbose output.
  851. TEXT;
  852. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  853. }
  854. /**
  855. * test that help() with a command param shows the help for a subcommand
  856. */
  857. public function testHelpSubcommandInheritParser(): void
  858. {
  859. $subParser = new ConsoleOptionParser('method', false);
  860. $subParser->addOption('connection', ['help' => 'Db connection.']);
  861. $subParser->addOption('zero', ['short' => '0', 'help' => 'Zero.']);
  862. $parser = new ConsoleOptionParser('mycommand', false);
  863. $parser->addSubcommand('method', [
  864. 'help' => 'This is another command',
  865. 'parser' => $subParser,
  866. ])
  867. ->addOption('test', ['help' => 'A test option.']);
  868. $result = $parser->help('method');
  869. $expected = <<<TEXT
  870. This is another command
  871. <info>Usage:</info>
  872. cake mycommand method [--connection] [-h] [-0]
  873. <info>Options:</info>
  874. --connection Db connection.
  875. --help, -h Display this help.
  876. --zero, -0 Zero.
  877. TEXT;
  878. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  879. }
  880. /**
  881. * test that help() with a custom rootName
  882. */
  883. public function testHelpWithRootName(): void
  884. {
  885. $parser = new ConsoleOptionParser('sample', false);
  886. $parser->setDescription('A command!')
  887. ->setRootName('tool')
  888. ->addOption('test', ['help' => 'A test option.'])
  889. ->addOption('reqd', ['help' => 'A required option.', 'required' => true]);
  890. $result = $parser->help();
  891. $expected = <<<TEXT
  892. A command!
  893. <info>Usage:</info>
  894. tool sample [-h] --reqd [--test]
  895. <info>Options:</info>
  896. --help, -h Display this help.
  897. --reqd A required option. <comment>(required)</comment>
  898. --test A test option.
  899. TEXT;
  900. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  901. }
  902. /**
  903. * test that getCommandError() with an unknown subcommand param shows a helpful message
  904. */
  905. public function testHelpUnknownSubcommand(): void
  906. {
  907. $subParser = [
  908. 'options' => [
  909. 'foo' => [
  910. 'short' => 'f',
  911. 'help' => 'Foo.',
  912. 'boolean' => true,
  913. ],
  914. ],
  915. ];
  916. $parser = new ConsoleOptionParser('mycommand', false);
  917. $parser
  918. ->addSubcommand('method', [
  919. 'help' => 'This is a subcommand',
  920. 'parser' => $subParser,
  921. ])
  922. ->addOption('test', ['help' => 'A test option.'])
  923. ->addSubcommand('unstash');
  924. try {
  925. $result = $parser->help('unknown');
  926. } catch (MissingOptionException $e) {
  927. $result = $e->getFullMessage();
  928. $this->assertStringContainsString(
  929. "Unable to find the `mycommand unknown` subcommand. See `bin/cake mycommand --help`.\n" .
  930. "\n" .
  931. "Other valid choices:\n" .
  932. "\n" .
  933. '- method',
  934. $result
  935. );
  936. }
  937. }
  938. /**
  939. * test building a parser from an array.
  940. */
  941. public function testBuildFromArray(): void
  942. {
  943. $spec = [
  944. 'command' => 'test',
  945. 'arguments' => [
  946. 'name' => ['help' => 'The name'],
  947. 'other' => ['help' => 'The other arg'],
  948. ],
  949. 'options' => [
  950. 'name' => ['help' => 'The name'],
  951. 'other' => ['help' => 'The other arg'],
  952. ],
  953. 'subcommands' => [
  954. 'initdb' => ['help' => 'make database'],
  955. ],
  956. 'description' => 'description text',
  957. 'epilog' => 'epilog text',
  958. ];
  959. $parser = ConsoleOptionParser::buildFromArray($spec);
  960. $this->assertSame($spec['description'], $parser->getDescription());
  961. $this->assertSame($spec['epilog'], $parser->getEpilog());
  962. $options = $parser->options();
  963. $this->assertArrayHasKey('name', $options);
  964. $this->assertArrayHasKey('other', $options);
  965. $args = $parser->arguments();
  966. $this->assertCount(2, $args);
  967. $commands = $parser->subcommands();
  968. $this->assertCount(1, $commands);
  969. }
  970. /**
  971. * test that create() returns instances
  972. */
  973. public function testCreateFactory(): void
  974. {
  975. $parser = ConsoleOptionParser::create('factory', false);
  976. $this->assertInstanceOf('Cake\Console\ConsoleOptionParser', $parser);
  977. $this->assertSame('factory', $parser->getCommand());
  978. }
  979. /**
  980. * test that getCommand() inflects the command name.
  981. */
  982. public function testCommandInflection(): void
  983. {
  984. $parser = new ConsoleOptionParser('CommandLine');
  985. $this->assertSame('command_line', $parser->getCommand());
  986. }
  987. /**
  988. * test that parse() takes a subcommand argument, and that the subcommand parser
  989. * is used.
  990. */
  991. public function testParsingWithSubParser(): void
  992. {
  993. $parser = new ConsoleOptionParser('test', false);
  994. $parser->addOption('primary')
  995. ->addArgument('one', ['required' => true, 'choices' => ['a', 'b']])
  996. ->addArgument('two', ['required' => true])
  997. ->addSubcommand('sub', [
  998. 'parser' => [
  999. 'options' => [
  1000. 'secondary' => ['boolean' => true],
  1001. 'fourth' => ['help' => 'fourth option'],
  1002. ],
  1003. 'arguments' => [
  1004. 'sub_arg' => ['choices' => ['c', 'd']],
  1005. ],
  1006. ],
  1007. ]);
  1008. $result = $parser->parse(['sub', '--secondary', '--fourth', '4', 'c'], $this->io);
  1009. $expected = [[
  1010. 'secondary' => true,
  1011. 'fourth' => '4',
  1012. 'help' => false,
  1013. 'verbose' => false,
  1014. 'quiet' => false], ['c']];
  1015. $this->assertEquals($expected, $result, 'Sub parser did not parse request.');
  1016. }
  1017. /**
  1018. * Tests toArray()
  1019. */
  1020. public function testToArray(): void
  1021. {
  1022. $spec = [
  1023. 'command' => 'test',
  1024. 'arguments' => [
  1025. 'name' => ['help' => 'The name'],
  1026. 'other' => ['help' => 'The other arg'],
  1027. ],
  1028. 'options' => [
  1029. 'name' => ['help' => 'The name'],
  1030. 'other' => ['help' => 'The other arg'],
  1031. ],
  1032. 'subcommands' => [
  1033. 'initdb' => ['help' => 'make database'],
  1034. ],
  1035. 'description' => 'description text',
  1036. 'epilog' => 'epilog text',
  1037. ];
  1038. $parser = ConsoleOptionParser::buildFromArray($spec);
  1039. $result = $parser->toArray();
  1040. $this->assertSame($spec['description'], $result['description']);
  1041. $this->assertSame($spec['epilog'], $result['epilog']);
  1042. $options = $result['options'];
  1043. $this->assertArrayHasKey('name', $options);
  1044. $this->assertArrayHasKey('other', $options);
  1045. $this->assertCount(2, $result['arguments']);
  1046. $this->assertCount(1, $result['subcommands']);
  1047. }
  1048. /**
  1049. * Tests merge()
  1050. */
  1051. public function testMerge(): void
  1052. {
  1053. $parser = new ConsoleOptionParser('test');
  1054. $parser->addOption('test', ['short' => 't', 'boolean' => true])
  1055. ->addArgument('one', ['required' => true, 'choices' => ['a', 'b']])
  1056. ->addArgument('two', ['required' => true]);
  1057. $parserTwo = new ConsoleOptionParser('test');
  1058. $parserTwo->addOption('file', ['short' => 'f', 'boolean' => true])
  1059. ->addOption('output', ['short' => 'o', 'boolean' => true])
  1060. ->addArgument('one', ['required' => true, 'choices' => ['a', 'b']]);
  1061. $parser->merge($parserTwo);
  1062. $result = $parser->toArray();
  1063. $options = $result['options'];
  1064. $this->assertArrayHasKey('quiet', $options);
  1065. $this->assertArrayHasKey('test', $options);
  1066. $this->assertArrayHasKey('file', $options);
  1067. $this->assertArrayHasKey('output', $options);
  1068. $this->assertCount(2, $result['arguments']);
  1069. $this->assertCount(6, $result['options']);
  1070. }
  1071. }