Chris@13: filter = new FilterOptions();
Chris@13:
Chris@13: parent::__construct($name);
Chris@13: }
Chris@13:
Chris@13: /**
Chris@13: * Set the Shell's Readline service.
Chris@13: *
Chris@13: * @param Readline $readline
Chris@13: */
Chris@13: public function setReadline(Readline $readline)
Chris@13: {
Chris@13: $this->readline = $readline;
Chris@13: }
Chris@13:
Chris@13: /**
Chris@13: * {@inheritdoc}
Chris@13: */
Chris@13: protected function configure()
Chris@13: {
Chris@13: list($grep, $insensitive, $invert) = FilterOptions::getOptions();
Chris@13:
Chris@13: $this
Chris@13: ->setName('history')
Chris@13: ->setAliases(['hist'])
Chris@13: ->setDefinition([
Chris@13: new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines.'),
Chris@13: new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'),
Chris@13: new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'),
Chris@13:
Chris@13: $grep,
Chris@13: $insensitive,
Chris@13: $invert,
Chris@13:
Chris@13: new InputOption('no-numbers', 'N', InputOption::VALUE_NONE, 'Omit line numbers.'),
Chris@13:
Chris@13: new InputOption('save', '', InputOption::VALUE_REQUIRED, 'Save history to a file.'),
Chris@13: new InputOption('replay', '', InputOption::VALUE_NONE, 'Replay.'),
Chris@13: new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the history.'),
Chris@13: ])
Chris@13: ->setDescription('Show the Psy Shell history.')
Chris@13: ->setHelp(
Chris@13: <<<'HELP'
Chris@13: Show, search, save or replay the Psy Shell history.
Chris@13:
Chris@13: e.g.
Chris@13: >>> history --grep /[bB]acon/
Chris@13: >>> history --show 0..10 --replay
Chris@13: >>> history --clear
Chris@13: >>> history --tail 1000 --save somefile.txt
Chris@13: HELP
Chris@13: );
Chris@13: }
Chris@13:
Chris@13: /**
Chris@13: * {@inheritdoc}
Chris@13: */
Chris@13: protected function execute(InputInterface $input, OutputInterface $output)
Chris@13: {
Chris@13: $this->validateOnlyOne($input, ['show', 'head', 'tail']);
Chris@13: $this->validateOnlyOne($input, ['save', 'replay', 'clear']);
Chris@13:
Chris@13: $history = $this->getHistorySlice(
Chris@13: $input->getOption('show'),
Chris@13: $input->getOption('head'),
Chris@13: $input->getOption('tail')
Chris@13: );
Chris@13: $highlighted = false;
Chris@13:
Chris@13: $this->filter->bind($input);
Chris@13: if ($this->filter->hasFilter()) {
Chris@13: $matches = [];
Chris@13: $highlighted = [];
Chris@13: foreach ($history as $i => $line) {
Chris@13: if ($this->filter->match($line, $matches)) {
Chris@13: if (isset($matches[0])) {
Chris@17: $chunks = \explode($matches[0], $history[$i]);
Chris@17: $chunks = \array_map([__CLASS__, 'escape'], $chunks);
Chris@17: $glue = \sprintf('%s', self::escape($matches[0]));
Chris@13:
Chris@17: $highlighted[$i] = \implode($glue, $chunks);
Chris@13: }
Chris@13: } else {
Chris@13: unset($history[$i]);
Chris@13: }
Chris@13: }
Chris@13: }
Chris@13:
Chris@13: if ($save = $input->getOption('save')) {
Chris@17: $output->writeln(\sprintf('Saving history in %s...', $save));
Chris@17: \file_put_contents($save, \implode(PHP_EOL, $history) . PHP_EOL);
Chris@13: $output->writeln('History saved.');
Chris@13: } elseif ($input->getOption('replay')) {
Chris@13: if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) {
Chris@13: throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying');
Chris@13: }
Chris@13:
Chris@17: $count = \count($history);
Chris@17: $output->writeln(\sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : ''));
Chris@13: $this->getApplication()->addInput($history);
Chris@13: } elseif ($input->getOption('clear')) {
Chris@13: $this->clearHistory();
Chris@13: $output->writeln('History cleared.');
Chris@13: } else {
Chris@13: $type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES;
Chris@13: if (!$highlighted) {
Chris@13: $type = $type | ShellOutput::OUTPUT_RAW;
Chris@13: }
Chris@13:
Chris@13: $output->page($highlighted ?: $history, $type);
Chris@13: }
Chris@13: }
Chris@13:
Chris@13: /**
Chris@13: * Extract a range from a string.
Chris@13: *
Chris@13: * @param string $range
Chris@13: *
Chris@13: * @return array [ start, end ]
Chris@13: */
Chris@13: private function extractRange($range)
Chris@13: {
Chris@17: if (\preg_match('/^\d+$/', $range)) {
Chris@13: return [$range, $range + 1];
Chris@13: }
Chris@13:
Chris@13: $matches = [];
Chris@17: if ($range !== '..' && \preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) {
Chris@17: $start = $matches[1] ? \intval($matches[1]) : 0;
Chris@17: $end = $matches[2] ? \intval($matches[2]) + 1 : PHP_INT_MAX;
Chris@13:
Chris@13: return [$start, $end];
Chris@13: }
Chris@13:
Chris@13: throw new \InvalidArgumentException('Unexpected range: ' . $range);
Chris@13: }
Chris@13:
Chris@13: /**
Chris@13: * Retrieve a slice of the readline history.
Chris@13: *
Chris@13: * @param string $show
Chris@13: * @param string $head
Chris@13: * @param string $tail
Chris@13: *
Chris@13: * @return array A slilce of history
Chris@13: */
Chris@13: private function getHistorySlice($show, $head, $tail)
Chris@13: {
Chris@13: $history = $this->readline->listHistory();
Chris@13:
Chris@13: // don't show the current `history` invocation
Chris@17: \array_pop($history);
Chris@13:
Chris@13: if ($show) {
Chris@13: list($start, $end) = $this->extractRange($show);
Chris@13: $length = $end - $start;
Chris@13: } elseif ($head) {
Chris@17: if (!\preg_match('/^\d+$/', $head)) {
Chris@13: throw new \InvalidArgumentException('Please specify an integer argument for --head');
Chris@13: }
Chris@13:
Chris@13: $start = 0;
Chris@17: $length = \intval($head);
Chris@13: } elseif ($tail) {
Chris@17: if (!\preg_match('/^\d+$/', $tail)) {
Chris@13: throw new \InvalidArgumentException('Please specify an integer argument for --tail');
Chris@13: }
Chris@13:
Chris@17: $start = \count($history) - $tail;
Chris@17: $length = \intval($tail) + 1;
Chris@13: } else {
Chris@13: return $history;
Chris@13: }
Chris@13:
Chris@17: return \array_slice($history, $start, $length, true);
Chris@13: }
Chris@13:
Chris@13: /**
Chris@13: * Validate that only one of the given $options is set.
Chris@13: *
Chris@13: * @param InputInterface $input
Chris@13: * @param array $options
Chris@13: */
Chris@13: private function validateOnlyOne(InputInterface $input, array $options)
Chris@13: {
Chris@13: $count = 0;
Chris@13: foreach ($options as $opt) {
Chris@13: if ($input->getOption($opt)) {
Chris@13: $count++;
Chris@13: }
Chris@13: }
Chris@13:
Chris@13: if ($count > 1) {
Chris@17: throw new \InvalidArgumentException('Please specify only one of --' . \implode(', --', $options));
Chris@13: }
Chris@13: }
Chris@13:
Chris@13: /**
Chris@13: * Clear the readline history.
Chris@13: */
Chris@13: private function clearHistory()
Chris@13: {
Chris@13: $this->readline->clearHistory();
Chris@13: }
Chris@13:
Chris@13: public static function escape($string)
Chris@13: {
Chris@13: return OutputFormatter::escape($string);
Chris@13: }
Chris@13: }