Skip to content

Improve the visualisation of commands in the default group #126

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

Merged
merged 9 commits into from
May 11, 2025
Merged
22 changes: 12 additions & 10 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="true"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="true"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
cacheResultFile="./tests/data/cache/.phpunit.result.cache"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
Expand Down
72 changes: 59 additions & 13 deletions src/Helper/OutputHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,24 @@ class OutputHelper
{
use InflectsString;

/**
* The output writer instance used to write formatted output.
*
* @var Writer
*/
protected Writer $writer;

/** @var int Max width of command name */
/**
* Max width of command name.
*
* @var int
*/
protected int $maxCmdName = 0;

/**
* Class constructor.
*
* @param Writer|null $writer The output writer instance used to write formatted output.
*/
public function __construct(?Writer $writer = null)
{
$this->writer = $writer ?? new Writer;
Expand All @@ -79,7 +92,7 @@ public function printTrace(Throwable $e): void
$eClass = get_class($e);

$this->writer->colors(
"{$eClass} <red>{$e->getMessage()}</end><eol/>" .
"$eClass <red>{$e->getMessage()}</end><eol/>" .
'(' . t('thrown in') . " <yellow>{$e->getFile()}</end><white>:{$e->getLine()})</end>"
);

Expand Down Expand Up @@ -107,6 +120,19 @@ public function printTrace(Throwable $e): void
$this->writer->colors($traceStr);
}

/**
* Converts an array of arguments into a string representation.
*
* Each array element is converted based on its type:
* - Scalar values (int, float, string, bool) are var_exported
* - Objects are converted using __toString() if available, otherwise class name is used
* - Arrays are recursively processed and wrapped in square brackets
* - Other types are converted to their type name
*
* @param array $args Array of arguments to be stringified
*
* @return string The comma-separated string representation of all arguments
*/
public function stringifyArgs(array $args): string
{
$holder = [];
Expand All @@ -118,7 +144,14 @@ public function stringifyArgs(array $args): string
return implode(', ', $holder);
}

protected function stringifyArg($arg): string
/**
* Converts the provided argument into a string representation.
*
* @param mixed $arg The argument to be converted into a string. This can be of any type.
*
* @return string A string representation of the provided argument.
*/
protected function stringifyArg(mixed $arg): string
{
if (is_scalar($arg)) {
return var_export($arg, true);
Expand Down Expand Up @@ -196,15 +229,17 @@ protected function showHelp(string $for, array $items, string $header = '', stri
return;
}

$space = 4;
$group = $lastGroup = null;
$space = 4;
$lastGroup = null;

$withDefault = $for === 'Options' || $for === 'Arguments';
foreach (array_values($this->sortItems($items, $padLen, $for)) as $idx => $item) {
$name = $this->getName($item);
if ($for === 'Commands' && $lastGroup !== $group = $item->group()) {
$this->writer->help_group($group ?: '*', true);
$lastGroup = $group;
if ($group !== '') {
$this->writer->help_group($group, true);
}
}
$desc = str_replace(["\r\n", "\n"], str_pad("\n", $padLen + $space + 3), $item->desc($withDefault));

Expand Down Expand Up @@ -254,12 +289,21 @@ public function showUsage(string $usage): self
return $this;
}

/**
* Shows an error message when a command is not found and suggests similar commands.
* Uses levenshtein distance to find commands that are similar to the attempted one.
*
* @param string $attempted The command name that was attempted to be executed
* @param array $available List of available command names
*
* @return OutputHelper For method chaining
*/
public function showCommandNotFound(string $attempted, array $available): self
{
$closest = [];
foreach ($available as $cmd) {
$lev = levenshtein($attempted, $cmd);
if ($lev > 0 || $lev < 5) {
if ($lev > 0 && $lev < 5) {
$closest[$cmd] = $lev;
}
}
Expand All @@ -278,12 +322,12 @@ public function showCommandNotFound(string $attempted, array $available): self
* Sort items by name. As a side effect sets max length of all names.
*
* @param Parameter[]|Command[] $items
* @param int $max
* @param int|null $max
* @param string $for
*
* @return array
*/
protected function sortItems(array $items, &$max = 0, string $for = ''): array
protected function sortItems(array $items, ?int &$max = 0, string $for = ''): array
{
$max = max(array_map(fn ($item) => strlen($this->getName($item)), $items));

Expand All @@ -292,8 +336,10 @@ protected function sortItems(array $items, &$max = 0, string $for = ''): array
}

uasort($items, static function ($a, $b) {
$aName = $a instanceof Groupable ? $a->group() . $a->name() : $a->name();
$bName = $b instanceof Groupable ? $b->group() . $b->name() : $b->name();
// Items in the default group (where group() returns empty/falsy) are prefixed with '__'
// to ensure they appear at the top of the sorted list, whilst grouped items follow after
$aName = $a instanceof Groupable ? ($a->group() ?: '__') . $a->name() : $a->name();
$bName = $b instanceof Groupable ? ($b->group() ?: '__') . $b->name() : $b->name();

return $aName <=> $bName;
});
Expand All @@ -308,7 +354,7 @@ protected function sortItems(array $items, &$max = 0, string $for = ''): array
*
* @return string
*/
protected function getName($item): string
protected function getName(Parameter|Command $item): string
{
$name = $item->name();

Expand Down
10 changes: 5 additions & 5 deletions tests/Helper/OutputHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

class OutputHelperTest extends TestCase
{
protected static $ou = __DIR__ . '/output';
protected static string $ou = __DIR__ . '/output';

public function setUp(): void
{
Expand Down Expand Up @@ -88,16 +88,16 @@ public function test_show_commands()
new Command('group:mkdir', 'Make a folder'),
], 'Cmd Header', 'Cmd Footer');

// If the default group exists, we expect visually to be rendered at the very top.
$this->assertSame([
'Cmd Header',
'',
'Commands:',
' mkdir Make a folder',
' rm Remove file or folder',
'group',
' group:mkdir Make a folder',
' group:rm Remove file or folder',
'*',
' mkdir Make a folder',
' rm Remove file or folder',
'',
'Cmd Footer',
], $this->output());
Expand Down Expand Up @@ -150,7 +150,7 @@ public function test_stringify()
$this->assertSame("[NULL, 'string', 10000, 12.345, DateTime]", $str);
}

public function newHelper()
public function newHelper(): OutputHelper
{
return new OutputHelper(new Writer(static::$ou, new class extends Color {
protected string $format = ':txt:';
Expand Down