TextDescriptor.php 12 KB

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