Completed
Push — master ( ea055a...aafc8c )
by Jitendra
17s
created

Command::alias()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Ahc\Cli\Input;
4
5
use Ahc\Cli\Application as App;
6
use Ahc\Cli\Exception\InvalidParameterException;
7
use Ahc\Cli\Exception\RuntimeException;
8
use Ahc\Cli\Helper\InflectsString;
9
use Ahc\Cli\Helper\OutputHelper;
10
use Ahc\Cli\IO\Interactor;
11
use Ahc\Cli\Output\Writer;
12
13
/**
14
 * Parser aware Command for the cli (based on tj/commander.js).
15
 *
16
 * @author  Jitendra Adhikari <[email protected]>
17
 * @license MIT
18
 *
19
 * @link    https://github.com/adhocore/cli
20
 */
21
class Command extends Parser
22
{
23
    use InflectsString;
24
25
    /** @var callable */
26
    protected $_action;
27
28
    /** @var string */
29
    protected $_version;
30
31
    /** @var string */
32
    protected $_name;
33
34
    /** @var string */
35
    protected $_desc;
36
37
    /** @var string Usage examples */
38
    protected $_usage;
39
40
    /** @var string Command alias */
41
    protected $_alias;
42
43
    /** @var App The cli app this command is bound to */
44
    protected $_app;
45
46
    /** @var callable[] Events for options */
47
    private $_events = [];
48
49
    /** @var bool Whether to allow unknown (not registered) options */
50
    private $_allowUnknown = false;
51
52
    /** @var bool If the last seen arg was variadic */
53
    private $_argVariadic = false;
54
55
    /**
56
     * Constructor.
57
     *
58
     * @param string $name
59
     * @param string $desc
60
     * @param bool   $allowUnknown
61
     * @param App    $app
62
     */
63
    public function __construct(string $name, string $desc = '', bool $allowUnknown = false, App $app = null)
64
    {
65
        $this->_name         = $name;
66
        $this->_desc         = $desc;
67
        $this->_allowUnknown = $allowUnknown;
68
        $this->_app          = $app;
69
70
        $this->defaults();
71
    }
72
73
    /**
74
     * Sets default options, actions and exit handler.
75
     *
76
     * @return self
77
     */
78
    protected function defaults(): self
79
    {
80
        $this->option('-h, --help', 'Show help')->on([$this, 'showHelp']);
81
        $this->option('-V, --version', 'Show version')->on([$this, 'showVersion']);
82
        $this->option('-v, --verbosity', 'Verbosity level', null, 0)->on(function () {
83
            $this->set('verbosity', ($this->verbosity ?? 0) + 1);
0 ignored issues
show
Bug Best Practice introduced by
The property verbosity does not exist on Ahc\Cli\Input\Command. Since you implemented __get, consider adding a @property annotation.
Loading history...
84
85
            return false;
86
        });
87
88
        // @codeCoverageIgnoreStart
89
        $this->onExit(function ($exitCode = 0) {
90
            exit($exitCode);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
91
        });
92
        // @codeCoverageIgnoreEnd
93
94
        return $this;
95
    }
96
97
    /**
98
     * Sets version.
99
     *
100
     * @param string $version
101
     *
102
     * @return self
103
     */
104
    public function version(string $version): self
105
    {
106
        $this->_version = $version;
107
108
        return $this;
109
    }
110
111
    /**
112
     * Gets command name.
113
     *
114
     * @return string
115
     */
116
    public function name(): string
117
    {
118
        return $this->_name;
119
    }
120
121
    /**
122
     * Gets command description.
123
     *
124
     * @return string
125
     */
126
    public function desc(): string
127
    {
128
        return $this->_desc;
129
    }
130
131
    /**
132
     * Get the app this command belongs to.
133
     *
134
     * @return null|App
135
     */
136
    public function app()
137
    {
138
        return $this->_app;
139
    }
140
141
    /**
142
     * Bind command to the app.
143
     *
144
     * @param App|null $app
145
     *
146
     * @return self
147
     */
148
    public function bind(App $app = null): self
149
    {
150
        $this->_app = $app;
151
152
        return $this;
153
    }
154
155
    /**
156
     * Registers argument definitions (all at once). Only last one can be variadic.
157
     *
158
     * @param string $definitions
159
     *
160
     * @return self
161
     */
162
    public function arguments(string $definitions): self
163
    {
164
        $definitions = \explode(' ', $definitions);
165
166
        foreach ($definitions as $raw) {
167
            $this->argument($raw);
168
        }
169
170
        return $this;
171
    }
172
173
    /**
174
     * Register an argument.
175
     *
176
     * @param string $raw
177
     * @param string $desc
178
     * @param mixed  $default
179
     *
180
     * @return self
181
     */
182
    public function argument(string $raw, string $desc = '', $default = null): self
183
    {
184
        $argument = new Argument($raw, $desc, $default);
185
186
        if ($this->_argVariadic) {
187
            throw new InvalidParameterException('Only last argument can be variadic');
188
        }
189
190
        if ($argument->variadic()) {
191
            $this->_argVariadic = true;
192
        }
193
194
        $this->register($argument);
195
196
        return $this;
197
    }
198
199
    /**
200
     * Registers new option.
201
     *
202
     * @param string        $raw
203
     * @param string        $desc
204
     * @param callable|null $filter
205
     * @param mixed         $default
206
     *
207
     * @return self
208
     */
209
    public function option(string $raw, string $desc = '', callable $filter = null, $default = null): self
210
    {
211
        $option = new Option($raw, $desc, $default, $filter);
212
213
        $this->register($option);
214
215
        return $this;
216
    }
217
218
    /**
219
     * Gets user options (i.e without defaults).
220
     *
221
     * @return array
222
     */
223
    public function userOptions(): array
224
    {
225
        $options = $this->allOptions();
226
227
        unset($options['help'], $options['version'], $options['verbosity']);
228
229
        return $options;
230
    }
231
232
    /**
233
     * Gets or sets usage info.
234
     *
235
     * @param string|null $usage
236
     *
237
     * @return string|self
238
     */
239
    public function usage(string $usage = null)
240
    {
241
        if (\func_num_args() === 0) {
242
            return $this->_usage;
243
        }
244
245
        $this->_usage = $usage;
246
247
        return $this;
248
    }
249
250
    /**
251
     * Gets or sets alias.
252
     *
253
     * @param string|null $alias
254
     *
255
     * @return string|self
256
     */
257
    public function alias(string $alias = null)
258
    {
259
        if (\func_num_args() === 0) {
260
            return $this->_alias;
261
        }
262
263
        $this->_alias = $alias;
264
265
        return $this;
266
    }
267
268
    /**
269
     * Sets event handler for last (or given) option.
270
     *
271
     * @param callable $fn
272
     * @param string   $option
273
     *
274
     * @return self
275
     */
276
    public function on(callable $fn, string $option = null): self
277
    {
278
        $names = \array_keys($this->allOptions());
279
280
        $this->_events[$option ?? \end($names)] = $fn;
281
282
        return $this;
283
    }
284
285
    /**
286
     * Register exit handler.
287
     *
288
     * @param callable $fn
289
     *
290
     * @return self
291
     */
292
    public function onExit(callable $fn): self
293
    {
294
        $this->_events['_exit'] = $fn;
295
296
        return $this;
297
    }
298
299
    /**
300
     * {@inheritdoc}
301
     */
302
    protected function handleUnknown(string $arg, string $value = null)
303
    {
304
        if ($this->_allowUnknown) {
305
            return $this->set($this->toCamelCase($arg), $value);
306
        }
307
308
        $values = \array_filter($this->values(false));
309
310
        // Has some value, error!
311
        if ($values) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $values of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
312
            throw new RuntimeException(
313
                \sprintf('Option "%s" not registered', $arg)
314
            );
315
        }
316
317
        // Has no value, show help!
318
        return $this->showHelp();
319
    }
320
321
    /**
322
     * Shows command help then aborts.
323
     *
324
     * @return mixed
325
     */
326
    public function showHelp()
327
    {
328
        $io     = $this->io();
329
        $helper = new OutputHelper($io->writer());
330
331
        $io->bold("Command {$this->_name}, version {$this->_version}", true)->eol();
332
        $io->comment($this->_desc, true)->eol();
333
        $io->bold('Usage: ')->yellow("{$this->_name} [OPTIONS...] [ARGUMENTS...]", true);
334
335
        $helper
336
            ->showArgumentsHelp($this->allArguments())
337
            ->showOptionsHelp($this->allOptions(), '', 'Legend: <required> [optional] variadic...');
338
339
        if ($this->_usage) {
340
            $io->eol();
341
            $io->boldGreen('Usage Examples:', true)->colors($this->_usage)->eol();
342
        }
343
344
        return $this->emit('_exit', 0);
345
    }
346
347
    /**
348
     * Shows command version then aborts.
349
     *
350
     * @return mixed
351
     */
352
    public function showVersion()
353
    {
354
        $this->writer()->bold($this->_version, true);
355
356
        return $this->emit('_exit', 0);
357
    }
358
359
    /**
360
     * {@inheritdoc}
361
     */
362
    public function emit(string $event, $value = null)
363
    {
364
        if (empty($this->_events[$event])) {
365
            return null;
366
        }
367
368
        return ($this->_events[$event])($value);
369
    }
370
371
    /**
372
     * Tap return given object or if that is null then app instance. This aids for chaining.
373
     *
374
     * @param mixed $object
375
     *
376
     * @return mixed
377
     */
378
    public function tap($object = null)
379
    {
380
        return $object ?? $this->_app;
381
    }
382
383
    /**
384
     * Performs user interaction if required to set some missing values.
385
     *
386
     * @param Interactor $io
387
     *
388
     * @return void
389
     */
390
    public function interact(Interactor $io)
0 ignored issues
show
Unused Code introduced by
The parameter $io is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

390
    public function interact(/** @scrutinizer ignore-unused */ Interactor $io)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
391
    {
392
        // Subclasses will do the needful.
393
    }
394
395
    /**
396
     * Get or set command action.
397
     *
398
     * @param callable|null $action If provided it is set
399
     *
400
     * @return callable|self If $action provided then self, otherwise the preset action.
401
     */
402
    public function action(callable $action = null)
403
    {
404
        if (\func_num_args() === 0) {
405
            return $this->_action;
406
        }
407
408
        $this->_action = $action;
409
410
        return $this;
411
    }
412
413
    /**
414
     * Get a writer instance.
415
     *
416
     * @return Writer
417
     */
418
    protected function writer(): Writer
419
    {
420
        return $this->_app ? $this->_app->io()->writer() : new Writer;
421
    }
422
423
    /**
424
     * Get IO instance.
425
     *
426
     * @return Interactor
427
     */
428
    protected function io(): Interactor
429
    {
430
        return $this->_app ? $this->_app->io() : new Interactor;
431
    }
432
}
433