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