You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
7.0 KiB

  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Core\Command;
  8. use OC\Core\Command\User\ListCommand;
  9. use OCP\Defaults;
  10. use OCP\Server;
  11. use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
  12. use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
  13. use Symfony\Component\Console\Command\Command;
  14. use Symfony\Component\Console\Helper\Table;
  15. use Symfony\Component\Console\Input\InputInterface;
  16. use Symfony\Component\Console\Input\InputOption;
  17. use Symfony\Component\Console\Output\OutputInterface;
  18. class Base extends Command implements CompletionAwareInterface {
  19. public const OUTPUT_FORMAT_PLAIN = 'plain';
  20. public const OUTPUT_FORMAT_JSON = 'json';
  21. public const OUTPUT_FORMAT_JSON_PRETTY = 'json_pretty';
  22. protected string $defaultOutputFormat = self::OUTPUT_FORMAT_PLAIN;
  23. private bool $php_pcntl_signal = false;
  24. private bool $interrupted = false;
  25. protected function configure() {
  26. // Some of our commands do not extend this class; and some of those that do do not call parent::configure()
  27. $defaultHelp = 'More extensive and thorough documentation may be found at ' . Server::get(Defaults::class)->getDocBaseUrl() . PHP_EOL;
  28. $this
  29. ->setHelp($defaultHelp)
  30. ->addOption(
  31. 'output',
  32. null,
  33. InputOption::VALUE_OPTIONAL,
  34. 'Output format (plain, json or json_pretty, default is plain)',
  35. $this->defaultOutputFormat
  36. )
  37. ;
  38. }
  39. protected function writeArrayInOutputFormat(InputInterface $input, OutputInterface $output, iterable $items, string $prefix = ' - '): void {
  40. switch ($input->getOption('output')) {
  41. case self::OUTPUT_FORMAT_JSON:
  42. $items = (is_array($items) ? $items : iterator_to_array($items));
  43. $output->writeln(json_encode($items));
  44. break;
  45. case self::OUTPUT_FORMAT_JSON_PRETTY:
  46. $items = (is_array($items) ? $items : iterator_to_array($items));
  47. $output->writeln(json_encode($items, JSON_PRETTY_PRINT));
  48. break;
  49. default:
  50. foreach ($items as $key => $item) {
  51. if (is_iterable($item)) {
  52. $output->writeln($prefix . $key . ':');
  53. $this->writeArrayInOutputFormat($input, $output, $item, ' ' . $prefix);
  54. continue;
  55. }
  56. if (!is_int($key) || get_class($this) === ListCommand::class) {
  57. $value = $this->valueToString($item);
  58. if (!is_null($value)) {
  59. $output->writeln($prefix . $key . ': ' . $value);
  60. } else {
  61. $output->writeln($prefix . $key);
  62. }
  63. } else {
  64. $output->writeln($prefix . $this->valueToString($item));
  65. }
  66. }
  67. break;
  68. }
  69. }
  70. protected function writeTableInOutputFormat(InputInterface $input, OutputInterface $output, array $items): void {
  71. switch ($input->getOption('output')) {
  72. case self::OUTPUT_FORMAT_JSON:
  73. $output->writeln(json_encode($items));
  74. break;
  75. case self::OUTPUT_FORMAT_JSON_PRETTY:
  76. $output->writeln(json_encode($items, JSON_PRETTY_PRINT));
  77. break;
  78. default:
  79. $table = new Table($output);
  80. $table->setRows($items);
  81. if (!empty($items) && is_string(array_key_first(reset($items)))) {
  82. $table->setHeaders(array_keys(reset($items)));
  83. }
  84. $table->render();
  85. break;
  86. }
  87. }
  88. protected function writeStreamingTableInOutputFormat(InputInterface $input, OutputInterface $output, \Iterator $items, int $tableGroupSize): void {
  89. switch ($input->getOption('output')) {
  90. case self::OUTPUT_FORMAT_JSON:
  91. case self::OUTPUT_FORMAT_JSON_PRETTY:
  92. $this->writeStreamingJsonArray($input, $output, $items);
  93. break;
  94. default:
  95. foreach ($this->chunkIterator($items, $tableGroupSize) as $chunk) {
  96. $this->writeTableInOutputFormat($input, $output, $chunk);
  97. }
  98. break;
  99. }
  100. }
  101. protected function writeStreamingJsonArray(InputInterface $input, OutputInterface $output, \Iterator $items): void {
  102. $first = true;
  103. $outputType = $input->getOption('output');
  104. $output->writeln('[');
  105. foreach ($items as $item) {
  106. if (!$first) {
  107. $output->writeln(',');
  108. }
  109. if ($outputType === self::OUTPUT_FORMAT_JSON_PRETTY) {
  110. $output->write(json_encode($item, JSON_PRETTY_PRINT));
  111. } else {
  112. $output->write(json_encode($item));
  113. }
  114. $first = false;
  115. }
  116. $output->writeln("\n]");
  117. }
  118. public function chunkIterator(\Iterator $iterator, int $count): \Iterator {
  119. $chunk = [];
  120. for ($i = 0; $iterator->valid(); $i++) {
  121. $chunk[] = $iterator->current();
  122. $iterator->next();
  123. if (count($chunk) == $count) {
  124. // Got a full chunk, yield and start a new one
  125. yield $chunk;
  126. $chunk = [];
  127. }
  128. }
  129. if (count($chunk)) {
  130. // Yield the last chunk even if incomplete
  131. yield $chunk;
  132. }
  133. }
  134. /**
  135. * @param mixed $item
  136. */
  137. protected function writeMixedInOutputFormat(InputInterface $input, OutputInterface $output, $item) {
  138. if (is_array($item)) {
  139. $this->writeArrayInOutputFormat($input, $output, $item, '');
  140. return;
  141. }
  142. switch ($input->getOption('output')) {
  143. case self::OUTPUT_FORMAT_JSON:
  144. $output->writeln(json_encode($item));
  145. break;
  146. case self::OUTPUT_FORMAT_JSON_PRETTY:
  147. $output->writeln(json_encode($item, JSON_PRETTY_PRINT));
  148. break;
  149. default:
  150. $output->writeln($this->valueToString($item, false));
  151. break;
  152. }
  153. }
  154. protected function valueToString($value, bool $returnNull = true): ?string {
  155. if ($value === false) {
  156. return 'false';
  157. } elseif ($value === true) {
  158. return 'true';
  159. } elseif ($value === null) {
  160. return $returnNull ? null : 'null';
  161. } if ($value instanceof \UnitEnum) {
  162. return $value->value;
  163. } else {
  164. return $value;
  165. }
  166. }
  167. /**
  168. * Throw InterruptedException when interrupted by user
  169. *
  170. * @throws InterruptedException
  171. */
  172. protected function abortIfInterrupted() {
  173. if ($this->php_pcntl_signal === false) {
  174. return;
  175. }
  176. pcntl_signal_dispatch();
  177. if ($this->interrupted === true) {
  178. throw new InterruptedException('Command interrupted by user');
  179. }
  180. }
  181. /**
  182. * Changes the status of the command to "interrupted" if ctrl-c has been pressed
  183. *
  184. * Gives a chance to the command to properly terminate what it's doing
  185. */
  186. public function cancelOperation(): void {
  187. $this->interrupted = true;
  188. }
  189. public function run(InputInterface $input, OutputInterface $output): int {
  190. // check if the php pcntl_signal functions are accessible
  191. $this->php_pcntl_signal = function_exists('pcntl_signal');
  192. if ($this->php_pcntl_signal) {
  193. // Collect interrupts and notify the running command
  194. pcntl_signal(SIGTERM, [$this, 'cancelOperation']);
  195. pcntl_signal(SIGINT, [$this, 'cancelOperation']);
  196. }
  197. return parent::run($input, $output);
  198. }
  199. /**
  200. * @param string $optionName
  201. * @param CompletionContext $context
  202. * @return string[]
  203. */
  204. public function completeOptionValues($optionName, CompletionContext $context) {
  205. if ($optionName === 'output') {
  206. return ['plain', 'json', 'json_pretty'];
  207. }
  208. return [];
  209. }
  210. /**
  211. * @param string $argumentName
  212. * @param CompletionContext $context
  213. * @return string[]
  214. */
  215. public function completeArgumentValues($argumentName, CompletionContext $context) {
  216. return [];
  217. }
  218. }