10000 feature #13220 [Console] Made output docopt compatible (WouterJ) · symfony/symfony@81bf910 · GitHub
[go: up one dir, main page]

Skip to content

Commit 81bf910

Browse files
committed
feature #13220 [Console] Made output docopt compatible (WouterJ)
This PR was squashed before being merged into the 2.7 branch (closes #13220). Discussion ---------- [Console] Made output docopt compatible | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #6329 | License | MIT | Doc PR | symfony/symfony-docs#5016 This was harder than I thought. To sum up: * The output now follows the [docopt](http://docopt.org/) specification * There is a new `addUsage` method to add more usage patterns * The handling of spaces in the descriptors is refactored to make it easier to understand and to make it render better (using sprintf's features only made it worse imo) Todo --- * [x] Add test for `addUsage` and friends * [x] Add test for multiline descriptions of arguments * <s>Convert long descriptions to multiline automatically</s> * [ ] Submit a doc PR for `addUsage` Question --- The docopt specification suggests we should add these usage patterns: %command.name% -h | --help %command.name% --version I didn't do that yet, as I think it'll only makes the output more verbose and it's already pretty obvious. I've taken some decisions which I don't think everybody agrees with. I'm willing to change it, so feel free to comment :) /cc @Seldaek Commits ------- 3910940 [Console] Made output docopt compatible
2 parents c0ddd3d + 3910940 commit 81bf910

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+471
-284
lines changed

src/Symfony/Component/Console/Command/Command.php

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class Command
4242
private $applicationDefinitionMerged = false;
4343
private $applicationDefinitionMergedWithArgs = false;
4444
private $code;
45-
private $synopsis;
45+
private $synopsis = array();
46+
private $usages = array();
4647
private $helperSet;
4748

4849
/**
@@ -219,7 +220,8 @@ protected function initialize(InputInterface $input, OutputInterface $output)
219220
public function run(InputInterface $input, OutputInterface $output)
220221
{
221222
// force the creation of the synopsis before the merge with the app definition
222-
$this->getSynopsis();
223+
$this->getSynopsis(true);
224+
$this->getSynopsis(false);
223225

224226
// add the application arguments and options
225227
$this->mergeApplicationDefinition();
@@ -577,15 +579,45 @@ public function getAliases()
577579
/**
578580
* Returns the synopsis for the command.
579581
*
582+
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
583+
*
580584
* @return string The synopsis
581585
*/
582-
public function getSynopsis()
586+
public function getSynopsis($short = false)
587+
{
588+
$key = $short ? 'short' : 'long';
589+
590+
if (!isset($this->synopsis[$key])) {
591+
$this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
592+
}
593+
594+
return $this->synopsis[$key];
595+
}
596+
597+
/**
598+
* Add a command usage example.
599+
*
600+
* @param string $usage The usage, it'll be prefixed with the command name
601+
*/
602+
public function addUsage($usage)
583603
{
584-
if (null === $this->synopsis) {
585-
$this->synopsis = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis()));
604+
if (0 !== strpos($usage, $this->name)) {
605+
$usage = sprintf('%s %s', $this->name, $usage);
586606
}
587607

588-
return $this->synopsis;
608+
$this->usages[] = $usage;
609+
610+
return $this;
611+
}
612+
613+
/**
614+
* Returns alternative usages of the command.
615+
*
616+
* @return array
617+
*/
618+
public function getUsages()
619+
{
620+
return $this->usages;
589621
}
590622

591623
/**

src/Symfony/Component/Console/Descriptor/JsonDescriptor.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private function getInputArgumentData(InputArgument $argument)
102102
'name' => $argument->getName(),
103103
'is_required' => $argument->isRequired(),
104104
'is_array' => $argument->isArray(),
105-
'description' => $argument->getDescription(),
105+
'description' => preg_replace('/\s*\R\s*/', ' ', $argument->getDescription()),
106106
'default' => $argument->getDefault(),
107107
);
108108
}
@@ -120,7 +120,7 @@ private function getInputOptionData(InputOption $option)
120120
'accept_value' => $option->acceptValue(),
121121
'is_value_required' => $option->isValueRequired(),
122122
'is_multiple' => $option->isArray(),
123-
'description' => $option->getDescription(),
123+
'description' => preg_replace('/\s*\R\s*/', ' ', $option->getDescription()),
124124
'default' => $option->getDefault(),
125125
);
126126
}
@@ -157,10 +157,9 @@ private function getCommandData(Command $command)
157157

158158
return array(
159159
'name' => $command->getName(),
160-
'usage' => $command->getSynopsis(),
160+
'usage' => array_merge(array($command->getSynopsis()), $command->getUsages(), $command->getAliases()),
161161
'description' => $command->getDescription(),
162162
'help' => $command->getProcessedHelp(),
163-
'aliases' => $command->getAliases(),
164163
'definition' => $this->getInputDefinitionData($command->getNativeDefinition()),
165164
);
166165
}

src/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ protected function describeInputArgument(InputArgument $argument, array $options
3636
.'* Name: '.($argument->getName() ?: '<none>')."\n"
3737
.'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n"
3838
.'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n"
39-
.'* Description: '.($argument->getDescription() ?: '<none>')."\n"
39+
.'* Description: '.preg_replace('/\s*\R\s*/', PHP_EOL.' ', $argument->getDescription() ?: '<none>')."\n"
4040
.'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`'
4141
);
4242
}
@@ -53,7 +53,7 @@ protected function describeInputOption(InputOption $option, array $options = arr
5353
.'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n"
5454
.'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
5555
.'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n"
56-
.'* Description: '.($option->getDescription() ?: '<none>')."\n"
56+
.'* Description: '.preg_replace('/\s*\R\s*/', PHP_EOL.' ', $option->getDescription() ?: '<none>')."\n"
5757
.'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
5858
);
5959
}
@@ -96,12 +96,14 @@ protected function describeCommand(Command $command, array $options = array())
9696
$command->getName()."\n"
9797
.str_repeat('-', strlen($command->getName()))."\n\n"
9898
.'* Description: '.($command->getDescription() ?: '<none>')."\n"
99-
.'* Usage: `'.$command->getSynopsis().'`'."\n"
100-
.'* Aliases: '.(count($command->getAliases()) ? '`'.implode('`, `', $command->getAliases()).'`' : '<none>')
99+
.'* Usage:'."\n\n"
100+
.array_reduce(array_merge(array($command->getSynopsis()), $command->getAliases(), $command->getUsages()), function ($carry, $usage) {
101+
return $carry .= ' * `'.$usage.'`'."\n";
102+
})
101103
);
102104

103105
if ($help = $command->getProcessedHelp()) {
104-
$this->write("\n\n");
106+
$this->write("\n");
105107
$this->write($help);
106108
}
107109

src/Symfony/Component/Console/Descriptor/TextDescriptor.php

Lines changed: 82 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,19 @@ class TextDescriptor extends Descriptor
3232
protected function describeInputArgument(InputArgument $argument, array $options = array())
3333
{
3434
if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) {
35-
$default = sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($argument->getDefault()));
35+
$default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
3636
} else {
3737
$default = '';
3838
}
3939

40-
$nameWidth = isset($options['name_width']) ? $options['name_width'] : strlen($argument->getName());
40+
$totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName());
41+
$spacingWidth = $totalWidth - strlen($argument->getName()) + 2;
4142

42-
$this->writeText(sprintf(" <info>%-${nameWidth}s</info> %s%s",
43+
$this->writeText(sprintf(" <info>%s</info>%s%s%s",
4344
$argument->getName(),
44-
str_replace("\n", "\n".str_repeat(' ', $nameWidth + 2), $argument->getDescription()),
45+
str_repeat(' ', $spacingWidth),
46+
// + 17 = 2 spaces + <info> + </info> + 2 spaces
47+
preg_replace('/\s*\R\s*/', PHP_EOL.str_repeat(' ', $totalWidth + 17), $argument->getDescription()),
4548
$default
4649
), $options);
4750
}
@@ -52,18 +55,33 @@ protected function describeInputArgument(InputArgument $argument, array $options
5255
protected function describeInputOption(InputOption $option, array $options = array())
5356
{
5457
if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) {
55-
$default = sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($option->getDefault()));
58+
$default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
5659
} else {
5760
$default = '';
5861
}
5962

60-
$nameWidth = isset($options['name_width']) ? $options['name_width'] : strlen($option->getName());
61-
$nameWithShortcutWidth = $nameWidth - strlen($option->getName()) - 2;
63+
$value = '';
64+
if ($option->acceptValue()) {
65+
$value = '='.strtoupper($option->getName());
6266

63-
$this->writeText(sprintf(" <info>%s</info> %-${nameWithShortcutWidth}s%s%s%s",
64-
'--'.$option->getName(),
65-
$option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '',
66-
str_replace("\n", "\n".str_repeat(' ', $nameWidth + 2), $option->getDescription()),
67+
if ($option->isValueOptional()) {
68+
$value = '['.$value.']';
69+
}
70+
}
71+
72+
$totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions(array($option));
73+
$synopsis = sprintf('%s%s',
74+
$option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ',
75+
sprintf('--%s%s', $option->getName(), $value)
76+
);
77+
78+
$spacingWidth = $totalWidth - strlen($synopsis) + 2;
79+
80+
$this->writeText(sprintf(" <info>%s</info>%s%s%s%s",
81+
$synopsis,
82+
str_repeat(' ', $spacingWidth),
83+
// + 17 = 2 spaces + <info> + </info> + 2 spaces
84+
preg_replace('/\s*\R\s*/', "\n".str_repeat(' ', $totalWidth + 17), $option->getDescription()),
6785
$default,
6886
$option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''
6987
), $options);
@@ -74,24 +92,16 @@ protected function describeInputOption(InputOption $option, array $options = arr
7492
*/
7593
protected function describeInputDefinition(InputDefinition $definition, array $options = array())
7694
{
77-
$nameWidth = 0;
78-
foreach ($definition->getOptions() as $option) {
79-
$nameLength = strlen($option->getName()) + 2;
80-
if ($option->getShortcut()) {
81-
$nameLength += strlen($option->getShortcut()) + 3;
82-
}
83-
$nameWidth = max($nameWidth, $nameLength);
84-
}
95+
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
8596
foreach ($definition->getArguments() as $argument) {
86-
$nameWidth = max($nameWidth, strlen($argument->getName()));
97+
$totalWidth = max($totalWidth, strlen($argument->getName()));
8798
}
88-
++$nameWidth;
8999

90100
if ($definition->getArguments()) {
91101
$this->writeText('<comment>Arguments:</comment>', $options);
92102
$this->writeText("\n");
93103
foreach ($definition->getArguments() as $argument) {
94-
$this->describeInputArgument($argument, array_merge($options, array('name_width' => $nameWidth)));
104+
$this->describeInputArgument($argument, array_merge($options, array('total_width' => $totalWidth)));
95105
$this->writeText("\n");
96106
}
97107
}
@@ -101,11 +111,20 @@ protected function describeInputDefinition(InputDefinition $definition, array $o
101111
}
102112

103113
if ($definition->getOptions()) {
114+
$laterOptions = array();
115+
104116
$this->writeText('<comment>Options:</comment>', $options);
105-
$this->writeText("\n");
106117
foreach ($definition->getOptions() as $option) {
107-
$this->describeInputOption($option, array_merge($options, array('name_width' => $nameWidth)));
118+
if (strlen($option->getShortcut()) > 1) {
119+
$laterOptions[] = $option;
120+
continue;
121+
}
108122
$this->writeText("\n");
123+
$this->describeInputOption($option, array_merge($options, array('total_width' => $totalWidth)));
124+
}
125+
foreach ($laterOptions as $option) {
126+
$this->writeText("\n");
127+
$this->describeInputOption($option, array_merge($options, array('total_width' => $totalWidth)));
109128
}
110129
}
111130
}
@@ -115,27 +134,26 @@ protected function describeInputDefinition(InputDefinition $definition, array $o
115134
*/
116135
protected function describeCommand(Command $command, array $options = array())
117136
{
118-
$command->getSynopsis();
137+
$command->getSynopsis(true);
138+
$command->getSynopsis(false);
119139
$command->mergeApplicationDefinition(false);
120140

121141
$this->writeText('<comment>Usage:</comment>', $options);
122-
$this->writeText("\n");
123-
$this->writeText(' '.$command->getSynopsis(), $options);
124-
$this->writeText("\n");
125-
126-
if (count($command->getAliases()) > 0) {
142+
foreach (array_merge(array($command->getSynopsis(true)), $command->getAliases(), $command->getUsages()) as $usage) {
127143
$this->writeText("\n");
128-
$this->writeText('<comment>Aliases:</comment> <info>'.implode(', ', $command->getAliases()).'</info>', $options);
144+
$this->writeText(' '.$usage, $options);
129145
}
146+
$this->writeText("\n");
130147

131-
if ($definition = $command->getNativeDefinition()) {
148+
$definition = $command->getNativeDefinition();
149+
if ($definition->getOptions() || $definition->getArguments()) {
132150
$this->writeText("\n");
133151
$this->describeInputDefinition($definition, $options);
152+
$this->writeText("\n");
134153
}
135154

136-
$this->writeText("\n");
137-
138155
if ($help = $command->getProcessedHelp()) {
156+
$this->writeText("\n");
139157
$this->writeText('<comment>Help:</comment>', $options);
140158
$this->writeText("\n");
141159
$this->writeText(' '.str_replace("\n", "\n ", $help), $options);
@@ -164,27 +182,12 @@ protected function describeApplication(Application $application, array $options
164182
}
165183

166184
$this->writeText("<comment>Usage:</comment>\n", $options);
167-
$this->writeText(" command [options] [arguments]\n\n", $options);
168-
$this->writeText('<comment>Options:</comment>', $options);
169-
170-
$inputOptions = $application->getDefinition()->getOptions();
171-
172-
$width = 0;
173-
foreach ($inputOptions as $option) {
174-
$nameLength = strlen($option->getName()) + 2;
175-
if ($option->getShortcut()) {
176-
$nameLength += strlen($option->getShortcut()) + 3;
177-
}
178-
$width = max($width, $nameLength);
179-
}
180-
++$width;
185+
$this->writeText(" command [options] [arguments]\n\n", $options);
181186

182-
foreach ($inputOptions as $option) {
183-
$this->writeText("\n", $options);
184-
$this->describeInputOption($option, array_merge($options, array('name_width' => $width)));
185-
}
187+
$this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options);
186188

187-
$this->writeText("\n\n", $options);
189+
$this->writeText("\n");
190+
$this->writeText("\n");
188191

189192
$width = $this->getColumnWidth($description->getCommands());
190193

@@ -198,12 +201,13 @@ protected function describeApplication(Application $application, array $options
198201
foreach ($description->getNamespaces() as $namespace) {
199202
if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
200203
$this->writeText("\n");
201-
$this->writeText('<comment>'.$namespace['id'].'</comment>', $options);
204+
$this->writeText(' <comment>'.$namespace['id'].'</comment>', $options);
202205
}
203206

204207
foreach ($namespace['commands'] as $name) {
205208
$this->writeText("\n");
206-
$this->writeText(sprintf(" <info>%-${width}s</info> %s", $name, $description->getCommand($name)->getDescription()), $options);
209+
$spacingWidth = $width - strlen($name);
210+
$this->writeText(sprintf(" <info>%s</info>%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)->getDescription()), $options);
207211
}
208212
}
209213

@@ -252,4 +256,27 @@ private function getColumnWidth(array $commands)
252256

253257
return $width + 2;
254258
}
259+
260+
/**
261+
* @param InputOption[] $options
262+
*
263+
* @return int
264+
*/
265+
private function calculateTotalWidthForOptions($options)
266+
{
267+
$totalWidth = 0;
268+
foreach ($options as $option) {
269+
$nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + --
270+
271+
if ($option->acceptValue()) {
272+
$valueLength = 1 + strlen($option->getName()); // = + value
273+
$valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
274+
275+
$nameLength += $valueLength;
276+
}
277+
$totalWidth = max($totalWidth, $nameLength);
278+
}
279+
280+
return $totalWidth;
281+
}
255282
}

src/Symfony/Component/Console/Descriptor/XmlDescriptor.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,18 @@ public function getCommandDocument(Com D3F6 mand $command)
6565
$commandXML->setAttribute('id', $command->getName());
6666
$commandXML->setAttribute('name', $command->getName());
6767

68-
$commandXML->appendChild($usageXML = $dom->createElement('usage'));
69-
$usageXML->appendChild($dom->createTextNode(sprintf($command->getSynopsis(), '')));
68+
$commandXML->appendChild($usagesXML = $dom->createElement('usages'));
69+
70+
foreach (array_merge(array($command->getSynopsis()), $command->getAliases(), $command->getUsages()) as $usage) {
71+
$usagesXML->appendChild($dom->createElement('usage', $usage));
72+
}
7073

7174
$commandXML->appendChild($descriptionXML = $dom->createElement('description'));
7275
$descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())));
7376

7477
$commandXML->appendChild($helpXML = $dom->createElement('help'));
7578
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
7679

77-
$commandXML->appendChild($aliasesXML = $dom->createElement('aliases'));
78-
foreach ($command->getAliases() as $alias) {
79-
$aliasesXML->appendChild($aliasXML = $dom->createElement('alias'));
80-
$aliasXML->appendChild($dom->createTextNode($alias));
81-
}
82-
8380
$definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition());
8481
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
8582

0 commit comments

Comments
 (0)
0