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