Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 5 additions & 87 deletions src/Argument/ArgumentList.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ArgumentList implements Iterator {

public function __construct(string $script, string...$arguments) {
$this->script = $script;
$this->buildArgumentList($arguments);
$this->parseArguments($arguments);
}

public function getScript():string {
Expand All @@ -27,92 +27,10 @@ public function getCommandName():string {
return $this->argumentList[0]->getValue() ?? "";
}

/**
* @param string[] $arguments
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
protected function buildArgumentList(array $arguments):void {
if(isset($arguments[0])
&& $arguments[0][0] !== "-") {
$commandArgument = array_shift($arguments);
array_push(
$this->argumentList,
new CommandArgument($commandArgument)
);
}
else {
$defaultCommandArgument = new CommandArgument(
self::DEFAULT_COMMAND
);
array_push($this->argumentList, $defaultCommandArgument);
}

$skipNextArgument = false;

foreach ($arguments as $i => $arg) {
if($skipNextArgument) {
$skipNextArgument = false;
continue;
}

if ($arg[0] === "-") {
if(strstr($arg, "=")) {
$name = substr(
$arg,
0,
strpos(
$arg,
"="
) ?: 0
);

$value = substr(
$arg,
strpos(
$arg,
"="
) + 1
);
}
else {
$name = $arg;

$nextArgument = $arguments[$i + 1] ?? null;

if($nextArgument
&& strpos($nextArgument, "-") !== 0) {
$value = $arguments[$i + 1];
$skipNextArgument = true;
}
else {
$value = null;
}
}

if ($arg[1] === "-") {
array_push(
$this->argumentList,
new LongOptionArgument(
$name,
$value
)
);
}
else {
array_push($this->argumentList,
new ShortOptionArgument(
$arg,
$value
)
);
}
} else {
array_push(
$this->argumentList,
new NamedArgument($arg)
);
}
}
/** @param string[] $arguments */
protected function parseArguments(array $arguments):void {
$parser = new ArgumentParser();
$this->argumentList = $parser->parse($arguments);
}

/**
Expand Down
158 changes: 158 additions & 0 deletions src/Argument/ArgumentParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php
namespace Gt\Cli\Argument;

class ArgumentParser {
/**
* @param string[] $arguments
* @return Argument[]
*/
public function parse(array $arguments):array {
$argumentList = [];
$argumentList[] = $this->createCommandArgument($arguments);
$skipNextArgument = false;

foreach ($arguments as $i => $arg) {
if($skipNextArgument) {
$skipNextArgument = false;
continue;
}

if($arg[0] === "-") {
$skipNextArgument = $this->appendOptionArgument(
$argumentList,
$arguments,
$i,
$arg
);
continue;
}

$argumentList[] = new NamedArgument($arg);
}

return $argumentList;
}

/** @param string[] $arguments */
private function createCommandArgument(array &$arguments):CommandArgument {
$commandName = ArgumentList::DEFAULT_COMMAND;

if(isset($arguments[0])
&& $arguments[0][0] !== "-") {
$commandName = array_shift($arguments);
}

return new CommandArgument($commandName);
}

/**
* @param Argument[] $argumentList
* @param string[] $arguments
* @return bool True if the next argument should be skipped.
*/
private function appendOptionArgument(
array &$argumentList,
array $arguments,
int $argumentIndex,
string $arg
):bool {
if($this->isChainedShortOption($arg)) {
return $this->appendChainedShortOptionArguments(
$argumentList,
$arguments,
$argumentIndex,
$arg
);
}

list($name, $value, $skipNextArgument) = $this->extractOptionData(
$arguments,
$argumentIndex,
$arg
);

if($arg[1] === "-") {
$argumentList[] = new LongOptionArgument($name, $value);
return $skipNextArgument;
}

$argumentList[] = new ShortOptionArgument($arg, $value);
return $skipNextArgument;
}

/**
* @param string[] $arguments
* @return array{string, ?string, bool}
*/
private function extractOptionData(
array $arguments,
int $argumentIndex,
string $arg
):array {
if(str_contains($arg, "=")) {
$separatorPosition = strpos($arg, "=");
$name = substr($arg, 0, $separatorPosition ?: 0);
$value = substr($arg, ($separatorPosition ?: 0) + 1);
return [$name, $value, false];
}

$nextArgument = $arguments[$argumentIndex + 1] ?? null;
$nextIsValue = $this->isNextArgumentValue($nextArgument);
$value = $nextIsValue ? $nextArgument : null;

return [$arg, $value, $nextIsValue];
}

/**
* @param Argument[] $argumentList
* @param string[] $arguments
* @return bool True if the next argument should be skipped.
*/
private function appendChainedShortOptionArguments(
array &$argumentList,
array $arguments,
int $argumentIndex,
string $arg
):bool {
$shortOptionCharList = str_split(substr($arg, 1));
$lastIndex = count($shortOptionCharList) - 1;
$lastValue = null;
$nextArgument = $arguments[$argumentIndex + 1] ?? null;
$nextIsValue = $this->isNextArgumentValue($nextArgument);

if($nextIsValue) {
$lastValue = $nextArgument;
}

foreach($shortOptionCharList as $shortOptionIndex => $char) {
$value = $shortOptionIndex === $lastIndex ? $lastValue : null;
$argumentList[] = new ShortOptionArgument("-" . $char, $value);
}

return $nextIsValue;
}

private function isNextArgumentValue(?string $arg):bool {
if(!$arg) {
return false;
}

return !str_starts_with($arg, "-");
}

private function isChainedShortOption(string $arg):bool {
if(strlen($arg) <= 2) {
return false;
}

if($arg[1] === "-") {
return false;
}

if(str_contains($arg, "=")) {
return false;
}

return preg_match('/^-[a-zA-Z]+$/', $arg) === 1;
}
}
11 changes: 6 additions & 5 deletions src/Command/HelpCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,25 @@ protected function getHelpForAllCommands():string {
protected function getHelpForCommand(?string $commandName = null):string {
$output = "";

$command = null;
$matchedCommand = null;
foreach($this->applicationCommandList as $command) {
if($command->getName() !== $commandName) {
continue;
}

$matchedCommand = $command;
break;
}

if(!$command) {
if(!$matchedCommand) {
return "No help for command `$commandName`." . PHP_EOL;
}

$output .= $command->getName();
$output .= $matchedCommand->getName();
$output .= ": ";
$output .= $command->getDescription();
$output .= $matchedCommand->getDescription();
$output .= PHP_EOL;
$output .= $command->getUsage(true);
$output .= $matchedCommand->getUsage(true);

return $output;
}
Expand Down
71 changes: 65 additions & 6 deletions test/phpunit/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,10 @@ public function __construct(Parameter...$requiredParams) {
$this->unitTestRequiredParams = $requiredParams;
}

public function run(?ArgumentValueList $arguments = null):int {
return 0;
}
public function run(?ArgumentValueList $arguments = null):int {
unset($arguments);
return 0;
}

public function getName():string {
return "example";
Expand Down Expand Up @@ -262,9 +263,10 @@ public function testExitCodeReturnedFromCommand():void {
->willReturn("example");

$command = new class extends Command {
public function run(?ArgumentValueList $arguments = null):int {
return 9;
}
public function run(?ArgumentValueList $arguments = null):int {
unset($arguments);
return 9;
}

public function getName():string {
return "example";
Expand Down Expand Up @@ -368,6 +370,63 @@ public function testExitCodeReturnedFromVersionFlag():void {
);
}

public function testMissingRequiredParameterIsReportedByApplication():void {
$arguments = new ArgumentList(
"script-name",
"requires-parameter",
"--flag"
);

$command = new class extends Command {
public function run(?ArgumentValueList $arguments = null):int {
unset($arguments);
return 0;
}

public function getName():string {
return "requires-parameter";
}

public function getDescription():string {
return "Command requiring --required.";
}

public function getRequiredNamedParameterList():array {
return [];
}

public function getOptionalNamedParameterList():array {
return [];
}

public function getRequiredParameterList():array {
return [new Parameter(false, "required", "r")];
}

public function getOptionalParameterList():array {
return [new Parameter(false, "flag", "f")];
}
};

$actualExitCode = null;
$application = new Application("test-app", $arguments, $command);
$application->setStream(
$this->inPath,
$this->outPath,
$this->errPath
);
$application->setExitHandler(function(int $exitCode) use(&$actualExitCode) {
$actualExitCode = $exitCode;
});
$application->run();

self::assertSame(1, $actualExitCode);
self::assertStreamContains(
"Error - Missing required parameter: Error: required (r)",
Stream::ERROR
);
}

protected function assertStreamContains(
string $message,
string $streamName
Expand Down
Loading