├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── src ├── Application.php ├── Dispatcher.php ├── DispatcherInterface.php ├── ExceptionHandler.php ├── Filter │ ├── Explode.php │ ├── Json.php │ └── QueryString.php ├── HelpCommand.php ├── Route.php └── RouteCollection.php └── views └── autocomplete.phtml /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 1.4.1 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 1.4.0 - 2017-11-27 28 | 29 | ### Added 30 | 31 | - [#41](https://github.com/zfcampus/zf-console/pull/41) adds support for PHP 32 | versions 7.1 and 7.2. 33 | 34 | ### Changed 35 | 36 | - [#40](https://github.com/zfcampus/zf-console/pull/40) changes the dependency 37 | on container-interop/container-interop to psr/container, and updates internal 38 | typehints against container-interop to reference the PSR-11 typehint instead. 39 | This should generally not be an issue, as most containers that still typehint 40 | against container-interop will use the last version of container-interop, 41 | which extends the PSR-11 interface. 42 | 43 | ### Deprecated 44 | 45 | - Nothing. 46 | 47 | ### Removed 48 | 49 | - [#41](https://github.com/zfcampus/zf-console/pull/41) removes support for 50 | HHVM. 51 | 52 | ### Fixed 53 | 54 | - Nothing. 55 | 56 | ## 1.3.0 - 2016-07-11 57 | 58 | ### Added 59 | 60 | - [#28](https://github.com/zfcampus/zf-console/pull/28) adds the ability to 61 | provide an `Interop\Container\ContainerInterface` instance to 62 | `ZF\Console\Dispatcher` during instantiation; when present, `dispatch()` will 63 | attempt to look up command callables via the container. This allows simpler 64 | configuration: 65 | 66 | ```php 67 | $dispatch = new Dispatcher($container); 68 | 69 | $routes = [ 70 | [ 71 | 'name' => 'hello', 72 | 'handler' => HelloCommand::class, 73 | ], 74 | ]; 75 | 76 | $app = new Application('App', 1.0, $routes, null, $dispatcher); 77 | ``` 78 | 79 | (vs wrapping the handler in a closure.) 80 | - [#29](https://github.com/zfcampus/zf-console/pull/29) adds the ability to 81 | disable output of the banner in two ways: 82 | 83 | ```php 84 | $application->setBannerDisabledForUserCommands(true); 85 | $application->setBanner(null); 86 | ``` 87 | 88 | You may also now disable a previously enabled footer by passing a null 89 | value: 90 | 91 | ```php 92 | $application->setFooter(null); 93 | ``` 94 | - [#30](https://github.com/zfcampus/zf-console/pull/30) adds 95 | `ZF\Console\DispatcherInterface`, which defines the methods `map()`, `has()`, 96 | and `dispatch()`; `Dispatcher` now implements the interface. By providing an 97 | interface, consumers may now provide their own implementation when desired. 98 | - [#35](https://github.com/zfcampus/zf-console/pull/35) adds support for v3 99 | components from Zend Framework, retaining backwards compatibility with v2 100 | releases. 101 | 102 | ### Deprecated 103 | 104 | - Nothing. 105 | 106 | ### Removed 107 | 108 | - [#35](https://github.com/zfcampus/zf-console/pull/35) removes support for PHP 5.5. 109 | 110 | ### Fixed 111 | 112 | - [#34](https://github.com/zfcampus/zf-console/pull/34) updates the 113 | `ExceptionHandler` to allow handling either exceptions or PHP 7 114 | `Throwable`s. 115 | 116 | ## 1.2.1 - 2016-07-11 117 | 118 | ### Added 119 | 120 | - Nothing. 121 | 122 | ### Deprecated 123 | 124 | - Nothing. 125 | 126 | ### Removed 127 | 128 | - Nothing. 129 | 130 | ### Fixed 131 | 132 | - [#26](https://github.com/zfcampus/zf-console/pull/26) fixes the 133 | `Route::isMatched()` implementation to use the stored `$matches` property. 134 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZF\Console: Console Tool Helper 2 | 3 | > ## Repository abandoned 2019-12-05 4 | > 5 | > This repository is no longer maintained. Other projects in the ecosystem, such as symfony/console, provide more robust and flexible solutions, and we recommend users target those runtimes instead. 6 | 7 | [![Build Status](https://secure.travis-ci.org/zfcampus/zf-console.svg?branch=master)](https://secure.travis-ci.org/zfcampus/zf-console) 8 | [![Coverage Status](https://coveralls.io/repos/github/zfcampus/zf-console/badge.svg?branch=master)](https://coveralls.io/github/zfcampus/zf-console?branch=master) 9 | 10 | ## Introduction 11 | 12 | `zf-console` provides functionality on top of `Zend\Console`, specifically a methodology for 13 | creating standalone PHP console applications using `Zend\Console`'s `DefaultRouteMatcher`. 14 | It includes built-in "help" and "version" commands, and colorization (via `Zend\Console`), as 15 | well as support for shell autocompletion. 16 | 17 | ## Requirements 18 | 19 | Please see the [composer.json](composer.json) file. 20 | 21 | ## Installation 22 | 23 | Run the following `composer` command: 24 | 25 | ```console 26 | $ composer require zfcampus/zf-console 27 | ``` 28 | 29 | Alternately, manually add the following to your `composer.json`, in the `require` section: 30 | 31 | ```javascript 32 | "require": { 33 | "zfcampus/zf-console": "^1.3" 34 | } 35 | ``` 36 | 37 | And then run `composer update` to ensure the module is installed. 38 | 39 | ## Creating an application 40 | 41 | Console applications written with `zf-console` consist of: 42 | 43 | - Defining console routes 44 | - Mapping route names to PHP callables 45 | - Creating and running the application 46 | 47 | ### Defining console routes 48 | 49 | Routes in `zf-console` are typically configuration driven. Each route is an associative array 50 | consisting of the following members: 51 | 52 | - **name** (required): The name of the route. Names MUST be unique across the application. 53 | - **route** (optional): The "route", or console arguments, to match (more below); if not specified, 54 | **name** is utilized. Additionally, if the route does not start with **name**, **name** will be 55 | prepended to the route (unless you opt out of this feature). 56 | - **description** (optional): A detailed help description for the given route. 57 | - **short_description** (optional): A short help description for the given route, used in command 58 | summaries. 59 | - **options_descriptions** (optional): An array of option name/description pairs, corresponding to 60 | the arguments the route matches. 61 | - **constraints** (optional): An array of name/regex pairs to use when matching arguments, 62 | corresponding to the arguments in the route. If a regex fails for a given argument, the route will 63 | not match. 64 | - **aliases** (optional): An array of alias/argument pairs; if an alias is provided in the 65 | arguments, it will be returned as the named argument on a successful match. 66 | - **defaults** (optional): Default values to return on a successful match for arguments that were 67 | not matched. 68 | - **filters** (optional): An array of name/`Zend\Filter\FilterInterface` pairs. The filter provided 69 | will be used to filter/normalize the named argument when matched. 70 | - **validators** (optional): An array of name/`Zend\Validator\ValidatorInterface` pairs. The 71 | validator provided will be used to validate the named argument when matched; failure to validate 72 | will cause the route not to match. 73 | - **handler** (optional): A PHP callable, or a class name of a class with no constructor arguments 74 | which is also invokable; if specified, and no command has been mapped in the `Dispatcher`, this 75 | handler will be used to handle the command when invoked. 76 | - **prepend_command_to_route** (optional): A flag that, if specified, indicates whether or not the 77 | command name will be prepended to the route. Since this is the default behavior, only a value of 78 | boolean false makes sense here. 79 | 80 | Alternately, you can create a `ZF\Console\Route` instance. The signature is similar: 81 | 82 | ```php 83 | $route = new ZF\Console\Route( 84 | $name, 85 | $route, 86 | $constraints, // optional 87 | $defaults, // optional 88 | $aliases, // optional 89 | $filters, // optional 90 | $validators // optional 91 | ); 92 | $route->setDescription($description); 93 | $route->setShortDescription($shortDescription); 94 | $route->setOptionsDescription($optionsDescription); 95 | ``` 96 | 97 | When defining routes, you will need to provide either an array or `Traversable` object of route 98 | configuration arrays or `Route` instances (they can be mixed). 99 | 100 | We suggest putting your routes in a configuration file: 101 | 102 | ```php 103 | // config/routes.php 104 | 105 | return array( 106 | array( 107 | 'name' => 'self-update', 108 | 'description' => 'When executed via the Phar file, performs a self-update by querying 109 | the package repository. If successful, it will report the new version.', 110 | 'short_description' => 'Perform a self-update of the script', 111 | ), 112 | array( 113 | 'name' => 'build', 114 | 'route' => ' [--target=]', 115 | 'description' => 'Build a package, using as the package filename, and --target 116 | as the application directory to be packaged.', 117 | 'short_description' => 'Build a package', 118 | 'options_descriptions' => array( 119 | '' => 'Package filename to build', 120 | '--target' => 'Name of the application directory to package; defaults to current working directory', 121 | ), 122 | 'defaults' => array( 123 | 'target' => getcwd(), // default to current working directory 124 | ), 125 | 'handler' => 'My\Builder', 126 | ), 127 | ); 128 | ``` 129 | 130 | > #### On Routes 131 | > 132 | > `ZF\Console\Route` is an extension of `Zend\Console\RouteMatcher\DefaultRouteMatcher`, and follows 133 | > its rules for route definitions and matching. In general, a route string will consist of: 134 | > 135 | > - Literal parameters (literal strings to match; e.g., `build`) 136 | > - Literal flags (e.g., `--help`, `-h`, etc; flags do not have associated values) 137 | > - Positional value parameters (named captures that do not use flags; e.g., ``) 138 | > - Value flag parameters (aka long options, _with_ associated values; e.g., '--target=') 139 | > 140 | > Most parameters may be made optional by surrounding them with brackets (e.g., `[--target=]`, 141 | > `[]`). 142 | > 143 | > For a full overview of how to create route specification strings, please review the [ZF2 console 144 | > routes 145 | > documentation](http://framework.zend.com/manual/2.3/en/modules/zend.console.routes.html). 146 | 147 | > #### Route definitions 148 | > 149 | > Note that, by default, the route name will be prefixed to the `route` you pass. In the example 150 | > above, the `build` route becomes `build [--target=]`. If you wish to be explicit, you 151 | > can include the command name in your route definition yourself, or pass the 152 | > `prepend_command_to_route` flag with a boolean false value to disable prepending the command name. 153 | > 154 | > Prepending is done to make explicit the idea the mapping of the command name to the route -- which 155 | > is particularly prudent when considering usage of the help system (which is command centric). 156 | 157 | ### Mapping routes to callables 158 | 159 | In order to execute commands, you will need to map route names to code that will dispatch them. 160 | `ZF\Console\Dispatcher` provides the ability to define such a map, via its `map()` method: 161 | 162 | ```php 163 | $dispatcher = new ZF\Console\Dispatcher; 164 | $dispatcher->map('some-command-name', $callable) 165 | ``` 166 | 167 | The `$callable` argument may be any PHP callable. Additionally, you may provide a string class name, 168 | so long as that class can be instantiated without constructor arguments, and so long as it defines 169 | an `__invoke()` method. 170 | 171 | All callables should expect up to two arguments: 172 | 173 | ```php 174 | function (\ZF\Console\Route $route, \Zend\Console\Adapter\AdapterInterface $console) { 175 | } 176 | ``` 177 | 178 | Additionally, callables should return an integer status to use as the application's exit status; a 179 | `0` indicates success, while anything else indicates a failure. 180 | 181 | > #### Callables may be defined in route configuration 182 | > 183 | > As noted in the previous section, you can also provide the callable for handling the route via the 184 | > `handler` key of your route configuration. The same rules apply to that argument as for the 185 | > `map()` method. 186 | > 187 | > Any callables mapped directly to the `Dispatcher` instance will be preferred over those passed via 188 | > configuration. 189 | 190 | ### Creating and running the application 191 | 192 | Creating the application consists of 193 | 194 | - Setting up or retrieving the list of routes 195 | - Setting up the dispatch map 196 | - Instantiating the application 197 | - Running the application 198 | 199 | For the following example, we'll assume that the classes `My\SelfUpdate` and `My\Build` are 200 | autoloadable, and each define the method `__invoke()`. 201 | 202 | ```php 203 | use My\SelfUpdate; 204 | use Zend\Console\Console; 205 | use ZF\Console\Application; 206 | use ZF\Console\Dispatcher; 207 | 208 | require_once __DIR__ . '/vendor/autoload.php'; // Composer autoloader 209 | 210 | define('VERSION', '1.1.3'); 211 | 212 | $dispatcher = new Dispatcher(); 213 | $dispatcher->map('self-update', new SelfUpdate($version)); 214 | $dispatcher->map('build', 'My\Build'); 215 | 216 | $application = new Application( 217 | 'Builder', 218 | VERSION, 219 | include __DIR__ . '/config/routes.php', 220 | Console::getInstance(), 221 | $dispatcher 222 | ); 223 | $exit = $application->run(); 224 | exit($exit); 225 | ``` 226 | 227 | ## Features 228 | 229 | `zf-console` provides a number of features "out of the box." These include: 230 | 231 | - Usage reporting 232 | - Help message reporting 233 | - Version reporting 234 | - Shell autocompletion 235 | - Exception handling 236 | 237 | Usage reporting may be observed by executing an application with no arguments, or with only the 238 | `help` argument: 239 | 240 | ```console 241 | $ ./script.php 242 | Builder, version 1.1.3 243 | 244 | Available commands: 245 | 246 | autocomplete Command autocompletion setup 247 | build Build a package 248 | help Get help for individual commands 249 | self-update Perform a self-update of the script 250 | version Display the version of the script 251 | ``` 252 | 253 | Help reporting for individual commands may be observed by executing `script help `: 254 | 255 | ```console 256 | $ ./script.php help self-update 257 | Builder, version 1.1.3 258 | 259 | Usage: 260 | self-update 261 | 262 | Help: 263 | When executed via the Phar file, performs a self-update by querying 264 | the package repository. If successful, it will report the new version. 265 | ``` 266 | 267 | > ### Name routes after the command 268 | > 269 | > We recommend naming routes after the command name. In part, this simplifies 270 | > finding the matching route definition, but more importantly: if a user 271 | > specifies the command, but does not specify valid arguments for it, the 272 | > command will be used to provide a help usage message for that route. 273 | > 274 | > As an example, in the above, if I typed `script.php build` without any 275 | > additional arguments, the usage message for the `build` command will be 276 | > displayed, since the command and route name match. 277 | 278 | Version reporting can be observed by executing `script --version` or `script -v`: 279 | 280 | ```console 281 | $ ./script --version 282 | Builder, version 1.1.3 283 | 284 | ``` 285 | 286 | You can override the default behavior in several ways. 287 | 288 | First, you can override either of the `help` or `version` commands by mapping them in your 289 | `Dispatcher` instance prior to creating your `Application` instance: 290 | 291 | ```php 292 | $dispatcher->map('help', $myCustomHelpCommand); 293 | $dispatcher->map('version', $myVersionCommand); 294 | ``` 295 | 296 | Second, you can set both custom banners and footers for the usage and help messages using the 297 | `setBanner()` and/or `setFooter()` methods of the `Application` instance. Each accepts either a 298 | string message, or a callable that to invoke in order to display the message; if using a callable, 299 | it will be passed the `Console` instance as the sole argument. 300 | 301 | ```php 302 | $application->setBanner('Some ASCI art for a banner!'); // string 303 | $application->setBanner(function ($console) { // callable 304 | $console->writeLine( 305 | $console->colorize('Builder', \Zend\Console\ColorInterface::BLUE) 306 | . ' - for building deployment packages' 307 | ); 308 | $console->writeLine(''); 309 | $console->writeLine('Usage:', \Zend\Console\ColorInterface::GREEN); 310 | $console->writeLine(' ' . basename(__FILE__) . ' command [options]'); 311 | $console->writeLine(''); 312 | }); 313 | 314 | $application->setFooter('Copyright 2014 Zend Technologies'); 315 | ``` 316 | 317 | > ### Disabling banners 318 | > 319 | > The banner is shown by default. In some cases, you may not want to display it; 320 | > e.g., when piping output to another process. 321 | > 322 | > Starting with version 1.3.0, you can disable banner output using: 323 | > 324 | > ```php 325 | > $application->setBannerDisabledForUserCommands(true); 326 | > ``` 327 | > 328 | > Additionally, starting with 1.3.0, you can explicitly nullify both the banner 329 | > and footer: 330 | > 331 | > ```php 332 | > $application->setBanner(null); 333 | > $application->setFooter(null); 334 | > ``` 335 | 336 | ### Autocompletion 337 | 338 | Autocompletion is a useful feature of many login shells. `zf-console` provides autocompletion 339 | support for bash, zsh, and any shell that understands autocompletion rules in a similar fashion. 340 | Rules are generated per-script, using the `autocomplete` command: 341 | 342 | ```console 343 | $ ./script autocomplete 344 | ``` 345 | 346 | Running this will output a shell script that you can save and add to your toolchain; the script 347 | itself contains information on how to save it and add it to your shell. In most cases, this will 348 | look something like: 349 | 350 | ```console 351 | $ {script} autocomplete > > $HOME/bin/{script}_autocomplete.sh 352 | $ echo "source \$HOME/bin/{script}_autocomplete.sh" > > $HOME/{your_shell_rc} 353 | ``` 354 | 355 | where `{script}` is the name of the command, and `{your_shell_rc}` is the location of your shell's 356 | runtime configutation file (e.g., `.bashrc`, `.zshrc`). 357 | 358 | ## Dispatcher callables 359 | 360 | The `Dispatcher` will invoke the callable associated with a given route by calling it with two 361 | arguments: 362 | 363 | - The `ZF\Console\Route` instance that matched 364 | - The `Zend\Console` adapter currently in use 365 | 366 | In most cases, you will use the `Route` instance to gather arguments passed to the application, and 367 | the `Console` instance to provide any feedback or to prompt for any additional information. 368 | 369 | The `Route` instance contains several methods of interest: 370 | 371 | - `getMatches()` will return an array of all named arguments matched. 372 | - `matchedParam($name)` will tell you if a given argument was matched. 373 | - `getMatchedParam($name, $default = null)` will return the value for the given argument as matched, 374 | and, if not matched, the `$default` value you provide. 375 | - `getName()` will return the name of the route (which may be useful if you use the same callable 376 | for multiple routes). 377 | 378 | ## Custom dispatchers 379 | 380 | > - Since 1.3.0 381 | 382 | You may create a custom dispatcher by implementing 383 | `ZF\Console\DispatcherInterface`, which defines the following methods: 384 | 385 | ```php 386 | namespace ZF\Console; 387 | 388 | use Zend\Console\Adapter\AdapterInterface as ConsoleAdapter; 389 | 390 | interface DispatcherInterface 391 | { 392 | /** 393 | * Map a command name to its handler. 394 | * 395 | * @param string $command 396 | * @param callable|string $command A callable command, or a string service 397 | * or class name to use as a handler. 398 | * @return self Should implement a fluent interface. 399 | */ 400 | public function map($command, $callable); 401 | 402 | /** 403 | * Does the dispatcher have a handler for the given command? 404 | * 405 | * @param string $command 406 | * @return bool 407 | */ 408 | public function has($command); 409 | 410 | /** 411 | * Dispatch a routed command to its handler. 412 | * 413 | * @param Route $route 414 | * @param ConsoleAdapter $console 415 | * @return int The exit status code from the command. 416 | */ 417 | public function dispatch(Route $route, ConsoleAdapter $console); 418 | } 419 | ``` 420 | 421 | When you do, instantiate your custom dispatcher and pass it to the `Application` 422 | instance when initializing it: 423 | 424 | ```php 425 | $application = new Application('App', 1.0, $routes, null, $dispatcher); 426 | ``` 427 | 428 | ## Pulling commands from a container 429 | 430 | > - Since 1.3.0 431 | 432 | Instead of specifying a callable or a class name for a command handler, you may 433 | store your handlers within a dependency injection container compatible with the 434 | [PSR-11 specification](https://github.com/php-fig/container); 435 | when you do so, you can specify the *service name* of the handler instead. 436 | 437 | To do this, you will need to create a `Dispatcher` instance, passing it the 438 | container you are using at instantiation: 439 | 440 | 441 | ```php 442 | $serviceManager = new ServiceManager(/* ... */); 443 | 444 | // use `zend-servicemanager` as container 445 | $dispatcher = new Dispatcher($serviceManager); 446 | ``` 447 | 448 | From there, you can configure routes using the service name (which is often a 449 | class name): 450 | 451 | ``` 452 | $routes = [ 453 | [ 454 | 'name' => 'hello', 455 | 'handler' => HelloCommand::class, 456 | ] 457 | ]; 458 | ``` 459 | 460 | Finally, do not forget to pass your dispatcher to your application when you 461 | initialize it: 462 | 463 | ```php 464 | $application = new Application('App', 1.0, $routes, null, $dispatcher); 465 | ``` 466 | 467 | In the above examples, when the `hello` route is matched, the `Dispatcher` will 468 | attempt to pull the `HelloCommand` service from the container prior to 469 | dispatching it. 470 | 471 | ## Exception Handling 472 | 473 | `zf-console` provides exception handling by default, via `ZF\Console\ExceptionHandler`. When your 474 | console application raises an exception, this handler will provide a "pretty" view of the error, 475 | instead of the full stack trace (unless you want to include the stack trace in your view!). 476 | 477 | The default message looks like the following: 478 | 479 | ```console 480 | ====================================================================== 481 | The application has thrown an exception! 482 | ====================================================================== 483 | 484 | :className: 485 | :message 486 | ``` 487 | 488 | where `:className` will be filled with the exception's class name, and `message` will contain the 489 | exception message, if any. 490 | 491 | You may provide your own template if desired: 492 | 493 | ```php 494 | $application->getExceptionHandler()->setMessageTemplate($template); 495 | ``` 496 | 497 | The following template variables are defined: 498 | 499 | - `:className` 500 | - `:message` 501 | - `:code` 502 | - `:file` 503 | - `:line` 504 | - `:stack` 505 | - `:previous` (this is used to report previous exceptions in a trace) 506 | 507 | If you want to provide your own exception handler, you may do so by providing any PHP callable to 508 | the `setExceptionHandler()` method: 509 | 510 | ```php 511 | $application->setExceptionHandler($handler); 512 | ``` 513 | 514 | ### Debug mode 515 | 516 | If you want normal PHP stack traces and error reporting, you can put the application into debug 517 | mode: 518 | 519 | ```php 520 | $application->setDebug(true); 521 | ``` 522 | 523 | ## Using zf-console in Zend Framework 2 Applications 524 | 525 | While Zend Framework 2 integrates console functionality into the MVC, you may want to write scripts 526 | that do not use the MVC. For instance, it may be easier to write an application-specific script 527 | without going through the hoops of creating a controller, adding console configuration, etc. 528 | However, you will likely still want access to services provided within modules, and also want the 529 | ability to honor service and configuration overrides. 530 | 531 | To do this, you will need to bootstrap your application first. We'll assume you're putting your 532 | script in your application's `bin/` directory for this example. 533 | 534 | ```php 535 | use Zend\Console\Console; 536 | use Zend\Console\ColorInterface as Color; 537 | use ZF\Console\Application; 538 | use ZF\Console\Dispatcher; 539 | 540 | chdir(dirname(__DIR__)); 541 | require 'init_autoloader.php'; // grabs the Composer autoloader and/or ZF2 autoloader 542 | $application = Zend\Mvc\Application::init(require 'config/application.config.php'); 543 | $services = $application->getServiceManager(); 544 | 545 | $buildModel = $services->get('My\BuildModel'); 546 | 547 | $dispatcher = new Dispatcher(); 548 | $dispatcher->map('build', function ($route, $console) use ($buildModel) { 549 | $opts = $route->getMatches(); 550 | $result = $buildModel->build($opts['package'], $opts['target']); 551 | if (! $result) { 552 | $console->writeLine('Error building package!', Color::WHITE, Color::RED); 553 | return 1; 554 | } 555 | 556 | $console->writeLine('Finished building package ' . $opts['package'], Color::GREEN); 557 | return 0; 558 | }); 559 | 560 | $application = new Application( 561 | 'Builder', 562 | VERSION, 563 | array( 564 | array( 565 | 'name' => 'build', 566 | 'route' => 'build [--target=]', 567 | 'description' => 'Build a package, using as the package filename, and --target 568 | as the application directory to be packaged.', 569 | 'short_description' => 'Build a package', 570 | 'options_descriptions' => array( 571 | '' => 'Package filename to build', 572 | '--target' => 'Name of the application directory to package; defaults to current working directory', 573 | ), 574 | 'defaults' => array( 575 | 'target' => getcwd(), // default to current working directory 576 | ), 577 | ), 578 | ), 579 | Console::getInstance(), 580 | $dispatcher 581 | ); 582 | $exit = $application->run(); 583 | exit($exit); 584 | ``` 585 | 586 | Essentially, you're calling `Zend\Mvc\Application::init()`, but not it's `run()` method. This 587 | ensures all modules are bootstrapped, which means all configuration is loaded and merged, all 588 | services are wired, and all listeners are attached. You then pull relevant services from the 589 | `ServiceManager` and pass them to your console callbacks. 590 | 591 | ## Best Practices 592 | 593 | We recommend the following practices when creating applications using `zf-console`. 594 | 595 | ### Use `Zend\Console` to create output 596 | 597 | Use `Zend\Console` to create any output you send. This ensures that the output works cross-platform 598 | (including Unix-like systems and Windows). As examples: 599 | 600 | ``` 601 | $dispatcher->map('some-command', function ($route, $console) { 602 | $console->writeLine('Executing some-command!'); 603 | }); 604 | ``` 605 | 606 | ### Install your script via Composer 607 | 608 | You can tell Composer to install your script in the `vendor/bin/` directory, making it trivial for 609 | end-users to locate and execute your script within their own applications. 610 | 611 | ```JSON 612 | { 613 | "require": { 614 | "php": ">=5.3.23", 615 | "zfcampus/zf-console": "~1.0-dev" 616 | }, 617 | "bin": ["script.php"] 618 | } 619 | ``` 620 | 621 | If you do this, be sure to name your script uniquely. 622 | 623 | ### Use filters or validators 624 | 625 | `Zend\Console`'s RouteMatcher sub-component allows you to specify filters and/or validators for each 626 | matched argument of a route. These let you provide normalization (filters) and more robust 627 | validation logic when desired. 628 | 629 | As an example, consider a common scenario of using comma-separated values for an argument; you could 630 | split those into an array as follows: 631 | 632 | ```php 633 | // config/routes.php 634 | 635 | use Zend\Filter\Callback as CallbackFilter; 636 | 637 | return array( 638 | array( 639 | 'name' => 'filter', 640 | 'route' => 'filter [--exclude=]', 641 | 'default' => array( 642 | 'exclude' => array(), 643 | ), 644 | 'filters' => array( 645 | 'exclude' => new CallbackFilter(function ($value) { 646 | if (! is_string($value)) { 647 | return $value; 648 | } 649 | $exclude = explode(',', $value); 650 | array_walk($exclude, 'trim'); 651 | return $exclude; 652 | }), 653 | ), 654 | ) 655 | ); 656 | ``` 657 | 658 | Using filters and validators well, you can ensure that when your dispatch callbacks receive data, it 659 | is already sanitized and ready to use. 660 | 661 | #### Filters provided by zf-console 662 | 663 | `zf-console` provides several filters for your convenience: 664 | 665 | - `ZF\Console\Filter\Explode` allows you to specify a delimiter to use to "explode" a string value 666 | to an array of values. As an example: 667 | 668 | ```php 669 | // config/routes.php 670 | 671 | use ZF\Console\Filter\Explode as ExplodeFilter; 672 | 673 | return array( 674 | array( 675 | 'name' => 'filter', 676 | 'route' => 'filter [--exclude=]', 677 | 'default' => array( 678 | 'exclude' => array(), 679 | ), 680 | 'filters' => array( 681 | 'exclude' => new ExplodeFilter(','), 682 | ), 683 | ) 684 | ); 685 | ``` 686 | 687 | The above would explode values provided to `--exclude` using a `,`; `--exclude=foo,bar,baz` would 688 | set `exclude` to `array('foo', 'bar', 'baz')`. By default, if no delimiter is provided, `,` is 689 | assumed. 690 | 691 | - `ZF\Console\Filter\Json` allows you to specify a JSON-formatted string; it will then deserialize 692 | it to native PHP values. 693 | 694 | ```php 695 | // config/routes.php 696 | 697 | use ZF\Console\Filter\Json as JsonFilter; 698 | 699 | return array( 700 | array( 701 | 'name' => 'filter', 702 | 'route' => 'filter [--exclude=]', 703 | 'default' => array( 704 | 'exclude' => array(), 705 | ), 706 | 'filters' => array( 707 | 'exclude' => new JsonFilter(), 708 | ), 709 | ) 710 | ); 711 | ``` 712 | 713 | The above would deserialize a JSON value provided to `--exclude`; `--exclude='["foo","bar","baz"]'` would 714 | set `exclude` to `array('foo', 'bar', 'baz')`. 715 | 716 | - `ZF\Console\Filter\QueryString` allows you to specify a form-encoded string; it will then 717 | deserialize it to native PHP values. 718 | 719 | ```php 720 | // config/routes.php 721 | 722 | use ZF\Console\Filter\QueryString; 723 | 724 | return array( 725 | array( 726 | 'name' => 'filter', 727 | 'route' => 'filter [--exclude=]', 728 | 'default' => array( 729 | 'exclude' => array(), 730 | ), 731 | 'filters' => array( 732 | 'exclude' => new QueryString(), 733 | ), 734 | ) 735 | ); 736 | ``` 737 | 738 | The above would deserialize a form-encoded value provided to `--exclude`; 739 | `--exclude='foo=bar&baz=bat'` would set `exclude` to `array('foo' => 'bar', 'baz' => 'bat')`. 740 | 741 | ## Classes 742 | 743 | This library defines the following classes: 744 | 745 | - `ZF\Console\Application`, which handles actual execution of the script, including usage reporting. 746 | - `ZF\Console\Dispatcher`, which maps route names to PHP callables, and dispatches them when 747 | selected. 748 | - `ZF\Console\HelpCommand`, which provides the default "help" logic for displaying command usage. 749 | - `ZF\Console\Route`, an extension of `Zend\Console\RouteMatcher\DefaultRouteMatcher` that adds 750 | aggregation of route metadata, including the name and description. 751 | - `ZF\Console\RouteCollection`, which implements `Zend\Console\RouteMatcher\RouteMatcherInterface`, 752 | aggregates `ZF\Console\Route` instances, and performs route matching. 753 | - `ZF\Console\Filter\Explode`, which implements `Zend\Filter\FilterInterface`, and which [is 754 | described above](#filters-provided-by-zf-console). 755 | - `ZF\Console\Filter\Json`, which implements `Zend\Filter\FilterInterface`, and which [is 756 | described above](#filters-provided-by-zf-console). 757 | - `ZF\Console\Filter\QueryString`, which implements `Zend\Filter\FilterInterface`, and which [is 758 | described above](#filters-provided-by-zf-console). -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zfcampus/zf-console", 3 | "description": "Library for creating and dispatching console commands", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "console" 9 | ], 10 | "support": { 11 | "issues": "https://github.com/zfcampus/zf-console/issues", 12 | "source": "https://github.com/zfcampus/zf-console", 13 | "rss": "https://github.com/zfcampus/zf-console/releases.atom", 14 | "slack": "https://zendframework-slack.herokuapp.com", 15 | "forum": "https://discourse.zendframework.com/c/questions/apigility" 16 | }, 17 | "require": { 18 | "php": "^5.6 || ^7.0", 19 | "psr/container": "^1.0", 20 | "zendframework/zend-console": "^2.6" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^5.7.25 || ^6.4.4", 24 | "zendframework/zend-coding-standard": "~1.0.0", 25 | "zendframework/zend-filter": "^2.7.1", 26 | "zendframework/zend-validator": "^2.8.1" 27 | }, 28 | "suggest": { 29 | "zendframework/zend-filter": "^2.7.1; Useful for filtering/normalizing argument values", 30 | "zendframework/zend-validator": "^2.8.1; Useful for providing more thorough argument validation logic" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "ZF\\Console\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "ZFTest\\Console\\": "test/" 40 | } 41 | }, 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "extra": { 46 | "branch-alias": { 47 | "dev-master": "1.4-dev", 48 | "dev-develop": "1.5-dev" 49 | } 50 | }, 51 | "scripts": { 52 | "check": [ 53 | "@cs-check", 54 | "@test" 55 | ], 56 | "cs-check": "phpcs", 57 | "cs-fix": "phpcbf", 58 | "test": "phpunit --colors=always", 59 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", 60 | "upload-coverage": "coveralls -v" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | name = $name; 101 | $this->version = $version; 102 | 103 | if (null === $console) { 104 | $console = DefaultConsole::getInstance(); 105 | } 106 | 107 | $this->console = $console; 108 | 109 | if (null === $dispatcher) { 110 | $dispatcher = new Dispatcher(); 111 | } 112 | 113 | $this->dispatcher = $dispatcher; 114 | 115 | $this->routeCollection = $routeCollection = new RouteCollection(); 116 | $this->setRoutes($routes); 117 | 118 | $this->banner = [$this, 'showVersion']; 119 | 120 | if (! $routeCollection->hasRoute('help')) { 121 | $this->setupHelpCommand($routeCollection, $dispatcher); 122 | } 123 | 124 | if (! $routeCollection->hasRoute('version')) { 125 | $this->setupVersionCommand($routeCollection, $dispatcher); 126 | } 127 | 128 | if (! $routeCollection->hasRoute('autocomplete')) { 129 | $this->setupAutocompleteCommand($routeCollection, $dispatcher); 130 | } 131 | } 132 | 133 | /** 134 | * @return DispatcherInterface 135 | */ 136 | public function getDispatcher() 137 | { 138 | return $this->dispatcher; 139 | } 140 | 141 | /** 142 | * Run the application 143 | * 144 | * If no arguments are provided, pulls them from $argv, stripping the 145 | * script argument first. 146 | * 147 | * If the argument list is empty, displays a usage message. 148 | * 149 | * If arguments are provided, but no routes match, displays a usage message 150 | * and returns a status of 1. 151 | * 152 | * Otherwise, attempts to dispatch the matched command, returning the 153 | * execution status. 154 | * 155 | * @param array $args 156 | * @return int 157 | */ 158 | public function run(array $args = null) 159 | { 160 | $this->initializeExceptionHandler(); 161 | $this->setProcessTitle(); 162 | 163 | if ($args === null) { 164 | global $argv; 165 | $args = array_slice($argv, 1); 166 | } 167 | 168 | $result = $this->processRun($args); 169 | 170 | $this->showMessage($this->footer); 171 | 172 | return $result; 173 | } 174 | 175 | /** 176 | * Process run 177 | * If the argument list is empty, displays a usage message. 178 | * 179 | * If arguments are provided, but no routes match, displays a usage message 180 | * and returns a status of 1. 181 | * 182 | * Otherwise, attempts to dispatch the matched command, returning the 183 | * execution status. 184 | * 185 | * @param array $args 186 | * @return int 187 | */ 188 | protected function processRun(array $args) 189 | { 190 | if (empty($args)) { 191 | $this->showMessage($this->banner); 192 | $this->showUsageMessage(); 193 | return 0; 194 | } 195 | 196 | $route = $this->routeCollection->match($args); 197 | if (! $route instanceof Route) { 198 | $this->showMessage($this->banner); 199 | 200 | $name = $args[0]; 201 | $route = $this->routeCollection->getRoute($name); 202 | if (! $route instanceof Route) { 203 | $this->showUnmatchedRouteMessage($args); 204 | return 1; 205 | } 206 | 207 | $this->showUsageMessageForRoute($route, true); 208 | return 1; 209 | } 210 | 211 | if (! $this->bannerDisabledForUserCommands) { 212 | $this->showMessage($this->banner); 213 | } 214 | 215 | return $this->dispatcher->dispatch($route, $this->console); 216 | } 217 | 218 | /** 219 | * Display the application version 220 | * 221 | * @param Console $console 222 | * @return int 223 | */ 224 | public function showVersion(Console $console) 225 | { 226 | $console->writeLine( 227 | $console->colorize($this->name . ',', Color::GREEN) 228 | . ' version ' 229 | . $console->colorize($this->version, Color::BLUE) 230 | ); 231 | $console->writeLine(''); 232 | return 0; 233 | } 234 | 235 | /** 236 | * Display a message (banner or footer) 237 | * 238 | * If the message is a string and not callable, uses the composed console 239 | * instance to render it. 240 | * 241 | * If the message is a callable, calls it with the composed console 242 | * instance as an argument. 243 | * 244 | * @param string|callable $messageOrCallable 245 | */ 246 | public function showMessage($messageOrCallable) 247 | { 248 | if (is_string($messageOrCallable) && ! is_callable($messageOrCallable)) { 249 | $this->console->writeLine($messageOrCallable); 250 | return; 251 | } 252 | 253 | if (is_callable($messageOrCallable)) { 254 | call_user_func($messageOrCallable, $this->console); 255 | } 256 | } 257 | 258 | /** 259 | * Displays a usage message for the router 260 | * 261 | * If a route name is provided, usage for that route only will be displayed; 262 | * otherwise, the name/short description for each will be present. 263 | * 264 | * @param null|string $name 265 | */ 266 | public function showUsageMessage($name = null) 267 | { 268 | $console = $this->console; 269 | 270 | if ($name === null) { 271 | $console->writeLine('Available commands:', Color::GREEN); 272 | $console->writeLine(''); 273 | } 274 | 275 | $maxSpaces = $this->calcMaxString($this->routeCollection->getRouteNames()) + 2; 276 | 277 | foreach ($this->routeCollection as $route) { 278 | if ($name === $route->getName()) { 279 | $this->showUsageMessageForRoute($route); 280 | return; 281 | } 282 | 283 | if ($name !== null) { 284 | continue; 285 | } 286 | 287 | $routeName = $route->getName(); 288 | $spaces = $maxSpaces - strlen($routeName); 289 | $console->write(' ' . $routeName, Color::GREEN); 290 | $console->writeLine(str_repeat(' ', $spaces) . $route->getShortDescription()); 291 | } 292 | 293 | if ($name) { 294 | $this->showUnrecognizedRouteMessage($name); 295 | return; 296 | } 297 | } 298 | 299 | /** 300 | * Set the banner to display. 301 | * 302 | * Used whenever the application is called without arguments. 303 | * 304 | * If the default help implementation is used, also displayed with help 305 | * messages. 306 | * 307 | * @param null|string|callable $bannerOrCallable 308 | * @return self 309 | */ 310 | public function setBanner($bannerOrCallable) 311 | { 312 | $this->validateMessage($bannerOrCallable); 313 | $this->banner = $bannerOrCallable; 314 | return $this; 315 | } 316 | 317 | /** 318 | * Set the footer to display. 319 | * 320 | * Used whenever the application is called without arguments. 321 | * 322 | * If the default help implementation is used, also displayed with help 323 | * messages. 324 | * 325 | * @param null|string|callable $footerOrCallable 326 | * @return self 327 | */ 328 | public function setFooter($footerOrCallable) 329 | { 330 | $this->validateMessage($footerOrCallable); 331 | $this->footer = $footerOrCallable; 332 | return $this; 333 | } 334 | 335 | /** 336 | * Sets the debug flag of the application 337 | * 338 | * @param boolean $flag 339 | * 340 | * @return $this 341 | */ 342 | public function setDebug($flag) 343 | { 344 | $this->debug = (boolean) $flag; 345 | return $this; 346 | } 347 | 348 | /** 349 | * Sets exception handler to use the expection Message 350 | * 351 | * @param callable $handler 352 | * @return self 353 | */ 354 | public function setExceptionHandler($handler) 355 | { 356 | if (! is_callable($handler)) { 357 | throw new InvalidArgumentException('Exception handler must be callable'); 358 | } 359 | 360 | $this->exceptionHandler = $handler; 361 | return $this; 362 | } 363 | 364 | /** 365 | * Gets the registered exception handler 366 | * 367 | * Lazy-instantiates an ExceptionHandler instance with the current console 368 | * instance if no handler has been specified. 369 | * 370 | * @return callable 371 | */ 372 | public function getExceptionHandler() 373 | { 374 | if (! is_callable($this->exceptionHandler)) { 375 | $this->exceptionHandler = new ExceptionHandler($this->console); 376 | } 377 | return $this->exceptionHandler; 378 | } 379 | 380 | /** 381 | * Calculate the maximum string length for an array 382 | * 383 | * @param array $data 384 | * 385 | * @return int 386 | */ 387 | protected function calcMaxString(array $data = []) 388 | { 389 | $maxLength = 0; 390 | 391 | foreach ($data as $name) { 392 | if (strlen($name) > $maxLength) { 393 | $maxLength = strlen($name); 394 | } 395 | } 396 | 397 | return $maxLength; 398 | } 399 | 400 | /** 401 | * Set routes to use 402 | * 403 | * Allows specifying an array of routes, which may be mixed Route instances or array 404 | * specifications for creating routes. 405 | * 406 | * @param array|Traversable $routes 407 | * @return self 408 | */ 409 | protected function setRoutes($routes) 410 | { 411 | foreach ($routes as $route) { 412 | if ($route instanceof Route) { 413 | $this->routeCollection->addRoute($route); 414 | continue; 415 | } 416 | 417 | if (is_array($route)) { 418 | $this->routeCollection->addRouteSpec($route); 419 | $this->mapRouteHandler($route); 420 | continue; 421 | } 422 | } 423 | 424 | return $this; 425 | } 426 | 427 | /** 428 | * Remove a route by name 429 | * 430 | * @param String $name 431 | * @return self 432 | */ 433 | public function removeRoute($name) 434 | { 435 | $this->routeCollection->removeRoute($name); 436 | return $this; 437 | } 438 | 439 | /** 440 | * Disables the banner for user commands. Still shows it before usage messages. 441 | * 442 | * @param bool $flag 443 | * 444 | * @return self 445 | */ 446 | public function setBannerDisabledForUserCommands($flag = true) 447 | { 448 | $this->bannerDisabledForUserCommands = (bool) $flag; 449 | return $this; 450 | } 451 | 452 | /** 453 | * Whether or not to disable the banner in user commands. False by default. 454 | * 455 | * @return bool 456 | */ 457 | public function isBannerDisabledForUserCommands() 458 | { 459 | return $this->bannerDisabledForUserCommands; 460 | } 461 | 462 | /** 463 | * Sets up the default help command 464 | * 465 | * Creates the route, and maps the command. 466 | * 467 | * @param RouteCollection $routeCollection 468 | * @param DispatcherInterface $dispatcher 469 | */ 470 | protected function setupHelpCommand(RouteCollection $routeCollection, DispatcherInterface $dispatcher) 471 | { 472 | $help = new HelpCommand($this); 473 | $routeCollection->addRouteSpec([ 474 | 'name' => 'help', 475 | 'route' => '[]', 476 | 'description' => "Display the help message for a given command.\n\n" 477 | . 'To display the list of available commands, ' 478 | . 'call the script or help with no arguments.', 479 | 'short_description' => 'Get help for individual commands', 480 | 'options_descriptions' => [ 481 | 'command' => 'Name of a command for which to get help', 482 | ], 483 | 'constraints' => [ 484 | 'command' => '/^[^\s]+$/', 485 | ], 486 | 'defaults' => [ 487 | 'help' => true, 488 | ], 489 | ]); 490 | 491 | $self = $this; // PHP < 5.4 compat 492 | $banner = $this->banner; // PHP < 5.4 compat 493 | $footer = $this->footer; // PHP < 5.4 compat 494 | $dispatcher->map('help', function ($route, $console) use ($help, $self, $banner, $footer) { 495 | $help($route, $console); 496 | return 0; 497 | }); 498 | } 499 | 500 | /** 501 | * Sets up the default version command 502 | * 503 | * Creates the route, and maps the command. 504 | * 505 | * @param RouteCollection $routeCollection 506 | * @param DispatcherInterface $dispatcher 507 | */ 508 | protected function setupVersionCommand(RouteCollection $routeCollection, DispatcherInterface $dispatcher) 509 | { 510 | $routeCollection->addRouteSpec([ 511 | 'name' => 'version', 512 | 'route' => '(--version|-v)', 513 | 'description' => 'Display the version of the script.', 514 | 'short_description' => 'Display the version of the script', 515 | 'defaults' => [ 516 | 'version' => true, 517 | ], 518 | 'prepend_command_to_route' => false, 519 | ]); 520 | 521 | $self = $this; // PHP < 5.4 compat 522 | $dispatcher->map('version', function ($route, $console) use ($self) { 523 | return $self->showVersion($console); 524 | }); 525 | } 526 | 527 | 528 | /** 529 | * Sets up the default autocomplete command 530 | * 531 | * Creates the route, and maps the command. 532 | * 533 | * @param RouteCollection $routeCollection 534 | * @param DispatcherInterface $dispatcher 535 | */ 536 | protected function setupAutocompleteCommand(RouteCollection $routeCollection, DispatcherInterface $dispatcher) 537 | { 538 | $routeCollection->addRouteSpec([ 539 | 'name' => 'autocomplete', 540 | 'description' => 'Shows how to activate autocompletion of this command for your login shell', 541 | 'short_description' => 'Command autocompletion setup', 542 | ]); 543 | 544 | $dispatcher->map('autocomplete', function ($route, $console) { 545 | ob_start(); 546 | include __DIR__.'/../views/autocomplete.phtml'; 547 | $content = ob_get_contents(); 548 | ob_end_clean(); 549 | 550 | return $console->write($content); 551 | }); 552 | } 553 | 554 | /** 555 | * Set CLI process title (PHP versions >= 5.5) 556 | */ 557 | protected function setProcessTitle() 558 | { 559 | if (version_compare(PHP_VERSION, '5.5', 'lt')) { 560 | return; 561 | } 562 | 563 | // Mac OS X does not support cli_set_process_title() due to security issues 564 | // Bug fix for issue https://github.com/zfcampus/zf-console/issues/21 565 | if (PHP_OS == 'Darwin') { 566 | return; 567 | } 568 | 569 | cli_set_process_title($this->name); 570 | } 571 | 572 | /** 573 | * Display an error message indicating a route name was not recognized 574 | * 575 | * @param string $name 576 | */ 577 | protected function showUnrecognizedRouteMessage($name) 578 | { 579 | $console = $this->console; 580 | $console->writeLine(''); 581 | $console->writeLine(sprintf('Unrecognized command "%s"', $name), Color::WHITE, Color::RED); 582 | $console->writeLine(''); 583 | } 584 | 585 | /** 586 | * Display the usage message for an individual route 587 | * 588 | * @param Route $route 589 | */ 590 | protected function showUsageMessageForRoute(Route $route, $log = false) 591 | { 592 | $console = $this->console; 593 | 594 | $console->writeLine('Usage:', Color::GREEN); 595 | $console->writeLine(' ' . $route->getRoute()); 596 | $console->writeLine(''); 597 | 598 | $options = $route->getOptionsDescription(); 599 | if (! empty($options)) { 600 | $console->writeLine('Arguments:', Color::GREEN); 601 | 602 | $maxSpaces = $this->calcMaxString(array_keys($options)) + 2; 603 | 604 | foreach ($options as $name => $description) { 605 | $spaces = $maxSpaces - strlen($name); 606 | $console->write(' ' . $name, Color::GREEN); 607 | $console->writeLine(str_repeat(' ', $spaces) . $description); 608 | } 609 | $console->writeLine(''); 610 | } 611 | 612 | $description = $route->getDescription(); 613 | if (! empty($description)) { 614 | $console->writeLine('Help:', Color::GREEN); 615 | $console->writeLine(''); 616 | $console->writeLine($description); 617 | } 618 | } 619 | 620 | /** 621 | * Show message indicating inability to match a route. 622 | * 623 | * @param array $args 624 | */ 625 | protected function showUnmatchedRouteMessage(array $args) 626 | { 627 | $this->console->write('Unrecognized command: ', Color::RED); 628 | $this->console->writeLine(implode(' ', $args)); 629 | $this->console->writeLine(''); 630 | $this->showUsageMessage(); 631 | } 632 | 633 | /** 634 | * Initialize the exception handler (if not in debug mode) 635 | */ 636 | protected function initializeExceptionHandler() 637 | { 638 | if ($this->debug) { 639 | return; 640 | } 641 | 642 | set_exception_handler($this->getExceptionHandler()); 643 | } 644 | 645 | /** 646 | * Map a route handler 647 | * 648 | * If a given route specification has a "handler" entry, and the dispatcher 649 | * does not currently have a handler for that command, map it. 650 | * 651 | * @param array $route 652 | */ 653 | protected function mapRouteHandler(array $route) 654 | { 655 | if (! isset($route['handler'])) { 656 | return; 657 | } 658 | 659 | $command = $route['name']; 660 | if ($this->dispatcher->has($command)) { 661 | return; 662 | } 663 | 664 | $this->dispatcher->map($command, $route['handler']); 665 | } 666 | 667 | /** 668 | * @param mixed $stringOrCallable 669 | */ 670 | protected function validateMessage($stringOrCallable) 671 | { 672 | if ($stringOrCallable !== null 673 | && ! is_string($stringOrCallable) 674 | && ! is_callable($stringOrCallable) 675 | ) { 676 | throw new InvalidArgumentException('Messages must be string or callable'); 677 | } 678 | } 679 | } 680 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | container = $container; 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | public function map($command, $callable) 39 | { 40 | if (! is_string($command) || empty($command)) { 41 | throw new InvalidArgumentException('Invalid command specified; must be a non-empty string'); 42 | } 43 | 44 | if (is_callable($callable)) { 45 | $this->commandMap[$command] = $callable; 46 | return $this; 47 | } 48 | 49 | if (! is_string($callable)) { 50 | throw new InvalidArgumentException( 51 | 'Invalid command callback specified; must be callable or a string class or service name' 52 | ); 53 | } 54 | 55 | if (class_exists($callable)) { 56 | $this->commandMap[$command] = $callable; 57 | return $this; 58 | } 59 | 60 | if (! $this->container || ! $this->container->has($callable)) { 61 | throw new InvalidArgumentException( 62 | 'Invalid command callback specified; must be callable or a string class or service name' 63 | ); 64 | } 65 | 66 | $this->commandMap[$command] = $callable; 67 | return $this; 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function has($command) 74 | { 75 | return isset($this->commandMap[$command]); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | public function dispatch(Route $route, ConsoleAdapter $console) 82 | { 83 | $name = $route->getName(); 84 | if (! isset($this->commandMap[$name])) { 85 | $console->writeLine(''); 86 | $console->writeLine(sprintf('Unhandled command "%s" invoked', $name), Color::WHITE, Color::RED); 87 | $console->writeLine(''); 88 | $console->writeLine('The command does not have a registered handler.'); 89 | return 1; 90 | } 91 | 92 | $callable = $this->commandMap[$name]; 93 | 94 | if (! is_callable($callable) && is_string($callable)) { 95 | $callable = ($this->container && $this->container->has($callable)) 96 | ? $this->container->get($callable) 97 | : new $callable(); 98 | 99 | if (! is_callable($callable)) { 100 | throw new RuntimeException( 101 | sprintf('Invalid command class specified for "%s"; class must be invokable', $name) 102 | ); 103 | } 104 | $this->commandMap[$name] = $callable; 105 | } 106 | 107 | $return = call_user_func($callable, $route, $console); 108 | return (int) $return; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/DispatcherInterface.php: -------------------------------------------------------------------------------- 1 | console = $console; 35 | 36 | // Set default exception Message 37 | $this->messageTemplate = <<messageTemplate = (string) $messageTemplate; 65 | } 66 | 67 | /** 68 | * Handle an exception 69 | * 70 | * On completion, exits with a non-zero status code. 71 | * 72 | * @param Exception|Throwable $exception 73 | */ 74 | public function __invoke($exception) 75 | { 76 | $this->validateException($exception); 77 | 78 | $message = $this->createMessage($exception); 79 | 80 | $this->console->writeLine('Application exception: ', Color::RED); 81 | $this->console->write($message); 82 | $this->console->writeLine(''); 83 | 84 | // Exceptions always indicate an error status; however, most have a 85 | // code of zero; set it to 1 in such cases. 86 | $exitCode = $exception->getCode(); 87 | $exitCode = $exitCode ?: 1; 88 | exit($exitCode); 89 | } 90 | 91 | /** 92 | * Create the message to emit based on the provided exception and current message template 93 | * 94 | * @param Exception|Throwable $exception 95 | * @return string 96 | */ 97 | public function createMessage($exception) 98 | { 99 | $this->validateException($exception); 100 | 101 | $previous = ''; 102 | $previousException = $exception->getPrevious(); 103 | while ($previousException) { 104 | $previous .= $this->fillTemplate($previousException, $previous); 105 | $previousException = $previousException->getPrevious(); 106 | } 107 | 108 | return $this->fillTemplate($exception, $previous); 109 | } 110 | 111 | /** 112 | * Fill the message template with details of the given exception 113 | * 114 | * @param Exception|Throwable $exception 115 | * @param false|string $previous If provided, adds the ":previous" template and this value 116 | * @return string 117 | */ 118 | protected function fillTemplate($exception, $previous = false) 119 | { 120 | $templates = [ 121 | ':className', 122 | ':message', 123 | ':code', 124 | ':file', 125 | ':line', 126 | ':stack', 127 | ]; 128 | 129 | $replacements = [ 130 | get_class($exception), 131 | $exception->getMessage(), 132 | $exception->getCode(), 133 | $exception->getFile(), 134 | $exception->getLine(), 135 | $exception->getTraceAsString(), 136 | ]; 137 | 138 | if ($previous) { 139 | array_push($templates, ':previous'); 140 | array_push($replacements, $previous); 141 | } 142 | 143 | $message = str_replace($templates, $replacements, $this->messageTemplate); 144 | 145 | // Strip unfilled ":previous" templates, if present 146 | if (! $previous) { 147 | return str_replace(':previous', '', $message); 148 | } 149 | 150 | return $message; 151 | } 152 | 153 | /** 154 | * Validate that an exception was received 155 | * 156 | * @param mixed $exception 157 | * @return void 158 | * @throws InvalidArgumentException if a non-Throwable, non-Exception was provided. 159 | */ 160 | private function validateException($exception) 161 | { 162 | if (! ($exception instanceof Throwable || $exception instanceof Exception)) { 163 | throw new InvalidArgumentException(sprintf( 164 | 'Expected an Exception or Throwable; received %s', 165 | (is_object($exception) ? get_class($exception) : gettype($exception)) 166 | )); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Filter/Explode.php: -------------------------------------------------------------------------------- 1 | defaultDelimiter; 39 | } 40 | } 41 | 42 | if (null === $delimiter 43 | || ! is_string($delimiter) 44 | ) { 45 | $delimiter = $this->defaultDelimiter; 46 | } 47 | 48 | $this->delimiter = $delimiter; 49 | } 50 | 51 | /** 52 | * @see \Zend\Filter\FilterInterface::filter() 53 | * @param mixed $value 54 | * @return array|mixed $value Returns an array if a string $value was provided 55 | */ 56 | public function filter($value) 57 | { 58 | if (! is_string($value)) { 59 | return $value; 60 | } 61 | 62 | return explode($this->delimiter, $value); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Filter/Json.php: -------------------------------------------------------------------------------- 1 | reportJsonDeserializationError(); 29 | return $value; 30 | } 31 | 32 | return $data; 33 | } 34 | 35 | /** 36 | * Raise a user warning if a JSON deserialization error occurred 37 | */ 38 | protected function reportJsonDeserializationError() 39 | { 40 | $error = json_last_error(); 41 | switch ($error) { 42 | case JSON_ERROR_NONE: 43 | return; 44 | default: 45 | trigger_error( 46 | sprintf('Error deserializing JSON (%d)', $error), 47 | E_USER_WARNING 48 | ); 49 | break; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Filter/QueryString.php: -------------------------------------------------------------------------------- 1 | jsonFilter = new Json(); 24 | } 25 | 26 | /** 27 | * @see \Zend\Filter\FilterInterface::filter() 28 | * @param mixed $value 29 | * @return mixed Original value, if not a string, or an array of key/value pairs 30 | */ 31 | public function filter($value) 32 | { 33 | if (! is_string($value)) { 34 | return $value; 35 | } 36 | 37 | // check if the value provided resembles a query string 38 | $pairs = explode('&', $value); 39 | foreach ($pairs as $pair) { 40 | list($k, $v) = explode('=', $pair); 41 | 42 | // Check if we have a normal key-value pair 43 | if (! preg_match("/^(.*?)((\[(.*?)\])+)$/m", $k, $m)) { 44 | $data[$k] = $v; 45 | continue; 46 | } 47 | 48 | // Array values 49 | $parts = explode('][', rtrim(ltrim($m[2], '['), ']')); 50 | $json = '{"' 51 | . implode('":{"', $parts) 52 | . '": ' 53 | . json_encode($v) 54 | . str_pad('', count($parts), '}'); 55 | 56 | if (isset($data[$m[1]])) { 57 | $data[$m[1]] = ArrayUtils::merge($data[$m[1]], $this->jsonFilter->filter($json)); 58 | continue; 59 | } 60 | 61 | $data[$m[1]] = $this->jsonFilter->filter($json); 62 | } 63 | 64 | return $data; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/HelpCommand.php: -------------------------------------------------------------------------------- 1 | application = $application; 24 | } 25 | 26 | /** 27 | * @param Route $route 28 | * @param Console $console 29 | * @return int 30 | */ 31 | public function __invoke(Route $route, Console $console) 32 | { 33 | $command = $route->getMatchedParam('command', null); 34 | $this->application->showUsageMessage($command); 35 | return 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Route.php: -------------------------------------------------------------------------------- 1 | name = $name; 61 | $this->route = $route; 62 | parent::__construct($route, $constraints, $defaults, $aliases, $filters, $validators); 63 | } 64 | 65 | /** 66 | * Override match() 67 | * 68 | * If matched, set the matches in the route 69 | * 70 | * @param array $params 71 | * @return array|null 72 | */ 73 | public function match($params) 74 | { 75 | $matches = parent::match($params); 76 | 77 | if (is_array($matches)) { 78 | $this->matches = $matches; 79 | } 80 | 81 | return $matches; 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | public function getName() 88 | { 89 | return $this->name; 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | public function getRoute() 96 | { 97 | return $this->route; 98 | } 99 | 100 | /** 101 | * @param string $description 102 | * @return self 103 | */ 104 | public function setDescription($description) 105 | { 106 | $this->description = $description; 107 | return $this; 108 | } 109 | 110 | /** 111 | * @return string 112 | */ 113 | public function getDescription() 114 | { 115 | return $this->description; 116 | } 117 | 118 | /** 119 | * @param array $descriptions 120 | * @return self 121 | */ 122 | public function setOptionsDescription(array $descriptions) 123 | { 124 | $this->optionsDescription = $descriptions; 125 | return $this; 126 | } 127 | 128 | /** 129 | * @return array 130 | */ 131 | public function getOptionsDescription() 132 | { 133 | return $this->optionsDescription; 134 | } 135 | 136 | /** 137 | * @param string $description 138 | * @return self 139 | */ 140 | public function setShortDescription($description) 141 | { 142 | $this->shortDescription = $description; 143 | return $this; 144 | } 145 | 146 | /** 147 | * @return string 148 | */ 149 | public function getShortDescription() 150 | { 151 | return $this->shortDescription; 152 | } 153 | 154 | /** 155 | * @return bool 156 | */ 157 | public function isMatched() 158 | { 159 | return is_array($this->matches); 160 | } 161 | 162 | /** 163 | * @return null|array 164 | */ 165 | public function getMatches() 166 | { 167 | return $this->matches; 168 | } 169 | 170 | /** 171 | * Was the parameter matched? 172 | * 173 | * @param string $param 174 | * @return bool 175 | */ 176 | public function matchedParam($param) 177 | { 178 | if (! is_array($this->matches)) { 179 | return false; 180 | } 181 | return array_key_exists($param, $this->matches); 182 | } 183 | 184 | /** 185 | * Retrieve a matched parameter 186 | * 187 | * @param string $param 188 | * @param mixed $default 189 | * @return mixed 190 | */ 191 | public function getMatchedParam($param, $default = null) 192 | { 193 | if (! $this->matchedParam($param)) { 194 | return $default; 195 | } 196 | return $this->matches[$param]; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/RouteCollection.php: -------------------------------------------------------------------------------- 1 | routes); 35 | } 36 | 37 | /** 38 | * Implement IteratorAggregate 39 | * 40 | * @return SplStack 41 | */ 42 | public function getIterator() 43 | { 44 | return new ArrayIterator($this->routes); 45 | } 46 | 47 | /** 48 | * @param Route $route 49 | * @return self 50 | */ 51 | public function addRoute(Route $route) 52 | { 53 | $name = $route->getName(); 54 | if (isset($this->routes[$name])) { 55 | throw new DomainException(sprintf( 56 | 'Failed adding route by name %s; a route by that name has already been registered', 57 | $name 58 | )); 59 | } 60 | 61 | $this->routes[$name] = $route; 62 | ksort($this->routes, defined('SORT_NATURAL') ? constant('SORT_NATURAL') : SORT_STRING); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @param string $route 69 | * @param array $constraints 70 | * @param array $defaults 71 | * @param array $aliases 72 | * @param null|array $filters 73 | * @param null|array $validators 74 | */ 75 | public function addRouteSpec(array $spec) 76 | { 77 | if (! isset($spec['name'])) { 78 | throw new InvalidArgumentException('Route specification is missing a route name'); 79 | } 80 | $name = $spec['name']; 81 | 82 | if (! isset($spec['route'])) { 83 | $spec['route'] = $spec['name']; 84 | } 85 | $routeString = $this->prependRouteWithCommand( 86 | $name, 87 | $spec['route'], 88 | array_key_exists('prepend_command_to_route', $spec) ? $spec['prepend_command_to_route'] : true 89 | ); 90 | 91 | $constraints = (isset($spec['constraints']) && is_array($spec['constraints'])) 92 | ? $spec['constraints'] 93 | : []; 94 | $defaults = (isset($spec['defaults']) && is_array($spec['defaults'])) 95 | ? $spec['defaults'] 96 | : []; 97 | $aliases = (isset($spec['aliases']) && is_array($spec['aliases'])) 98 | ? $spec['aliases'] 99 | : []; 100 | $filters = (isset($spec['filters']) && is_array($spec['filters'])) 101 | ? $spec['filters'] 102 | : null; 103 | $validators = (isset($spec['validators']) && is_array($spec['validators'])) 104 | ? $spec['validators'] 105 | : null; 106 | $description = (isset($spec['description']) && is_string($spec['description'])) 107 | ? $spec['description'] 108 | : ''; 109 | $shortDescription = (isset($spec['short_description']) && is_string($spec['short_description'])) 110 | ? $spec['short_description'] 111 | : ''; 112 | $optionsDescription = (isset($spec['options_descriptions']) && is_array($spec['options_descriptions'])) 113 | ? $spec['options_descriptions'] 114 | : []; 115 | 116 | $filters = $this->prepareFilters($filters); 117 | $validators = $this->prepareValidators($validators); 118 | 119 | $route = new Route($name, $routeString, $constraints, $defaults, $aliases, $filters, $validators); 120 | $route->setDescription($description); 121 | $route->setShortDescription($shortDescription); 122 | $route->setOptionsDescription($optionsDescription); 123 | 124 | $this->addRoute($route); 125 | return $this; 126 | } 127 | 128 | /** 129 | * @param String $name 130 | * @return self 131 | * @throws DomainException if the provided route does not exist 132 | */ 133 | public function removeRoute($name) 134 | { 135 | if (! isset($this->routes[$name])) { 136 | throw new DomainException(sprintf( 137 | 'Failed removing route by name %s; the route by that name has not been registered', 138 | $name 139 | )); 140 | } 141 | 142 | unset($this->routes[$name]); 143 | return $this; 144 | } 145 | 146 | /** 147 | * Does the named route exist? 148 | * 149 | * @param string $name 150 | * @return bool 151 | */ 152 | public function hasRoute($name) 153 | { 154 | return array_key_exists($name, $this->routes); 155 | } 156 | 157 | /** 158 | * Retrieve a named route 159 | * 160 | * @param string $name 161 | * @return null|Route 162 | */ 163 | public function getRoute($name) 164 | { 165 | if (! $this->hasRoute($name)) { 166 | return null; 167 | } 168 | return $this->routes[$name]; 169 | } 170 | 171 | /** 172 | * Retrieve all route names 173 | * 174 | * @return array 175 | */ 176 | public function getRouteNames() 177 | { 178 | return array_keys($this->routes); 179 | } 180 | 181 | /** 182 | * Determine if any route matches 183 | * 184 | * @param array|null $params 185 | * @return false|Route 186 | */ 187 | public function match($params) 188 | { 189 | if (! is_array($params) && null !== $params) { 190 | throw new InvalidArgumentException(sprintf( 191 | '%s expects an array of arguments (typically $argv) or a null value', 192 | __METHOD__ 193 | )); 194 | } 195 | 196 | $params = (array) $params; 197 | 198 | foreach ($this as $route) { 199 | $matches = $route->match($params); 200 | if (is_array($matches)) { 201 | return $route; 202 | } 203 | } 204 | 205 | return false; 206 | } 207 | 208 | /** 209 | * Prepare filters 210 | * 211 | * If a filter is a class name, instantiate it. 212 | * 213 | * If a filter is a callback, casts to Callback filter. 214 | * 215 | * If the filter is not valid, raises an exception. 216 | * 217 | * @param null|array $filters 218 | * @return array|null 219 | * @throws DomainException 220 | */ 221 | protected function prepareFilters(array $filters = null) 222 | { 223 | if (null === $filters) { 224 | return null; 225 | } 226 | 227 | foreach ($filters as $name => $filter) { 228 | if (is_string($filter) && class_exists($filter)) { 229 | $filter = new $filter(); 230 | } 231 | 232 | if ($filter instanceof FilterInterface) { 233 | $filters[$name] = $filter; 234 | continue; 235 | } 236 | 237 | if (is_callable($filter)) { 238 | $filters[$name] = new CallbackFilter($filter); 239 | continue; 240 | } 241 | 242 | throw new DomainException(sprintf( 243 | 'Invalid filter provided for "%s"; expected Callable or Zend\Filter\FilterInterface, received "%s"', 244 | $name, 245 | $this->getType($filter) 246 | )); 247 | } 248 | 249 | return $filters; 250 | } 251 | 252 | /** 253 | * Prepare validators 254 | * 255 | * If a validator is a class name, instantiate it. 256 | * 257 | * If a validator is a callback, casts to Callback validator. 258 | * 259 | * If the validator is not valid, raises an exception. 260 | * 261 | * @param array $validators 262 | * @return array|null 263 | * @throws DomainException 264 | */ 265 | protected function prepareValidators(array $validators = null) 266 | { 267 | if (null === $validators) { 268 | return null; 269 | } 270 | 271 | foreach ($validators as $name => $validator) { 272 | if (is_string($validator) && class_exists($validator)) { 273 | $validator = new $validator(); 274 | } 275 | 276 | if ($validator instanceof ValidatorInterface) { 277 | $validators[$name] = $validator; 278 | continue; 279 | } 280 | 281 | if (is_callable($validator)) { 282 | $validators[$name] = new CallbackValidator($validator); 283 | continue; 284 | } 285 | 286 | throw new DomainException(sprintf( 287 | 'Invalid validator provided for "%s"; expected Callable or ' 288 | . 'Zend\Validator\ValidatorInterface, received "%s"', 289 | $name, 290 | $this->getType($validator) 291 | )); 292 | } 293 | 294 | return $validators; 295 | } 296 | 297 | /** 298 | * Get an item's type, for error reporting 299 | * 300 | * @param mixed $subject 301 | * @return string 302 | */ 303 | protected function getType($subject) 304 | { 305 | switch (true) { 306 | case (is_object($subject)): 307 | $type = get_class($subject); 308 | break; 309 | case (is_string($subject)): 310 | $type = $subject; 311 | break; 312 | default: 313 | $type = gettype($subject); 314 | break; 315 | } 316 | return $type; 317 | } 318 | 319 | /** 320 | * Prepend the route with the command 321 | * 322 | * If the route does not start with the command already, and the 323 | * `prepend_command_to_route` flag has not been toggled off, then prepend 324 | * the command to the route and return it. 325 | * 326 | * @param string $command 327 | * @param string $route 328 | * @param bool $prependFlag 329 | * @return string 330 | */ 331 | protected function prependRouteWithCommand($command, $route, $prependFlag) 332 | { 333 | if (true !== $prependFlag) { 334 | return $route; 335 | } 336 | 337 | if (preg_match('/^(?:' . preg_quote($command) . ')(?:\s|$)/', $route)) { 338 | return $route; 339 | } 340 | 341 | return sprintf('%s %s', $command, $route); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /views/autocomplete.phtml: -------------------------------------------------------------------------------- 1 | 5 | #!/bin/sh 6 | # 7 | # == System wide installation == 8 | # 9 | # --- bash --- 10 | # 11 | # To install system-wide autocompletion for bash, copy the complete contents of 12 | # this file to /etc/bash_completion.d/.sh . 13 | # 14 | # The easiest way will be to run 15 | # 16 | # $ sudo autocomplete > /etc/bash_completion.d/.sh 17 | # $ source /etc/bash_completion.d/.sh 18 | # 19 | # --- zsh --- 20 | # 21 | # To install system-wide autocompletion for zsh, copy the complete contents of 22 | # this file to /etc/zsh/.sh, and run: 23 | # 24 | # $ sudo echo "source /etc/zsh/.sh" >> /etc/zsh/zprofile 25 | # 26 | # == User installation only == 27 | # 28 | # --- bash --- 29 | # 30 | # $ autocomplete >> ~/.bashrc 31 | # $ source ~/.bashrc 32 | # 33 | # --- zsh --- 34 | # 35 | # $ autocomplete >> ~/.zshrc 36 | # $ source ~/.zshrc 37 | 38 | if [[ -n ${ZSH_VERSION-} ]]; then 39 | autoload -U +X bashcompinit && bashcompinit 40 | fi 41 | 42 | _complete_() { 43 | local cur 44 | 45 | COMPREPLY=() 46 | cur="${COMP_WORDS[COMP_CWORD]}" 47 | 48 | # Assume first word is the actual app/console command 49 | console="${COMP_WORDS[0]}" 50 | 51 | if [[ ${COMP_CWORD} == 1 ]] ; then 52 | # No command found, return the list of available commands 53 | cmds=` ${console} help | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g" | grep "^ [a-z\-]\{1,\}[[:space:]]\{1,\}" | awk '{print $1}'` 54 | else 55 | # Commands found, parse options 56 | RESULT=`${console} ${COMP_WORDS[1]} --help` 57 | cmds=` echo "$RESULT" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g" | grep '^ -' | awk '{ print $1 }'` 58 | cmds=`echo -e "$cmds\n--help"` # --help is a non-existing option, BUT it will give back the help for a command which is what we want 59 | fi 60 | 61 | COMPREPLY=( $(compgen -W "${cmds}" -- ${cur}) ) 62 | return 0 63 | } 64 | 65 | export COMP_WORDBREAKS="\ \"\\'><=;|&(" 66 | complete -F _complete_ 67 | --------------------------------------------------------------------------------