1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Console\Descriptor; 13 14use Symfony\Component\Console\Application; 15use Symfony\Component\Console\Command\Command; 16use Symfony\Component\Console\Formatter\OutputFormatter; 17use Symfony\Component\Console\Helper\Helper; 18use Symfony\Component\Console\Input\InputArgument; 19use Symfony\Component\Console\Input\InputDefinition; 20use Symfony\Component\Console\Input\InputOption; 21 22/** 23 * Text descriptor. 24 * 25 * @author Jean-François Simon <contact@jfsimon.fr> 26 * 27 * @internal 28 */ 29class TextDescriptor extends Descriptor 30{ 31 /** 32 * {@inheritdoc} 33 */ 34 protected function describeInputArgument(InputArgument $argument, array $options = []) 35 { 36 if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { 37 $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault())); 38 } else { 39 $default = ''; 40 } 41 42 $totalWidth = isset($options['total_width']) ? $options['total_width'] : Helper::strlen($argument->getName()); 43 $spacingWidth = $totalWidth - \strlen($argument->getName()); 44 45 $this->writeText(sprintf(' <info>%s</info> %s%s%s', 46 $argument->getName(), 47 str_repeat(' ', $spacingWidth), 48 // + 4 = 2 spaces before <info>, 2 spaces after </info> 49 preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()), 50 $default 51 ), $options); 52 } 53 54 /** 55 * {@inheritdoc} 56 */ 57 protected function describeInputOption(InputOption $option, array $options = []) 58 { 59 if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { 60 $default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault())); 61 } else { 62 $default = ''; 63 } 64 65 $value = ''; 66 if ($option->acceptValue()) { 67 $value = '='.strtoupper($option->getName()); 68 69 if ($option->isValueOptional()) { 70 $value = '['.$value.']'; 71 } 72 } 73 74 $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); 75 $synopsis = sprintf('%s%s', 76 $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', 77 sprintf('--%s%s', $option->getName(), $value) 78 ); 79 80 $spacingWidth = $totalWidth - Helper::strlen($synopsis); 81 82 $this->writeText(sprintf(' <info>%s</info> %s%s%s%s', 83 $synopsis, 84 str_repeat(' ', $spacingWidth), 85 // + 4 = 2 spaces before <info>, 2 spaces after </info> 86 preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()), 87 $default, 88 $option->isArray() ? '<comment> (multiple values allowed)</comment>' : '' 89 ), $options); 90 } 91 92 /** 93 * {@inheritdoc} 94 */ 95 protected function describeInputDefinition(InputDefinition $definition, array $options = []) 96 { 97 $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); 98 foreach ($definition->getArguments() as $argument) { 99 $totalWidth = max($totalWidth, Helper::strlen($argument->getName())); 100 } 101 102 if ($definition->getArguments()) { 103 $this->writeText('<comment>Arguments:</comment>', $options); 104 $this->writeText("\n"); 105 foreach ($definition->getArguments() as $argument) { 106 $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); 107 $this->writeText("\n"); 108 } 109 } 110 111 if ($definition->getArguments() && $definition->getOptions()) { 112 $this->writeText("\n"); 113 } 114 115 if ($definition->getOptions()) { 116 $laterOptions = []; 117 118 $this->writeText('<comment>Options:</comment>', $options); 119 foreach ($definition->getOptions() as $option) { 120 if (\strlen($option->getShortcut()) > 1) { 121 $laterOptions[] = $option; 122 continue; 123 } 124 $this->writeText("\n"); 125 $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); 126 } 127 foreach ($laterOptions as $option) { 128 $this->writeText("\n"); 129 $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); 130 } 131 } 132 } 133 134 /** 135 * {@inheritdoc} 136 */ 137 protected function describeCommand(Command $command, array $options = []) 138 { 139 $command->getSynopsis(true); 140 $command->getSynopsis(false); 141 $command->mergeApplicationDefinition(false); 142 143 if ($description = $command->getDescription()) { 144 $this->writeText('<comment>Description:</comment>', $options); 145 $this->writeText("\n"); 146 $this->writeText(' '.$description); 147 $this->writeText("\n\n"); 148 } 149 150 $this->writeText('<comment>Usage:</comment>', $options); 151 foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { 152 $this->writeText("\n"); 153 $this->writeText(' '.OutputFormatter::escape($usage), $options); 154 } 155 $this->writeText("\n"); 156 157 $definition = $command->getNativeDefinition(); 158 if ($definition->getOptions() || $definition->getArguments()) { 159 $this->writeText("\n"); 160 $this->describeInputDefinition($definition, $options); 161 $this->writeText("\n"); 162 } 163 164 $help = $command->getProcessedHelp(); 165 if ($help && $help !== $description) { 166 $this->writeText("\n"); 167 $this->writeText('<comment>Help:</comment>', $options); 168 $this->writeText("\n"); 169 $this->writeText(' '.str_replace("\n", "\n ", $help), $options); 170 $this->writeText("\n"); 171 } 172 } 173 174 /** 175 * {@inheritdoc} 176 */ 177 protected function describeApplication(Application $application, array $options = []) 178 { 179 $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; 180 $description = new ApplicationDescription($application, $describedNamespace); 181 182 if (isset($options['raw_text']) && $options['raw_text']) { 183 $width = $this->getColumnWidth($description->getCommands()); 184 185 foreach ($description->getCommands() as $command) { 186 $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options); 187 $this->writeText("\n"); 188 } 189 } else { 190 if ('' != $help = $application->getHelp()) { 191 $this->writeText("$help\n\n", $options); 192 } 193 194 $this->writeText("<comment>Usage:</comment>\n", $options); 195 $this->writeText(" command [options] [arguments]\n\n", $options); 196 197 $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options); 198 199 $this->writeText("\n"); 200 $this->writeText("\n"); 201 202 $commands = $description->getCommands(); 203 $namespaces = $description->getNamespaces(); 204 if ($describedNamespace && $namespaces) { 205 // make sure all alias commands are included when describing a specific namespace 206 $describedNamespaceInfo = reset($namespaces); 207 foreach ($describedNamespaceInfo['commands'] as $name) { 208 $commands[$name] = $description->getCommand($name); 209 } 210 } 211 212 // calculate max. width based on available commands per namespace 213 $width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) { 214 return array_intersect($namespace['commands'], array_keys($commands)); 215 }, $namespaces)))); 216 217 if ($describedNamespace) { 218 $this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options); 219 } else { 220 $this->writeText('<comment>Available commands:</comment>', $options); 221 } 222 223 foreach ($namespaces as $namespace) { 224 $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) { 225 return isset($commands[$name]); 226 }); 227 228 if (!$namespace['commands']) { 229 continue; 230 } 231 232 if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { 233 $this->writeText("\n"); 234 $this->writeText(' <comment>'.$namespace['id'].'</comment>', $options); 235 } 236 237 foreach ($namespace['commands'] as $name) { 238 $this->writeText("\n"); 239 $spacingWidth = $width - Helper::strlen($name); 240 $command = $commands[$name]; 241 $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : ''; 242 $this->writeText(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options); 243 } 244 } 245 246 $this->writeText("\n"); 247 } 248 } 249 250 /** 251 * {@inheritdoc} 252 */ 253 private function writeText(string $content, array $options = []) 254 { 255 $this->write( 256 isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, 257 isset($options['raw_output']) ? !$options['raw_output'] : true 258 ); 259 } 260 261 /** 262 * Formats command aliases to show them in the command description. 263 */ 264 private function getCommandAliasesText(Command $command): string 265 { 266 $text = ''; 267 $aliases = $command->getAliases(); 268 269 if ($aliases) { 270 $text = '['.implode('|', $aliases).'] '; 271 } 272 273 return $text; 274 } 275 276 /** 277 * Formats input option/argument default value. 278 * 279 * @param mixed $default 280 */ 281 private function formatDefaultValue($default): string 282 { 283 if (INF === $default) { 284 return 'INF'; 285 } 286 287 if (\is_string($default)) { 288 $default = OutputFormatter::escape($default); 289 } elseif (\is_array($default)) { 290 foreach ($default as $key => $value) { 291 if (\is_string($value)) { 292 $default[$key] = OutputFormatter::escape($value); 293 } 294 } 295 } 296 297 return str_replace('\\\\', '\\', json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); 298 } 299 300 /** 301 * @param (Command|string)[] $commands 302 */ 303 private function getColumnWidth(array $commands): int 304 { 305 $widths = []; 306 307 foreach ($commands as $command) { 308 if ($command instanceof Command) { 309 $widths[] = Helper::strlen($command->getName()); 310 foreach ($command->getAliases() as $alias) { 311 $widths[] = Helper::strlen($alias); 312 } 313 } else { 314 $widths[] = Helper::strlen($command); 315 } 316 } 317 318 return $widths ? max($widths) + 2 : 0; 319 } 320 321 /** 322 * @param InputOption[] $options 323 */ 324 private function calculateTotalWidthForOptions(array $options): int 325 { 326 $totalWidth = 0; 327 foreach ($options as $option) { 328 // "-" + shortcut + ", --" + name 329 $nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName()); 330 331 if ($option->acceptValue()) { 332 $valueLength = 1 + Helper::strlen($option->getName()); // = + value 333 $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] 334 335 $nameLength += $valueLength; 336 } 337 $totalWidth = max($totalWidth, $nameLength); 338 } 339 340 return $totalWidth; 341 } 342} 343