Revision 148 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?php/** This file is part of Psy Shell.** (c) 2012-2023 Justin Hileman** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace Psy\Command;use Psy\Input\FilterOptions;use Psy\Output\ShellOutput;use Psy\Readline\Readline;use Symfony\Component\Console\Formatter\OutputFormatter;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Input\InputOption;use Symfony\Component\Console\Output\OutputInterface;/*** Psy Shell history command.** Shows, searches and replays readline history. Not too shabby.*/class HistoryCommand extends Command{private $filter;private $readline;/*** {@inheritdoc}*/public function __construct($name = null){$this->filter = new FilterOptions();parent::__construct($name);}/*** Set the Shell's Readline service.** @param Readline $readline*/public function setReadline(Readline $readline){$this->readline = $readline;}/*** {@inheritdoc}*/protected function configure(){list($grep, $insensitive, $invert) = FilterOptions::getOptions();$this->setName('history')->setAliases(['hist'])->setDefinition([new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines.'),new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'),new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'),$grep,$insensitive,$invert,new InputOption('no-numbers', 'N', InputOption::VALUE_NONE, 'Omit line numbers.'),new InputOption('save', '', InputOption::VALUE_REQUIRED, 'Save history to a file.'),new InputOption('replay', '', InputOption::VALUE_NONE, 'Replay.'),new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the history.'),])->setDescription('Show the Psy Shell history.')->setHelp(<<<'HELP'Show, search, save or replay the Psy Shell history.e.g.<return>>>> history --grep /[bB]acon/</return><return>>>> history --show 0..10 --replay</return><return>>>> history --clear</return><return>>>> history --tail 1000 --save somefile.txt</return>HELP);}/*** {@inheritdoc}** @return int 0 if everything went fine, or an exit code*/protected function execute(InputInterface $input, OutputInterface $output){$this->validateOnlyOne($input, ['show', 'head', 'tail']);$this->validateOnlyOne($input, ['save', 'replay', 'clear']);$history = $this->getHistorySlice($input->getOption('show'),$input->getOption('head'),$input->getOption('tail'));$highlighted = false;$this->filter->bind($input);if ($this->filter->hasFilter()) {$matches = [];$highlighted = [];foreach ($history as $i => $line) {if ($this->filter->match($line, $matches)) {if (isset($matches[0])) {$chunks = \explode($matches[0], $history[$i]);$chunks = \array_map([__CLASS__, 'escape'], $chunks);$glue = \sprintf('<urgent>%s</urgent>', self::escape($matches[0]));$highlighted[$i] = \implode($glue, $chunks);}} else {unset($history[$i]);}}}if ($save = $input->getOption('save')) {$output->writeln(\sprintf('Saving history in %s...', $save));\file_put_contents($save, \implode(\PHP_EOL, $history).\PHP_EOL);$output->writeln('<info>History saved.</info>');} elseif ($input->getOption('replay')) {if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) {throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying');}$count = \count($history);$output->writeln(\sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : ''));$this->getApplication()->addInput($history);} elseif ($input->getOption('clear')) {$this->clearHistory();$output->writeln('<info>History cleared.</info>');} else {$type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES;if (!$highlighted) {$type = $type | OutputInterface::OUTPUT_RAW;}$output->page($highlighted ?: $history, $type);}return 0;}/*** Extract a range from a string.** @param string $range** @return array [ start, end ]*/private function extractRange(string $range): array{if (\preg_match('/^\d+$/', $range)) {return [$range, $range + 1];}$matches = [];if ($range !== '..' && \preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) {$start = $matches[1] ? (int) $matches[1] : 0;$end = $matches[2] ? (int) $matches[2] + 1 : \PHP_INT_MAX;return [$start, $end];}throw new \InvalidArgumentException('Unexpected range: '.$range);}/*** Retrieve a slice of the readline history.** @param string|null $show* @param string|null $head* @param string|null $tail** @return array A slice of history*/private function getHistorySlice($show, $head, $tail): array{$history = $this->readline->listHistory();// don't show the current `history` invocation\array_pop($history);if ($show) {list($start, $end) = $this->extractRange($show);$length = $end - $start;} elseif ($head) {if (!\preg_match('/^\d+$/', $head)) {throw new \InvalidArgumentException('Please specify an integer argument for --head');}$start = 0;$length = (int) $head;} elseif ($tail) {if (!\preg_match('/^\d+$/', $tail)) {throw new \InvalidArgumentException('Please specify an integer argument for --tail');}$start = \count($history) - $tail;$length = (int) $tail + 1;} else {return $history;}return \array_slice($history, $start, $length, true);}/*** Validate that only one of the given $options is set.** @param InputInterface $input* @param array $options*/private function validateOnlyOne(InputInterface $input, array $options){$count = 0;foreach ($options as $opt) {if ($input->getOption($opt)) {$count++;}}if ($count > 1) {throw new \InvalidArgumentException('Please specify only one of --'.\implode(', --', $options));}}/*** Clear the readline history.*/private function clearHistory(){$this->readline->clearHistory();}public static function escape(string $string): string{return OutputFormatter::escape($string);}}