1<?php
2
3/*
4 * This file is part of the PHP-CLI package.
5 *
6 * (c) Jitendra Adhikari <jiten.adhikary@gmail.com>
7 *     <https://github.com/adhocore>
8 *
9 * Licensed under MIT license.
10 */
11
12namespace Ahc\Cli\Input;
13
14use Ahc\Cli\Application as App;
15use Ahc\Cli\Exception\InvalidParameterException;
16use Ahc\Cli\Exception\RuntimeException;
17use Ahc\Cli\Helper\InflectsString;
18use Ahc\Cli\Helper\OutputHelper;
19use Ahc\Cli\IO\Interactor;
20use Ahc\Cli\Output\Writer;
21
22/**
23 * Parser aware Command for the cli (based on tj/commander.js).
24 *
25 * @author  Jitendra Adhikari <jiten.adhikary@gmail.com>
26 * @license MIT
27 *
28 * @link    https://github.com/adhocore/cli
29 */
30class Command extends Parser
31{
32    use InflectsString;
33
34    /** @var callable */
35    protected $_action;
36
37    /** @var string */
38    protected $_version;
39
40    /** @var string */
41    protected $_name;
42
43    /** @var string */
44    protected $_desc;
45
46    /** @var string Usage examples */
47    protected $_usage;
48
49    /** @var string Command alias */
50    protected $_alias;
51
52    /** @var App The cli app this command is bound to */
53    protected $_app;
54
55    /** @var callable[] Events for options */
56    private $_events = [];
57
58    /** @var bool Whether to allow unknown (not registered) options */
59    private $_allowUnknown = false;
60
61    /** @var bool If the last seen arg was variadic */
62    private $_argVariadic = false;
63
64    /**
65     * Constructor.
66     *
67     * @param string $name
68     * @param string $desc
69     * @param bool   $allowUnknown
70     * @param App    $app
71     */
72    public function __construct(string $name, string $desc = '', bool $allowUnknown = false, App $app = null)
73    {
74        $this->_name         = $name;
75        $this->_desc         = $desc;
76        $this->_allowUnknown = $allowUnknown;
77        $this->_app          = $app;
78
79        $this->defaults();
80    }
81
82    /**
83     * Sets default options, actions and exit handler.
84     *
85     * @return self
86     */
87    protected function defaults(): self
88    {
89        $this->option('-h, --help', 'Show help')->on([$this, 'showHelp']);
90        $this->option('-V, --version', 'Show version')->on([$this, 'showVersion']);
91        $this->option('-v, --verbosity', 'Verbosity level', null, 0)->on(function () {
92            $this->set('verbosity', ($this->verbosity ?? 0) + 1);
93
94            return false;
95        });
96
97        // @codeCoverageIgnoreStart
98        $this->onExit(function ($exitCode = 0) {
99            exit($exitCode);
100        });
101        // @codeCoverageIgnoreEnd
102
103        return $this;
104    }
105
106    /**
107     * Sets version.
108     *
109     * @param string $version
110     *
111     * @return self
112     */
113    public function version(string $version): self
114    {
115        $this->_version = $version;
116
117        return $this;
118    }
119
120    /**
121     * Gets command name.
122     *
123     * @return string
124     */
125    public function name(): string
126    {
127        return $this->_name;
128    }
129
130    /**
131     * Gets command description.
132     *
133     * @return string
134     */
135    public function desc(): string
136    {
137        return $this->_desc;
138    }
139
140    /**
141     * Get the app this command belongs to.
142     *
143     * @return null|App
144     */
145    public function app()
146    {
147        return $this->_app;
148    }
149
150    /**
151     * Bind command to the app.
152     *
153     * @param App|null $app
154     *
155     * @return self
156     */
157    public function bind(App $app = null): self
158    {
159        $this->_app = $app;
160
161        return $this;
162    }
163
164    /**
165     * Registers argument definitions (all at once). Only last one can be variadic.
166     *
167     * @param string $definitions
168     *
169     * @return self
170     */
171    public function arguments(string $definitions): self
172    {
173        $definitions = \explode(' ', $definitions);
174
175        foreach ($definitions as $raw) {
176            $this->argument($raw);
177        }
178
179        return $this;
180    }
181
182    /**
183     * Register an argument.
184     *
185     * @param string $raw
186     * @param string $desc
187     * @param mixed  $default
188     *
189     * @return self
190     */
191    public function argument(string $raw, string $desc = '', $default = null): self
192    {
193        $argument = new Argument($raw, $desc, $default);
194
195        if ($this->_argVariadic) {
196            throw new InvalidParameterException('Only last argument can be variadic');
197        }
198
199        if ($argument->variadic()) {
200            $this->_argVariadic = true;
201        }
202
203        $this->register($argument);
204
205        return $this;
206    }
207
208    /**
209     * Registers new option.
210     *
211     * @param string        $raw
212     * @param string        $desc
213     * @param callable|null $filter
214     * @param mixed         $default
215     *
216     * @return self
217     */
218    public function option(string $raw, string $desc = '', callable $filter = null, $default = null): self
219    {
220        $option = new Option($raw, $desc, $default, $filter);
221
222        $this->register($option);
223
224        return $this;
225    }
226
227    /**
228     * Gets user options (i.e without defaults).
229     *
230     * @return array
231     */
232    public function userOptions(): array
233    {
234        $options = $this->allOptions();
235
236        unset($options['help'], $options['version'], $options['verbosity']);
237
238        return $options;
239    }
240
241    /**
242     * Gets or sets usage info.
243     *
244     * @param string|null $usage
245     *
246     * @return string|self
247     */
248    public function usage(string $usage = null)
249    {
250        if (\func_num_args() === 0) {
251            return $this->_usage;
252        }
253
254        $this->_usage = $usage;
255
256        return $this;
257    }
258
259    /**
260     * Gets or sets alias.
261     *
262     * @param string|null $alias
263     *
264     * @return string|self
265     */
266    public function alias(string $alias = null)
267    {
268        if (\func_num_args() === 0) {
269            return $this->_alias;
270        }
271
272        $this->_alias = $alias;
273
274        return $this;
275    }
276
277    /**
278     * Sets event handler for last (or given) option.
279     *
280     * @param callable $fn
281     * @param string   $option
282     *
283     * @return self
284     */
285    public function on(callable $fn, string $option = null): self
286    {
287        $names = \array_keys($this->allOptions());
288
289        $this->_events[$option ?? \end($names)] = $fn;
290
291        return $this;
292    }
293
294    /**
295     * Register exit handler.
296     *
297     * @param callable $fn
298     *
299     * @return self
300     */
301    public function onExit(callable $fn): self
302    {
303        $this->_events['_exit'] = $fn;
304
305        return $this;
306    }
307
308    /**
309     * {@inheritdoc}
310     */
311    protected function handleUnknown(string $arg, string $value = null)
312    {
313        if ($this->_allowUnknown) {
314            return $this->set($this->toCamelCase($arg), $value);
315        }
316
317        $values = \array_filter($this->values(false));
318
319        // Has some value, error!
320        if ($values) {
321            throw new RuntimeException(
322                \sprintf('Option "%s" not registered', $arg)
323            );
324        }
325
326        // Has no value, show help!
327        return $this->showHelp();
328    }
329
330    /**
331     * Shows command help then aborts.
332     *
333     * @return mixed
334     */
335    public function showHelp()
336    {
337        $io     = $this->io();
338        $helper = new OutputHelper($io->writer());
339
340        $io->bold("Command {$this->_name}, version {$this->_version}", true)->eol();
341        $io->comment($this->_desc, true)->eol();
342        $io->bold('Usage: ')->yellow("{$this->_name} [OPTIONS...] [ARGUMENTS...]", true);
343
344        $helper
345            ->showArgumentsHelp($this->allArguments())
346            ->showOptionsHelp($this->allOptions(), '', 'Legend: <required> [optional] variadic...');
347
348        if ($this->_usage) {
349            $helper->showUsage($this->_usage);
350        }
351
352        return $this->emit('_exit', 0);
353    }
354
355    /**
356     * Shows command version then aborts.
357     *
358     * @return mixed
359     */
360    public function showVersion()
361    {
362        $this->writer()->bold($this->_version, true);
363
364        return $this->emit('_exit', 0);
365    }
366
367    /**
368     * {@inheritdoc}
369     */
370    public function emit(string $event, $value = null)
371    {
372        if (empty($this->_events[$event])) {
373            return null;
374        }
375
376        return ($this->_events[$event])($value);
377    }
378
379    /**
380     * Tap return given object or if that is null then app instance. This aids for chaining.
381     *
382     * @param mixed $object
383     *
384     * @return mixed
385     */
386    public function tap($object = null)
387    {
388        return $object ?? $this->_app;
389    }
390
391    /**
392     * Performs user interaction if required to set some missing values.
393     *
394     * @param Interactor $io
395     *
396     * @return void
397     */
398    public function interact(Interactor $io)
399    {
400        // Subclasses will do the needful.
401    }
402
403    /**
404     * Get or set command action.
405     *
406     * @param callable|null $action If provided it is set
407     *
408     * @return callable|self If $action provided then self, otherwise the preset action.
409     */
410    public function action(callable $action = null)
411    {
412        if (\func_num_args() === 0) {
413            return $this->_action;
414        }
415
416        $this->_action = $action instanceof \Closure ? \Closure::bind($action, $this) : $action;
417
418        return $this;
419    }
420
421    /**
422     * Get a writer instance.
423     *
424     * @return Writer
425     */
426    protected function writer(): Writer
427    {
428        return $this->_app ? $this->_app->io()->writer() : new Writer;
429    }
430
431    /**
432     * Get IO instance.
433     *
434     * @return Interactor
435     */
436    protected function io(): Interactor
437    {
438        return $this->_app ? $this->_app->io() : new Interactor;
439    }
440}
441