{"id":13792899,"url":"https://github.com/clue/commander","last_synced_at":"2025-04-06T04:12:04.713Z","repository":{"id":48993359,"uuid":"70326355","full_name":"clue/commander","owner":"clue","description":"Finally a sane way to register available commands and arguments and match your command line in PHP","archived":false,"fork":false,"pushed_at":"2021-07-01T17:39:46.000Z","size":133,"stargazers_count":169,"open_issues_count":3,"forks_count":8,"subscribers_count":12,"default_branch":"master","last_synced_at":"2024-07-09T08:42:33.771Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/clue.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"clue","custom":"https://clue.engineering/support"}},"created_at":"2016-10-08T11:21:39.000Z","updated_at":"2024-07-09T08:42:33.772Z","dependencies_parsed_at":"2022-09-05T13:11:56.582Z","dependency_job_id":null,"html_url":"https://github.com/clue/commander","commit_stats":null,"previous_names":["clue/php-commander"],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clue%2Fcommander","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clue%2Fcommander/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clue%2Fcommander/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clue%2Fcommander/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clue","download_url":"https://codeload.github.com/clue/commander/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247430872,"owners_count":20937874,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T22:01:17.627Z","updated_at":"2025-04-06T04:12:04.696Z","avatar_url":"https://github.com/clue.png","language":"PHP","funding_links":["https://github.com/sponsors/clue","https://clue.engineering/support"],"categories":["类库"],"sub_categories":["CLI"],"readme":"# clue/commander\n\n[![CI status](https://github.com/clue/commander/workflows/CI/badge.svg)](https://github.com/clue/commander/actions)\n[![installs on Packagist](https://img.shields.io/packagist/dt/clue/commander?color=blue\u0026label=installs%20on%20Packagist)](https://packagist.org/packages/clue/commander)\n\nFinally a sane way to register available commands and arguments and match your command line in PHP.\n\nYou want to build a command line interface (CLI) tool in PHP which accepts\nadditional arguments and you now want to route these to individual functions?\nThen this library is for you!\n\nThis is also useful for interactive CLI tools or anywhere where you can break up\na command line string into an array of command line arguments and you now want\nto execute individual functions depending on the arguments given.\n\n**Table of contents**\n\n* [Support us](#support-us)\n* [Quickstart example](#quickstart-example)\n* [Usage](#usage)\n    * [Router](#router)\n        * [add()](#add)\n        * [remove()](#remove)\n        * [getRoutes()](#getroutes)\n        * [execArgv()](#execargv)\n        * [handleArgv()](#handleargv)\n        * [handleArgs()](#handleargs)\n    * [Route](#route)\n    * [NoRouteFoundException](#noroutefoundexception)\n* [Install](#install)\n* [Tests](#tests)\n* [License](#license)\n* [More](#more)\n\n## Support us\n\nWe invest a lot of time developing, maintaining and updating our awesome\nopen-source projects. You can help us sustain this high-quality of our work by\n[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get\nnumerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)\nfor details.\n\nLet's take these projects to the next level together! 🚀\n\n### Quickstart example\n\nThe following example code demonstrates how this library can be used to build\na very simple command line interface (CLI) tool that accepts command line\narguments passed to this program:\n\n```php\n$router = new Clue\\Commander\\Router();\n$router-\u003eadd('exit [\u003ccode:uint\u003e]', function (array $args) {\n    exit(isset($args['code']) ? $args['code'] : 0);\n});\n$router-\u003eadd('sleep \u003cseconds:uint\u003e', function (array $args) {\n    sleep($args['seconds']);\n});\n$router-\u003eadd('echo \u003cwords\u003e...', function (array $args) {\n    echo join(' ', $args['words']) . PHP_EOL;\n});\n$router-\u003eadd('[--help | -h]', function () use ($router) {\n    echo 'Usage:' . PHP_EOL;\n    foreach ($router-\u003egetRoutes() as $route) {\n        echo '  ' .$route . PHP_EOL;\n    }\n});\n\n$router-\u003eexecArgv();\n```\n\nSee also the [examples](examples).\n\n## Usage\n\n### Router\n\nThe `Router` is the main class in this package.\n\nIt is responsible for registering new Routes, matching the given args against\nthese routes and then executing the registered route callback.\n\n```php\n$router = new Router();\n```\n\n\u003e Advanced usage: The `Router` accepts an optional [`Tokenizer`](#tokenizer)\n  instance as the first parameter to the constructor.\n\n#### add()\n\nThe `add(string $route, callable $handler): Route` method can be used to\nregister a new [`Route`](#route) with this Router.\n\nIt accepts a route expression to match and a route callback that will be\nexecuted when this route expression matches.\n\nThis is very similar to how common PHP (micro-)frameworks offer \"HTTP routers\"\nto route incoming HTTP requests to the corresponding \"controller functions\":\n\n```php\n$route = $router-\u003eadd($path, $fn);\n```\n\nThe route expression uses a custom domain-specific language (DSL) which aims to\nbe so simple that both consumers of this library\n(i.e. developers) and users of your resulting tools should be able to understand\nthem.\n\nNote that this is a left-associative grammar (LAG) and all tokens are greedy.\nThis means that the tokens will be processed from left to right and each token\nwill try to match as many of the input arguments as possible.\nThis implies that certain route expressions make little sense, such as having\nan optional argument after an argument with ellipses.\nFor more details, see below.\n\nYou can use an empty string like this to match when no arguments have been given:\n\n```php\n$router-\u003eadd('', function() {\n    echo 'No arguments given. Need help?' . PHP_EOL;\n});\n// matches: (empty string)\n// does not match: hello (too many arguments)\n```\n\nYou can use any number of static keywords like this:\n\n```php\n$router-\u003eadd('user list', function () {\n    echo 'Here are all our users…' . PHP_EOL;\n});\n// matches: user list\n// does not match: user (missing required keyword)\n// does not match: user list hello (too many arguments)\n```\n\nYou can use alternative blocks to support any of the static keywords like this:\n\n```php\n$router-\u003eadd('user (list | listing | ls)', function () {\n    echo 'Here are all our users…' . PHP_EOL;\n});\n// matches: user list\n// matches: user listing\n// matches: user ls\n// does not match: user (missing required keyword)\n// does not match: user list hello (too many arguments)\n```\n\nNote that alternative blocks can be added to pretty much any token in your route\nexpression.\nNote that alternative blocks do not require parentheses and the alternative mark\n(`|`) always works at the current block level, which may not always be obvious.\nUnless you add some parentheses, `a b | c d` will be be interpreted as\n`(a b) | (c d)` by default.\nParentheses can be used to interpret this as `a (b | c) d` instead.\nIn particular, you can also combine alternative blocks with optional blocks\n(see below) in order to optionally accept only one of the alternatives, but not\nmultiple.\n\nYou can use any number of placeholders to mark required arguments like this:\n\n```php\n$router-\u003eadd('user add \u003cname\u003e', function (array $args) {\n    assert(is_string($args['name']));\n    var_dump($args['name']);\n});\n// matches: user add clue\n// does not match: user add (missing required argument)\n// does not match: user add hello world (too many arguments)\n// does not match: user add --test (argument looks like an option)\n\n// matches: user add -- clue     (value: clue)\n// matches: user add -- --test   (value: --test)\n// matches: user add -- -nobody- (value: -nobody-)\n// matches: user add -- --       (value: --)\n```\n\nNote that arguments that start with a dash (`-`) are not simply accepted in the\nuser input, because they may be confused with (optional) options (see below).\nIf users wish to process arguments that start with a dash (`-`), they either\nhave to use filters (see below) or may use a double dash separator (`--`),\nas everything after this separator will be processed as-is.\nSee also the last examples above that demonstrate this behavior.\n\nYou can use one the predefined filters to limit what values are accepted like this:\n\n```php\n$router-\u003eadd('user ban \u003cid:int\u003e \u003cforce:bool\u003e', function (array $args) {\n    assert(is_int($args['id']));\n    assert(is_bool($args['force']));\n});\n// matches: user ban 10 true\n// matches: user ban 10 0\n// matches: user ban -10 yes\n// matches: user ban -- -10 no\n// does not match: user ban 10 (missing required argument)\n// does not match: user ban hello true (invalid value does not validate)\n```\n\nNote that the filters also return the value casted to the correct data type.\nAlso note how using the double dash separator (`--`) is optional when matching\na filtered value.\nThe following predefined filters are currently available:\n\n* `int` accepts any positive or negative integer value, such as `10` or `-4`\n* `uint` accepts any positive (unsigned) integer value, such `10` or `0`\n* `float` accepts any positive or negative float value, such as `1.5` or `-2.3`\n* `ufloat` accepts any positive (unsigned) float value, such as `1.5` or `0`\n* `bool` accepts any boolean value, such as `yes/true/1` or `no/false/0`\n\n\u003e If you want to add a custom filter function, see also [`Tokenizer`](#tokenizer)\n  for advanced usage below.\n\nYou can mark arguments as optional by enclosing them in square brackets like this:\n\n```php\n$router-\u003eadd('user search [\u003cquery\u003e]', function (array $args) {\n    assert(!isset($args['query']) || is_string($args['query']));\n    var_dump(isset($args['query']);\n});\n// matches: user search\n// matches: user search clue\n// does not match: user search hello world (too many arguments)\n```\n\nNote that square brackets can be added to pretty much any token in your route\nexpression, however they are most commonly used for arguments as above or for\noptional options as below.\nOptional tokens can appear anywhere in the route expression, but keep in mind\nthat the tokens will be matched from left to right, so if the optional token\nmatches, then the remainder will be processed by the following tokens.\nAs a rule of thumb, make sure optional tokens are near the end of your route\nexpressions and you won't notice this subtle effect.\nOptional blocks accept alternative groups, so that `[a | b]` is actually\nequivalent to the longer form `[(a | b)]`.\nIn particular, this is often used for alternative options as below.\n\nYou can accept any number of arguments by appending ellipses like this:\n\n```php\n$router-\u003eadd('user delete \u003cnames\u003e...', function (array $args) {\n    assert(is_array($args);\n    assert(count($args) \u003e 0);\n    var_dump($args['names']);\n});\n// matches: user delete clue\n// matches: user delete hello world\n// does not match: user delete (missing required argument)\n```\n\nNote that trailing ellipses can be added to any argument, word or option token\nin your route expression. They are most commonly used for arguments as above.\nThe above requires at least one argument, see the following if you want this\nto be completely optional.\nTechnically, the ellipse tokens can appear anywhere in the route expression, but\nkeep in mind that the tokens will be matched from the left to the right, so if\nthe ellipse matches, it will consume all input arguments and not leave anything\nfor following tokens.\nAs a rule of thumb, make sure ellipse tokens are near the end of your route\nexpression and you won't notice this subtle effect.\n\nYou can accept any number of optional arguments by appending ellipses within square brackets like this:\n\n```php\n$router-\u003eadd('user dump [\u003cnames\u003e...]', function (array $args) {\n    if (isset($args['names'])) {\n        assert(is_array($args);\n        assert(count($args) \u003e 0);\n        var_dump($args['names']);\n    } else {\n        var_dump('no names');\n    }\n});\n// matches: user dump\n// matches: user dump clue\n// matches: user dump hello world\n```\n\nThe above does not require any arguments, it works with zero or more arguments.\n\nYou can add any number of optional short or long options like this:\n\n```php\n$router-\u003eadd('user list [--json] [-f]', function (array $args) {\n    assert(!isset($args['json']) || $args['json'] === false);\n    assert(!isset($args['f']) || $args['f'] === false);\n});\n// matches: user list\n// matches: user list --json\n// matches: user list -f\n// matches: user list -f --json\n// matches: user -f list\n// matches: --json user list\n```\n\nAs seen in the example, options in the `$args` array can either be unset when\nthey have not been passed in the user input or set to `false` when they have\nbeen passed (which is in line with how other parsers such as `getopt()` work).\nNote that options are accepted anywhere in the user input argument, regardless\nof where they have been defined.\nNote that the square brackets are in the route expression are required to mark\nthis optional as optional, you can also omit these square brackets if you really\nwant a required option.\n\nYou can combine short and long options in an alternative block like this:\n\n```php\n$router-\u003eadd('user setup [--help | -h]', function (array $args) {\n    assert(!isset($args['help']) || $args['help'] === false);\n    assert(!isset($args['h']) || $args['h'] === false);\n    assert(!isset($args['help'], $args['h']); \n});\n// matches: user setup\n// matches: user setup --help\n// matches: user setup -h\n// does not match: user setup --help -h (only accept eithers, not both)\n```\n\nAs seen in the example, this optionally accepts either the short or the long\noption anywhere in the user input, but never both at the same time.\n\nYou can optionally accept or require values for short and long options like this:\n\n```php\n$router-\u003eadd('[--sort[=\u003cparam\u003e]] [-i=\u003cstart:int\u003e] user list', function (array $args) {\n    assert(!isset($args['sort']) || $args['sort'] === false || is_string($args['sort']));\n    assert(!isset($args['i']) || is_int($args['i']));\n});\n// matches: user list\n// matches: user list --sort\n// matches: user list --sort=size\n// matches: user list --sort size\n// matches: user list -i=10\n// matches: user list -i 10\n// matches: user list -i10\n// matches: user list -i=-10\n// matches: user list -i -10\n// matches: user list -i-10\n// matches: user -i=10 list\n// matches: --sort -- user list\n// matches: --sort size user list\n// matches: user list --sort -i=10\n// does not match: user list -i (missing option value)\n// does not match: user list -i --sort (missing option value)\n// does not match: user list -i=a (invalid value does not validate)\n// does not match: --sort user list (user will be interpreted as option value)\n// does not match: user list --sort -2 (value looks like an option)\n```\n\nAs seen in the example, option values in the `$args` array will be given as\nstrings or their filtered and casted value if passed in the user input.\nBoth short and long options can accept values with the recommended equation\nsymbol syntax (`-i=10` and `--sort=size`  respectively) in the user input.\nBoth short and long options can also accept values with the common space-separated\nsyntax (`-i 10` and `--sort size` respectively) in the user input.\nShort options can also accept values with the common concatenated syntax\nwith no separator inbetween (`-i10`) in the user input.\nNote that it is highly recommended to always make sure any options that accept\nvalues are near the left side of your route expression.\nThis is needed in order to make sure space-separated values are consumed as\noption values instead of being misinterpreted as keywords or arguments.\n\nYou can limit the values for short and long options to a given preset like this:\n\n```php\n$router-\u003eadd('[--ask=(yes | no)] [-l[=0]] user purge', function (array $args) {\n    assert(!isset($args['ask']) || $args['sort'] === 'yes' || $args['sort'] === 'no');\n    assert(!isset($args['l']) || $args['l'] === '0');\n});\n// matches: user purge\n// matches: user purge --ask=yes\n// matches: user purge --ask=no\n// matches: user purge -l\n// matches: user purge -l=0\n// matches: user purge -l 0\n// matches: user purge -l0\n// matches: user purge -l --ask=no\n// does not match: user purge --ask (missing option value)\n// does not match: user purge --ask=maybe (invalid option value)\n// does not match: user purge -l4 (invalid option value)\n```\n\nAs seen in the example, option values can be restricted to a given preset of\nvalues by using any of the above tokens.\nTechnically, it's valid to use any of the above tokens to restrict the option\nvalues.\nIn practice, this is mostly used for static keyword tokens or alternative groups\nthereof.\nIt's recommended to always use parentheses for optional groups, however they're\nnot strictly required within options with optional values.\nThis also helps making it more obvious `[--ask=(yes | no)]` would accept either\noption value, while the (less useful) expression `[--ask=yes | no]` would\naccept either the option `--ask=yes` or the static keyword `no`.\n\n#### remove()\n\nThe `remove(Route $route): void` method can be used to remove the given\n[`Route`](#route) object from the registered routes.\n\n```php\n$route = $router-\u003eadd('hello \u003cname\u003e', $fn);\n$router-\u003eremove($route);\n```\n\nIt will throw an `UnderflowException` if the given route does not exist.\n\n#### getRoutes()\n\nThe `getRoutes(): Route[]` method can be used to return an array of all\nregistered [`Route`](#route) objects.\n\n```php\necho 'Usage help:' . PHP_EOL;\nforeach ($router-\u003egetRoutes() as $route) {\n    echo $route . PHP_EOL;\n}\n```\n\nThis array will be empty if you have not added any routes yet.\n\n#### execArgv()\n\nThe `execArgv(array $argv = null): void` method can be used to\nexecute by matching the `argv` against all registered routes and then exit.\n\nYou can explicitly pass in your `$argv` or it will automatically use the\nvalues from the `$_SERVER` superglobal. The `argv` is an array that will\nalways start with the calling program as the first element. We simply\nignore this first element and then process the remaining elements\naccording to the registered routes.\n\nThis is a convenience method that will match and execute a route and then\nexit the program without returning.\n\nIf no route could be found or if the route callback throws an Exception,\nit will print out an error message to STDERR and set an appropriate\nnon-zero exit code.\n\nNote that this is for convenience only and only useful for the most\nsimple of all programs. If you need more control, then consider using\nthe underlying [`handleArgv()`](#handleargv) method and handle any error situations\nyourself.\n\n#### handleArgv()\n\nThe `handleArgv(array $argv = null): mixed` method can be used to\nexecute by matching the `argv` against all registered routes and then return.\n\nYou can explicitly pass in your `$argv` or it will automatically use the\nvalues from the `$_SERVER` superglobal. The `argv` is an array that will\nalways start with the calling program as the first element. We simply\nignore this first element and then process the remaining elements\naccording to the registered routes.\n\nUnlike [`execArgv()`](#execargv) this method will try to execute the route callback\nand then return whatever the route callback returned.\n\n```php\n$router-\u003eadd('hello \u003cname\u003e', function (array $args) {\n    return strlen($args[$name]);\n});\n\n$length = $router-\u003ehandleArgv(array('program', 'hello', 'test'));\n\nassert($length === 4);\n```\n\nIf no route could be found, it will throw a [`NoRouteFoundException`](#noroutefoundexception).\n\n```php\n// throws NoRouteFoundException\n$router-\u003ehandleArgv(array('program', 'invalid'));\n```\n\nIf the route callback throws an `Exception`, it will pass through this `Exception`.\n\n```php\n$router-\u003eadd('hello \u003cname\u003e', function (array $args) {\n    if ($args['name'] === 'admin') {\n        throw new InvalidArgumentException();\n    }\n    \n    return strlen($args['name']);\n});\n\n// throws InvalidArgumentException\n$router-\u003ehandleArgv(array('program', 'hello', 'admin'));\n```\n\n#### handleArgs()\n\nThe `handleArgs(array $args): mixed` method can be used to\nexecute by matching the given args against all registered routes and then return.\n\nUnlike [`handleArgv()`](#handleargv) this method will use the complete `$args` array\nto match the registered routes (i.e. it will not ignore the first element).\nThis is particularly useful if you build this array yourself or if you\nuse an interactive command line interface (CLI) and ask your user to\nsupply the arguments.\n\n```php\n$router-\u003eadd('hello \u003cname\u003e', function (array $args) {\n    return strlen($args[$name]);\n});\n\n$length = $router-\u003ehandleArgs(array('hello', 'test'));\n\nassert($length === 4);\n```\n\nThe arguments have to be given as an array of individual elements. If you\nonly have a command line string that you want to split into an array of\nindividual command line arguments, consider using\n[clue/arguments](https://github.com/clue/arguments).\n\n```php\n$line = fgets(STDIN, 2048);\nassert($line === 'hello \"Christian Lück\"');\n\n$args = Clue\\Arguments\\split($line);\nassert($args === array('hello', 'Christian Lück'));\n\n$router-\u003ehandleArgs($args);\n```\n\nIf no route could be found, it will throw a [`NoRouteFoundException`](#noroutefoundexception).\n\n```php\n// throws NoRouteFoundException\n$router-\u003ehandleArgs(array('invalid'));\n```\n\nIf the route callback throws an `Exception`, it will pass through this `Exception`.\n\n```php\n$router-\u003eadd('hello \u003cname\u003e', function (array $args) {\n    if ($args['name'] === 'admin') {\n        throw new InvalidArgumentException();\n    }\n    \n    return strlen($args['name']);\n});\n\n// throws InvalidArgumentException\n$router-\u003ehandleArgs(array('hello', 'admin'));\n```\n\n### Route\n\nThe `Route` represents a single registered route within the [Router](#router).\n\nIt holds the required route tokens to match and the route callback to\nexecute if this route matches.\n\nSee [`Router`](#router).\n\n### NoRouteFoundException\n\nThe `NoRouteFoundException` will be raised by [`handleArgv()`](#handleargv)\nor [`handleArgs()`](#handleargs) if no matching route could be found.\nIt extends PHP's built-in `RuntimeException`.\n\n### Tokenizer\n\nThe `Tokenizer` class is responsible for parsing a route expression into a\nvalid token instance.\nThis class is mostly used internally and not something you have to worry about\nin most cases.\n\nIf you need custom logic for your route expression, you may explicitly pass an\ninstance of your `Tokenizer` to the constructor of the `Router`:\n\n```php\n$tokenizer = new Tokenizer();\n\n$router = new Router($tokenizer);\n```\n\n#### addFilter()\n\nThe `addFilter(string $name, callable $filter): void` method can be used to\nadd a custom filter function.\n\nThe filter name can then be used in argument or option expressions such as\n`add \u003cname:lower\u003e` or `--search=\u003caddress:ip\u003e`.\n\nThe filter function will be invoked with the filter value and MUST return a\nboolean success value if this filter accepts the given value.\nThe filter value will be passed by reference, so it can be updated if the\nfiltering was successful.\n\n```php\n$tokenizer = new Tokenizer();\n$tokenizer-\u003eaddFilter('ip', function ($value) {\n    return filter_var($ip, FILTER_VALIDATE_IP);\n});\n$tokenizer-\u003eaddFilter('lower', function (\u0026$value) {\n    $value = strtolower($value);\n    return true;\n});\n\n$router = new Router($tokenizer);\n$router-\u003eadd('add \u003cname:lower\u003e', function ($args) { });\n$router-\u003eadd('--search=\u003caddress:ip\u003e', function ($args) { });\n```\n\n## Install\n\nThe recommended way to install this library is [through Composer](https://getcomposer.org).\n[New to Composer?](https://getcomposer.org/doc/00-intro.md)\n\nThis project follows [SemVer](https://semver.org/).\nThis will install the latest supported version:\n\n```bash\n$ composer require clue/commander:^1.4\n```\n\nSee also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.\n\nThis project aims to run on any platform and thus does not require any PHP\nextensions and supports running on legacy PHP 5.3 through current PHP 8+ and\nHHVM.\nIt's *highly recommended to use PHP 7+* for this project.\n\n## Tests\n\nTo run the test suite, you first need to clone this repo and then install all\ndependencies [through Composer](https://getcomposer.org):\n\n```bash\n$ composer install\n```\n\nTo run the test suite, go to the project root and run:\n\n```bash\n$ php vendor/bin/phpunit\n```\n\n## License\n\nThis project is released under the permissive [MIT license](LICENSE).\n\n\u003e Did you know that I offer custom development services and issuing invoices for\n  sponsorships of releases and for contributions? Contact me (@clue) for details.\n\n## More\n\n* If you want to build an interactive CLI tool, you may want to look into using\n  [clue/reactphp-stdio](https://github.com/clue/reactphp-stdio) in order to react\n  to commands from STDIN.\n* If you build an interactive CLI tool that reads a command line from STDIN, you\n  may want to use [clue/arguments](https://github.com/clue/arguments) in\n  order to split this string up into its individual arguments.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclue%2Fcommander","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclue%2Fcommander","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclue%2Fcommander/lists"}