├── .gitattributes
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── example
└── git.php
├── phpbench.json
├── phpstan.neon
├── phpunit.xml
└── src
├── Application
├── Application.php
├── CommandMiddleware.php
├── Context.php
├── Exception
│ ├── CommandHandlerNotFound.php
│ └── MiddlewareExhausted.php
├── ExceptionContext.php
├── ExceptionHandler.php
├── Handler.php
├── Middleware.php
└── Middleware
│ ├── ClosureMiddleware.php
│ └── HelpMiddleware.php
├── ApplicationBuilder.php
├── Attribute
├── App.php
├── Arg.php
├── Cmd.php
└── Opt.php
├── Error
├── ParseError.php
└── ParseErrorWithContext.php
├── Metadata
├── AbstractCommandDefinition.php
├── ApplicationDefinition.php
├── ArgumentDefinition.php
├── ArgumentDefinitions.php
├── CommandDefinition.php
├── CommandDefinitions.php
├── Loader.php
├── OptionDefinition.php
└── OptionDefinitions.php
├── Parser
└── Parser.php
├── Printer
└── AsciiPrinter.php
└── Type
├── BooleanType.php
├── FloatType.php
├── IntegerType.php
├── ListType.php
├── MixedType.php
├── StringType.php
├── Type.php
├── TypeFactory.php
└── UnionType.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.php diff=php
2 | /tests export-ignore
3 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: "CI"
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - 'main'
8 |
9 | env:
10 | fail-fast: true
11 | TZ: "Europe/Paris"
12 |
13 | jobs:
14 | phpunit:
15 | name: "PHPUnit (${{ matrix.php-version }})"
16 |
17 | runs-on: "ubuntu-latest"
18 |
19 | strategy:
20 | matrix:
21 | php-version:
22 | - '8.1'
23 | - '8.2'
24 |
25 | steps:
26 | -
27 | name: "Checkout code"
28 | uses: "actions/checkout@v2"
29 | -
30 | name: "Install PHP"
31 | uses: "shivammathur/setup-php@v2"
32 | with:
33 | coverage: "none"
34 | extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}"
35 | php-version: "${{ matrix.php-version }}"
36 | tools: composer:v2
37 |
38 | -
39 | name: "Composer install"
40 | uses: "ramsey/composer-install@v1"
41 | with:
42 | composer-options: "--no-scripts"
43 | -
44 | name: "Run PHPUnit"
45 | run: "php -dzend.assertions=1 vendor/bin/phpunit"
46 | phpstan:
47 | name: "PHPStan (${{ matrix.php-version }})"
48 |
49 | runs-on: "ubuntu-latest"
50 |
51 | strategy:
52 | matrix:
53 | php-version:
54 | - '8.1'
55 |
56 | steps:
57 | -
58 | name: "Checkout code"
59 | uses: "actions/checkout@v2"
60 | -
61 | name: "Install PHP"
62 | uses: "shivammathur/setup-php@v2"
63 | with:
64 | coverage: "none"
65 | extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}"
66 | php-version: "${{ matrix.php-version }}"
67 | tools: composer:v2
68 |
69 | -
70 | name: "Composer install"
71 | uses: "ramsey/composer-install@v1"
72 | with:
73 | composer-options: "--no-scripts"
74 | -
75 | name: "Run PHPStan"
76 | run: "vendor/bin/phpstan analyse"
77 | php-cs-fixer:
78 | name: "PHP-CS-Fixer (${{ matrix.php-version }})"
79 |
80 | runs-on: "ubuntu-latest"
81 |
82 | strategy:
83 | matrix:
84 | php-version:
85 | - '8.1'
86 | steps:
87 | -
88 | name: "Checkout code"
89 | uses: "actions/checkout@v2"
90 | -
91 | name: "Install PHP"
92 | uses: "shivammathur/setup-php@v2"
93 | with:
94 | coverage: "none"
95 | extensions: "${{ env.REQUIRED_PHP_EXTENSIONS }}"
96 | php-version: "${{ matrix.php-version }}"
97 | tools: composer:v2
98 |
99 | -
100 | name: "Composer install"
101 | uses: "ramsey/composer-install@v1"
102 | with:
103 | composer-options: "--no-scripts"
104 | -
105 | name: "Run PHP-CS_Fixer"
106 | run: "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --dry-run --diff"
107 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /composer.lock
3 | /.phpunit.cache
4 | /.php-cs-fixer.cache
5 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in('src')
8 | ->in('tests')
9 | ;
10 |
11 | return (new Config())
12 | ->setRiskyAllowed(true)
13 | ->setRules([
14 | '@PSR2' => true,
15 | 'final_class' => true,
16 | 'no_unused_imports' => true,
17 | 'phpdoc_to_property_type' => true,
18 | 'no_superfluous_phpdoc_tags' => [
19 | 'remove_inheritdoc' => true,
20 | 'allow_mixed' => true,
21 | ],
22 | 'class_attributes_separation' => [
23 | 'elements' => [
24 | 'const' => 'only_if_meta',
25 | 'property' => 'one',
26 | 'trait_import' => 'only_if_meta',
27 | ],
28 | ],
29 | 'ordered_class_elements' => true,
30 | 'no_empty_phpdoc' => true,
31 | 'phpdoc_trim' => true,
32 | 'array_syntax' => ['syntax' => 'short'],
33 | 'list_syntax' => ['syntax' => 'short'],
34 | 'void_return' => true,
35 | 'ordered_class_elements' => true,
36 | 'single_quote' => true,
37 | 'heredoc_indentation' => true,
38 | 'global_namespace_import' => true,
39 | 'no_trailing_whitespace' => true,
40 | 'no_whitespace_in_blank_line' => true,
41 | ])
42 | ->setFinder($finder)
43 | ;
44 |
45 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | CHANGELOG
2 | =========
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Daniel Leech
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is furnished
10 | to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | CLI Parser and Handler
2 | ======================
3 |
4 | > warning: don't usr this, it's a failed experiment!
5 |
6 | Type safe CLI parser and command handler for PHP inspired by [Kong](https://github.com/alecthomas/kong).
7 |
8 | Features
9 | --------
10 |
11 | - Casts arguments and options into objects.
12 | - Command bus and middleware architecture.
13 | - Parsing
14 | - Command nesting
15 | - Type support for `string`, `int`, `float`, `boolean` and `lists`.
16 | - Repeated arguments
17 | - Optional arguments
18 | - Long and short options
19 | - Boolean options (flags)
20 | - List options
21 |
22 | Usage Example
23 | -------------
24 |
25 | ```php
26 | init = new InitCmd();
77 | $cli->clone = new CloneCmd();
78 |
79 | $application = ApplicationBuilder::fromSpecification($cli)
80 | ->prependMiddleware(new HelpMiddleware(new AsciiPrinter()))
81 | ->addHandler(InitCmd::class, function (Context $ctx) {
82 | echo 'Initializing' . "\n";
83 | return 0;
84 | })
85 | ->addHandler(CloneCmd::class, function (Context $ctx) {
86 | echo sprintf('Git cloning %s...', $ctx->command()->repo) . "\n";
87 | if ($ctx->command()->recurseSubModules) {
88 | echo '... and recursing submodules' . "\n";
89 | }
90 | return 0;
91 | })
92 | ->build();
93 |
94 | exit($application->run($argv));
95 | ```
96 |
97 | Contribution
98 | ------------
99 |
100 | Contribute!
101 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "php-tui/cli-parser",
3 | "description": "Type-safe CLI parser",
4 | "type": "library",
5 | "license": "MIT",
6 | "autoload": {
7 | "psr-4": {
8 | "PhpTui\\CliParser\\": "src/"
9 | }
10 | },
11 | "autoload-dev": {
12 | "psr-4": {
13 | "PhpTui\\CliParser\\Tests\\": "tests/"
14 | }
15 | },
16 | "authors": [
17 | {
18 | "name": "Daniel Leech"
19 | }
20 | ],
21 | "require": {
22 | "php": "^8.1"
23 | },
24 | "require-dev": {
25 | "friendsofphp/php-cs-fixer": "^3.34",
26 | "phpstan/phpstan": "^1.10",
27 | "phpunit/phpunit": "^10.4",
28 | "symfony/var-dumper": "^6.3"
29 | },
30 | "scripts": {
31 | "phpstan": "./vendor/bin/phpstan --memory-limit=1G",
32 | "php-cs-fixer": "./vendor/bin/php-cs-fixer fix",
33 | "phpunit": "./vendor/bin/phpunit",
34 | "integrate": [
35 | "@php-cs-fixer",
36 | "@phpstan",
37 | "@phpunit"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/example/git.php:
--------------------------------------------------------------------------------
1 | init = new InitCmd();
54 | $cli->clone = new CloneCmd();
55 |
56 | $application = ApplicationBuilder::fromSpecification($cli)
57 | ->prependMiddleware(new HelpMiddleware(new AsciiPrinter()))
58 | ->addHandler(GitCmd::class, function (Context $ctx) {
59 | return 0;
60 | })
61 | ->addHandler(InitCmd::class, function (Context $ctx) {
62 | dump($ctx->application());
63 | return 0;
64 | })
65 | ->addHandler(CloneCmd::class, function (
66 | Context $ctx
67 | ) {
68 | println(sprintf('Git cloning %s...', $ctx->command()->repo));
69 | if ($ctx->command()->recurseSubModules) {
70 | println('... and recursing submodules');
71 | }
72 | return 0;
73 | })
74 | ->build();
75 |
76 | exit($application->run($argv));
77 |
78 | function println(string $message): void
79 | {
80 | echo $message ."\n";
81 | }
82 |
--------------------------------------------------------------------------------
/phpbench.json:
--------------------------------------------------------------------------------
1 | {
2 | "runner.path": "tests/Benchmark",
3 | "$schema": "vendor/phpbench/phpbench/phpbench.schema.json",
4 | "runner.bootstrap": "vendor/autoload.php"
5 | }
6 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: max
3 | paths:
4 | - src
5 | - tests
6 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 | tests
19 |
20 |
21 |
22 |
23 |
24 | src
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Application/Application.php:
--------------------------------------------------------------------------------
1 | $argv
22 | */
23 | public function run(array $argv): int
24 | {
25 | $applicationDefinition = $this->loader->load($this->cli);
26 |
27 | array_shift($argv);
28 |
29 | try {
30 | [$commandDefinition, $command] = $this->parser->parse(
31 | $applicationDefinition,
32 | $this->cli,
33 | $argv
34 | );
35 | } catch (Throwable $exception) {
36 | return $this->exceptionHandler->handle(
37 | new ExceptionContext($applicationDefinition, $exception)
38 | );
39 | }
40 |
41 | return $this->handler->handle(new Context(
42 | $applicationDefinition,
43 | $commandDefinition,
44 | $this->cli,
45 | $command
46 | ));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Application/CommandMiddleware.php:
--------------------------------------------------------------------------------
1 | ):int> $handlers
15 | */
16 | public function __construct(private array $handlers)
17 | {
18 | }
19 | /**
20 | * @param Context $context
21 | */
22 | public function handle(Handler $handler, Context $context): int
23 | {
24 | if (!isset($this->handlers[$context->command()::class])) {
25 | throw new CommandHandlerNotFound($context->command()::class);
26 | }
27 |
28 | return ($this->handlers[$context->command()::class])($context);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Application/Context.php:
--------------------------------------------------------------------------------
1 | applicationDefinition;
27 | }
28 |
29 | public function commandDefinition(): AbstractCommandDefinition
30 | {
31 | return $this->commandDefinition;
32 | }
33 |
34 | /**
35 | * @return TApplication
36 | */
37 | public function application(): object
38 | {
39 | return $this->application;
40 | }
41 |
42 | /**
43 | * @return TCommand
44 | */
45 | public function command(): object
46 | {
47 | return $this->command;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Application/Exception/CommandHandlerNotFound.php:
--------------------------------------------------------------------------------
1 | printer->print($exceptionContext);
16 | return 127;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Application/Handler.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | private array $middlewares;
13 |
14 | public function __construct(Middleware ...$middlewares)
15 | {
16 | $this->middlewares = array_values($middlewares);
17 | }
18 | /**
19 | * @param Context