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