ConsoleOptionParserTest.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248
  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. public function testParseArgumentsDoubleDash(): void
  647. {
  648. $parser = new ConsoleOptionParser('test');
  649. $result = $parser->parse(['one', 'two', '--', '-h', '--help', '--test=value'], $this->io);
  650. $this->assertEquals(['one', 'two', '-h', '--help', '--test=value'], $result[1]);
  651. }
  652. public function testParseArgumentsOptionsDoubleDash(): void
  653. {
  654. $parser = new ConsoleOptionParser('test', false);
  655. $parser->addOption('test');
  656. $result = $parser->parse(['--test=value', '--', '--test'], $this->io);
  657. $this->assertEquals(['test' => 'value', 'help' => false], $result[0]);
  658. $this->assertEquals(['--test'], $result[1]);
  659. $result = $parser->parse(['--', '--test'], $this->io);
  660. $this->assertEquals(['help' => false], $result[0]);
  661. $this->assertEquals(['--test'], $result[1]);
  662. }
  663. /**
  664. * test setting a subcommand up.
  665. */
  666. public function testSubcommand(): void
  667. {
  668. $parser = new ConsoleOptionParser('test', false);
  669. $result = $parser->addSubcommand('initdb', [
  670. 'help' => 'Initialize the database',
  671. ]);
  672. $this->assertEquals($parser, $result, 'Adding a subcommand is not chainable');
  673. }
  674. /**
  675. * Tests setting a subcommand up for a Shell method `initMyDb`.
  676. */
  677. public function testSubcommandCamelBacked(): void
  678. {
  679. $parser = new ConsoleOptionParser('test', false);
  680. $result = $parser->addSubcommand('initMyDb', [
  681. 'help' => 'Initialize the database',
  682. ]);
  683. $subcommands = array_keys($result->subcommands());
  684. $this->assertEquals(['init_my_db'], $subcommands, 'Adding a subcommand does not work with camel backed method names.');
  685. }
  686. /**
  687. * Test addSubcommand inherits options as no
  688. * parser is created.
  689. */
  690. public function testAddSubcommandInheritOptions(): void
  691. {
  692. $parser = new ConsoleOptionParser('test', false);
  693. $parser->addSubcommand('build', [
  694. 'help' => 'Build things',
  695. ])->addOption('connection', [
  696. 'short' => 'c',
  697. 'default' => 'default',
  698. ])->addArgument('name', ['required' => false]);
  699. $result = $parser->parse(['build'], $this->io);
  700. $this->assertSame('default', $result[0]['connection']);
  701. $result = $parser->subcommands();
  702. $this->assertArrayHasKey('build', $result);
  703. $this->assertNull($result['build']->parser(), 'No parser should be created');
  704. }
  705. /**
  706. * test addSubcommand with an object.
  707. */
  708. public function testAddSubcommandObject(): void
  709. {
  710. $parser = new ConsoleOptionParser('test', false);
  711. $parser->addSubcommand(new ConsoleInputSubcommand('test'));
  712. $result = $parser->subcommands();
  713. $this->assertCount(1, $result);
  714. $this->assertSame('test', $result['test']->name());
  715. }
  716. /**
  717. * test addSubcommand without sorting applied.
  718. */
  719. public function testAddSubcommandSort(): void
  720. {
  721. $parser = new ConsoleOptionParser('test', false);
  722. $this->assertTrue($parser->isSubcommandSortEnabled());
  723. $parser->enableSubcommandSort(false);
  724. $this->assertFalse($parser->isSubcommandSortEnabled());
  725. $parser->addSubcommand(new ConsoleInputSubcommand('betaTest'), []);
  726. $parser->addSubcommand(new ConsoleInputSubcommand('alphaTest'), []);
  727. $result = $parser->subcommands();
  728. $this->assertCount(2, $result);
  729. $firstResult = key($result);
  730. $this->assertSame('betaTest', $firstResult);
  731. }
  732. /**
  733. * test removeSubcommand with an object.
  734. */
  735. public function testRemoveSubcommand(): void
  736. {
  737. $parser = new ConsoleOptionParser('test', false);
  738. $parser->addSubcommand(new ConsoleInputSubcommand('test'));
  739. $result = $parser->subcommands();
  740. $this->assertCount(1, $result);
  741. $parser->removeSubcommand('test');
  742. $result = $parser->subcommands();
  743. $this->assertCount(0, $result, 'Remove a subcommand does not work');
  744. }
  745. /**
  746. * test adding multiple subcommands
  747. */
  748. public function testAddSubcommands(): void
  749. {
  750. $parser = new ConsoleOptionParser('test', false);
  751. $result = $parser->addSubcommands([
  752. 'initdb' => ['help' => 'Initialize the database'],
  753. 'create' => ['help' => 'Create something'],
  754. ]);
  755. $this->assertEquals($parser, $result, 'Adding a subcommands is not chainable');
  756. $result = $parser->subcommands();
  757. $this->assertCount(2, $result, 'Not enough subcommands');
  758. }
  759. /**
  760. * test that no exception is triggered for required arguments when help is being generated
  761. */
  762. public function testHelpNoExceptionForRequiredArgumentsWhenGettingHelp(): void
  763. {
  764. $parser = new ConsoleOptionParser('mycommand', false);
  765. $parser->addOption('test', ['help' => 'A test option.'])
  766. ->addArgument('model', ['help' => 'The model to make.', 'required' => true]);
  767. $result = $parser->parse(['--help'], $this->io);
  768. $this->assertTrue($result[0]['help']);
  769. }
  770. /**
  771. * test that no exception is triggered for required options when help is being generated
  772. */
  773. public function testHelpNoExceptionForRequiredOptionsWhenGettingHelp(): void
  774. {
  775. $parser = new ConsoleOptionParser('mycommand', false);
  776. $parser->addOption('test', ['help' => 'A test option.'])
  777. ->addOption('model', ['help' => 'The model to make.', 'required' => true]);
  778. $result = $parser->parse(['--help']);
  779. $this->assertTrue($result[0]['help']);
  780. }
  781. /**
  782. * test that help() with a command param shows the help for a subcommand
  783. */
  784. public function testHelpSubcommandHelp(): void
  785. {
  786. $subParser = new ConsoleOptionParser('method', false);
  787. $subParser->addOption('connection', ['help' => 'Db connection.']);
  788. $subParser->addOption('zero', ['short' => '0', 'help' => 'Zero.']);
  789. $parser = new ConsoleOptionParser('mycommand', false);
  790. $parser->addSubcommand('method', [
  791. 'help' => 'This is another command',
  792. 'parser' => $subParser,
  793. ])
  794. ->addOption('test', ['help' => 'A test option.']);
  795. $result = $parser->help('method');
  796. $expected = <<<TEXT
  797. This is another command
  798. <info>Usage:</info>
  799. cake mycommand method [--connection] [-h] [-0]
  800. <info>Options:</info>
  801. --connection Db connection.
  802. --help, -h Display this help.
  803. --zero, -0 Zero.
  804. TEXT;
  805. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  806. }
  807. /**
  808. * Test addSubcommand inherits options as no
  809. * parser is created.
  810. */
  811. public function testHelpSubcommandInheritOptions(): void
  812. {
  813. $parser = new ConsoleOptionParser('mycommand', false);
  814. $parser->addSubcommand('build', [
  815. 'help' => 'Build things.',
  816. ])->addSubcommand('destroy', [
  817. 'help' => 'Destroy things.',
  818. ])->addOption('connection', [
  819. 'help' => 'Db connection.',
  820. 'short' => 'c',
  821. ])->addArgument('name', ['required' => false]);
  822. $result = $parser->help('build');
  823. $expected = <<<TEXT
  824. Build things.
  825. <info>Usage:</info>
  826. cake mycommand build [-c] [-h] [-q] [-v] [<name>]
  827. <info>Options:</info>
  828. --connection, -c Db connection.
  829. --help, -h Display this help.
  830. --quiet, -q Enable quiet output.
  831. --verbose, -v Enable verbose output.
  832. <info>Arguments:</info>
  833. name <comment>(optional)</comment>
  834. TEXT;
  835. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  836. }
  837. /**
  838. * test that help() with a command param shows the help for a subcommand
  839. */
  840. public function testHelpSubcommandHelpArray(): void
  841. {
  842. $subParser = [
  843. 'options' => [
  844. 'foo' => [
  845. 'short' => 'f',
  846. 'help' => 'Foo.',
  847. 'boolean' => true,
  848. ],
  849. ],
  850. ];
  851. $parser = new ConsoleOptionParser('mycommand', false);
  852. $parser->addSubcommand('method', [
  853. 'help' => 'This is a subcommand',
  854. 'parser' => $subParser,
  855. ])
  856. ->setRootName('tool')
  857. ->addOption('test', ['help' => 'A test option.']);
  858. $result = $parser->help('method');
  859. $expected = <<<TEXT
  860. This is a subcommand
  861. <info>Usage:</info>
  862. tool mycommand method [-f] [-h] [-q] [-v]
  863. <info>Options:</info>
  864. --foo, -f Foo.
  865. --help, -h Display this help.
  866. --quiet, -q Enable quiet output.
  867. --verbose, -v Enable verbose output.
  868. TEXT;
  869. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  870. }
  871. /**
  872. * test that help() with a command param shows the help for a subcommand
  873. */
  874. public function testHelpSubcommandInheritParser(): void
  875. {
  876. $subParser = new ConsoleOptionParser('method', false);
  877. $subParser->addOption('connection', ['help' => 'Db connection.']);
  878. $subParser->addOption('zero', ['short' => '0', 'help' => 'Zero.']);
  879. $parser = new ConsoleOptionParser('mycommand', false);
  880. $parser->addSubcommand('method', [
  881. 'help' => 'This is another command',
  882. 'parser' => $subParser,
  883. ])
  884. ->addOption('test', ['help' => 'A test option.']);
  885. $result = $parser->help('method');
  886. $expected = <<<TEXT
  887. This is another command
  888. <info>Usage:</info>
  889. cake mycommand method [--connection] [-h] [-0]
  890. <info>Options:</info>
  891. --connection Db connection.
  892. --help, -h Display this help.
  893. --zero, -0 Zero.
  894. TEXT;
  895. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  896. }
  897. /**
  898. * test that help() with a custom rootName
  899. */
  900. public function testHelpWithRootName(): void
  901. {
  902. $parser = new ConsoleOptionParser('sample', false);
  903. $parser->setDescription('A command!')
  904. ->setRootName('tool')
  905. ->addOption('test', ['help' => 'A test option.'])
  906. ->addOption('reqd', ['help' => 'A required option.', 'required' => true]);
  907. $result = $parser->help();
  908. $expected = <<<TEXT
  909. A command!
  910. <info>Usage:</info>
  911. tool sample [-h] --reqd [--test]
  912. <info>Options:</info>
  913. --help, -h Display this help.
  914. --reqd A required option. <comment>(required)</comment>
  915. --test A test option.
  916. TEXT;
  917. $this->assertTextEquals($expected, $result, 'Help is not correct.');
  918. }
  919. /**
  920. * test that getCommandError() with an unknown subcommand param shows a helpful message
  921. */
  922. public function testHelpUnknownSubcommand(): void
  923. {
  924. $subParser = [
  925. 'options' => [
  926. 'foo' => [
  927. 'short' => 'f',
  928. 'help' => 'Foo.',
  929. 'boolean' => true,
  930. ],
  931. ],
  932. ];
  933. $parser = new ConsoleOptionParser('mycommand', false);
  934. $parser
  935. ->addSubcommand('method', [
  936. 'help' => 'This is a subcommand',
  937. 'parser' => $subParser,
  938. ])
  939. ->addOption('test', ['help' => 'A test option.'])
  940. ->addSubcommand('unstash');
  941. try {
  942. $result = $parser->help('unknown');
  943. } catch (MissingOptionException $e) {
  944. $result = $e->getFullMessage();
  945. $this->assertStringContainsString(
  946. "Unable to find the `mycommand unknown` subcommand. See `bin/cake mycommand --help`.\n" .
  947. "\n" .
  948. "Other valid choices:\n" .
  949. "\n" .
  950. '- method',
  951. $result
  952. );
  953. }
  954. }
  955. /**
  956. * test building a parser from an array.
  957. */
  958. public function testBuildFromArray(): void
  959. {
  960. $spec = [
  961. 'command' => 'test',
  962. 'arguments' => [
  963. 'name' => ['help' => 'The name'],
  964. 'other' => ['help' => 'The other arg'],
  965. ],
  966. 'options' => [
  967. 'name' => ['help' => 'The name'],
  968. 'other' => ['help' => 'The other arg'],
  969. ],
  970. 'subcommands' => [
  971. 'initdb' => ['help' => 'make database'],
  972. ],
  973. 'description' => 'description text',
  974. 'epilog' => 'epilog text',
  975. ];
  976. $parser = ConsoleOptionParser::buildFromArray($spec);
  977. $this->assertSame($spec['description'], $parser->getDescription());
  978. $this->assertSame($spec['epilog'], $parser->getEpilog());
  979. $options = $parser->options();
  980. $this->assertArrayHasKey('name', $options);
  981. $this->assertArrayHasKey('other', $options);
  982. $args = $parser->arguments();
  983. $this->assertCount(2, $args);
  984. $commands = $parser->subcommands();
  985. $this->assertCount(1, $commands);
  986. }
  987. /**
  988. * test that create() returns instances
  989. */
  990. public function testCreateFactory(): void
  991. {
  992. $parser = ConsoleOptionParser::create('factory', false);
  993. $this->assertInstanceOf('Cake\Console\ConsoleOptionParser', $parser);
  994. $this->assertSame('factory', $parser->getCommand());
  995. }
  996. /**
  997. * test that getCommand() inflects the command name.
  998. */
  999. public function testCommandInflection(): void
  1000. {
  1001. $parser = new ConsoleOptionParser('CommandLine');
  1002. $this->assertSame('command_line', $parser->getCommand());
  1003. }
  1004. /**
  1005. * test that parse() takes a subcommand argument, and that the subcommand parser
  1006. * is used.
  1007. */
  1008. public function testParsingWithSubParser(): void
  1009. {
  1010. $parser = new ConsoleOptionParser('test', false);
  1011. $parser->addOption('primary')
  1012. ->addArgument('one', ['required' => true, 'choices' => ['a', 'b']])
  1013. ->addArgument('two', ['required' => true])
  1014. ->addSubcommand('sub', [
  1015. 'parser' => [
  1016. 'options' => [
  1017. 'secondary' => ['boolean' => true],
  1018. 'fourth' => ['help' => 'fourth option'],
  1019. ],
  1020. 'arguments' => [
  1021. 'sub_arg' => ['choices' => ['c', 'd']],
  1022. ],
  1023. ],
  1024. ]);
  1025. $result = $parser->parse(['sub', '--secondary', '--fourth', '4', 'c'], $this->io);
  1026. $expected = [[
  1027. 'secondary' => true,
  1028. 'fourth' => '4',
  1029. 'help' => false,
  1030. 'verbose' => false,
  1031. 'quiet' => false], ['c']];
  1032. $this->assertEquals($expected, $result, 'Sub parser did not parse request.');
  1033. }
  1034. /**
  1035. * Tests toArray()
  1036. */
  1037. public function testToArray(): void
  1038. {
  1039. $spec = [
  1040. 'command' => 'test',
  1041. 'arguments' => [
  1042. 'name' => ['help' => 'The name'],
  1043. 'other' => ['help' => 'The other arg'],
  1044. ],
  1045. 'options' => [
  1046. 'name' => ['help' => 'The name'],
  1047. 'other' => ['help' => 'The other arg'],
  1048. ],
  1049. 'subcommands' => [
  1050. 'initdb' => ['help' => 'make database'],
  1051. ],
  1052. 'description' => 'description text',
  1053. 'epilog' => 'epilog text',
  1054. ];
  1055. $parser = ConsoleOptionParser::buildFromArray($spec);
  1056. $result = $parser->toArray();
  1057. $this->assertSame($spec['description'], $result['description']);
  1058. $this->assertSame($spec['epilog'], $result['epilog']);
  1059. $options = $result['options'];
  1060. $this->assertArrayHasKey('name', $options);
  1061. $this->assertArrayHasKey('other', $options);
  1062. $this->assertCount(2, $result['arguments']);
  1063. $this->assertCount(1, $result['subcommands']);
  1064. }
  1065. /**
  1066. * Tests merge()
  1067. */
  1068. public function testMerge(): void
  1069. {
  1070. $parser = new ConsoleOptionParser('test');
  1071. $parser->addOption('test', ['short' => 't', 'boolean' => true])
  1072. ->addArgument('one', ['required' => true, 'choices' => ['a', 'b']])
  1073. ->addArgument('two', ['required' => true]);
  1074. $parserTwo = new ConsoleOptionParser('test');
  1075. $parserTwo->addOption('file', ['short' => 'f', 'boolean' => true])
  1076. ->addOption('output', ['short' => 'o', 'boolean' => true])
  1077. ->addArgument('one', ['required' => true, 'choices' => ['a', 'b']]);
  1078. $parser->merge($parserTwo);
  1079. $result = $parser->toArray();
  1080. $options = $result['options'];
  1081. $this->assertArrayHasKey('quiet', $options);
  1082. $this->assertArrayHasKey('test', $options);
  1083. $this->assertArrayHasKey('file', $options);
  1084. $this->assertArrayHasKey('output', $options);
  1085. $this->assertCount(2, $result['arguments']);
  1086. $this->assertCount(6, $result['options']);
  1087. }
  1088. }