eef8ef9d0905c5348f26ba03327c32a0a2881303
[yaffs-website] / vendor / psy / psysh / src / Command / HistoryCommand.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2018 Justin Hileman
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Psy\Command;
13
14 use Psy\Input\FilterOptions;
15 use Psy\Output\ShellOutput;
16 use Psy\Readline\Readline;
17 use Symfony\Component\Console\Formatter\OutputFormatter;
18 use Symfony\Component\Console\Input\InputInterface;
19 use Symfony\Component\Console\Input\InputOption;
20 use Symfony\Component\Console\Output\OutputInterface;
21
22 /**
23  * Psy Shell history command.
24  *
25  * Shows, searches and replays readline history. Not too shabby.
26  */
27 class HistoryCommand extends Command
28 {
29     private $filter;
30     private $readline;
31
32     /**
33      * {@inheritdoc}
34      */
35     public function __construct($name = null)
36     {
37         $this->filter = new FilterOptions();
38
39         parent::__construct($name);
40     }
41
42     /**
43      * Set the Shell's Readline service.
44      *
45      * @param Readline $readline
46      */
47     public function setReadline(Readline $readline)
48     {
49         $this->readline = $readline;
50     }
51
52     /**
53      * {@inheritdoc}
54      */
55     protected function configure()
56     {
57         list($grep, $insensitive, $invert) = FilterOptions::getOptions();
58
59         $this
60             ->setName('history')
61             ->setAliases(['hist'])
62             ->setDefinition([
63                 new InputOption('show',        's', InputOption::VALUE_REQUIRED, 'Show the given range of lines.'),
64                 new InputOption('head',        'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'),
65                 new InputOption('tail',        'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'),
66
67                 $grep,
68                 $insensitive,
69                 $invert,
70
71                 new InputOption('no-numbers',  'N', InputOption::VALUE_NONE,     'Omit line numbers.'),
72
73                 new InputOption('save',        '',  InputOption::VALUE_REQUIRED, 'Save history to a file.'),
74                 new InputOption('replay',      '',  InputOption::VALUE_NONE,     'Replay.'),
75                 new InputOption('clear',       '',  InputOption::VALUE_NONE,     'Clear the history.'),
76             ])
77             ->setDescription('Show the Psy Shell history.')
78             ->setHelp(
79                 <<<'HELP'
80 Show, search, save or replay the Psy Shell history.
81
82 e.g.
83 <return>>>> history --grep /[bB]acon/</return>
84 <return>>>> history --show 0..10 --replay</return>
85 <return>>>> history --clear</return>
86 <return>>>> history --tail 1000 --save somefile.txt</return>
87 HELP
88             );
89     }
90
91     /**
92      * {@inheritdoc}
93      */
94     protected function execute(InputInterface $input, OutputInterface $output)
95     {
96         $this->validateOnlyOne($input, ['show', 'head', 'tail']);
97         $this->validateOnlyOne($input, ['save', 'replay', 'clear']);
98
99         $history = $this->getHistorySlice(
100             $input->getOption('show'),
101             $input->getOption('head'),
102             $input->getOption('tail')
103         );
104         $highlighted = false;
105
106         $this->filter->bind($input);
107         if ($this->filter->hasFilter()) {
108             $matches     = [];
109             $highlighted = [];
110             foreach ($history as $i => $line) {
111                 if ($this->filter->match($line, $matches)) {
112                     if (isset($matches[0])) {
113                         $chunks = explode($matches[0], $history[$i]);
114                         $chunks = array_map([__CLASS__, 'escape'], $chunks);
115                         $glue   = sprintf('<urgent>%s</urgent>', self::escape($matches[0]));
116
117                         $highlighted[$i] = implode($glue, $chunks);
118                     }
119                 } else {
120                     unset($history[$i]);
121                 }
122             }
123         }
124
125         if ($save = $input->getOption('save')) {
126             $output->writeln(sprintf('Saving history in %s...', $save));
127             file_put_contents($save, implode(PHP_EOL, $history) . PHP_EOL);
128             $output->writeln('<info>History saved.</info>');
129         } elseif ($input->getOption('replay')) {
130             if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) {
131                 throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying');
132             }
133
134             $count = count($history);
135             $output->writeln(sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : ''));
136             $this->getApplication()->addInput($history);
137         } elseif ($input->getOption('clear')) {
138             $this->clearHistory();
139             $output->writeln('<info>History cleared.</info>');
140         } else {
141             $type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES;
142             if (!$highlighted) {
143                 $type = $type | ShellOutput::OUTPUT_RAW;
144             }
145
146             $output->page($highlighted ?: $history, $type);
147         }
148     }
149
150     /**
151      * Extract a range from a string.
152      *
153      * @param string $range
154      *
155      * @return array [ start, end ]
156      */
157     private function extractRange($range)
158     {
159         if (preg_match('/^\d+$/', $range)) {
160             return [$range, $range + 1];
161         }
162
163         $matches = [];
164         if ($range !== '..' && preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) {
165             $start = $matches[1] ? intval($matches[1]) : 0;
166             $end   = $matches[2] ? intval($matches[2]) + 1 : PHP_INT_MAX;
167
168             return [$start, $end];
169         }
170
171         throw new \InvalidArgumentException('Unexpected range: ' . $range);
172     }
173
174     /**
175      * Retrieve a slice of the readline history.
176      *
177      * @param string $show
178      * @param string $head
179      * @param string $tail
180      *
181      * @return array A slilce of history
182      */
183     private function getHistorySlice($show, $head, $tail)
184     {
185         $history = $this->readline->listHistory();
186
187         // don't show the current `history` invocation
188         array_pop($history);
189
190         if ($show) {
191             list($start, $end) = $this->extractRange($show);
192             $length = $end - $start;
193         } elseif ($head) {
194             if (!preg_match('/^\d+$/', $head)) {
195                 throw new \InvalidArgumentException('Please specify an integer argument for --head');
196             }
197
198             $start  = 0;
199             $length = intval($head);
200         } elseif ($tail) {
201             if (!preg_match('/^\d+$/', $tail)) {
202                 throw new \InvalidArgumentException('Please specify an integer argument for --tail');
203             }
204
205             $start  = count($history) - $tail;
206             $length = intval($tail) + 1;
207         } else {
208             return $history;
209         }
210
211         return array_slice($history, $start, $length, true);
212     }
213
214     /**
215      * Validate that only one of the given $options is set.
216      *
217      * @param InputInterface $input
218      * @param array          $options
219      */
220     private function validateOnlyOne(InputInterface $input, array $options)
221     {
222         $count = 0;
223         foreach ($options as $opt) {
224             if ($input->getOption($opt)) {
225                 $count++;
226             }
227         }
228
229         if ($count > 1) {
230             throw new \InvalidArgumentException('Please specify only one of --' . implode(', --', $options));
231         }
232     }
233
234     /**
235      * Clear the readline history.
236      */
237     private function clearHistory()
238     {
239         $this->readline->clearHistory();
240     }
241
242     public static function escape($string)
243     {
244         return OutputFormatter::escape($string);
245     }
246 }