Skip to content

Commit 6e54cff

Browse files
committed
Merge branch 'fix-cli-man-page-syntax'
2 parents d000cc0 + e21086c commit 6e54cff

File tree

6 files changed

+255
-50
lines changed

6 files changed

+255
-50
lines changed

src/Toolkit/Cli/CliApplication.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,9 @@ private function generateHelp(string $name, $node, int $target, string ...$args)
463463
{
464464
$collapseSynopsis = null;
465465

466+
// Make empty values `null`
467+
$args = Arr::trim($args, null, false, true);
468+
466469
switch ($target) {
467470
case CliHelpTarget::MARKDOWN:
468471
$formats = ConsoleMarkdownFormat::getTagFormats();

src/Toolkit/Console/ConsoleFormatter.php

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -522,14 +522,17 @@ function (array $match) use (
522522
: max(0, $wrapToWidth + $width);
523523
}
524524
if ($wrapToWidth !== null) {
525-
if (strlen($break) === 1) {
526-
$string = Str::wrap($string, $wrapToWidth, $break);
525+
if ($break === "\n") {
526+
$string = Str::wrap($string, $wrapToWidth);
527527
} else {
528-
if (strpos($string, "\x7f") !== false) {
529-
$string = $this->insertPlaceholders($string, '/\x7f/', $replace);
528+
// Only replace new line breaks with `$break`
529+
$wrapped = Str::wrap($string, $wrapToWidth);
530+
$length = strlen($wrapped);
531+
for ($i = 0; $i < $length; $i++) {
532+
if ($wrapped[$i] === "\n" && $string[$i] !== "\n") {
533+
$replace[] = [$i, 1, $break];
534+
}
530535
}
531-
$string = Str::wrap($string, $wrapToWidth, "\x7f");
532-
$string = $this->insertPlaceholders($string, '/\x7f/', $replace, "\n", $break);
533536
}
534537
}
535538

@@ -688,36 +691,4 @@ private function applyTags(
688691
new TagAttributes($tagId, $tag, $depth, (bool) $count)
689692
);
690693
}
691-
692-
/**
693-
* @param array<array{int,int,string}> $replace
694-
*/
695-
private function insertPlaceholders(
696-
string $string,
697-
string $pattern,
698-
array &$replace,
699-
string $placeholder = 'x',
700-
?string $replacement = null
701-
): string {
702-
return Regex::replaceCallback(
703-
$pattern,
704-
function (array $match) use (
705-
$placeholder,
706-
$replacement,
707-
&$replace
708-
): string {
709-
$replacement ??= $match[0][0];
710-
$replace[] = [
711-
$match[0][1],
712-
strlen($placeholder),
713-
$replacement,
714-
];
715-
return $placeholder;
716-
},
717-
$string,
718-
-1,
719-
$count,
720-
\PREG_OFFSET_CAPTURE
721-
);
722-
}
723694
}

src/Toolkit/Utility/Arr.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -616,19 +616,20 @@ public static function implode(
616616

617617
/**
618618
* Trim characters from each value in an array of strings and Stringables
619-
* before removing empty strings
619+
* before removing or optionally replacing empty strings
620620
*
621621
* @template TKey of array-key
622622
*
623623
* @param iterable<TKey,int|float|string|bool|Stringable|null> $array
624624
* @param string|null $characters Characters to trim, `null` (the default)
625625
* to trim whitespace, or an empty string to trim nothing.
626-
* @return ($removeEmpty is false ? array<TKey,string> : list<string>)
626+
* @return ($removeEmpty is false ? ($nullEmpty is true ? array<TKey,string|null> : array<TKey,string>) : list<string>)
627627
*/
628628
public static function trim(
629629
iterable $array,
630630
?string $characters = null,
631-
bool $removeEmpty = true
631+
bool $removeEmpty = true,
632+
bool $nullEmpty = false
632633
): array {
633634
foreach ($array as $key => $value) {
634635
$value = (string) $value;
@@ -643,7 +644,9 @@ public static function trim(
643644
}
644645
continue;
645646
}
646-
$trimmed[$key] = $value;
647+
$trimmed[$key] = $nullEmpty && $value === ''
648+
? null
649+
: $value;
647650
}
648651
return $trimmed ?? [];
649652
}

tests/fixtures/Toolkit/Cli/Command/TestOptions.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ protected function getOptionList(): iterable
5353
Str::split(',', Env::get('required', ''))
5454
);
5555
$positional = Env::getBool('positional', false);
56+
$extra = Env::getBool('extra', false);
5657

5758
return [
5859
// CliOption::build()
@@ -157,6 +158,16 @@ protected function getOptionList(): iterable
157158
->defaultValue('/./')
158159
->inSchema()
159160
->bindTo($this->OptionalValue),
161+
...($extra
162+
? [
163+
CliOption::build()
164+
->long('extra-flag'),
165+
CliOption::build()
166+
->long('extra-value')
167+
->valueName('EXTRA_VALUE')
168+
->optionType(CliOptionType::VALUE),
169+
]
170+
: []),
160171
...($positional
161172
? [
162173
CliOption::build()

tests/unit/Toolkit/Cli/CliApplicationTest.php

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,216 @@ public static function commandProvider(): array
457457
EOF],
458458
],
459459
],
460+
'markdown (positional, extra)' => [
461+
<<<'EOF'
462+
## NAME
463+
464+
app - Test CliCommand options
465+
466+
## SYNOPSIS
467+
468+
**`app`** \[**`-fF`**] \[**`--nullable`**] \[**`-v`** *<u>entity</u>*]
469+
\[**`-V`** *<u>value</u>*,...] \[**`-s`** *<u>date</u>*]
470+
\[**`-r`**\[*<u>pattern</u>*]] \
471+
    \[**`--extra-flag`**] \[**`--extra-value`** *<u>EXTRA-VALUE</u>*]
472+
\[**`--`**] *<u>INPUT-FILE</u>* *<u>endpoint-uri</u>* \
473+
    \[*<u>key</u>*=*<u>VALUE</u>*...]
474+
475+
## OPTIONS
476+
477+
- **`-f`**, **`--flag`**
478+
479+
Flag
480+
481+
- **`-F`**, **`--flags`**
482+
483+
Flag with multipleAllowed()
484+
485+
- **`--nullable`**
486+
487+
Flag with nullable() and no short form
488+
489+
- **`-v`**, **`--value`** *<u>entity</u>*
490+
491+
Value with defaultValue() and valueName *<u>entity</u>*
492+
493+
The default entity is: `foo`
494+
495+
- **`-V`**, **`--values`** *<u>value</u>*,...
496+
497+
Value with multipleAllowed(), unique() and nullable()
498+
499+
- **`-s`**, **`--start`** *<u>date</u>*
500+
501+
Value with conditional required(), valueType DATE and valueName *<u>date</u>*
502+
503+
- **`-r`**, **`--filter-regex`**\[=*<u>pattern</u>*]
504+
505+
VALUE_OPTIONAL with valueName *<u>pattern</u>* and a default value
506+
507+
The default pattern is: `/./`
508+
509+
- **`--extra-flag`**
510+
511+
- **`--extra-value`** *<u>EXTRA-VALUE</u>*
512+
513+
- *<u>INPUT-FILE</u>*
514+
515+
required() VALUE_POSITIONAL with valueType FILE and valueName "INPUT_FILE"
516+
517+
- *<u>endpoint-uri</u>*
518+
519+
required() VALUE_POSITIONAL with valueName "endpoint_uri"
520+
521+
- *<u>key</u>*=*<u>VALUE</u>*...
522+
523+
VALUE_POSITIONAL with multipleAllowed() and valueName "\<key>=\<VALUE>"
524+
525+
EOF,
526+
0,
527+
['_md'],
528+
null,
529+
['positional' => '1', 'extra' => '1'],
530+
],
531+
'markdown (positional, extra, collapsed synopsis)' => [
532+
<<<'EOF'
533+
## NAME
534+
535+
app - Test CliCommand options
536+
537+
## SYNOPSIS
538+
539+
**`app`** \[*<u>options</u>*] \[**`--`**] *<u>INPUT-FILE</u>*
540+
*<u>endpoint-uri</u>* \[*<u>key</u>*=*<u>VALUE</u>*...]
541+
542+
## OPTIONS
543+
544+
- **`-f`**, **`--flag`**
545+
546+
Flag
547+
548+
- **`-F`**, **`--flags`**
549+
550+
Flag with multipleAllowed()
551+
552+
- **`--nullable`**
553+
554+
Flag with nullable() and no short form
555+
556+
- **`-v`**, **`--value`** *<u>entity</u>*
557+
558+
Value with defaultValue() and valueName *<u>entity</u>*
559+
560+
The default entity is: `foo`
561+
562+
- **`-V`**, **`--values`** *<u>value</u>*,...
563+
564+
Value with multipleAllowed(), unique() and nullable()
565+
566+
- **`-s`**, **`--start`** *<u>date</u>*
567+
568+
Value with conditional required(), valueType DATE and valueName *<u>date</u>*
569+
570+
- **`-r`**, **`--filter-regex`**\[=*<u>pattern</u>*]
571+
572+
VALUE_OPTIONAL with valueName *<u>pattern</u>* and a default value
573+
574+
The default pattern is: `/./`
575+
576+
- **`--extra-flag`**
577+
578+
- **`--extra-value`** *<u>EXTRA-VALUE</u>*
579+
580+
- *<u>INPUT-FILE</u>*
581+
582+
required() VALUE_POSITIONAL with valueType FILE and valueName "INPUT_FILE"
583+
584+
- *<u>endpoint-uri</u>*
585+
586+
required() VALUE_POSITIONAL with valueName "endpoint_uri"
587+
588+
- *<u>key</u>*=*<u>VALUE</u>*...
589+
590+
VALUE_POSITIONAL with multipleAllowed() and valueName "\<key>=\<VALUE>"
591+
592+
EOF,
593+
0,
594+
['_md', '1'],
595+
null,
596+
['positional' => '1', 'extra' => '1'],
597+
],
598+
'man page (positional, extra)' => [
599+
<<<'EOF'
600+
% APP(1) v1.0.0 | app Documentation
601+
602+
# NAME
603+
604+
app - Test CliCommand options
605+
606+
# SYNOPSIS
607+
608+
| **`app`** \[**`-fF`**] \[**`--nullable`**] \[**`-v`** *entity*]
609+
\[**`-V`** *value*,...] \[**`-s`** *date*] \[**`-r`**\[*pattern*]]
610+
| \[**`--extra-flag`**] \[**`--extra-value`** *EXTRA-VALUE*] \[**`--`**]
611+
*INPUT-FILE* *endpoint-uri*
612+
| \[*key*=*VALUE*...]
613+
614+
# OPTIONS
615+
616+
**`-f`**, **`--flag`**
617+
618+
: Flag
619+
620+
**`-F`**, **`--flags`**
621+
622+
: Flag with multipleAllowed()
623+
624+
**`--nullable`**
625+
626+
: Flag with nullable() and no short form
627+
628+
**`-v`**, **`--value`** *entity*
629+
630+
: Value with defaultValue() and valueName *entity*
631+
632+
The default entity is: foo
633+
634+
**`-V`**, **`--values`** *value*,...
635+
636+
: Value with multipleAllowed(), unique() and nullable()
637+
638+
**`-s`**, **`--start`** *date*
639+
640+
: Value with conditional required(), valueType DATE and valueName *date*
641+
642+
**`-r`**, **`--filter-regex`**\[=*pattern*]
643+
644+
: VALUE_OPTIONAL with valueName *pattern* and a default value
645+
646+
The default pattern is: /./
647+
648+
**`--extra-flag`**
649+
650+
**`--extra-value`** *EXTRA-VALUE*
651+
652+
*INPUT-FILE*
653+
654+
: required() VALUE_POSITIONAL with valueType FILE and valueName "INPUT_FILE"
655+
656+
*endpoint-uri*
657+
658+
: required() VALUE_POSITIONAL with valueName "endpoint_uri"
659+
660+
*key*=*VALUE*...
661+
662+
: VALUE_POSITIONAL with multipleAllowed() and valueName "\<key>=\<VALUE>"
663+
664+
EOF,
665+
0,
666+
['_man', '', 'v1.0.0'],
667+
null,
668+
['positional' => '1', 'extra' => '1'],
669+
],
460670
];
461671
}
462672

tests/unit/Toolkit/Utility/ArrTest.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,16 +1790,16 @@ public function testSetIf(): void
17901790
* @template TKey of array-key
17911791
* @template TValue of int|float|string|bool|Stringable|null
17921792
*
1793-
* @param array<TKey,string>|list<string> $expected
1793+
* @param array<TKey,string|null>|list<string> $expected
17941794
* @param iterable<TKey,TValue> $array
17951795
*/
1796-
public function testTrim(array $expected, iterable $array, ?string $characters = null, bool $removeEmpty = true): void
1796+
public function testTrim(array $expected, iterable $array, ?string $characters = null, bool $removeEmpty = true, bool $nullEmpty = false): void
17971797
{
1798-
$this->assertSame($expected, Arr::trim($array, $characters, $removeEmpty));
1798+
$this->assertSame($expected, Arr::trim($array, $characters, $removeEmpty, $nullEmpty));
17991799
}
18001800

18011801
/**
1802-
* @return array<array{mixed[],mixed[],2?:string|null,3?:bool}>
1802+
* @return array<array{mixed[],mixed[],2?:string|null,3?:bool,4?:bool}>
18031803
*/
18041804
public static function trimProvider(): array
18051805
{
@@ -1828,19 +1828,26 @@ public static function trimProvider(): array
18281828
],
18291829
[
18301830
['0', '1', '1', 'a', 'b', 'c'],
1831-
[null, 0, 1, true, false, ' ', 'a' => 'a ', 'b' => ' b ', 'c' => ' c'],
1831+
[null, 0, 1, true, false, ' ', 'a' => 'a ', 'b' => ' b ', 'c' => ' c', 'd' => ' '],
18321832
],
18331833
[
18341834
['0', '1', '1', ' ', 'a', 'b', 'c'],
1835-
[null, 0, 1, true, false, ' ', '/', 'a' => 'a/', 'b' => '/b/', 'c' => '/c'],
1835+
[null, 0, 1, true, false, ' ', '/', 'a' => 'a/', 'b' => '/b/', 'c' => '/c', 'd' => '/'],
18361836
'/',
18371837
],
18381838
[
1839-
['', '0', '1', '1', '', '', 'a' => 'a', 'b' => 'b', 'c' => 'c'],
1840-
[null, 0, 1, true, false, ' ', 'a' => 'a ', 'b' => ' b ', 'c' => ' c'],
1839+
['', '0', '1', '1', '', '', 'a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => ''],
1840+
[null, 0, 1, true, false, ' ', 'a' => 'a ', 'b' => ' b ', 'c' => ' c', 'd' => ' '],
18411841
null,
18421842
false,
18431843
],
1844+
[
1845+
[null, '0', '1', '1', null, null, 'a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => null],
1846+
[null, 0, 1, true, false, ' ', 'a' => 'a ', 'b' => ' b ', 'c' => ' c', 'd' => ' '],
1847+
null,
1848+
false,
1849+
true,
1850+
],
18441851
];
18451852
}
18461853

0 commit comments

Comments
 (0)