Passed
Push — master ( 763e25...c31763 )
by Jitendra
01:39
created

OutputHelper::showCommandNotFound()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 18
rs 9.6111
1
<?php
2
3
/*
4
 * This file is part of the PHP-CLI package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace Ahc\Cli\Helper;
13
14
use Ahc\Cli\Exception;
15
use Ahc\Cli\Input\Argument;
16
use Ahc\Cli\Input\Command;
17
use Ahc\Cli\Input\Option;
18
use Ahc\Cli\Input\Parameter;
19
use Ahc\Cli\Output\Writer;
20
21
/**
22
 * This helper helps you by showing you help information :).
23
 *
24
 * @author  Jitendra Adhikari <[email protected]>
25
 * @license MIT
26
 *
27
 * @link    https://github.com/adhocore/cli
28
 */
29
class OutputHelper
30
{
31
    /** @var Writer */
32
    protected $writer;
33
34
    /** @var int Max width of command name */
35
    protected $maxCmdName;
36
37
    public function __construct(Writer $writer = null)
38
    {
39
        $this->writer = $writer ?? new Writer;
40
    }
41
42
    /**
43
     * Print stack trace and error msg of an exception.
44
     *
45
     * @param \Throwable $e
46
     *
47
     * @return void
48
     */
49
    public function printTrace(\Throwable $e)
50
    {
51
        $eClass = \get_class($e);
52
53
        $this->writer->colors(
54
            "{$eClass} <red>{$e->getMessage()}</end><eol/>" .
55
            "(thrown in <yellow>{$e->getFile()}</end><white>:{$e->getLine()})</end>"
56
        );
57
58
        // @codeCoverageIgnoreStart
59
        if ($e instanceof Exception) {
60
            // Internal exception traces are not printed.
61
            return;
62
        }
63
        // @codeCoverageIgnoreEnd
64
65
        $traceStr = '<eol/><eol/><bold>Stack Trace:</end><eol/><eol/>';
66
67
        foreach ($e->getTrace() as $i => $trace) {
68
            $trace += ['class' => '', 'type' => '', 'function' => '', 'file' => '', 'line' => '', 'args' => []];
69
            $symbol = $trace['class'] . $trace['type'] . $trace['function'];
70
            $args   = $this->stringifyArgs($trace['args']);
71
72
            $traceStr .= "  <comment>$i)</end> <red>$symbol</end><comment>($args)</end>";
73
            if ('' !== $trace['file']) {
74
                $file      = \realpath($trace['file']);
75
                $traceStr .= "<eol/>     <yellow>at $file</end><white>:{$trace['line']}</end><eol/>";
76
            }
77
        }
78
79
        $this->writer->colors($traceStr);
80
    }
81
82
    protected function stringifyArgs(array $args)
83
    {
84
        $holder = [];
85
86
        foreach ($args as $arg) {
87
            $holder[] = $this->stringifyArg($arg);
88
        }
89
90
        return \implode(', ', $holder);
91
    }
92
93
    protected function stringifyArg($arg)
94
    {
95
        if (\is_scalar($arg)) {
96
            return \var_export($arg, true);
97
        }
98
99
        if (\is_object($arg)) {
100
            return \method_exists($arg, '__toString') ? (string) $arg : \get_class($arg);
101
        }
102
103
        if (\is_array($arg)) {
104
            return '[' . $this->stringifyArgs($arg) . ']';
105
        }
106
107
        return \gettype($arg);
108
    }
109
110
    /**
111
     * @param Argument[] $arguments
112
     * @param string     $header
113
     * @param string     $footer
114
     *
115
     * @return self
116
     */
117
    public function showArgumentsHelp(array $arguments, string $header = '', string $footer = ''): self
118
    {
119
        $this->showHelp('Arguments', $arguments, $header, $footer);
120
121
        return $this;
122
    }
123
124
    /**
125
     * @param Option[] $options
126
     * @param string   $header
127
     * @param string   $footer
128
     *
129
     * @return self
130
     */
131
    public function showOptionsHelp(array $options, string $header = '', string $footer = ''): self
132
    {
133
        $this->showHelp('Options', $options, $header, $footer);
134
135
        return $this;
136
    }
137
138
    /**
139
     * @param Command[] $commands
140
     * @param string    $header
141
     * @param string    $footer
142
     *
143
     * @return self
144
     */
145
    public function showCommandsHelp(array $commands, string $header = '', string $footer = ''): self
146
    {
147
        $this->maxCmdName = $commands ? \max(\array_map(function (Command $cmd) {
148
            return \strlen($cmd->name());
149
        }, $commands)) : 0;
150
151
        $this->showHelp('Commands', $commands, $header, $footer);
152
153
        return $this;
154
    }
155
156
    /**
157
     * Show help with headers and footers.
158
     *
159
     * @param string $for
160
     * @param array  $items
161
     * @param string $header
162
     * @param string $footer
163
     *
164
     * @return void
165
     */
166
    protected function showHelp(string $for, array $items, string $header = '', string $footer = '')
167
    {
168
        if ($header) {
169
            $this->writer->bold($header, true);
170
        }
171
172
        $this->writer->eol()->boldGreen($for . ':', true);
173
174
        if (empty($items)) {
175
            $this->writer->bold('  (n/a)', true);
176
177
            return;
178
        }
179
180
        $space = 4;
181
        foreach ($this->sortItems($items, $padLen) as $item) {
182
            $name = $this->getName($item);
183
            $desc = \str_replace(["\r\n", "\n"], \str_pad("\n", $padLen + $space + 3), $item->desc());
184
185
            $this->writer->bold('  ' . \str_pad($name, $padLen + $space));
186
            $this->writer->comment($desc, true);
187
        }
188
189
        if ($footer) {
190
            $this->writer->eol()->yellow($footer, true);
191
        }
192
    }
193
194
    /**
195
     * Show usage examples of a Command.
196
     *
197
     * It replaces $0 with actual command name and properly pads ` ## ` segments.
198
     *
199
     * @param string $usage Usage description.
200
     *
201
     * @return self
202
     */
203
    public function showUsage(string $usage): self
204
    {
205
        $usage = \str_replace('$0', $_SERVER['argv'][0] ?? '[cmd]', $usage);
206
207
        if (\strpos($usage, ' ## ') === false) {
208
            $this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol();
209
210
            return $this;
211
        }
212
213
        $lines = \explode("\n", \str_replace(['<eol>', '<eol/>', '</eol>', "\r\n"], "\n", $usage));
214
        foreach ($lines as $i => &$pos) {
215
            if (false === $pos = \strrpos(\preg_replace('~</?\w+/?>~', '', $pos), ' ##')) {
216
                unset($lines[$i]);
217
            }
218
        }
219
220
        $maxlen = \max($lines) + 4;
221
        $usage  = \preg_replace_callback('~ ## ~', function () use (&$lines, $maxlen) {
222
            return \str_pad('# ', $maxlen - \array_shift($lines), ' ', \STR_PAD_LEFT);
223
        }, $usage);
224
225
        $this->writer->eol()->boldGreen('Usage Examples:', true)->colors($usage)->eol();
226
227
        return $this;
228
    }
229
230
    public function showCommandNotFound(string $attempted, array $available): self
231
    {
232
        $closest = [];
233
        foreach ($available as $cmd) {
234
            $lev = \levenshtein($attempted, $cmd);
235
            if ($lev > 0 || $lev < 5) {
236
                $closest[$cmd] = $lev;
237
            }
238
        }
239
240
        $this->writer->error("Command $attempted not found", true);
241
        if ($closest) {
242
            \asort($closest);
243
            $closest = \key($closest);
244
            $this->writer->bgRed("Did you mean $closest?", true);
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Sort items by name. As a side effect sets max length of all names.
252
     *
253
     * @param Parameter[]|Command[] $items
254
     * @param int                   $max
255
     *
256
     * @return array
257
     */
258
    protected function sortItems(array $items, &$max = 0): array
259
    {
260
        $max = \max(\array_map(function ($item) {
261
            return \strlen($this->getName($item));
262
        }, $items));
263
264
        \uasort($items, function ($a, $b) {
265
            /* @var Parameter $b */
266
            /* @var Parameter $a */
267
            return $a->name() <=> $b->name();
268
        });
269
270
        return $items;
271
    }
272
273
    /**
274
     * Prepare name for different items.
275
     *
276
     * @param Parameter|Command $item
277
     *
278
     * @return string
279
     */
280
    protected function getName($item): string
281
    {
282
        $name = $item->name();
283
284
        if ($item instanceof Command) {
285
            return \trim(\str_pad($name, $this->maxCmdName) . ' ' . $item->alias());
286
        }
287
288
        return $this->label($item);
289
    }
290
291
    /**
292
     * Get parameter label for humans.
293
     *
294
     * @param Parameter $item
295
     *
296
     * @return string
297
     */
298
    protected function label(Parameter $item)
299
    {
300
        $name = $item->name();
301
302
        if ($item instanceof Option) {
303
            $name = $item->short() . '|' . $item->long();
304
        }
305
306
        $variad = $item->variadic() ? '...' : '';
307
308
        if ($item->required()) {
309
            return "<$name$variad>";
310
        }
311
312
        return "[$name$variad]";
313
    }
314
}
315