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: }