Skip to content

Commit e87614a

Browse files
committed
Merge branch 'improve-cli-chaining'
2 parents df4334b + 8075802 commit e87614a

File tree

3 files changed

+101
-60
lines changed

3 files changed

+101
-60
lines changed

src/Cli/CliApplication.php

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class CliApplication extends Application implements ICliApplication
3636
private $RunningCommand;
3737

3838
/**
39-
* @var CliHelpType::*
39+
* @var int&CliHelpType::*
4040
*/
4141
private $HelpType = CliHelpType::TTY;
4242

@@ -45,6 +45,11 @@ class CliApplication extends Application implements ICliApplication
4545
*/
4646
private $HelpStyle;
4747

48+
/**
49+
* @var int
50+
*/
51+
private $LastExitStatus = 0;
52+
4853
public function __construct(
4954
?string $basePath = null,
5055
?string $appName = null,
@@ -67,11 +72,22 @@ public function __construct(
6772
Sys::handleExitSignals();
6873
}
6974

75+
/**
76+
* @inheritDoc
77+
*/
7078
public function getRunningCommand(): ?CliCommand
7179
{
7280
return $this->RunningCommand;
7381
}
7482

83+
/**
84+
* @inheritDoc
85+
*/
86+
public function getLastExitStatus(): int
87+
{
88+
return $this->LastExitStatus;
89+
}
90+
7591
/**
7692
* Get a CliCommand instance from the given node in the command tree
7793
*
@@ -336,8 +352,10 @@ public function buildHelp(array $sections): string
336352
/**
337353
* @inheritDoc
338354
*/
339-
public function run(): int
355+
public function run()
340356
{
357+
$this->LastExitStatus = 0;
358+
341359
$args = array_slice($_SERVER['argv'], 1);
342360

343361
$lastNode = null;
@@ -352,15 +370,15 @@ public function run(): int
352370
if ($arg === '--help' && !$args) {
353371
$usage = $this->getUsage($name, $node);
354372
Console::stdout($usage);
355-
return 0;
373+
return $this;
356374
}
357375

358376
// or version number if it's "--version"
359377
if ($arg === '--version' && !$args) {
360378
$appName = $this->getAppName();
361379
$version = Composer::getRootPackageVersion(true, true);
362380
Console::stdout("__{$appName}__ $version");
363-
return 0;
381+
return $this;
364382
}
365383

366384
// - If $args was empty before this iteration, print terse usage
@@ -371,9 +389,11 @@ public function run(): int
371389
!preg_match('/^[a-zA-Z][a-zA-Z0-9_-]*$/', $arg)) {
372390
$usage = $this->getUsage($name, $node, true);
373391
Console::out($usage);
374-
return $arg === null
375-
? 0
376-
: 1;
392+
$this->LastExitStatus =
393+
$arg === null
394+
? 0
395+
: 1;
396+
return $this;
377397
}
378398

379399
// Descend into the command tree if $arg is a registered subcommand
@@ -406,12 +426,14 @@ public function run(): int
406426

407427
if (($args[0] ?? null) === '_md') {
408428
array_shift($args);
409-
return $this->generateHelp($name, $node, CliHelpType::MARKDOWN, ...$args);
429+
$this->generateHelp($name, $node, CliHelpType::MARKDOWN, ...$args);
430+
return $this;
410431
}
411432

412433
if (($args[0] ?? null) === '_man') {
413434
array_shift($args);
414-
return $this->generateHelp($name, $node, CliHelpType::MAN_PAGE, ...$args);
435+
$this->generateHelp($name, $node, CliHelpType::MAN_PAGE, ...$args);
436+
return $this;
415437
}
416438

417439
$command = $this->getNodeCommand($name, $node);
@@ -422,7 +444,8 @@ public function run(): int
422444
);
423445
}
424446
$this->RunningCommand = $command;
425-
return $command(...$args);
447+
$this->LastExitStatus = $command(...$args);
448+
return $this;
426449
} catch (CliInvalidArgumentsException $ex) {
427450
$ex->reportErrors();
428451
if (!$node) {
@@ -433,22 +456,34 @@ public function run(): int
433456
($usage = $this->getUsage($name, $node, true)) !== null) {
434457
Console::out("\n{$usage}");
435458
}
436-
return 1;
459+
$this->LastExitStatus = 1;
460+
return $this;
437461
} finally {
438462
$this->RunningCommand = null;
439463
}
440464
}
441465

466+
/**
467+
* @inheritDoc
468+
*/
469+
public function exit()
470+
{
471+
exit ($this->LastExitStatus);
472+
}
473+
474+
/**
475+
* @inheritDoc
476+
*/
442477
public function runAndExit()
443478
{
444-
exit ($this->run());
479+
$this->run()->exit();
445480
}
446481

447482
/**
448483
* @param array<string,class-string<CliCommand>|mixed[]>|class-string<CliCommand> $node
449484
* @param CliHelpType::* $helpType
450485
*/
451-
private function generateHelp(string $name, $node, int $helpType, string ...$args): int
486+
private function generateHelp(string $name, $node, int $helpType, string ...$args): void
452487
{
453488
$this->HelpType = $helpType;
454489
$this->HelpStyle = null;
@@ -477,7 +512,5 @@ private function generateHelp(string $name, $node, int $helpType, string ...$arg
477512
$usage = $this->getUsage($name, $node);
478513
$usage = $formatter->formatTags($usage, false, null, false);
479514
printf("%s\n", str_replace('\ ', ' ', $usage));
480-
481-
return 0;
482515
}
483516
}

src/Cli/CliCommand.php

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -241,15 +241,15 @@ private function addOption(CliOption $option)
241241
try {
242242
$option->validate();
243243
} catch (CliUnknownValueException $ex) {
244-
$this->deferOptionError($ex->getMessage());
244+
// If an exception is thrown over a value found in the environment,
245+
// defer it because we may only be loading options to generate a
246+
// synopsis
247+
$this->DeferredOptionErrors[] = $ex->getMessage();
245248
}
246249

247250
$names = $option->getNames();
248251

249-
if (array_intersect_key(
250-
array_flip($names),
251-
$this->OptionsByName
252-
)) {
252+
if (array_intersect_key(array_flip($names), $this->OptionsByName)) {
253253
throw new LogicException('Option names must be unique: ' . implode(', ', $names));
254254
}
255255

@@ -262,6 +262,7 @@ private function addOption(CliOption $option)
262262
)) {
263263
throw new LogicException('Required positional options must be added before optional ones');
264264
}
265+
265266
if (!$option->Required &&
266267
array_filter(
267268
$this->PositionalOptions,
@@ -270,6 +271,7 @@ private function addOption(CliOption $option)
270271
)) {
271272
throw new LogicException("'multipleAllowed' positional options must be added after optional ones");
272273
}
274+
273275
if ($option->MultipleAllowed &&
274276
array_filter(
275277
$this->PositionalOptions,
@@ -690,34 +692,8 @@ private function optionError(string $message)
690692
return $this;
691693
}
692694

693-
/**
694-
* Record an option-related error to report only if the command is running
695-
*
696-
* @return $this
697-
* @phpstan-impure
698-
*/
699-
private function deferOptionError(string $message)
700-
{
701-
$this->DeferredOptionErrors[] = $message;
702-
703-
return $this;
704-
}
705-
706-
/**
707-
* @return $this
708-
*/
709-
private function loadOptionValues()
695+
private function loadOptionValues(): void
710696
{
711-
if ($this->OptionValues !== null) {
712-
return $this;
713-
}
714-
715-
$this->ArgumentValues = [];
716-
$this->OptionErrors = [];
717-
$this->NextArgumentIndex = null;
718-
$this->HasHelpArgument = false;
719-
$this->HasVersionArgument = false;
720-
721697
$this->loadOptions();
722698

723699
try {
@@ -764,8 +740,6 @@ private function loadOptionValues()
764740
...$this->DeferredOptionErrors
765741
);
766742
}
767-
768-
return $this;
769743
} catch (Throwable $ex) {
770744
$this->OptionValues = null;
771745

@@ -1225,9 +1199,9 @@ private function assertHasRun()
12251199
*/
12261200
final public function __invoke(string ...$args): int
12271201
{
1202+
$this->reset();
1203+
12281204
$this->Arguments = $args;
1229-
$this->OptionValues = null;
1230-
$this->ExitStatus = 0;
12311205
$this->Runs++;
12321206

12331207
$this->loadOptionValues();
@@ -1308,6 +1282,18 @@ final protected function getRuns(): int
13081282
return $this->Runs;
13091283
}
13101284

1285+
private function reset(): void
1286+
{
1287+
$this->Arguments = [];
1288+
$this->ArgumentValues = [];
1289+
$this->OptionValues = null;
1290+
$this->OptionErrors = [];
1291+
$this->NextArgumentIndex = null;
1292+
$this->HasHelpArgument = false;
1293+
$this->HasVersionArgument = false;
1294+
$this->ExitStatus = 0;
1295+
}
1296+
13111297
private function getLoopbackFormatter(): Formatter
13121298
{
13131299
return $this->LoopbackFormatter

src/Cli/Contract/ICliApplication.php

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ interface ICliApplication extends IApplication
1818
*/
1919
public function getRunningCommand(): ?ICliCommand;
2020

21+
/**
22+
* Get the return value most recently recorded by run()
23+
*
24+
* Returns `0` if {@see ICliApplication::run()} has not recorded a return
25+
* value.
26+
*/
27+
public function getLastExitStatus(): int;
28+
2129
/**
2230
* Register a command with the container
2331
*
@@ -84,30 +92,44 @@ public function getHelpWidth(bool $terse = false): ?int;
8492
public function buildHelp(array $sections): string;
8593

8694
/**
87-
* Process command-line arguments passed to the script
95+
* Process command-line arguments passed to the script and record a return
96+
* value
8897
*
8998
* The first applicable action is taken:
9099
*
91100
* - If `--help` is the only remaining argument after processing subcommand
92-
* arguments, a help message is printed to `STDOUT` and `0` is returned.
101+
* arguments, a help message is printed to `STDOUT` and the return value
102+
* is `0`.
93103
* - If `--version` is the only remaining argument, the application's name
94-
* and version number is printed to `STDOUT` and `0` is returned.
104+
* and version number is printed to `STDOUT` and the return value is `0`.
95105
* - If subcommand arguments resolve to a registered command, it is invoked
96-
* and its exit status is returned.
106+
* and the return value is its exit status.
97107
* - If, after processing subcommand arguments, there are no further
98108
* arguments but there are further subcommands, a one-line synopsis of
99-
* each registered subcommand is printed and `0` is returned.
109+
* each registered subcommand is printed and the return value is `0`.
100110
*
101111
* Otherwise, an error is reported, a one-line synopsis of each registered
102-
* subcommand is printed, and `1` is returned.
112+
* subcommand is printed, and the return value is `1`.
113+
*
114+
* @return $this
115+
*/
116+
public function run();
117+
118+
/**
119+
* Exit with the return value most recently recorded by run()
120+
*
121+
* The exit status is `0` if {@see ICliApplication::run()} has not recorded
122+
* a return value.
123+
*
124+
* @return never
103125
*/
104-
public function run(): int;
126+
public function exit();
105127

106128
/**
107129
* Exit after processing command-line arguments passed to the script
108130
*
109-
* The value returned by {@see ICliApplication::run()} is used as the exit
110-
* status.
131+
* The return value recorded by {@see ICliApplication::run()} is used as the
132+
* exit status.
111133
*
112134
* @return never
113135
*/

0 commit comments

Comments
 (0)