Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of Psy Shell.
5
 *
200 lars 6
 * (c) 2012-2023 Justin Hileman
148 lars 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
     * @return int 0 if everything went fine, or an exit code
95
     */
96
    protected function execute(InputInterface $input, OutputInterface $output)
97
    {
98
        $this->validateOnlyOne($input, ['show', 'head', 'tail']);
99
        $this->validateOnlyOne($input, ['save', 'replay', 'clear']);
100
 
101
        $history = $this->getHistorySlice(
102
            $input->getOption('show'),
103
            $input->getOption('head'),
104
            $input->getOption('tail')
105
        );
106
        $highlighted = false;
107
 
108
        $this->filter->bind($input);
109
        if ($this->filter->hasFilter()) {
110
            $matches = [];
111
            $highlighted = [];
112
            foreach ($history as $i => $line) {
113
                if ($this->filter->match($line, $matches)) {
114
                    if (isset($matches[0])) {
115
                        $chunks = \explode($matches[0], $history[$i]);
116
                        $chunks = \array_map([__CLASS__, 'escape'], $chunks);
117
                        $glue = \sprintf('<urgent>%s</urgent>', self::escape($matches[0]));
118
 
119
                        $highlighted[$i] = \implode($glue, $chunks);
120
                    }
121
                } else {
122
                    unset($history[$i]);
123
                }
124
            }
125
        }
126
 
127
        if ($save = $input->getOption('save')) {
128
            $output->writeln(\sprintf('Saving history in %s...', $save));
129
            \file_put_contents($save, \implode(\PHP_EOL, $history).\PHP_EOL);
130
            $output->writeln('<info>History saved.</info>');
131
        } elseif ($input->getOption('replay')) {
132
            if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) {
133
                throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying');
134
            }
135
 
136
            $count = \count($history);
137
            $output->writeln(\sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : ''));
138
            $this->getApplication()->addInput($history);
139
        } elseif ($input->getOption('clear')) {
140
            $this->clearHistory();
141
            $output->writeln('<info>History cleared.</info>');
142
        } else {
143
            $type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES;
144
            if (!$highlighted) {
145
                $type = $type | OutputInterface::OUTPUT_RAW;
146
            }
147
 
148
            $output->page($highlighted ?: $history, $type);
149
        }
150
 
151
        return 0;
152
    }
153
 
154
    /**
155
     * Extract a range from a string.
156
     *
157
     * @param string $range
158
     *
159
     * @return array [ start, end ]
160
     */
161
    private function extractRange(string $range): array
162
    {
163
        if (\preg_match('/^\d+$/', $range)) {
164
            return [$range, $range + 1];
165
        }
166
 
167
        $matches = [];
168
        if ($range !== '..' && \preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) {
169
            $start = $matches[1] ? (int) $matches[1] : 0;
170
            $end = $matches[2] ? (int) $matches[2] + 1 : \PHP_INT_MAX;
171
 
172
            return [$start, $end];
173
        }
174
 
175
        throw new \InvalidArgumentException('Unexpected range: '.$range);
176
    }
177
 
178
    /**
179
     * Retrieve a slice of the readline history.
180
     *
181
     * @param string|null $show
182
     * @param string|null $head
183
     * @param string|null $tail
184
     *
185
     * @return array A slice of history
186
     */
187
    private function getHistorySlice($show, $head, $tail): array
188
    {
189
        $history = $this->readline->listHistory();
190
 
191
        // don't show the current `history` invocation
192
        \array_pop($history);
193
 
194
        if ($show) {
195
            list($start, $end) = $this->extractRange($show);
196
            $length = $end - $start;
197
        } elseif ($head) {
198
            if (!\preg_match('/^\d+$/', $head)) {
199
                throw new \InvalidArgumentException('Please specify an integer argument for --head');
200
            }
201
 
202
            $start = 0;
203
            $length = (int) $head;
204
        } elseif ($tail) {
205
            if (!\preg_match('/^\d+$/', $tail)) {
206
                throw new \InvalidArgumentException('Please specify an integer argument for --tail');
207
            }
208
 
209
            $start = \count($history) - $tail;
210
            $length = (int) $tail + 1;
211
        } else {
212
            return $history;
213
        }
214
 
215
        return \array_slice($history, $start, $length, true);
216
    }
217
 
218
    /**
219
     * Validate that only one of the given $options is set.
220
     *
221
     * @param InputInterface $input
222
     * @param array          $options
223
     */
224
    private function validateOnlyOne(InputInterface $input, array $options)
225
    {
226
        $count = 0;
227
        foreach ($options as $opt) {
228
            if ($input->getOption($opt)) {
229
                $count++;
230
            }
231
        }
232
 
233
        if ($count > 1) {
234
            throw new \InvalidArgumentException('Please specify only one of --'.\implode(', --', $options));
235
        }
236
    }
237
 
238
    /**
239
     * Clear the readline history.
240
     */
241
    private function clearHistory()
242
    {
243
        $this->readline->clearHistory();
244
    }
245
 
246
    public static function escape(string $string): string
247
    {
248
        return OutputFormatter::escape($string);
249
    }
250
}