Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds argument suggestion support for unknown arguments #204

Closed
wants to merge 2 commits into from
Closed
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
20 changes: 20 additions & 0 deletions src/Argument/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,24 @@ public function trailingArray()
{
return $this->parser->trailingArray();
}

/**
* Returns the list of unknown prefixed arguments and their suggestions.
*
* @return array The list of unknown prefixed arguments and their suggestions.
*/
public function getUnknowPrefixedArgumentsAndSuggestions()
{
return $this->parser->getUnknowPrefixedArgumentsAndSuggestions();
}

/**
* Sets the minimum similarity percentage for finding suggestions.
*
* @param float $percentage The minimum similarity percentage to set.
*/
public function setMinimumSimilarityPercentage(float $percentage)
{
$this->parser->setMinimumSimilarityPercentage($percentage);
}
}
127 changes: 120 additions & 7 deletions src/Argument/Parser.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php

namespace League\CLImate\Argument;

use League\CLImate\Exceptions\InvalidArgumentException;

class Parser
{

/**
* Filter class to find various types of arguments
*
Expand All @@ -19,11 +19,26 @@ class Parser
* @var \League\CLImate\Argument\Summary $summary
*/
protected $summary;

protected $trailing;

protected $trailingArray;

/**
* List of unknown arguments and best argument suggestion.
*
* The key corresponds to the unknown argument and the value to the
* argument suggestion, if any.
*
* @var array
*/
protected $unknowPrefixedArguments = [];

/**
* Minimum similarity percentage to detect similar arguments.
*
* @var float
*/
protected $minimumSimilarityPercentage = 0.6;

public function __construct()
{
$this->summary = new Summary();
Expand Down Expand Up @@ -61,6 +76,10 @@ public function parse(array $argv = null)

$unParsedArguments = $this->prefixedArguments($cliArguments);

// Searches for unknown prefixed arguments and finds a suggestion
// within the list of valid arguments.
$this->unknowPrefixedArguments($unParsedArguments);

$this->nonPrefixedArguments($unParsedArguments);

// After parsing find out which arguments were required but not
Expand All @@ -69,9 +88,9 @@ public function parse(array $argv = null)

if (count($missingArguments) > 0) {
throw new InvalidArgumentException(
'The following arguments are required: '
. $this->summary->short($missingArguments) . '.'
);
'The following arguments are required: '
. $this->summary->short($missingArguments) . '.'
);
}
}

Expand Down Expand Up @@ -302,8 +321,102 @@ protected function getCommandAndArguments(array $argv = null)
}

$arguments = $argv;
$command = array_shift($arguments);
$command = array_shift($arguments);

return compact('arguments', 'command');
}

/**
* Processes unknown prefixed arguments and sets suggestions if no matching
* prefix is found.
*
* @param array $unParsedArguments The array of unparsed arguments to
* process.
*/
protected function unknowPrefixedArguments(array $unParsedArguments)
{
foreach ($unParsedArguments as $arg) {
$unknowArgumentName = $this->getUnknowArgumentName($arg);
if (!$this->findPrefixedArgument($unknowArgumentName)) {
if (is_null($unknowArgumentName)) {
continue;
}
$suggestion = $this->findSuggestionsForUnknowPrefixedArguments(
$unknowArgumentName,
$this->filter->withPrefix()
);
$this->setSuggestion($unknowArgumentName, $suggestion);
}
}
}

/**
* Sets the suggestion for an unknown argument name.
*
* @param string $unknowArgName The name of the unknown argument.
* @param string $suggestion The suggestion for the unknown argument.
*/
protected function setSuggestion(string $unknowArgName, string $suggestion)
{
$this->unknowPrefixedArguments[$unknowArgName] = $suggestion;
}

/**
* Extracts the unknown argument name from a given argument string.
*
* @param string $arg The argument string to process.
* @return string|null The extracted unknown argument name or null if not
* found.
*/
protected function getUnknowArgumentName(string $arg)
{
if (preg_match('/^[-]{1,2}([^-]+?)(?:=|$)/', $arg, $matches)) {
return $matches[1];
}
return null;
}

/**
* Finds the most similar known argument for an unknown prefixed argument.
*
* @param string $argName The name of the unknown argument to find
* suggestions for.
* @param array $argList The list of known arguments to compare against.
* @return string The most similar known argument name.
*/
protected function findSuggestionsForUnknowPrefixedArguments(
string $argName,
array $argList
) {
$mostSimilar = '';
$greatestSimilarity = $this->minimumSimilarityPercentage * 100;
foreach ($argList as $arg) {
similar_text($argName, $arg->name(), $percent);
if ($percent > $greatestSimilarity) {
$greatestSimilarity = $percent;
$mostSimilar = $arg->name();
}
}
return $mostSimilar;
}

/**
* Returns the list of unknown prefixed arguments and their suggestions.
*
* @return array The list of unknown prefixed arguments and their suggestions.
*/
public function getUnknowPrefixedArgumentsAndSuggestions()
{
return $this->unknowPrefixedArguments;
}

/**
* Sets the minimum similarity percentage for finding suggestions.
*
* @param float $percentage The minimum similarity percentage to set.
*/
public function setMinimumSimilarityPercentage(float $percentage)
{
$this->minimumSimilarityPercentage = $percentage;
}
}
38 changes: 28 additions & 10 deletions src/TerminalObject/Dynamic/Progress.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php

namespace League\CLImate\TerminalObject\Dynamic;

use League\CLImate\Exceptions\UnexpectedValueException;

class Progress extends DynamicTerminalObject
{

/**
* The total number of items involved
*
Expand Down Expand Up @@ -111,7 +111,7 @@ public function current($current, $label = null)
$this->drawProgressBar($current, $label);

$this->current = $current;
$this->label = $label;
$this->label = $label;
}

/**
Expand All @@ -138,7 +138,6 @@ public function forceRedraw($force = true)
return $this;
}


/**
* Update a progress bar using an iterable.
*
Expand Down Expand Up @@ -168,7 +167,6 @@ public function each($items, callable $callback = null)
}
}


/**
* Draw the progress bar, if necessary
*
Expand Down Expand Up @@ -207,7 +205,7 @@ protected function getProgressBar($current, $label)
// Move the cursor up and clear it to the end
$line_count = $this->has_label_line ? 2 : 1;

$progress_bar = $this->util->cursor->up($line_count);
$progress_bar = $this->util->cursor->up($line_count);
$progress_bar .= $this->util->cursor->startOfCurrentLine();
$progress_bar .= $this->util->cursor->deleteCurrentLine();
$progress_bar .= $this->getProgressBarStr($current, $label);
Expand All @@ -234,13 +232,13 @@ protected function getProgressBarStr($current, $label)
$percentage = $current / $this->total;
$bar_length = round($this->getBarStrLen() * $percentage);

$bar = $this->getBar($bar_length);
$number = $this->percentageFormatted($percentage);
$bar = $this->getBar($bar_length);
$number = $this->percentageFormatted($percentage);

if ($label) {
$label = $this->labelFormatted($label);
// If this line doesn't have a label, but we've had one before,
// then ensure the label line is cleared
// If this line doesn't have a label, but we've had one before,
// then ensure the label line is cleared
} elseif ($this->has_label_line) {
$label = $this->labelFormatted('');
}
Expand All @@ -257,7 +255,7 @@ protected function getProgressBarStr($current, $label)
*/
protected function getBar($length)
{
$bar = str_repeat('=', $length);
$bar = str_repeat('=', $length);
$padding = str_repeat(' ', $this->getBarStrLen() - $length);

return "{$bar}>{$padding}";
Expand Down Expand Up @@ -312,4 +310,24 @@ protected function shouldRedraw($percentage, $label)
{
return ($this->force_redraw || $percentage != $this->current_percentage || $label != $this->label);
}

/**
* Gets the current progress value.
*
* @return int The current progress value.
*/
public function getCurrent()
{
return $this->current;
}

/**
* Gets de total value for the progress.
*
* @return int The total progress value.
*/
public function getTotal()
{
return $this->total;
}
}
32 changes: 32 additions & 0 deletions tests/Argument/ManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,36 @@ public function testItStoresTrailingInArray()
$this->assertEquals('test trailing with spaces', $this->manager->trailing());
$this->assertEquals(['test', 'trailing with spaces'], $this->manager->trailingArray());
}

public function testItSuggestAlternativesToUnknowArguments()
{
$this->manager->add([
'user' => [
'longPrefix' => 'user',
],
'password' => [
'longPrefix' => 'password',
],
'flag' => [
'longPrefix' => 'flag',
'noValue' => true,
],
]);

$argv = [
'test-script',
'--user=baz',
'--pass=123',
'--fag',
'--xyz',
];

$this->manager->parse($argv);
$processed = $this->manager->getUnknowPrefixedArgumentsAndSuggestions();

$this->assertCount(3, $processed);
$this->assertEquals('password', $processed['pass']);
$this->assertEquals('flag', $processed['fag']);
$this->assertEquals('', $processed['xyz']);
}
}