├── .github
├── changelog.yml
├── dependabot.yml
└── workflows
│ ├── php.yml
│ └── release.yml
├── .gitignore
├── .nojekyll
├── .php-cs-fixer.php
├── LICENSE
├── Makefile
├── README.md
├── README.zh-CN.md
├── _navbar.md
├── composer.json
├── example
├── cliapp.php
├── clicmd.php
├── flags-demo.php
├── images
│ ├── cli-app-cmd-help.png
│ ├── cli-app-cmd-run.png
│ ├── cli-app-help.png
│ ├── cli-cmd-help.png
│ ├── cli-cmd-run.png
│ └── flags-demo.png
├── not-stop_on_first.php
├── refer.php
└── sflags-demo.php
├── index.html
├── phpunit.xml
├── psalm.xml
├── sflags-usage.md
├── src
├── CliApp.php
├── CliCmd.php
├── Concern
│ ├── HelperRenderTrait.php
│ └── RuleParserTrait.php
├── Contract
│ ├── CmdHandlerInterface.php
│ ├── FlagInterface.php
│ ├── ParserInterface.php
│ ├── ValidatorInterface.php
│ └── ValueInterface.php
├── Exception
│ ├── FlagException.php
│ └── FlagParseException.php
├── Flag
│ ├── AbstractFlag.php
│ ├── Argument.php
│ ├── Arguments.php
│ ├── Option.php
│ └── Options.php
├── FlagType.php
├── FlagUtil.php
├── Flags.php
├── FlagsParser.php
├── Helper
│ ├── ValueBinding.php
│ └── ValueCollector.php
├── SFlags.php
└── Validator
│ ├── AbstractValidator.php
│ ├── CondValidator.php
│ ├── EmptyValidator.php
│ ├── EnumValidator.php
│ ├── FuncValidator.php
│ ├── LenValidator.php
│ ├── MultiValidator.php
│ ├── NameValidator.php
│ └── RegexValidator.php
└── test
├── BaseFlagsTestCase.php
├── Cases
├── DemoCmdHandler.php
└── RuleParser.php
├── CliAppTest.php
├── Concern
└── RuleParserTest.php
├── Flag
├── ArgumentTest.php
└── OptionTest.php
├── FlagUtilTest.php
├── FlagsParserTest.php
├── FlagsTest.php
├── SFlagsTest.php
├── ValidatorTest.php
├── bootstrap.php
└── testdata
└── .keep
/.github/changelog.yml:
--------------------------------------------------------------------------------
1 | title: '## Change Log'
2 | # style allow: simple, markdown(mkdown), ghr(gh-release)
3 | style: gh-release
4 | # group names
5 | names: [Refactor, Fixed, Feature, Update, Other]
6 | #repo_url: https://github.com/gookit/gcli
7 |
8 | filters:
9 | # message length should >= 12
10 | - name: msg_len
11 | min_len: 12
12 | # message words should >= 3
13 | - name: words_len
14 | min_len: 3
15 | - name: keyword
16 | keyword: format code
17 | exclude: true
18 | - name: keywords
19 | keywords: format code, action test
20 | exclude: true
21 |
22 | # group match rules
23 | # not matched will use 'Other' group.
24 | rules:
25 | - name: Refactor
26 | start_withs: [refactor, break]
27 | contains: ['refactor:', 'break:']
28 | - name: Fixed
29 | start_withs: [fix]
30 | contains: ['fix:']
31 | - name: Feature
32 | start_withs: [feat, new]
33 | contains: ['feat:', 'new:']
34 | - name: Update
35 | start_withs: [up]
36 | contains: ['update:', 'up:']
37 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: composer
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
9 | - package-ecosystem: "github-actions"
10 | directory: "/"
11 | schedule:
12 | # Check for updates to GitHub Actions every weekday
13 | interval: "daily"
14 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: Unit-Tests
2 |
3 | # https://docs.github.com/cn/actions/reference/workflow-syntax-for-github-actions
4 | on:
5 | push:
6 | paths:
7 | - '**.php'
8 | - 'composer.json'
9 | - '**.yml'
10 |
11 | jobs:
12 | test:
13 | name: Test on php ${{ matrix.php}}
14 | runs-on: ubuntu-latest
15 | timeout-minutes: 10
16 | strategy:
17 | fail-fast: true
18 | matrix:
19 | php: [8.3, 8.1, 8.2, 8.4]
20 | # os: [ubuntu-latest] # , macOS-latest, windows-latest,
21 | coverage: ['none']
22 |
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v4
26 |
27 | - uses: actions/cache@v4
28 | with:
29 | path: ~/.composer/cache/files
30 | key: ${{ matrix.php }}
31 |
32 | - name: Set ENV vars
33 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
34 | run: |
35 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV
36 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV
37 |
38 | - name: Display Env
39 | run: env
40 |
41 | # usage refer https://github.com/shivammathur/setup-php
42 | - name: Setup PHP
43 | timeout-minutes: 5
44 | uses: shivammathur/setup-php@v2
45 | with:
46 | php-version: ${{ matrix.php}}
47 | tools: php-cs-fixer, phpunit:${{ matrix.phpunit }} # pecl,
48 | extensions: mbstring # , swoole-4.4.19 #optional, setup extensions
49 | coverage: ${{ matrix.coverage }} #optional, setup coverage driver: xdebug, none
50 |
51 | - name: Install dependencies
52 | run: composer update --no-progress
53 |
54 | # phpunit -v --debug
55 | # phpunit --coverage-clover ./test/clover.info
56 | # phpdbg -dauto_globals_jit=Off-qrr $(which phpunit) --coverage-clover ./test/clover.info
57 | - name: Run test suite
58 | run: |
59 | php example/flags-demo.php -h
60 | php example/sflags-demo.php --help
61 | phpunit
62 |
63 | # - name: Coveralls parallel
64 | # uses: coverallsapp/github-action@master
65 | # if: matrix.coverage == 'xdebug'
66 | # with:
67 | # github-token: ${{ secrets.github_token }}
68 | # path-to-lcov: ./test/clover.info
69 | # flag-name: run-${{ matrix.php }}
70 | # parallel: true
71 | #
72 | # finish:
73 | # needs: test
74 | # runs-on: ubuntu-latest
75 | # steps:
76 | # - name: Coveralls Finished
77 | # uses: coverallsapp/github-action@master
78 | # with:
79 | # github-token: ${{ secrets.github_token }}
80 | # parallel-finished: true
81 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Tag-release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | release:
10 | name: Tag release
11 | runs-on: ubuntu-latest
12 | timeout-minutes: 10
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Set ENV for github-release
21 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
22 | run: |
23 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV
24 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV
25 |
26 | - name: Generate changelog
27 | run: |
28 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog
29 | chmod a+x /usr/local/bin/chlog
30 | chlog -c .github/changelog.yml -o changelog.md prev last
31 |
32 | # https://github.com/softprops/action-gh-release
33 | - name: Create release and upload assets
34 | uses: softprops/action-gh-release@v2
35 | with:
36 | name: ${{ env.RELEASE_TAG }}
37 | tag_name: ${{ env.RELEASE_TAG }}
38 | body_path: changelog.md
39 | env:
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .buildpath
2 | .settings/
3 | .project
4 | *.patch
5 | .idea/
6 | .git/
7 | tmp/
8 | vendor/
9 | *.lock
10 | *.phar
11 | *.log
12 | *.tgz
13 | *.txt
14 | *.cache
15 | *.bak
16 | .phpintel/
17 | .env
18 | .phpstorm.meta.php
19 | .DS_Store
20 | .kite.php
21 | node_modules/
22 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/.nojekyll
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
13 | ->setRules([
14 | '@PSR2' => true,
15 | 'array_syntax' => [
16 | 'syntax' => 'short'
17 | ],
18 | 'list_syntax' => [
19 | 'syntax' => 'short'
20 | ],
21 | 'class_attributes_separation' => true,
22 | 'declare_strict_types' => true,
23 | 'global_namespace_import' => [
24 | 'import_constants' => true,
25 | 'import_functions' => true,
26 | ],
27 | 'header_comment' => [
28 | 'comment_type' => 'PHPDoc',
29 | 'header' => $header,
30 | 'separate' => 'bottom'
31 | ],
32 | 'no_unused_imports' => true,
33 | 'single_quote' => true,
34 | 'standardize_not_equals' => true,
35 | 'void_return' => true, // add :void for method
36 | ])
37 | ->setFinder(
38 | PhpCsFixer\Finder::create()
39 | ->exclude('test')
40 | ->exclude('runtime')
41 | ->exclude('.github')
42 | ->exclude('vendor')
43 | ->in(__DIR__)
44 | )
45 | ->setUsingCache(false);
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 PHPComLab
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
10 | furnished 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # link https://github.com/humbug/box/blob/master/Makefile
2 | #SHELL = /bin/sh
3 | # 每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX 声明
4 | # mac 下这条声明 没起作用 !!
5 | .RECIPEPREFIX = >
6 | .PHONY: all usage help clean test
7 |
8 | # 需要注意的是,每行命令在一个单独的shell中执行。这些Shell之间没有继承关系。
9 | # - 解决办法是将两行命令写在一行,中间用分号分隔。
10 | # - 或者在换行符前加反斜杠转义 \
11 |
12 | # 接收命令行传入参数 make COMMAND tag=v2.0.4
13 | # TAG=$(tag) # 使用 $(TAG)
14 |
15 | # 定义变量
16 | #SHELL := /bin/bash
17 | PHPUNIT =$(which phpunit)
18 |
19 | # Full build flags used when building binaries. Not used for test compilation/execution.
20 | #BUILDFLAGS := -ldflags \
21 | # " -X $(ROOT_PACKAGE)/pkg/cmd/version.Version=$(VERSION)
22 |
23 | # if 条件
24 | #ifdef DEBUG
25 | #BUILDFLAGS := -gcflags "all=-N -l" $(BUILDFLAGS)
26 | #endif
27 |
28 | .DEFAULT_GOAL := help
29 | help:
30 | @echo "There some make command for the project\n"
31 | @echo "Available Commands:"
32 | @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
33 |
34 | clean: ## Clean all created artifacts
35 | git clean --exclude=.idea/ -fdx
36 |
37 | test: ## run phpunit tests with --debug
38 | phpunit --debug
39 |
40 | cover-test: ## run phpunit tests with --coverage-text
41 | phpdbg -qrr $(PHPUNIT) --coverage-text
42 |
43 | csfix: ## Fix code style for all files
44 | php-cs-fixer fix ./
45 |
46 | cs-diff: ## Display code style error files
47 | gofmt -l ./
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP Flag
2 |
3 | [](LICENSE)
4 | [](https://github.com/php-toolkit/pflag)
5 | [](https://github.com/php-toolkit/pflag/actions)
6 | [](https://packagist.org/packages/toolkit/pflag)
7 | [](https://packagist.org/packages/toolkit/pflag)
8 | [](https://coveralls.io/github/php-toolkit/pflag?branch=main)
9 | [](README.zh-CN.md)
10 |
11 | Generic PHP command line flags parse library
12 |
13 | > Github: [php-toolkit/pflag](https://github.com/php-toolkit/pflag)
14 |
15 | ## Features
16 |
17 | - Generic command line options and arguments parser.
18 | - Support set value data type(`int,string,bool,array`), will auto format input value.
19 | - Support set multi alias names for an option.
20 | - Support set multi short names for an option.
21 | - Support set default value for option/argument.
22 | - Support read flag value from ENV var.
23 | - Support set option/argument is required.
24 | - Support set validator for check input value.
25 | - Support auto render beautiful help message.
26 |
27 | **Flag Options**:
28 |
29 | - Options start with `-` or `--`, and the first character must be a letter
30 | - Support long option. eg: `--long` `--long value`
31 | - Support short option. eg: `-s -a value`
32 | - Support define array option
33 | - eg: `--tag php --tag go` will get `tag: [php, go]`
34 |
35 | **Flag Arguments**:
36 |
37 | - Support binding named arguemnt
38 | - Support define array argument
39 |
40 | ### Quick build command
41 |
42 | - Use `Toolkit\PFlag\CliCmd` to quickly build a simple command application
43 | - Use `Toolkit\PFlag\CliApp` to quickly build a command application that supports subcommands
44 |
45 | ## Install
46 |
47 | - Require PHP 8.0+
48 |
49 | **composer**
50 |
51 | ```bash
52 | composer require toolkit/pflag
53 | ```
54 |
55 | -----------
56 |
57 | ## Flags Usage
58 |
59 | Flags - is an cli flags(options&argument) parser and manager.
60 |
61 | > example codes please see [example/flags-demo.php](example/flags-demo.php)
62 |
63 | ### Create Flags
64 |
65 | ```php
66 | use Toolkit\PFlag\Flags;
67 |
68 | require dirname(__DIR__) . '/test/bootstrap.php';
69 |
70 | $flags = $_SERVER['argv'];
71 | // NOTICE: must shift first element.
72 | $scriptFile = array_shift($flags);
73 |
74 | $fs = Flags::new();
75 | // can with some config
76 | $fs->setScriptFile($scriptFile);
77 | /** @see Flags::$settings */
78 | $fs->setSettings([
79 | 'descNlOnOptLen' => 26
80 | ]);
81 |
82 | // ...
83 | ```
84 |
85 | ### Define options
86 |
87 | Examples for add flag option define:
88 |
89 | ```php
90 | use Toolkit\PFlag\Flag\Option;
91 | use Toolkit\PFlag\FlagType;
92 | use Toolkit\PFlag\Validator\EnumValidator;
93 |
94 | // add options
95 | // - quick add
96 | $fs->addOpt('age', 'a', 'this is a int option', FlagType::INT);
97 |
98 | // - use string rule
99 | $fs->addOptByRule('name,n', 'string;this is a string option;true');
100 |
101 | // - use array rule
102 | /** @see Flags::DEFINE_ITEM for array rule */
103 | $fs->addOptByRule('name-is-very-lang', [
104 | 'type' => FlagType::STRING,
105 | 'desc' => 'option name is to lang, desc will print on newline',
106 | 'shorts' => ['d','e','f'],
107 | // TIP: add validator limit input value.
108 | 'validator' => EnumValidator::new(['one', 'two', 'three']),
109 | ]);
110 |
111 | // -- add multi option at once.
112 | $fs->addOptsByRules([
113 | 'tag,t' => 'strings;array option, allow set multi times',
114 | 'f' => 'bool;this is an bool option',
115 | ]);
116 |
117 | // - use Option
118 | $opt = Option::new('str1', "this is string option, \ndesc has multi line, \nhaha...");
119 | $opt->setDefault('defVal');
120 | $fs->addOption($opt);
121 | ```
122 |
123 | ### Define Arguments
124 |
125 | Examples for add flag argument define:
126 |
127 | ```php
128 | use Toolkit\PFlag\Flag\Argument;
129 | use Toolkit\PFlag\FlagType;
130 |
131 | // add arguments
132 | // - quick add
133 | $fs->addArg('strArg1', 'the is string arg and is required', 'string', true);
134 |
135 | // - use string rule
136 | $fs->addArgByRule('intArg2', 'int;this is a int arg and with default value;no;89');
137 |
138 | // - use Argument object
139 | $arg = Argument::new('arrArg');
140 | // OR $arg->setType(FlagType::ARRAY);
141 | $arg->setType(FlagType::STRINGS);
142 | $arg->setDesc("this is an array arg,\n allow multi value,\n must define at last");
143 |
144 | $fs->addArgument($arg);
145 | ```
146 |
147 | ### Parse Input
148 |
149 | ```php
150 | use Toolkit\PFlag\Flags;
151 | use Toolkit\PFlag\FlagType;
152 |
153 | // ...
154 |
155 | if (!$fs->parse($flags)) {
156 | // on render help
157 | return;
158 | }
159 |
160 | vdump($fs->getOpts(), $fs->getArgs());
161 | ```
162 |
163 | **Show help**
164 |
165 | ```bash
166 | $ php example/flags-demo.php --help
167 | ```
168 |
169 | Output:
170 |
171 | 
172 |
173 | **Run demo:**
174 |
175 | ```bash
176 | $ php example/flags-demo.php --name inhere --age 99 --tag go -t php -t java -d one -f arg0 80 arr0 arr1
177 | ```
178 |
179 | Output:
180 |
181 | ```text
182 | # options
183 | array(6) {
184 | ["str1"]=> string(6) "defVal"
185 | ["name"]=> string(6) "inhere"
186 | ["age"]=> int(99)
187 | ["tag"]=> array(3) {
188 | [0]=> string(2) "go"
189 | [1]=> string(3) "php"
190 | [2]=> string(4) "java"
191 | }
192 | ["name-is-very-lang"]=> string(3) "one"
193 | ["f"]=> bool(true)
194 | }
195 |
196 | # arguments
197 | array(3) {
198 | [0]=> string(4) "arg0"
199 | [1]=> int(80)
200 | [2]=> array(2) {
201 | [0]=> string(4) "arr0"
202 | [1]=> string(4) "arr1"
203 | }
204 | }
205 | ```
206 |
207 | -----------
208 |
209 | ## Get Value
210 |
211 | Get flag value is very simple, use method `getOpt(string $name)` `getArg($nameOrIndex)`.
212 |
213 | > TIP: Will auto format input value by define type.
214 |
215 | **Options**
216 |
217 | ```php
218 | $force = $fs->getOpt('f'); // bool(true)
219 | $age = $fs->getOpt('age'); // int(99)
220 | $name = $fs->getOpt('name'); // string(inhere)
221 | $tags = $fs->getOpt('tags'); // array{"php", "go", "java"}
222 | ```
223 |
224 | **Arguments**
225 |
226 | ```php
227 | $arg0 = $fs->getArg(0); // string(arg0)
228 | // get an array arg
229 | $arrArg = $fs->getArg(1); // array{"arr0", "arr1"}
230 | // get value by name
231 | $arrArg = $fs->getArg('arrArg'); // array{"arr0", "arr1"}
232 | ```
233 |
234 | -----------
235 |
236 | ## Build simple cli app
237 |
238 | In the pflag, built in `CliApp` and `CliCmd` for quick create and run an simple console application.
239 |
240 | ### Create simple alone command
241 |
242 | Build and run a simple command handler. see example file [example/clicmd.php](example/clicmd.php)
243 |
244 | ```php
245 | use Toolkit\Cli\Cli;
246 | use Toolkit\PFlag\CliCmd;
247 | use Toolkit\PFlag\FlagsParser;
248 |
249 | CliCmd::new()
250 | ->config(function (CliCmd $cmd) {
251 | $cmd->name = 'demo';
252 | $cmd->desc = 'description for demo command';
253 |
254 | // config flags
255 | $cmd->options = [
256 | 'age, a' => 'int;the option age, is int',
257 | 'name, n' => 'the option name, is string and required;true',
258 | 'tags, t' => 'array;the option tags, is array',
259 | ];
260 | // or use property
261 | // $cmd->arguments = [...];
262 | })
263 | ->withArguments([
264 | 'arg1' => 'this is arg1, is string'
265 | ])
266 | ->setHandler(function (FlagsParser $fs) {
267 | Cli::info('options:');
268 | vdump($fs->getOpts());
269 | Cli::info('arguments:');
270 | vdump($fs->getArgs());
271 | })
272 | ->run();
273 | ```
274 |
275 | **Usage:**
276 |
277 | ```php
278 | # show help
279 | php example/clicmd.php -h
280 | # run command
281 | php example/clicmd.php --age 23 --name inhere value1
282 | ```
283 |
284 | - Display help:
285 |
286 | 
287 |
288 | - Run command:
289 |
290 | 
291 |
292 | ### Create an multi commands app
293 |
294 | Create an multi commands application, run subcommand. see example file [example/cliapp.php](example/cliapp.php)
295 |
296 | ```php
297 | use Toolkit\Cli\Cli;
298 | use Toolkit\PFlag\CliApp;
299 | use Toolkit\PFlag\FlagsParser;
300 | use Toolkit\PFlagTest\Cases\DemoCmdHandler;
301 |
302 | $app = new CliApp();
303 |
304 | $app->add('test1', fn(FlagsParser $fs) => vdump($fs->getOpts()), [
305 | 'desc' => 'the test 1 command',
306 | 'options' => [
307 | 'opt1' => 'opt1 for command test1',
308 | 'opt2' => 'int;opt2 for command test1',
309 | ],
310 | ]);
311 |
312 | $app->add('test2', function (FlagsParser $fs) {
313 | Cli::info('options:');
314 | vdump($fs->getOpts());
315 | Cli::info('arguments:');
316 | vdump($fs->getArgs());
317 | }, [
318 | // 'desc' => 'the test2 command',
319 | 'options' => [
320 | 'opt1' => 'a string opt1 for command test2',
321 | 'opt2' => 'int;a int opt2 for command test2',
322 | ],
323 | 'arguments' => [
324 | 'arg1' => 'required arg1 for command test2;true',
325 | ]
326 | ]);
327 |
328 | // fn - required php 7.4+
329 | $app->add('show-err', fn() => throw new RuntimeException('test show exception'));
330 |
331 | $app->addHandler(DemoCmdHandler::class);
332 |
333 | $app->run();
334 | ```
335 |
336 | **Usage:**
337 |
338 | ```php
339 | # show help
340 | php example/cliapp.php -h
341 | # run command
342 | php example/cliapp.php test2 --opt1 val1 --opt2 23 value1
343 | ```
344 |
345 | - Display commands:
346 |
347 | 
348 |
349 | - Command help:
350 |
351 | 
352 |
353 | - Run command:
354 |
355 | 
356 |
357 | -----------
358 |
359 | ## Flag rule
360 |
361 | The options/arguments rules. Use rule can quick define an option or argument.
362 |
363 | - string value is rule(`type;desc;required;default;shorts`).
364 | - array is define item `SFlags::DEFINE_ITEM`
365 | - supported type see `FlagType::*`
366 |
367 | ```php
368 | use Toolkit\PFlag\FlagType;
369 |
370 | $rules = [
371 | // v: only value, as name and use default type FlagType::STRING
372 | // k-v: key is name, value can be string|array
373 | 'long,s',
374 | // name => rule
375 | 'long,a,b' => 'int', // long is option name, a and b is shorts.
376 | 'f' => FlagType::BOOL,
377 | 'str1' => ['type' => 'int', 'desc' => 'an string option'],
378 | 'tags' => 'array', // can also: ints, strings
379 | 'name' => 'type;the description message;required;default', // with desc, default, required
380 | ]
381 | ```
382 |
383 | **For options**
384 |
385 | - option allow set shorts
386 |
387 | > TIP: name `long,a,b` - `long` is the option name. remaining `a,b` is short names.
388 |
389 | **For arguments**
390 |
391 | - argument no alias/shorts
392 | - array value only allow defined at last
393 |
394 | **Definition item**
395 |
396 | The const `Flags::DEFINE_ITEM`:
397 |
398 | ```php
399 | public const DEFINE_ITEM = [
400 | 'name' => '',
401 | 'desc' => '',
402 | 'type' => FlagType::STRING,
403 | 'helpType' => '', // use for render help
404 | // 'index' => 0, // only for argument
405 | 'required' => false,
406 | 'default' => null,
407 | 'shorts' => [], // only for option
408 | // value validator
409 | 'validator' => null,
410 | // 'category' => null
411 | ];
412 | ```
413 |
414 | -----------
415 |
416 | ## Custom settings
417 |
418 | ### Settings for parse
419 |
420 | ```php
421 | // -------------------- settings for parse option --------------------
422 |
423 | /**
424 | * Stop parse option on found first argument.
425 | *
426 | * - Useful for support multi commands. eg: `top --opt ... sub --opt ...`
427 | *
428 | * @var bool
429 | */
430 | protected $stopOnFistArg = true;
431 |
432 | /**
433 | * Skip on found undefined option.
434 | *
435 | * - FALSE will throw FlagException error.
436 | * - TRUE will skip it and collect as raw arg, then continue parse next.
437 | *
438 | * @var bool
439 | */
440 | protected $skipOnUndefined = false;
441 |
442 | // -------------------- settings for parse argument --------------------
443 |
444 | /**
445 | * Whether auto bind remaining args after option parsed
446 | *
447 | * @var bool
448 | */
449 | protected $autoBindArgs = true;
450 |
451 | /**
452 | * Strict match args number.
453 | * if exist unbind args, will throw FlagException
454 | *
455 | * @var bool
456 | */
457 | protected $strictMatchArgs = false;
458 |
459 | ```
460 |
461 | ### Setting for render help
462 |
463 | support some settings for render help
464 |
465 | ```php
466 |
467 | // -------------------- settings for built-in render help --------------------
468 |
469 | /**
470 | * Auto render help on provide '-h', '--help'
471 | *
472 | * @var bool
473 | */
474 | protected $autoRenderHelp = true;
475 |
476 | /**
477 | * Show flag data type on render help.
478 | *
479 | * if False:
480 | *
481 | * -o, --opt Option desc
482 | *
483 | * if True:
484 | *
485 | * -o, --opt STRING Option desc
486 | *
487 | * @var bool
488 | */
489 | protected $showTypeOnHelp = true;
490 |
491 | /**
492 | * Will call it on before print help message
493 | *
494 | * @var callable
495 | */
496 | private $beforePrintHelp;
497 |
498 | ```
499 |
500 | - custom help message renderer
501 |
502 | ```php
503 | $fs->setHelpRenderer(function (\Toolkit\PFlag\FlagsParser $fs) {
504 | // render help messages
505 | });
506 | ```
507 |
508 | -----------
509 |
510 | ## Unit tests
511 |
512 | ```bash
513 | phpunit --debug
514 | ```
515 |
516 | test with coverage:
517 |
518 | ```bash
519 | phpdbg -dauto_globals_jit=Off -qrr $(which phpunit) --coverage-text
520 | phpdbg -dauto_globals_jit=Off -qrr $(which phpunit) --coverage-clover ./test/clover.info
521 | # use xdebug
522 | phpunit --coverage-clover ./test/clover.info
523 | ```
524 |
525 | ## Project use
526 |
527 | Check out these projects, which use https://github.com/php-toolkit/pflag :
528 |
529 | - [inhere/console](https://github.com/inhere/console) Full-featured php command line application library.
530 | - [kite](https://github.com/inhere/kite) Kite is a tool for help development.
531 | - More, please see [Packagist](https://packagist.org/packages/toolkit/pflag)
532 |
533 | ## License
534 |
535 | [MIT](LICENSE)
536 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # PHP Flag
2 |
3 | [](LICENSE)
4 | [](https://github.com/php-toolkit/pflag)
5 | [](https://github.com/php-toolkit/pflag/actions)
6 | [](https://packagist.org/packages/toolkit/pflag)
7 | [](https://packagist.org/packages/toolkit/pflag)
8 | [](https://coveralls.io/github/php-toolkit/pflag?branch=main)
9 | [](README.md)
10 |
11 | `pflag` - PHP编写的,通用的命令行标志(选项和参数)解析库
12 |
13 | > Github: [php-toolkit/pflag](https://github.com/php-toolkit/pflag)
14 |
15 | ## 功能说明
16 |
17 | - 通用的命令行选项和参数解析器
18 | - 支持设置值数据类型(`int,string,bool,array`),将自动格式化输入值
19 | - 支持为选项/参数设置默认值
20 | - 支持为一个选项设置多个别名
21 | - 支持为一个选项设置多个短名称
22 | - 支持从环境变量读取标志值
23 | - 支持设置选项/参数为必须的(`required`)
24 | - 支持设置验证器以检查输入值
25 | - 支持自动渲染漂亮的帮助信息。
26 |
27 | **命令行选项**:
28 |
29 | - 选项以 `-` 或者 `--` 开头的,且首字符必须是字母
30 | - 以 `--` 开头的为长选项. eg: `--long` `--long value`
31 | - 以 `-` 开头的为短选项 `-s -a value`
32 | - 支持定义数组选项
33 | - eg: `--tag php --tag go` 将会得到 `$tag = [php, go]`
34 |
35 | **命令行参数**:
36 |
37 | - 不能满足选项的都认作参数
38 | - 支持绑定命名参数
39 | - 支持定义数组参数
40 |
41 | ### 快速构建命令
42 |
43 | - 使用 `Toolkit\PFlag\CliCmd` 可以快速的构建一个简单的命令应用
44 | - 使用 `Toolkit\PFlag\CliApp` 可以快速的构建一个支持子命令的命令应用
45 |
46 | ## 安装
47 |
48 | - Require PHP 8.0+
49 |
50 | **composer 安装**
51 |
52 | ```bash
53 | composer require toolkit/pflag
54 | ```
55 |
56 | -----------
57 |
58 | ## Flags 使用
59 |
60 | Flags - 是一个命令行标志(选项和参数)解析器和管理器。
61 |
62 | > 示例代码请参见 [example/flags-demo.php](example/flags-demo.php)
63 |
64 | ### 创建解析器
65 |
66 | 创建和初始化解析器
67 |
68 | ```php
69 | use Toolkit\PFlag\Flags;
70 |
71 | require dirname(__DIR__) . '/test/bootstrap.php';
72 |
73 | $flags = $_SERVER['argv'];
74 | // NOTICE: must shift first element.
75 | $scriptFile = array_shift($flags);
76 |
77 | $fs = Flags::new();
78 |
79 | // (可选的)可以添加一些自定义设置
80 | $fs->setScriptFile($scriptFile);
81 | /** @see Flags::$settings */
82 | $fs->setSettings([
83 | 'descNlOnOptLen' => 26
84 | ]);
85 |
86 | // ...
87 | ```
88 |
89 | ### 定义选项
90 |
91 | 定义选项 - 定义好支持的选项设置,解析时将会根据定义来解析输入
92 |
93 | 添加选项定义的示例:
94 |
95 | ```php
96 | use Toolkit\PFlag\Flag\Option;
97 | use Toolkit\PFlag\FlagType;
98 | use Toolkit\PFlag\Validator\EnumValidator;
99 |
100 | // add options
101 | // - quick add
102 | $fs->addOpt('age', 'a', 'this is a int option', FlagType::INT);
103 |
104 | // - 使用字符串规则快速添加选项定义
105 | $fs->addOptByRule('name,n', 'string;this is a string option;true');
106 |
107 | // -- 一次添加多个选项
108 | $fs->addOptsByRules([
109 | 'tag,t' => 'strings;array option, allow set multi times',
110 | 'f' => 'bool;this is an bool option',
111 | ]);
112 |
113 | // - 使用数组定义
114 | /** @see Flags::DEFINE_ITEM for array rule */
115 | $fs->addOptByRule('name-is-very-lang', [
116 | 'type' => FlagType::STRING,
117 | 'desc' => 'option name is to lang, desc will print on newline',
118 | 'shorts' => ['d','e','f'],
119 | // TIP: add validator limit input value.
120 | 'validator' => EnumValidator::new(['one', 'two', 'three']),
121 | ]);
122 |
123 | // - 使用 Option 对象
124 | $opt = Option::new('str1', "this is string option, \ndesc has multi line, \nhaha...");
125 | $opt->setDefault('defVal');
126 | $fs->addOption($opt);
127 | ```
128 |
129 | ### 定义参数
130 |
131 | 定义参数 - 定义好支持的选项设置,解析时将会根据定义来解析输入
132 |
133 | 添加参数定义的示例:
134 |
135 | ```php
136 | use Toolkit\PFlag\Flag\Argument;
137 | use Toolkit\PFlag\FlagType;
138 |
139 | // add arguments
140 | // - quick add
141 | $fs->addArg('strArg1', 'the is string arg and is required', 'string', true);
142 |
143 | // - 使用字符串规则快速添加定义
144 | $fs->addArgByRule('intArg2', 'int;this is a int arg and with default value;no;89');
145 |
146 | // - 使用 Argument 对象
147 | $arg = Argument::new('arrArg');
148 | // OR $arg->setType(FlagType::ARRAY);
149 | $arg->setType(FlagType::STRINGS);
150 | $arg->setDesc("this is an array arg,\n allow multi value,\n must define at last");
151 |
152 | $fs->addArgument($arg);
153 | ```
154 |
155 | ### 解析命令行输入
156 |
157 | 最后调用 `parse()` 解析命令行输入数据
158 |
159 | ```php
160 | // ...
161 |
162 | if (!$fs->parse($flags)) {
163 | // on render help
164 | return;
165 | }
166 |
167 | vdump($fs->getOpts(), $fs->getArgs());
168 | ```
169 |
170 | **显示帮助**
171 |
172 | 当输入 `-h` 或 `--help` 会自动渲染帮助信息。
173 |
174 | ```bash
175 | $ php example/flags-demo.php --help
176 | ```
177 |
178 | Output:
179 |
180 | 
181 |
182 | **运行示例:**
183 |
184 | ```bash
185 | $ php example/flags-demo.php --name inhere --age 99 --tag go -t php -t java -d one -f arg0 80 arr0 arr1
186 | ```
187 |
188 | 输出结果:
189 |
190 | ```text
191 | # 选项数据
192 | array(6) {
193 | ["str1"]=> string(6) "defVal"
194 | ["name"]=> string(6) "inhere"
195 | ["age"]=> int(99)
196 | ["tag"]=> array(3) {
197 | [0]=> string(2) "go"
198 | [1]=> string(3) "php"
199 | [2]=> string(4) "java"
200 | }
201 | ["name-is-very-lang"]=> string(3) "one"
202 | ["f"]=> bool(true)
203 | }
204 |
205 | # 参数数据
206 | array(3) {
207 | [0]=> string(4) "arg0"
208 | [1]=> int(80)
209 | [2]=> array(2) {
210 | [0]=> string(4) "arr0"
211 | [1]=> string(4) "arr1"
212 | }
213 | }
214 | ```
215 |
216 | -----------
217 |
218 | ## SFlags 使用
219 |
220 | SFlags - 是一个简洁版本的标志(选项、参数)解析器和管理器
221 |
222 | ### 使用示例
223 |
224 | ```php
225 | use Toolkit\PFlag\SFlags;
226 |
227 | $fs = SFlags::new();
228 |
229 | // 模拟输入参数
230 | $flags = ['--name', 'inhere', '--age', '99', '--tag', 'php', '-t', 'go', '--tag', 'java', '-f', 'arg0'];
231 |
232 | $optRules = [
233 | 'name', // string
234 | 'age' => 'int;an int option;required', // set required
235 | 'tag,t' => FlagType::ARRAY,
236 | 'f' => FlagType::BOOL,
237 | ];
238 | $argRules = [
239 | // some argument rules
240 | ];
241 |
242 | $fs->setOptRules($optRules);
243 | $fs->setArgRules($argRules);
244 | $fs->parse($rawFlags);
245 | // or use
246 | // $fs->parseDefined($flags, $optRules, $argRules);
247 |
248 | vdump($fs->getOpts(), $fs->getRawArgs());
249 | ```
250 |
251 | Output:
252 |
253 | ```text
254 | array(3) {
255 | ["name"]=> string(6) "inhere"
256 | ["tag"]=> array(3) {
257 | [0]=> string(3) "php"
258 | [1]=> string(2) "go"
259 | [2]=> string(4) "java"
260 | }
261 | ["f"]=> bool(true)
262 | }
263 | array(1) {
264 | [0]=> string(4) "arg0"
265 | }
266 | ```
267 |
268 | ### 解析命令行输入
269 |
270 | 将代码写入 php 文件(示例请看 [example/sflags-demo.php](example/sflags-demo.php))
271 |
272 | ```php
273 | use Toolkit\PFlag\SFlags;
274 |
275 | $rawFlags = $_SERVER['argv'];
276 | // NOTICE: must shift first element.
277 | $scriptFile = array_shift($rawFlags);
278 |
279 | $optRules = [
280 | // some option rules
281 | 'name', // string
282 | 'age' => 'int;an int option;required', // set required
283 | 'tag,t' => FlagType::ARRAY,
284 | 'f' => FlagType::BOOL,
285 | ];
286 | $argRules = [
287 | // some argument rules
288 | 'string',
289 | // set name
290 | 'arrArg' => 'array',
291 | ];
292 |
293 | $fs->setOptRules($optRules);
294 | $fs->setArgRules($argRules);
295 | $fs->parse($rawFlags);
296 | ```
297 |
298 | **运行示例:**
299 |
300 | ```bash
301 | php example/sflags-demo.php --name inhere --age 99 --tag go -t php -t java -f arg0 arr0 arr1
302 | ```
303 |
304 | 输出:
305 |
306 | ```text
307 | # 选项数据
308 | array(4) {
309 | ["name"]=> string(6) "inhere"
310 | ["age"]=> int(99)
311 | ["tag"]=> array(3) {
312 | [0]=> string(2) "go"
313 | [1]=> string(3) "php"
314 | [2]=> string(4) "java"
315 | }
316 | ["f"]=> bool(true)
317 | }
318 |
319 | # 参数数据
320 | array(2) {
321 | [0]=> string(4) "arg0"
322 | [1]=> array(2) {
323 | [0]=> string(4) "arr0"
324 | [1]=> string(4) "arr1"
325 | }
326 | }
327 | ```
328 |
329 | **显示帮助**
330 |
331 | ```bash
332 | $ php example/sflags-demo.php --help
333 | ```
334 |
335 | -----------
336 |
337 | ## 获取输入值
338 |
339 | 获取flag值很简单,使用方法 `getOpt(string $name)` `getArg($nameOrIndex)` 即可.
340 |
341 | > TIP: 将通过定义的数据类型自动格式化输入值
342 |
343 | **选项数据**
344 |
345 | ```php
346 | $force = $fs->getOpt('f'); // bool(true)
347 | $age = $fs->getOpt('age'); // int(99)
348 | $name = $fs->getOpt('name'); // string(inhere)
349 | $tags = $fs->getOpt('tags'); // array{"php", "go", "java"}
350 | ```
351 |
352 | **参数数据**
353 |
354 | ```php
355 | $arg0 = $fs->getArg(0); // string(arg0)
356 | // get an array arg
357 | $arrArg = $fs->getArg(1); // array{"arr0", "arr1"}
358 | // get value by name
359 | $arrArg = $fs->getArg('arrArg'); // array{"arr0", "arr1"}
360 | ```
361 | -----------
362 |
363 | ## 创建简单的独立命令或应用程序
364 |
365 | 在 pflag 中,内置了 `CliApp` 和 `CliCmd` 两个独立类,用于快速创建和运行一个简单的控制台应用程序。
366 |
367 | ### 创建简单的单独命令
368 |
369 | 使用 `CliCmd` 可以方便的构建并运行一个简单的命令处理程序。查看示例文件 [example/clicmd.php](example/clicmd.php)
370 |
371 | ```php
372 | use Toolkit\Cli\Cli;
373 | use Toolkit\PFlag\CliCmd;
374 | use Toolkit\PFlag\FlagsParser;
375 |
376 | CliCmd::new()
377 | ->config(function (CliCmd $cmd) {
378 | $cmd->name = 'demo';
379 | $cmd->desc = 'description for demo command';
380 |
381 | // config flags
382 | $cmd->options = [
383 | 'age, a' => 'int;the option age, is int',
384 | 'name, n' => 'the option name, is string and required;true',
385 | 'tags, t' => 'array;the option tags, is array',
386 | ];
387 | // or use property
388 | // $cmd->arguments = [...];
389 | })
390 | ->withArguments([
391 | 'arg1' => 'this is arg1, is string'
392 | ])
393 | ->setHandler(function (FlagsParser $fs) {
394 | Cli::info('options:');
395 | vdump($fs->getOpts());
396 | Cli::info('arguments:');
397 | vdump($fs->getArgs());
398 | })
399 | ->run();
400 | ```
401 |
402 | **使用:**
403 |
404 | ```php
405 | # show help
406 | php example/clicmd.php -h
407 | # run command
408 | php example/clicmd.php --age 23 --name inhere value1
409 | ```
410 |
411 | - 显示帮助:
412 |
413 | 
414 |
415 | - 运行命令:
416 |
417 | 
418 |
419 | ### Create an multi commands app
420 |
421 | Create an multi commands application, run subcommand. see example file [example/cliapp.php](example/cliapp.php)
422 |
423 | ```php
424 | use Toolkit\Cli\Cli;
425 | use Toolkit\PFlag\CliApp;
426 | use Toolkit\PFlag\FlagsParser;
427 |
428 | $app = new CliApp();
429 |
430 | $app->add('test1', fn(FlagsParser $fs) => vdump($fs->getOpts()), [
431 | 'desc' => 'the test 1 command',
432 | 'options' => [
433 | 'opt1' => 'opt1 for command test1',
434 | 'opt2' => 'int;opt2 for command test1',
435 | ],
436 | ]);
437 |
438 | $app->add('test2', function (FlagsParser $fs) {
439 | Cli::info('options:');
440 | vdump($fs->getOpts());
441 | Cli::info('arguments:');
442 | vdump($fs->getArgs());
443 | }, [
444 | // 'desc' => 'the test2 command',
445 | 'options' => [
446 | 'opt1' => 'a string opt1 for command test2',
447 | 'opt2' => 'int;a int opt2 for command test2',
448 | ],
449 | 'arguments' => [
450 | 'arg1' => 'required arg1 for command test2;true',
451 | ]
452 | ]);
453 |
454 | // fn - required php 7.4+
455 | $app->add('show-err', fn() => throw new RuntimeException('test show exception'));
456 |
457 | $app->run();
458 | ```
459 |
460 | **使用:**
461 |
462 | ```php
463 | # show help
464 | php example/cliapp.php -h
465 | # run command
466 | php example/cliapp.php test2 --opt1 val1 --opt2 23 value1
467 | ```
468 |
469 | - 显示帮助,命令列表:
470 |
471 | 
472 |
473 | - 显示子命令帮助:
474 |
475 | 
476 |
477 | - 运行一个命令:
478 |
479 | 
480 |
481 | -----------
482 |
483 | ## 扩展:规则定义
484 |
485 | 选项参数规则。使用规则可以快速定义一个选项或参数。
486 |
487 | - string 字符串规则以分号 `;` 分割每个部分 (完整规则:`type;desc;required;default;shorts`).
488 | - array 规则按 `SFlags::DEFINE_ITEM` 设置定义
489 | - 支持的类型常量请看 `FlagType::*`
490 |
491 | ```php
492 | use Toolkit\PFlag\FlagType;
493 |
494 | $rules = [
495 | // v: 只有值,作为名称并使用默认类型 FlagType::STRING
496 | // k-v: 键是名称,值可以是字符串|数组
497 | 'long,s',
498 | // name => rule
499 | 'long,a,b' => 'int;an int option', // long is option name, a and b is shorts.
500 | 'f' => FlagType::BOOL,
501 | 'str1' => ['type' => 'int', 'desc' => 'an string option'],
502 | 'tags' => 'array; an array option', // can also: ints, strings
503 | 'name' => 'type;the description message;required;default', // with desc, default, required
504 | ]
505 | ```
506 |
507 | **对于选项**
508 |
509 | - 选项允许设置短名称 `shorts`
510 |
511 | > TIP: 例如 `long,a,b` - `long` 是选项名称. 剩余的 `a,b` 都是它的短选项名.
512 |
513 | **对于参数**
514 |
515 | - 参数没有别名或者短名称
516 | - 数组参数只允许定义在最后
517 |
518 | **数组定义项**
519 |
520 | 常量 `Flags::DEFINE_ITEM`:
521 |
522 | ```php
523 | public const DEFINE_ITEM = [
524 | 'name' => '',
525 | 'desc' => '',
526 | 'type' => FlagType::STRING,
527 | 'helpType' => '', // use for render help
528 | // 'index' => 0, // only for argument
529 | 'required' => false,
530 | 'default' => null,
531 | 'shorts' => [], // only for option
532 | // value validator
533 | 'validator' => null,
534 | // 'category' => null
535 | ];
536 | ```
537 |
538 | -----------
539 |
540 | ## 自定义设置
541 |
542 | ### 解析设置
543 |
544 | ```php
545 | // -------------------- 选项解析设置 --------------------
546 |
547 | /**
548 | * Stop parse option on found first argument.
549 | *
550 | * - Useful for support multi commands. eg: `top --opt ... sub --opt ...`
551 | *
552 | * @var bool
553 | */
554 | protected $stopOnFistArg = true;
555 |
556 | /**
557 | * Skip on found undefined option.
558 | *
559 | * - FALSE will throw FlagException error.
560 | * - TRUE will skip it and collect as raw arg, then continue parse next.
561 | *
562 | * @var bool
563 | */
564 | protected $skipOnUndefined = false;
565 |
566 | // -------------------- 参数解析设置 --------------------
567 |
568 | /**
569 | * Whether auto bind remaining args after option parsed
570 | *
571 | * @var bool
572 | */
573 | protected $autoBindArgs = true;
574 |
575 | /**
576 | * Strict match args number.
577 | * if exist unbind args, will throw FlagException
578 | *
579 | * @var bool
580 | */
581 | protected $strictMatchArgs = false;
582 |
583 | ```
584 |
585 | ### 渲染帮助设置
586 |
587 | support some settings for render help
588 |
589 | ```php
590 |
591 | // -------------------- settings for built-in render help --------------------
592 |
593 | /**
594 | * 自动渲染帮助信息当输入 '-h', '--help' 选项时
595 | *
596 | * @var bool
597 | */
598 | protected $autoRenderHelp = true;
599 |
600 | /**
601 | * 在渲染的帮助信息上显示数据类型
602 | *
603 | * if False:
604 | *
605 | * -o, --opt Option desc
606 | *
607 | * if True:
608 | *
609 | * -o, --opt STRING Option desc
610 | *
611 | * @var bool
612 | */
613 | protected $showTypeOnHelp = true;
614 |
615 | /**
616 | * 将在打印帮助消息之前调用它
617 | *
618 | * @var callable
619 | */
620 | private $beforePrintHelp;
621 |
622 | ```
623 |
624 | 自定义帮助消息渲染:
625 |
626 | ```php
627 | $fs->setHelpRenderer(function (\Toolkit\PFlag\FlagsParser $fs) {
628 | // render help messages
629 | });
630 | ```
631 |
632 | -----------
633 |
634 | ## 单元测试
635 |
636 | ```bash
637 | phpunit --debug
638 | ```
639 |
640 | test with coverage:
641 |
642 | ```bash
643 | phpdbg -qrr $(which phpunit) --coverage-text
644 | ```
645 |
646 | ## 使用pflag的项目
647 |
648 | Check out these projects, which use https://github.com/php-toolkit/pflag :
649 |
650 | - [inhere/console](https://github.com/inhere/console) Full-featured php command line application library.
651 | - [kite](https://github.com/inhere/kite) Kite is a tool for help development.
652 | - More, please see [Packagist](https://packagist.org/packages/toolkit/pflag)
653 |
654 | ## License
655 |
656 | [MIT](LICENSE)
657 |
--------------------------------------------------------------------------------
/_navbar.md:
--------------------------------------------------------------------------------
1 | * PhpPkg
2 | * [EasyTpl](https://phppkg.github.io/easytpl/ "template engine")
3 | * [Validate](https://inhere.github.io/php-validate/ "data validate engine")
4 | * Toolkit
5 | * [PFlag](https://php-toolkit.github.io/pflag/ "console option and argument parse")
6 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "toolkit/pflag",
3 | "description": "Command line flag parse library of the php",
4 | "type": "library",
5 | "license": "MIT",
6 | "homepage": "https://github.com/php-toolkit/pflag",
7 | "authors": [
8 | {
9 | "name": "inhere",
10 | "email": "in.798@qq.com"
11 | }
12 | ],
13 | "require": {
14 | "php": ">8.0.0",
15 | "ext-mbstring": "*",
16 | "toolkit/cli-utils":"~2.0",
17 | "toolkit/stdlib":"~2.0"
18 | },
19 | "autoload": {
20 | "psr-4": {
21 | "Toolkit\\PFlag\\": "src/"
22 | }
23 | },
24 | "autoload-dev": {
25 | "psr-4": {
26 | "Toolkit\\PFlagTest\\": "test/"
27 | }
28 | },
29 | "scripts": {
30 | "test": "php vender/bin/phpunit -v --debug"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example/cliapp.php:
--------------------------------------------------------------------------------
1 | setName('myApp');
22 | $app->setDesc('my cli application. v1.0.1');
23 | })
24 | ->add('test1', fn (FlagsParser $fs) => vdump($fs->getOpts()), [
25 | 'desc' => 'the test 1 command',
26 | 'options' => [
27 | 'opt1' => 'opt1 for command test1',
28 | 'opt2' => 'int;opt2 for command test1',
29 | ],
30 | ]);
31 |
32 | $cli->add('test2', function (FlagsParser $fs): void {
33 | Cli::info('options:');
34 | vdump($fs->getOpts());
35 | Cli::info('arguments:');
36 | vdump($fs->getArgs());
37 | }, [
38 | // 'desc' => 'the test2 command',
39 | 'options' => [
40 | 'opt1' => 'string;a string opt1 for command test2, and is required;true',
41 | 'opt2' => 'int;a int opt2 for command test2',
42 | ],
43 | 'arguments' => [
44 | 'arg1' => 'required arg1 for command test2;true',
45 | ]
46 | ]);
47 |
48 | $cli->add('show-err', fn () => throw new RuntimeException('test show exception'));
49 |
50 | $cli->addHandler(DemoCmdHandler::class);
51 |
52 | $cli->run();
53 |
--------------------------------------------------------------------------------
/example/clicmd.php:
--------------------------------------------------------------------------------
1 | name = 'demo';
23 | $cmd->desc = 'description for demo command';
24 |
25 | // config flags
26 | $cmd->options = [
27 | 'age, a' => 'int;the option age, is int',
28 | 'name, n' => 'the option name, is string and required;true',
29 | 'tags, t' => 'array;the option tags, is array',
30 | ];
31 |
32 | // or use property
33 | // $cmd->arguments = [...];
34 | // $cmd->getFlags()->setExample($example);
35 | })
36 | ->withArguments([
37 | 'arg1' => 'this is arg1, is string'
38 | ])
39 | ->setHandler(function (FlagsParser $fs): void {
40 | Cli::info('options:');
41 | vdump($fs->getOpts());
42 | Cli::info('arguments:');
43 | vdump($fs->getArgs());
44 | })
45 | ->run();
46 |
--------------------------------------------------------------------------------
/example/flags-demo.php:
--------------------------------------------------------------------------------
1 | setScriptFile($scriptFile);
30 | /** @see Flags::$settings */
31 | $fs->setSettings([
32 | 'descNlOnOptLen' => 26
33 | ]);
34 |
35 | // add options
36 | // - quick add
37 | $fs->addOpt('age', 'a', 'this is a int option', FlagType::INT);
38 |
39 | // - use string rule
40 | $fs->addOptByRule('name,n', 'string;this is a string option;true;');
41 | // -- add multi option at once.
42 | $fs->addOptsByRules([
43 | 'tag,t' => 'strings;array option, allow set multi times',
44 | 'f' => 'bool;this is an bool option',
45 | ]);
46 | // - use array rule
47 | /** @see Flags::DEFINE_ITEM for array rule */
48 | $fs->addOptByRule('name-is-very-lang', [
49 | 'type' => FlagType::STRING,
50 | 'desc' => 'option name is to lang, desc will print on newline',
51 | 'shorts' => ['d','e'],
52 | 'alias' => 'nv',
53 | // TIP: add validator limit input value.
54 | 'validator' => EnumValidator::new(['one', 'two', 'three']),
55 | ]);
56 |
57 | // - use Option
58 | $opt = Option::new('str1', "this is string option, \ndesc has multi line, \nhaha...");
59 | $opt->setDefault('defVal');
60 | $fs->addOption($opt);
61 |
62 | // add arguments
63 | // - quick add
64 | $fs->addArg('strArg1', 'the is string arg and is required', 'string', true);
65 | // - use string rule
66 | $fs->addArgByRule('intArg2', 'int;this is a int arg and with default value;no;89');
67 | // - use Argument object
68 | $arg = Argument::new('arrArg');
69 | // OR $arg->setType(FlagType::ARRAY);
70 | $arg->setType(FlagType::STRINGS);
71 | $arg->setDesc("this is an array arg,\n allow multi value,\n must define at last");
72 | $fs->addArgument($arg);
73 |
74 | $fs->setMoreHelp('more help message ...');
75 |
76 | $fs->setExampleHelp([
77 | 'example usage 1',
78 | 'example usage 2',
79 | ]);
80 |
81 | // edump($fs);
82 |
83 | // do parsing
84 | try {
85 | if (!$fs->parse($flags)) {
86 | // on render help
87 | return;
88 | }
89 | } catch (Throwable $e) {
90 | if ($e instanceof FlagException) {
91 | Cli::colored('ERROR: ' . $e->getMessage(), 'error');
92 | } else {
93 | $code = $e->getCode() !== 0 ? $e->getCode() : -1;
94 | $eTpl = "Exception(%d): %s\nFile: %s(Line %d)\nTrace:\n%s\n";
95 |
96 | // print exception message
97 | printf($eTpl, $code, $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString());
98 | }
99 |
100 | return;
101 | }
102 |
103 | vdump($fs->getOpts(), $fs->getArgs());
104 |
--------------------------------------------------------------------------------
/example/images/cli-app-cmd-help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/example/images/cli-app-cmd-help.png
--------------------------------------------------------------------------------
/example/images/cli-app-cmd-run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/example/images/cli-app-cmd-run.png
--------------------------------------------------------------------------------
/example/images/cli-app-help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/example/images/cli-app-help.png
--------------------------------------------------------------------------------
/example/images/cli-cmd-help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/example/images/cli-cmd-help.png
--------------------------------------------------------------------------------
/example/images/cli-cmd-run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/example/images/cli-cmd-run.png
--------------------------------------------------------------------------------
/example/images/flags-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/example/images/flags-demo.png
--------------------------------------------------------------------------------
/example/not-stop_on_first.php:
--------------------------------------------------------------------------------
1 | addOptsByRules([
17 | 'name' => 'string',
18 | 'age' => 'int',
19 | ]);
20 | $flags = ['--name', 'inhere', '--age', '90', 'arg0', 'arg1'];
21 |
22 | // set stopOnFirstArg=false
23 | $fs->setStopOnFistArg(false);
24 |
25 | $fs->parse($flags);
26 | vdump($fs->toArray());
27 |
28 | $fs->resetResults();
29 |
30 | // move an arg in middle
31 | $flags1 = ['--name', 'INHERE', 'arg0', '--age', '980', 'arg1'];
32 |
33 | // will skip 'arg0' and continue parse '--age', '90'
34 | $fs->parse($flags1);
35 | vdump($fs->toArray());
36 |
--------------------------------------------------------------------------------
/example/refer.php:
--------------------------------------------------------------------------------
1 | 'string;this is an string option', // string
27 | 'age' => 'int;this is an int option;required', // set required
28 | 'tag,t' => 'strings;array option, allow set multi times',
29 | 'f' => 'bool;this is an bool option',
30 | ];
31 | $argRules = [
32 | // some argument rules
33 | 'string',
34 | // set name
35 | 'arrArg' => 'strings;this is an array arg, allow multi value;;[a,b]',
36 | ];
37 |
38 | $fs = SFlags::new();
39 | $fs->setScriptFile($scriptFile);
40 |
41 | $fs->setOptRules($optRules);
42 | $fs->setArgRules($argRules);
43 |
44 | $fs->setMoreHelp('more help message ...');
45 |
46 | $fs->setExample([
47 | 'example usage 1',
48 | 'example usage 2',
49 | ]);
50 |
51 | // do parsing
52 | try {
53 | if (!$fs->parse($flags)) {
54 | // on render help
55 | return;
56 | }
57 | } catch (Throwable $e) {
58 | if ($e instanceof FlagException) {
59 | Cli::colored('ERROR: ' . $e->getMessage(), 'error');
60 | } else {
61 | $code = $e->getCode() !== 0 ? $e->getCode() : -1;
62 | $eTpl = "Exception(%d): %s\nFile: %s(Line %d)\nTrace:\n%s\n";
63 |
64 | // print exception message
65 | printf($eTpl, $code, $e->getMessage(), $e->getFile(), $e->getLine(), $e->getTraceAsString());
66 | }
67 |
68 | return;
69 | }
70 |
71 | vdump(
72 | // $fs->getRawArgs(),
73 | $fs->getOpts(),
74 | $fs->getArgs()
75 | );
76 |
77 | // vdump($fs->getArg('arrArg'));
78 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Generic PHP command line flags parse library
9 |
10 |
11 |
12 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | test/
6 |
7 |
8 |
9 |
10 | src
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/sflags-usage.md:
--------------------------------------------------------------------------------
1 | # SFlags
2 |
3 | ## SFlags Usage
4 |
5 | SFlags - is an simple flags(options&argument) parser and manager.
6 |
7 | ### Examples
8 |
9 | ```php
10 | use Toolkit\PFlag\SFlags;
11 |
12 | $flags = ['--name', 'inhere', '--age', '99', '--tag', 'php', '-t', 'go', '--tag', 'java', '-f', 'arg0'];
13 |
14 | $optRules = [
15 | 'name', // string
16 | 'age' => 'int;an int option;required', // set required
17 | 'tag,t' => FlagType::ARRAY,
18 | 'f' => FlagType::BOOL,
19 | ];
20 | $argRules = [
21 | // some argument rules
22 | ];
23 |
24 | $fs->setOptRules($optRules);
25 | $fs->setArgRules($argRules);
26 | $fs->parse($rawFlags);
27 | // or use
28 | // $fs->parseDefined($flags, $optRules, $argRules);
29 |
30 | vdump($fs->getOpts(), $fs->getRawArgs());
31 | ```
32 |
33 | Output:
34 |
35 | ```text
36 | array(3) {
37 | ["name"]=> string(6) "inhere"
38 | ["tag"]=> array(3) {
39 | [0]=> string(3) "php"
40 | [1]=> string(2) "go"
41 | [2]=> string(4) "java"
42 | }
43 | ["f"]=> bool(true)
44 | }
45 | array(1) {
46 | [0]=> string(4) "arg0"
47 | }
48 | ```
49 |
50 | ### Parse CLI Input
51 |
52 | write the codes to an php file(see [example/sflags-demo.php](example/sflags-demo.php))
53 |
54 | ```php
55 | use Toolkit\PFlag\SFlags;
56 |
57 | $rawFlags = $_SERVER['argv'];
58 | // NOTICE: must shift first element.
59 | $scriptFile = array_shift($rawFlags);
60 |
61 | $optRules = [
62 | // some option rules
63 | 'name', // string
64 | 'age' => 'int;an int option;required', // set required
65 | 'tag,t' => FlagType::ARRAY,
66 | 'f' => FlagType::BOOL,
67 | ];
68 | $argRules = [
69 | // some argument rules
70 | 'string',
71 | // set name
72 | 'arrArg' => 'array',
73 | ];
74 |
75 | $fs = SFlags::new();
76 | $fs->parseDefined($rawFlags, $optRules, $argRules);
77 | ```
78 |
79 | **Run demo:**
80 |
81 | ```bash
82 | php example/sflags-demo.php --name inhere --age 99 --tag go -t php -t java -f arg0 arr0 arr1
83 | ```
84 |
85 | Output:
86 |
87 | ```text
88 | array(4) {
89 | ["name"]=> string(6) "inhere"
90 | ["age"]=> int(99)
91 | ["tag"]=> array(3) {
92 | [0]=> string(2) "go"
93 | [1]=> string(3) "php"
94 | [2]=> string(4) "java"
95 | }
96 | ["f"]=> bool(true)
97 | }
98 | array(2) {
99 | [0]=> string(4) "arg0"
100 | [1]=> array(2) {
101 | [0]=> string(4) "arr0"
102 | [1]=> string(4) "arr1"
103 | }
104 | }
105 | ```
106 |
107 | **Show help**
108 |
109 | ```bash
110 | $ php example/sflags-demo.php --help
111 | ```
112 |
--------------------------------------------------------------------------------
/src/CliCmd.php:
--------------------------------------------------------------------------------
1 |
33 | */
34 | public array $options = [];
35 |
36 | /**
37 | * @var array
38 | */
39 | public array $arguments = [];
40 |
41 | /**
42 | * @var FlagsParser
43 | */
44 | private FlagsParser $flags;
45 |
46 | /**
47 | * @var callable(FlagsParser): mixed
48 | */
49 | private $handler;
50 |
51 | /**
52 | * @param callable(self): void $fn
53 | *
54 | * @return $this
55 | */
56 | public static function newWith(callable $fn): self
57 | {
58 | return (new self)->config($fn);
59 | }
60 |
61 | /**
62 | * Class constructor.
63 | *
64 | * @param array $config
65 | */
66 | public function __construct(array $config = [])
67 | {
68 | $this->supper($config);
69 |
70 | $this->flags = new SFlags();
71 | }
72 |
73 | /**
74 | * @param callable(self): void $fn
75 | *
76 | * @return $this
77 | */
78 | public function config(callable $fn): self
79 | {
80 | $fn($this);
81 | return $this;
82 | }
83 |
84 | /**
85 | * @param callable $handler
86 | *
87 | * @return $this
88 | */
89 | public function withHandler(callable $handler): self
90 | {
91 | return $this->setHandler($handler);
92 | }
93 |
94 | /**
95 | * @param array $options
96 | *
97 | * @return $this
98 | */
99 | public function withOptions(array $options): self
100 | {
101 | $this->options = $options;
102 | return $this;
103 | }
104 |
105 | /**
106 | * @param array $arguments
107 | *
108 | * @return $this
109 | */
110 | public function withArguments(array $arguments): self
111 | {
112 | $this->arguments = $arguments;
113 | return $this;
114 | }
115 |
116 | /**
117 | * @param FlagsParser $fs
118 | */
119 | protected function prepare(FlagsParser $fs): void
120 | {
121 | $fs->setName($this->name);
122 | $fs->setDesc($this->desc);
123 |
124 | $options = $this->options;
125 | if (!isset($options['help'])) {
126 | $options['help'] = 'bool;display command help;;;h';
127 | }
128 |
129 | $fs->addOptsByRules($options);
130 | $fs->addArgsByRules($this->arguments);
131 | }
132 |
133 | /**
134 | * @return mixed
135 | */
136 | public function run(): mixed
137 | {
138 | $handler = $this->handler;
139 | if (!$handler) {
140 | throw new RuntimeException('command handler must be set before run.');
141 | }
142 |
143 | $this->prepare($this->flags);
144 |
145 | try {
146 | if (!$this->flags->parse()) {
147 | return 0;
148 | }
149 |
150 | return $handler($this->flags);
151 | } catch (Throwable $e) {
152 | CliApp::handleException($e);
153 | }
154 |
155 | return -1;
156 | }
157 |
158 | /**
159 | * @param callable $handler
160 | *
161 | * @return CliCmd
162 | */
163 | public function setHandler(callable $handler): self
164 | {
165 | $this->handler = $handler;
166 | return $this;
167 | }
168 |
169 | /**
170 | * @return FlagsParser
171 | */
172 | public function getFlags(): FlagsParser
173 | {
174 | return $this->flags;
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/Concern/HelperRenderTrait.php:
--------------------------------------------------------------------------------
1 | desc) {
109 | $buf->writeln(Str::ucfirst($title) . "\n");
110 | }
111 |
112 | $hasArgs = count($argDefines) > 0;
113 | $hasOpts = count($optDefines) > 0;
114 |
115 | // ------- usage -------
116 | $binName = $this->getScriptName();
117 | if ($hasArgs || $hasOpts) {
118 | $buf->writeln("Usage: $binName [--Options ...] [Arguments ...]\n");
119 | }
120 |
121 | // ------- opts -------
122 | if ($hasOpts) {
123 | $buf->writeln('Options:');
124 | }
125 |
126 | $nameTag = 'info';
127 | $fmtOpts = $this->buildOptsForHelp($optDefines, $hasShortOpt);
128 |
129 | $nameLen = $this->settings['optNameLen'];
130 | $maxWidth = $this->settings['descNlOnOptLen'];
131 | foreach ($fmtOpts as $hName => $opt) {
132 | [$desc, $lines] = $this->formatDesc($opt);
133 |
134 | // need echo desc at newline.
135 | $hName = Str::padRight($hName, $nameLen);
136 | if (strlen($hName) > $maxWidth) {
137 | $buf->writef(" <%s>%s%s>\n", $nameTag, $hName, $nameTag);
138 | $buf->writef(" %s%s\n", Str::repeat(' ', $nameLen), $desc);
139 | } else {
140 | $buf->writef(" <%s>%s%s> %s\n", $nameTag, $hName, $nameTag, $desc);
141 | }
142 |
143 | // remaining desc lines
144 | if ($lines) {
145 | $indent = Str::repeat(' ', $nameLen);
146 | foreach ($lines as $line) {
147 | $buf->writef(" %s%s\n", $indent, $line);
148 | }
149 | }
150 | }
151 |
152 | $hasOpts && $buf->writeln('');
153 |
154 | // ------- args -------
155 | // $nameTag = 'info';
156 | $fmtArgs = $this->buildArgsForHelp($argDefines);
157 |
158 | if ($hasArgs) {
159 | $buf->writeln('Arguments:');
160 | }
161 |
162 | $nameLen = $this->settings['argNameLen'];
163 | foreach ($fmtArgs as $hName => $arg) {
164 | [$desc, $lines] = $this->formatDesc($arg);
165 |
166 | // write to buffer.
167 | $hName = Str::padRight($hName, $nameLen);
168 | $buf->writef(" <%s>%s%s> %s\n", $nameTag, $hName, $nameTag, $desc);
169 |
170 | // remaining desc lines
171 | if ($lines) {
172 | $indent = Str::repeat(' ', $nameLen);
173 | foreach ($lines as $line) {
174 | $buf->writef(" %s%s\n", $indent, $line);
175 | }
176 | }
177 | }
178 |
179 | // --------------- extra: moreHelp, example -----------------
180 | if ($this->exampleHelp) {
181 | $buf->writeln("\nExamples:");
182 |
183 | $lines = is_array($this->exampleHelp) ? $this->exampleHelp : [$this->exampleHelp];
184 | $buf->writeln(' ' . implode("\n ", $lines));
185 | }
186 |
187 | if ($this->moreHelp) {
188 | $buf->writeln("\nMore Help:");
189 |
190 | $lines = is_array($this->moreHelp) ? $this->moreHelp : [$this->moreHelp];
191 | $buf->writeln(' ' . implode("\n ", $lines));
192 | }
193 |
194 | // fire event
195 | if ($fn = $this->beforePrintHelp) {
196 | $text = $fn($buf->getAndClear());
197 | } else {
198 | $text = $buf->getAndClear();
199 | }
200 |
201 | return $withColor ? $text : ColorTag::clear($text);
202 | }
203 |
204 | /**
205 | * @param array|Argument|Option $define = FlagsParser::DEFINE_ITEM
206 | *
207 | * @return array
208 | * @see FlagsParser::DEFINE_ITEM for array $define
209 | */
210 | protected function formatDesc(Argument|Option|array $define): array
211 | {
212 | $desc = $define['desc'] ?: 'No description';
213 | if ($define['required']) {
214 | $desc = '*' . $desc;
215 | }
216 |
217 | // validator limit
218 | if (!empty($define['validator'])) {
219 | /** @see ValidatorInterface */
220 | $v = $define['validator'];
221 |
222 | if (is_object($v) && method_exists($v, '__toString')) {
223 | $limit = (string)$v;
224 | $desc .= $limit ? "\n" . $limit : '';
225 | }
226 | }
227 |
228 | // default value.
229 | if (isset($define['default']) && $define['default'] !== null) {
230 | $desc .= sprintf('(default %s)', DataHelper::toString($define['default']));
231 | }
232 |
233 | // desc has multi line
234 | $lines = [];
235 | if (strpos($desc, "\n") > 0) {
236 | $lines = explode("\n", $desc);
237 | $desc = array_shift($lines);
238 | }
239 |
240 | if (FlagType::isArray($define['type'])) {
241 | $desc .= ColorTag::wrap(' (repeatable)', 'cyan');
242 | }
243 |
244 | return [$desc, $lines];
245 | }
246 |
247 | /**
248 | * @param array $argDefines
249 | *
250 | * @return array
251 | */
252 | protected function buildArgsForHelp(array $argDefines): array
253 | {
254 | $fmtArgs = [];
255 | $maxLen = $this->settings['argNameLen'];
256 |
257 | /** @var array|Argument $arg {@see DEFINE_ITEM} */
258 | foreach ($argDefines as $arg) {
259 | $helpName = $arg['name'] ?: 'arg' . $arg['index'];
260 | if ($desc = $arg['desc']) {
261 | $desc = trim($desc);
262 | }
263 |
264 | // ensure desc is not empty
265 | $arg['desc'] = $desc ? Str::ucfirst($desc) : "Argument $helpName";
266 |
267 | $type = $arg['type'];
268 | if (FlagType::isArray($type)) {
269 | $helpName .= '...';
270 | }
271 |
272 | if ($this->showTypeOnHelp) {
273 | $typeName = FlagType::getHelpName($type);
274 | $helpName .= $typeName ? " $typeName" : '';
275 | }
276 |
277 | $maxLen = IntHelper::getMax($maxLen, strlen($helpName));
278 |
279 | // append
280 | $fmtArgs[$helpName] = $arg;
281 | }
282 |
283 | // $this->settings['argNameLen'] = $maxLen;
284 | $this->set('argNameLen', $maxLen);
285 | return $fmtArgs;
286 | }
287 |
288 | /**
289 | * @param array $optDefines
290 | * @param bool $hasShortOpt
291 | *
292 | * @return array
293 | */
294 | protected function buildOptsForHelp(array $optDefines, bool $hasShortOpt): array
295 | {
296 | if (!$optDefines) {
297 | return [];
298 | }
299 |
300 | $fmtOpts = [];
301 | $nameLen = $this->settings['optNameLen'];
302 | ksort($optDefines);
303 |
304 | // $hasShortOpt=true will add `strlen('-h, ')` indent.
305 | $prefix = $hasShortOpt ? ' ' : '';
306 |
307 | /** @var array|Option $opt {@see FlagsParser::DEFINE_ITEM} */
308 | foreach ($optDefines as $name => $opt) {
309 | // hidden option
310 | if ($this->showHiddenOpt === false && $opt['hidden']) {
311 | continue;
312 | }
313 |
314 | $names = $opt['shorts'];
315 | // support multi alias names.
316 | if (isset($opt['aliases']) && $opt['aliases']) {
317 | array_push($names, ...$opt['aliases']);
318 | }
319 |
320 | // option name.
321 | $names[] = $name;
322 | // option description
323 | $desc = $opt['desc'] ? trim($opt['desc']) : '';
324 |
325 | // ensure desc is not empty
326 | $opt['desc'] = $desc ? Str::ucfirst($desc) : "Option $name";
327 | $helpName = FlagUtil::buildOptHelpName($names);
328 |
329 | // first elem is long option name.
330 | if (isset($names[0][1])) {
331 | $helpName = $prefix . $helpName;
332 | }
333 |
334 | // show type name.
335 | if ($this->showTypeOnHelp) {
336 | $typeName = $opt['helpType'] ?: FlagType::getHelpName($opt['type']);
337 | $helpName .= $typeName ? " $typeName" : '';
338 | }
339 |
340 | $nameLen = IntHelper::getMax($nameLen, strlen($helpName));
341 | // append
342 | $fmtOpts[$helpName] = $opt;
343 | }
344 |
345 | // limit option name width
346 | $maxLen = IntHelper::getMax($this->settings['descNlOnOptLen'], self::OPT_MAX_WIDTH);
347 |
348 | // $this->settings['descNlOnOptLen'] = $maxLen;
349 | $this->set('descNlOnOptLen', $maxLen);
350 | // set opt name len
351 | // $this->settings['optNameLen'] = IntHelper::getMin($nameLen, $maxLen);
352 | $this->set('optNameLen', IntHelper::getMin($nameLen, $maxLen));
353 | return $fmtOpts;
354 | }
355 |
356 | /**
357 | * @param string $name
358 | * @param array $opt
359 | *
360 | * @return array
361 | */
362 | protected function buildOptHelpLine(string $name, array $opt): array
363 | {
364 | $names = $opt['shorts'];
365 | // has aliases
366 | if ($opt['aliases']) {
367 | $names = array_merge($names, $opt['aliases']);
368 | }
369 |
370 | $names[] = $name;
371 | $helpName = FlagUtil::buildOptHelpName($names);
372 |
373 | // show type name.
374 | if ($this->showTypeOnHelp) {
375 | $typeName = $opt['helpType'] ?: FlagType::getHelpName($opt['type']);
376 | $helpName .= $typeName ? " $typeName" : '';
377 | }
378 |
379 | $opt['desc'] = $opt['desc'] ? ucfirst($opt['desc']) : "Option $name";
380 |
381 | // format desc
382 | [$desc, $otherLines] = $this->formatDesc($opt);
383 | if ($otherLines) {
384 | $desc .= "\n" . implode("\n", $otherLines);
385 | }
386 |
387 | return [$helpName, $desc];
388 | }
389 |
390 | /****************************************************************
391 | * getter/setter methods
392 | ***************************************************************/
393 |
394 | /**
395 | * @return callable
396 | */
397 | public function getHelpRenderer(): callable
398 | {
399 | return $this->helpRenderer;
400 | }
401 |
402 | /**
403 | * @param callable $helpRenderer
404 | */
405 | public function setHelpRenderer(callable $helpRenderer): void
406 | {
407 | $this->helpRenderer = $helpRenderer;
408 | }
409 |
410 | /**
411 | * @return bool
412 | */
413 | public function isAutoRenderHelp(): bool
414 | {
415 | return $this->autoRenderHelp;
416 | }
417 |
418 | /**
419 | * @param bool $autoRenderHelp
420 | */
421 | public function setAutoRenderHelp(bool $autoRenderHelp): void
422 | {
423 | $this->autoRenderHelp = $autoRenderHelp;
424 | }
425 |
426 | /**
427 | * @return bool
428 | */
429 | public function isShowTypeOnHelp(): bool
430 | {
431 | return $this->showTypeOnHelp;
432 | }
433 |
434 | /**
435 | * @param bool $showTypeOnHelp
436 | */
437 | public function setShowTypeOnHelp(bool $showTypeOnHelp): void
438 | {
439 | $this->showTypeOnHelp = $showTypeOnHelp;
440 | }
441 |
442 | /**
443 | * @return array|string|null
444 | */
445 | public function getMoreHelp(): array|string|null
446 | {
447 | return $this->moreHelp;
448 | }
449 |
450 | /**
451 | * @param array|string|null $moreHelp
452 | */
453 | public function setHelp(array|string|null $moreHelp): void
454 | {
455 | $this->setMoreHelp($moreHelp);
456 | }
457 |
458 | /**
459 | * @param array|string|null $moreHelp
460 | */
461 | public function setMoreHelp(array|string|null $moreHelp): void
462 | {
463 | if ($moreHelp) {
464 | $this->moreHelp = $moreHelp;
465 | }
466 | }
467 |
468 | /**
469 | * @return array|string|null
470 | */
471 | public function getExampleHelp(): array|string|null
472 | {
473 | return $this->exampleHelp;
474 | }
475 |
476 | /**
477 | * @param array|string|null $example
478 | */
479 | public function setExample(array|string|null $example): void
480 | {
481 | $this->setExampleHelp($example);
482 | }
483 |
484 | /**
485 | * @param array|string|null $exampleHelp
486 | */
487 | public function setExampleHelp(array|string|null $exampleHelp): void
488 | {
489 | if ($exampleHelp) {
490 | $this->exampleHelp = $exampleHelp;
491 | }
492 | }
493 |
494 | /**
495 | * @param callable(string): string $beforePrintHelp
496 | */
497 | public function setBeforePrintHelp(callable $beforePrintHelp): void
498 | {
499 | $this->beforePrintHelp = $beforePrintHelp;
500 | }
501 |
502 | /**
503 | * @param bool $showHiddenOpt
504 | */
505 | public function setShowHiddenOpt(bool $showHiddenOpt): void
506 | {
507 | $this->showHiddenOpt = $showHiddenOpt;
508 | }
509 | }
510 |
--------------------------------------------------------------------------------
/src/Concern/RuleParserTrait.php:
--------------------------------------------------------------------------------
1 | rule
55 | * // TIP: name 'long,s' - first is the option name. remaining is shorts.
56 | * 'long,s' => int,
57 | * 'f' => bool,
58 | * 'long' => string,
59 | * 'tags' => array, // can also: ints, strings
60 | * 'name' => 'type;the description message;required;default', // with desc, default, required
61 | * ]
62 | * ```
63 | *
64 | * @var array
65 | */
66 | protected array $optRules = [];
67 |
68 | /**
69 | * The arguments rules
70 | *
71 | * **rule item**
72 | * - array It is define item, see {@see FlagsParser::DEFINE_ITEM}
73 | * - string Value is rule(format: `type;desc;required;default;shorts`)
74 | *
75 | * **data type**
76 | *
77 | * - type see FlagType::*
78 | * - default type is FlagType::STRING
79 | *
80 | * ```php
81 | * [
82 | * // v: only value, as rule and use default type
83 | * // k-v: key is name, value is rule
84 | * 'type',
85 | * 'name' => 'type',
86 | * 'name' => 'type;required', // arg option
87 | * 'name' => 'type;the description message;required;default', // with default, desc, required
88 | * ]
89 | * ```
90 | *
91 | * @var array
92 | */
93 | protected array $argRules = [];
94 |
95 | /****************************************************************
96 | * add rule methods
97 | ***************************************************************/
98 |
99 | /**
100 | * @param array $rules see {@see optRules} for each rule.
101 | */
102 | public function addOptsByRules(array $rules): void
103 | {
104 | foreach ($rules as $name => $rule) {
105 | if (is_int($name)) { // only name.
106 | $name = (string)$rule;
107 | $rule = FlagType::STRING;
108 | } else {
109 | $name = (string)$name;
110 | }
111 |
112 | $this->addOptByRule($name, $rule);
113 | }
114 | }
115 |
116 | /**
117 | * Add and option by rule
118 | *
119 | * @param string $name
120 | * @param array|string $rule {@see optRules}
121 | *
122 | * @return static
123 | */
124 | public function addOptByRule(string $name, array|string $rule): static
125 | {
126 | $this->optRules[$name] = $rule;
127 | return $this;
128 | }
129 |
130 | /**
131 | * @param array $rules data like: [name => rule, ...]
132 | *
133 | * @see addArgByRule()
134 | */
135 | public function addArgsByRules(array $rules): void
136 | {
137 | foreach ($rules as $name => $rule) {
138 | if (!$rule) {
139 | throw new FlagException('flag argument rule cannot be empty');
140 | }
141 |
142 | $this->addArgByRule((string)$name, $rule);
143 | }
144 | }
145 |
146 | /**
147 | * Add and argument by rule
148 | *
149 | * @param string $name
150 | * @param array|string $rule please see {@see argRules}
151 | *
152 | * @return static
153 | */
154 | public function addArgByRule(string $name, array|string $rule): static
155 | {
156 | if ($name && !is_numeric($name)) {
157 | $this->argRules[$name] = $rule;
158 | } else {
159 | $this->argRules[] = $rule;
160 | }
161 |
162 | return $this;
163 | }
164 |
165 | /****************************************************************
166 | * parse rule to definition
167 | ***************************************************************/
168 |
169 | /**
170 | * Parse rule
171 | *
172 | * **array rule**
173 | *
174 | * - will merge an {@see FlagsParser::DEFINE_ITEM}
175 | *
176 | * **string rule**
177 | *
178 | * - full rule. (format: 'type;desc;required;default;shorts')
179 | * - rule item position is fixed.
180 | * - if ignore `type`, will use default type: string.
181 | *
182 | * can ignore item use empty:
183 | * - 'type' - only set type.
184 | * - 'type;desc;;' - not set required,default
185 | * - 'type;;;default' - not set required,desc
186 | *
187 | * @param array|string $rule
188 | * @param string $name
189 | * @param int $index
190 | * @param bool $isOption
191 | *
192 | * @return array {@see FlagsParser::DEFINE_ITEM}
193 | * @see argRules
194 | * @see optRules
195 | */
196 | protected function parseRule(array|string $rule, string $name = '', int $index = 0, bool $isOption = true): array
197 | {
198 | if (!$rule) {
199 | $rule = FlagType::STRING;
200 | }
201 |
202 | $shortsFromRule = [];
203 | if (is_array($rule)) {
204 | $item = Arr::replace(FlagsParser::DEFINE_ITEM, $rule);
205 | // set alias by array item
206 | $shortsFromRule = $item['shorts'];
207 | } else { // parse string rule.
208 | $sep = FlagsParser::RULE_SEP;
209 | $item = FlagsParser::DEFINE_ITEM;
210 | $rule = trim((string)$rule, FlagsParser::TRIM_CHARS);
211 |
212 | // not found sep char.
213 | if (!str_contains($rule, $sep)) {
214 | // has multi words, is an desc string.
215 | if (strpos($rule, ' ') > 0) {
216 | $item['desc'] = $rule;
217 | } else { // only type name.
218 | $item['type'] = $rule;
219 | }
220 | } else { // has multi node. eg: 'type;desc;required;default;shorts'
221 | $limit = $isOption ? 5 : 4;
222 | $nodes = Str::splitTrimmed($rule, $sep, $limit);
223 |
224 | // optimize: has multi words, is an desc. auto padding type: string
225 | if (strpos($nodes[0], ' ') > 1) {
226 | array_unshift($nodes, FlagType::STRING);
227 | }
228 |
229 | // first is type.
230 | $item['type'] = $nodes[0];
231 |
232 | // second is desc
233 | if (!empty($nodes[1])) {
234 | $item['desc'] = $nodes[1];
235 | }
236 |
237 | // required
238 | $item['required'] = false;
239 | if (isset($nodes[2]) && ($nodes[2] === 'required' || Str::toBool($nodes[2]))) {
240 | $item['required'] = true;
241 | }
242 |
243 | // default
244 | if (isset($nodes[3]) && $nodes[3] !== '') {
245 | $item['default'] = FlagType::str2ArrValue($nodes[0], $nodes[3]);
246 | }
247 |
248 | // for option: shorts
249 | if ($isOption && isset($nodes[4]) && $nodes[4] !== '') {
250 | $shortsFromRule = Str::explode($nodes[4], ',');
251 | }
252 | }
253 | }
254 |
255 | $name = $name ?: $item['name'];
256 | if ($isOption) {
257 | // parse option name.
258 | [$name, $shorts, $aliases] = $this->parseRuleOptName($name);
259 |
260 | // save shorts and aliases
261 | $item['shorts'] = $shorts ?: $shortsFromRule;
262 | $item['aliases'] = $aliases;
263 | } else {
264 | $item['index'] = $index;
265 | }
266 |
267 | $item['name'] = $name;
268 | return $item;
269 | }
270 |
271 | /**
272 | * Parse option name and shorts
273 | *
274 | * @param string $key 'lang,s' => option name is 'lang', alias 's'
275 | *
276 | * @return array{string, array, array} [name, shorts, aliases]
277 | */
278 | protected function parseRuleOptName(string $key): array
279 | {
280 | $key = trim($key, FlagsParser::TRIM_CHARS);
281 | if (!$key) {
282 | throw new FlagException('flag option name cannot be empty');
283 | }
284 |
285 | // only name.
286 | if (!str_contains($key, ',')) {
287 | $name = ltrim($key, '-');
288 | return [$name, [], []];
289 | }
290 |
291 | $name = '';
292 | $keys = Str::explode($key, ',');
293 |
294 | $shorts = $aliases = [];
295 | foreach ($keys as $k) {
296 | // support like '--name, -n'
297 | $k = ltrim($k, '-');
298 |
299 | // max length string as option name.
300 | if (($kl = strlen($k)) > 1) {
301 | if (!$name) {
302 | $name = $k;
303 | } elseif ($kl > strlen($name)) {
304 | $aliases[] = $name;
305 | // update name
306 | $name = $k;
307 | } else {
308 | $aliases[] = $k;
309 | }
310 | continue;
311 | }
312 |
313 | // one char, as shorts
314 | $shorts[] = $k;
315 | }
316 |
317 | // no long name, first short name as option name.
318 | if (!$name) {
319 | $name = array_shift($shorts);
320 | }
321 |
322 | return [$name, $shorts, $aliases];
323 | }
324 |
325 | /**
326 | * @return array
327 | */
328 | public function getOptRules(): array
329 | {
330 | return $this->optRules;
331 | }
332 |
333 | /**
334 | * @param array $optRules
335 | *
336 | * @see optRules
337 | */
338 | public function setOptRules(array $optRules): void
339 | {
340 | $this->addOptsByRules($optRules);
341 | }
342 |
343 | /**
344 | * @return array
345 | */
346 | public function getArgRules(): array
347 | {
348 | return $this->argRules;
349 | }
350 |
351 | /**
352 | * @param array $argRules
353 | *
354 | * @see argRules
355 | */
356 | public function setArgRules(array $argRules): void
357 | {
358 | $this->addArgsByRules($argRules);
359 | }
360 | }
361 |
--------------------------------------------------------------------------------
/src/Contract/CmdHandlerInterface.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | public function getFlags(): array;
28 |
29 | /**
30 | * @return array
31 | * @psalm-return list
32 | */
33 | public function getRawArgs(): array;
34 |
35 | /**
36 | * @return bool
37 | */
38 | public function isEmpty(): bool;
39 |
40 | /**
41 | * @return bool
42 | */
43 | public function isNotEmpty(): bool;
44 |
45 | /**
46 | * @return bool
47 | */
48 | public function hasShortOpts(): bool;
49 |
50 | /**
51 | * Add option
52 | *
53 | * @param string $name
54 | * @param string $shortcut
55 | * @param string $desc
56 | * @param string $type The argument data type. default is: string. {@see FlagType}
57 | * @param bool $required
58 | * @param mixed|null $default
59 | * @param array{aliases: array, helpType: string} $moreInfo
60 | *
61 | * @return self
62 | */
63 | public function addOpt(
64 | string $name,
65 | string $shortcut,
66 | string $desc,
67 | string $type = '',
68 | bool $required = false,
69 | mixed $default = null,
70 | array $moreInfo = []
71 | ): static;
72 |
73 | /**
74 | * Add an argument
75 | *
76 | * @param string $name
77 | * @param string $desc
78 | * @param string $type The argument data type. default is: string. {@see FlagType}
79 | * @param bool $required
80 | * @param mixed|null $default
81 | * @param array{helpType: string, validator: callable|ValidatorInterface} $moreInfo
82 | *
83 | * @return self
84 | */
85 | public function addArg(
86 | string $name,
87 | string $desc,
88 | string $type = '',
89 | bool $required = false,
90 | mixed $default = null,
91 | array $moreInfo = []
92 | ): static;
93 |
94 | /**
95 | * @param array|null $flags If NULL, will parse the $_SERVER['argv]
96 | *
97 | * @return bool
98 | */
99 | public function parse(?array $flags = null): bool;
100 |
101 | /**
102 | * Whether defined the option
103 | *
104 | * @param string $name
105 | *
106 | * @return bool
107 | */
108 | public function hasOpt(string $name): bool;
109 |
110 | /**
111 | * Whether input argument
112 | *
113 | * @param string $name
114 | *
115 | * @return bool
116 | */
117 | public function hasInputOpt(string $name): bool;
118 |
119 | /**
120 | * Get an option value by name
121 | *
122 | * @param string $name
123 | * @param mixed|null $default
124 | *
125 | * @return mixed
126 | */
127 | public function getOpt(string $name, mixed $default = null): mixed;
128 |
129 | /**
130 | * Must get an option value by name, will throw exception on not input
131 | *
132 | * @param string $name
133 | * @param string $errMsg
134 | *
135 | * @return mixed
136 | */
137 | public function getMustOpt(string $name, string $errMsg = ''): mixed;
138 |
139 | /**
140 | * @param string $name
141 | *
142 | * @return array
143 | * @see FlagsParser::DEFINE_ITEM
144 | */
145 | public function getOptDefine(string $name): array;
146 |
147 | /**
148 | * Set option value, will format and validate value.
149 | *
150 | * @param string $name
151 | * @param mixed $value
152 | *
153 | * @return mixed
154 | */
155 | public function setOpt(string $name, mixed $value): void;
156 |
157 | /**
158 | * Set trusted option value, will not format and validate value.
159 | *
160 | * @param mixed $value
161 | */
162 | public function setTrustedOpt(string $name, mixed $value): void;
163 |
164 | /**
165 | * Whether defined the argument
166 | *
167 | * @param int|string $nameOrIndex
168 | *
169 | * @return bool
170 | */
171 | public function hasArg(int|string $nameOrIndex): bool;
172 |
173 | /**
174 | * Whether input argument
175 | *
176 | * @param int|string $nameOrIndex
177 | *
178 | * @return bool
179 | */
180 | public function hasInputArg(int|string $nameOrIndex): bool;
181 |
182 | /**
183 | * @param int|string $nameOrIndex
184 | *
185 | * @return int Will return -1 if arg not exists
186 | */
187 | public function getArgIndex(int|string $nameOrIndex): int;
188 |
189 | /**
190 | * @param int|string $nameOrIndex
191 | *
192 | * @return array
193 | * @see FlagsParser::DEFINE_ITEM
194 | */
195 | public function getArgDefine(int|string $nameOrIndex): array;
196 |
197 | /**
198 | * Get an argument value by name
199 | *
200 | * @param int|string $nameOrIndex
201 | * @param mixed|null $default
202 | *
203 | * @return mixed
204 | */
205 | public function getArg(int|string $nameOrIndex, mixed $default = null): mixed;
206 |
207 | /**
208 | * Must get an argument value by name, will throw exception on not input
209 | *
210 | * @param int|string $nameOrIndex
211 | * @param string $errMsg
212 | *
213 | * @return mixed
214 | */
215 | public function getMustArg(int|string $nameOrIndex, string $errMsg = ''): mixed;
216 |
217 | /**
218 | * Set trusted argument value, will not format and validate value.
219 | *
220 | * @param int|string $nameOrIndex
221 | * @param mixed $value
222 | *
223 | * @return mixed
224 | */
225 | public function setArg(int|string $nameOrIndex, mixed $value): void;
226 |
227 | /**
228 | * Set trusted argument value, will not format and validate value.
229 | *
230 | * @param string $name
231 | * @param mixed $value
232 | */
233 | public function setTrustedArg(string $name, mixed $value): void;
234 |
235 | /**
236 | * @return array
237 | * @psalm-return array
238 | */
239 | public function getOpts(): array;
240 |
241 | /**
242 | * @return array
243 | */
244 | public function getArgs(): array;
245 |
246 | /**
247 | * Get args help lines data
248 | *
249 | * ```php
250 | * [
251 | * helpName => format desc,
252 | * ]
253 | * ```
254 | *
255 | * @return array
256 | * @psalm-return array
257 | */
258 | public function getArgsHelpLines(): array;
259 |
260 | /**
261 | * Get opts help lines data
262 | *
263 | * ```php
264 | * [
265 | * helpName => format desc,
266 | * ]
267 | * ```
268 | *
269 | * @return array
270 | * @psalm-return array
271 | */
272 | public function getOptsHelpLines(): array;
273 |
274 | public function lock(): void;
275 |
276 | public function unlock(): void;
277 |
278 | /**
279 | * @return bool
280 | */
281 | public function isLocked(): bool;
282 | }
283 |
--------------------------------------------------------------------------------
/src/Contract/ValidatorInterface.php:
--------------------------------------------------------------------------------
1 | flagType = $flagType;
27 |
28 | parent::__construct($message, $code);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Flag/AbstractFlag.php:
--------------------------------------------------------------------------------
1 | default = $default;
158 | $this->required = $required;
159 |
160 | $this->setName($name);
161 | $this->setType($type);
162 | $this->setDesc($desc);
163 | }
164 |
165 | public function init(): void
166 | {
167 | // init default value.
168 | if ($this->default !== null) {
169 | $this->default = FlagType::fmtBasicTypeValue($this->type, $this->default);
170 | $this->value = $this->default;
171 | }
172 |
173 | // support set value from ENV.
174 | if ($this->envVar && ($envVal = OS::getEnvVal($this->envVar))) {
175 | $this->value = FlagType::fmtBasicTypeValue($this->type, $envVal);
176 | }
177 | }
178 |
179 | /**
180 | * @return mixed
181 | */
182 | public function getValue(): mixed
183 | {
184 | return $this->value;
185 | }
186 |
187 | /**
188 | * @param mixed $value
189 | */
190 | public function setTrustedValue(mixed $value): void
191 | {
192 | $this->value = $value;
193 | }
194 |
195 | /**
196 | * @param mixed $value
197 | */
198 | public function setValue(mixed $value): void
199 | {
200 | // format value by type
201 | $value = FlagType::fmtBasicTypeValue($this->type, $value);
202 |
203 | // has validator
204 | $cb = $this->validator;
205 | if ($cb && is_scalar($value)) {
206 | /** @see CondValidator::setFs() */
207 | // if (method_exists($cb, 'setFs')) {
208 | // $cb->setFs($this);
209 | // }
210 |
211 | $ok = true;
212 | $ret = $cb($value, $this->name);
213 |
214 | if (is_array($ret)) {
215 | [$ok, $value] = $ret;
216 | } elseif (is_bool($ret)) {
217 | $ok = $ret;
218 | }
219 |
220 | if (false === $ok) {
221 | $kind = $this->getKind();
222 | throw new FlagException("set invalid value for flag $kind: " . $this->getNameMark());
223 | }
224 | }
225 |
226 | if ($this->isArray()) {
227 | if (is_array($value)) {
228 | $this->value = $value;
229 | } else {
230 | $this->value[] = $value;
231 | }
232 | } else {
233 | $this->value = $value;
234 | }
235 | }
236 |
237 | /**
238 | * @return bool
239 | */
240 | public function hasValue(): bool
241 | {
242 | return $this->value !== null;
243 | }
244 |
245 | /**
246 | * @return bool
247 | */
248 | public function hasDefault(): bool
249 | {
250 | return $this->default !== null;
251 | }
252 |
253 | /**
254 | * @return string
255 | */
256 | public function getNameMark(): string
257 | {
258 | return $this->name;
259 | }
260 |
261 | /**
262 | * @return string
263 | */
264 | public function getKind(): string
265 | {
266 | return FlagsParser::KIND_OPT;
267 | }
268 |
269 | /******************************************************************
270 | * getter/setter methods
271 | *****************************************************************/
272 |
273 | /**
274 | * @return string
275 | */
276 | public function getType(): string
277 | {
278 | return $this->type;
279 | }
280 |
281 | /**
282 | * @param string $type
283 | */
284 | public function setType(string $type): void
285 | {
286 | if (!$type) {
287 | return;
288 | }
289 |
290 | if (!FlagType::isValid($type)) {
291 | $kind = $this->getKind();
292 | $name = $this->getNameMark();
293 | throw new FlagException("invalid flag type '$type', $kind: $name");
294 | }
295 |
296 | $this->type = $type;
297 | }
298 |
299 | /**
300 | * @return string
301 | */
302 | public function getName(): string
303 | {
304 | return $this->name;
305 | }
306 |
307 | /**
308 | * @param string $name
309 | */
310 | public function setName(string $name): void
311 | {
312 | if (!FlagUtil::isValidName($name)) {
313 | throw new FlagException("invalid flag option name: $name");
314 | }
315 |
316 | $this->name = $name;
317 | }
318 |
319 | /**
320 | * @return array|false|float|int|string|null
321 | */
322 | public function getTypeDefault(): float|bool|int|array|string|null
323 | {
324 | return FlagType::getDefault($this->type);
325 | }
326 |
327 | /**
328 | * @return mixed
329 | */
330 | public function getDefault(): mixed
331 | {
332 | return $this->default;
333 | }
334 |
335 | /**
336 | * @param mixed $default
337 | */
338 | public function setDefault(mixed $default): void
339 | {
340 | $this->default = $default;
341 | }
342 |
343 | /**
344 | * @param string $desc
345 | */
346 | public function setDesc(string $desc): void
347 | {
348 | $this->desc = $desc;
349 | }
350 |
351 | /**
352 | * @return array
353 | */
354 | public function toArray(): array
355 | {
356 | return [
357 | 'name' => $this->name,
358 | 'desc' => $this->desc,
359 | 'type' => $this->type,
360 | 'default' => $this->default,
361 | 'envVar' => $this->envVar,
362 | 'required' => $this->required,
363 | 'validator' => $this->validator,
364 | 'isArray' => $this->isArray(),
365 | 'helpType' => $this->getHelpType(),
366 | ];
367 | }
368 |
369 | /**
370 | * @return bool
371 | */
372 | public function isArray(): bool
373 | {
374 | return FlagType::isArray($this->type);
375 | }
376 |
377 | /**
378 | * @return bool
379 | */
380 | public function isRequired(): bool
381 | {
382 | return $this->required;
383 | }
384 |
385 | /**
386 | * @return bool
387 | */
388 | public function isOptional(): bool
389 | {
390 | return $this->required === false;
391 | }
392 |
393 | /**
394 | * @param bool $required
395 | */
396 | public function setRequired(bool $required): void
397 | {
398 | $this->required = $required;
399 | }
400 |
401 | /**
402 | * @param bool $useTypeOnEmpty
403 | *
404 | * @return string
405 | */
406 | public function getHelpType(bool $useTypeOnEmpty = false): string
407 | {
408 | if ($useTypeOnEmpty) {
409 | return $this->helpType ?: $this->type;
410 | }
411 |
412 | return $this->helpType;
413 | }
414 |
415 | /**
416 | * @param string $helpType
417 | */
418 | public function setHelpType(string $helpType): void
419 | {
420 | if ($helpType) {
421 | $this->helpType = $helpType;
422 | }
423 | }
424 |
425 | /**
426 | * @param callable|null $validator
427 | */
428 | public function setValidator(?callable $validator): void
429 | {
430 | if ($validator) {
431 | $this->validator = $validator;
432 | }
433 | }
434 |
435 | /**
436 | * @return callable|ValidatorInterface|null
437 | */
438 | public function getValidator(): callable|ValidatorInterface|null
439 | {
440 | return $this->validator;
441 | }
442 |
443 | /**
444 | * @return string
445 | */
446 | public function getEnvVar(): string
447 | {
448 | return $this->envVar;
449 | }
450 |
451 | /**
452 | * @param string $envVar
453 | */
454 | public function setEnvVar(string $envVar): void
455 | {
456 | $this->envVar = trim($envVar);
457 | }
458 | }
459 |
--------------------------------------------------------------------------------
/src/Flag/Argument.php:
--------------------------------------------------------------------------------
1 | index, $name);
41 | throw new FlagException("invalid flag argument name: $mark");
42 | }
43 |
44 | $this->name = $name;
45 | }
46 | }
47 |
48 | public function toArray(): array
49 | {
50 | $info = parent::toArray();
51 |
52 | $info['index'] = $this->index;
53 | return $info;
54 | }
55 |
56 | /**
57 | * @return string
58 | */
59 | public function getKind(): string
60 | {
61 | return FlagsParser::KIND_ARG;
62 | }
63 |
64 | /**
65 | * @return string
66 | */
67 | public function getNameMark(): string
68 | {
69 | $name = $this->name;
70 | $mark = $name ? "($name)" : '';
71 |
72 | return sprintf('#%d%s', $this->index, $mark);
73 | }
74 |
75 | /**
76 | * @param bool $forHelp
77 | *
78 | * @return string
79 | */
80 | public function getDesc(bool $forHelp = false): string
81 | {
82 | $desc = $this->desc;
83 | if ($forHelp) {
84 | $desc = $desc ? Str::ucfirst($desc) : 'Argument ' . $this->index;
85 | }
86 |
87 | return $desc;
88 | }
89 |
90 | /**
91 | * @return string
92 | */
93 | public function getHelpName(): string
94 | {
95 | return $this->name ?: 'arg' . $this->index;
96 | }
97 |
98 | /**
99 | * @return int
100 | */
101 | public function getIndex(): int
102 | {
103 | return $this->index;
104 | }
105 |
106 | /**
107 | * @param int $index
108 | */
109 | public function setIndex(int $index): void
110 | {
111 | $this->index = $index;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Flag/Arguments.php:
--------------------------------------------------------------------------------
1 | type === FlagType::BOOL;
62 | }
63 |
64 | /**
65 | * @return string[]
66 | */
67 | public function getAliases(): array
68 | {
69 | return $this->aliases;
70 | }
71 |
72 | /**
73 | * @param string[] $aliases
74 | */
75 | public function setAliases(array $aliases): void
76 | {
77 | foreach ($aliases as $alias) {
78 | $this->setAlias($alias);
79 | }
80 | }
81 |
82 | /**
83 | * @param string $alias
84 | */
85 | public function setAlias(string $alias): void
86 | {
87 | if (!$alias) {
88 | return;
89 | }
90 |
91 | if (!FlagUtil::isValidName($alias)) {
92 | throw new FlagException('invalid option alias: ' . $alias);
93 | }
94 |
95 | if (strlen($alias) < 2) {
96 | throw new FlagException('flag option alias length cannot be < 2 ');
97 | }
98 |
99 | $this->aliases[] = $alias;
100 | }
101 |
102 | /**
103 | * @return string
104 | */
105 | public function getShortcut(): string
106 | {
107 | return $this->shortcut;
108 | }
109 |
110 | /**
111 | * @param string $shortcut eg: 'a,b' Or '-a,-b' Or '-a, -b'
112 | */
113 | public function setShortcut(string $shortcut): void
114 | {
115 | $shortcuts = preg_split('{,\s?-?}', ltrim($shortcut, '-'));
116 |
117 | $this->setShorts(array_filter($shortcuts));
118 | }
119 |
120 | /**
121 | * @return array
122 | */
123 | public function getShorts(): array
124 | {
125 | return $this->shorts;
126 | }
127 |
128 | /**
129 | * @param array $shorts
130 | */
131 | public function setShorts(array $shorts): void
132 | {
133 | if ($shorts) {
134 | $this->shorts = $shorts;
135 | $this->shortcut = '-' . implode(', -', $shorts);
136 | }
137 | }
138 |
139 | /**
140 | * @param bool $forHelp
141 | *
142 | * @return string
143 | */
144 | public function getDesc(bool $forHelp = false): string
145 | {
146 | $desc = $this->desc;
147 | if ($forHelp) {
148 | $desc = $desc ? Str::ucfirst($desc) : 'Option ' . $this->name;
149 | }
150 |
151 | return $desc;
152 | }
153 |
154 | /**
155 | * @return string
156 | */
157 | public function getHelpName(): string
158 | {
159 | $longs = $this->aliases;
160 | // append name
161 | $longs[] = $this->name;
162 |
163 | // prepend '--'
164 | $nodes = array_map(static function (string $name) {
165 | return (strlen($name) > 1 ? '--' : '-') . $name;
166 | }, $longs);
167 |
168 | if ($this->shortcut) {
169 | array_unshift($nodes, $this->shortcut);
170 | }
171 |
172 | return implode(', ', $nodes);
173 | }
174 |
175 | /**
176 | * @return bool
177 | */
178 | public function isHidden(): bool
179 | {
180 | return $this->hidden;
181 | }
182 |
183 | /**
184 | * @param bool $hidden
185 | */
186 | public function setHidden(bool $hidden): void
187 | {
188 | $this->hidden = $hidden;
189 | }
190 |
191 | /**
192 | * @return array
193 | */
194 | public function toArray(): array
195 | {
196 | $info = parent::toArray();
197 |
198 | $info['aliases'] = $this->aliases;
199 | $info['shorts'] = $this->shorts;
200 | return $info;
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/src/Flag/Options.php:
--------------------------------------------------------------------------------
1 | 2,
56 | self::INTS => 3,
57 | self::STRINGS => 3,
58 | ];
59 |
60 | public const TYPES_MAP = [
61 | self::INT => 1,
62 | self::BOOL => 1,
63 | self::FLOAT => 1,
64 | self::STRING => 1,
65 |
66 | // ------ complex types ------
67 | self::ARRAY => 2,
68 | self::OBJECT => 2,
69 | self::CALLABLE => 2,
70 |
71 | // ------ extend types ------
72 | self::INTS => 3,
73 | self::STRINGS => 3,
74 | self::MIXED => 3,
75 | self::CUSTOM => 3,
76 | self::UNKNOWN => 3,
77 | ];
78 |
79 | public const TYPE_HELP_NAME = [
80 | // self::INTS => 'int...',
81 | // self::STRINGS => 'string...',
82 | self::ARRAY => '',
83 | self::BOOL => '',
84 | ];
85 |
86 | /**
87 | * @param string $type
88 | *
89 | * @return bool
90 | */
91 | public static function isValid(string $type): bool
92 | {
93 | return isset(self::TYPES_MAP[$type]);
94 | }
95 |
96 | /**
97 | * @param string $type
98 | *
99 | * @return bool
100 | */
101 | public static function isArray(string $type): bool
102 | {
103 | return isset(self::ARRAY_TYPES[$type]);
104 | }
105 |
106 | /**
107 | * @param string $type
108 | * @param bool $toUpper
109 | *
110 | * @return string
111 | */
112 | public static function getHelpName(string $type, bool $toUpper = true): string
113 | {
114 | $name = self::TYPE_HELP_NAME[$type] ?? $type;
115 |
116 | return $toUpper ? strtoupper($name) : $name;
117 | }
118 |
119 | /**
120 | * Get type default value.
121 | *
122 | * @param string $type
123 | *
124 | * @return array|false|float|int|string|null
125 | */
126 | public static function getDefault(string $type): float|bool|int|array|string|null
127 | {
128 | return match ($type) {
129 | self::INT => 0,
130 | self::BOOL => false,
131 | self::FLOAT => 0.0,
132 | self::STRING => '',
133 | self::INTS, self::ARRAY, self::STRINGS => [],
134 | default => null,
135 | };
136 | }
137 |
138 | /**
139 | * @param string $type
140 | * @param mixed $value
141 | *
142 | * @return mixed
143 | */
144 | public static function fmtBasicTypeValue(string $type, mixed $value): mixed
145 | {
146 | if (!is_scalar($value)) {
147 | return $value;
148 | }
149 |
150 | // convert to bool
151 | if ($type === self::BOOL) {
152 | $value = is_string($value) ? Str::tryToBool($value) : (bool)$value;
153 |
154 | if (is_string($value)) {
155 | throw new FlagException("convert value '$value' to bool failed");
156 | }
157 | return $value;
158 | }
159 |
160 | // format value by type
161 | return match ($type) {
162 | self::INT, self::INTS => (int)$value,
163 | // self::BOOL => is_string($value) ? Str::toBool2($value) : (bool)$value,
164 | self::FLOAT => (float)$value,
165 | self::STRING, self::STRINGS => (string)$value,
166 | default => $value,
167 | };
168 | }
169 |
170 | /**
171 | * Convert string to array
172 | *
173 | * - eg: '23, 45' => [23, 45]
174 | * - eg: 'a, b' => ['a', 'b']
175 | * - eg: '[a, b]' => ['a', 'b']
176 | *
177 | * @param string $type
178 | * @param string $str
179 | *
180 | * @return array|string
181 | */
182 | public static function str2ArrValue(string $type, string $str): array|string
183 | {
184 | return match ($type) {
185 | self::INTS => Str::toInts(trim($str, '[] ')),
186 | self::ARRAY, self::STRINGS => Str::toArray(trim($str, '[] ')),
187 | default => $str,
188 | };
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/FlagUtil.php:
--------------------------------------------------------------------------------
1 | 1 ? '--' : '-') . $name;
42 | }, $names);
43 |
44 | return implode(', ', $nodes);
45 | }
46 |
47 | /**
48 | * check and get option Name
49 | *
50 | * valid:
51 | * `-a`
52 | * `-b=value`
53 | * `--long`
54 | * `--long=value1`
55 | *
56 | * invalid:
57 | * - empty string
58 | * - no prefix '-' (is argument)
59 | * - invalid option name as argument. eg: '-9' '--34' '- '
60 | *
61 | * @param string $val
62 | *
63 | * @return string
64 | */
65 | public static function filterOptionName(string $val): string
66 | {
67 | // is not an option.
68 | if ('' === $val || $val[0] !== '-') {
69 | return '';
70 | }
71 |
72 | $name = ltrim($val, '- ');
73 | if (is_numeric($name)) {
74 | return '';
75 | }
76 |
77 | return $name;
78 | }
79 |
80 | /**
81 | * @param int $val1
82 | * @param int $val2
83 | *
84 | * @return int
85 | */
86 | public static function getMaxInt(int $val1, int $val2): int
87 | {
88 | return max($val1, $val2);
89 | }
90 |
91 | /**
92 | * @param bool $refresh
93 | *
94 | * @return string
95 | */
96 | public static function getBinName(bool $refresh = false): string
97 | {
98 | if (!$refresh && self::$scriptName !== null) {
99 | return self::$scriptName;
100 | }
101 |
102 | $scriptName = '';
103 | if (isset($_SERVER['argv']) && ($argv = $_SERVER['argv'])) {
104 | $scriptFile = array_shift($argv);
105 | $scriptName = basename($scriptFile);
106 | }
107 |
108 | self::$scriptName = $scriptName;
109 | return self::$scriptName;
110 | }
111 |
112 | /**
113 | * check input is valid option value
114 | *
115 | * @param mixed $val
116 | *
117 | * @return bool
118 | */
119 | public static function isOptionValue(mixed $val): bool
120 | {
121 | if ($val === false) {
122 | return false;
123 | }
124 |
125 | // if is: '', 0 || is not option name
126 | if (!$val || $val[0] !== '-') {
127 | return true;
128 | }
129 |
130 | // ensure is option value.
131 | if (!str_contains($val, '=')) {
132 | return true;
133 | }
134 |
135 | // is string value, but contains '='
136 | [$name,] = explode('=', $val, 2);
137 |
138 | // named argument OR invalid: 'some = string'
139 | return false === self::isValidName($name);
140 | }
141 |
142 | /**
143 | * @param string $name
144 | *
145 | * @return bool
146 | */
147 | public static function isValidName(string $name): bool
148 | {
149 | return preg_match('#^[a-zA-Z_][\w-]{0,36}$#', $name) === 1;
150 | }
151 |
152 | /**
153 | * Escapes a token through escape shell arg if it contains unsafe chars.
154 | *
155 | * @param string $token
156 | *
157 | * @return string
158 | */
159 | public static function escapeToken(string $token): string
160 | {
161 | return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
162 | }
163 |
164 | /**
165 | * Align command option names.
166 | *
167 | * @param array $options
168 | *
169 | * @return array
170 | */
171 | public static function alignOptions(array $options): array
172 | {
173 | if (!$options) {
174 | return [];
175 | }
176 |
177 | // check has short option. e.g '-h, --help'
178 | $nameString = '|' . implode('|', array_keys($options));
179 | if (preg_match('/\|-\w/', $nameString) !== 1) {
180 | return $options;
181 | }
182 |
183 | $formatted = [];
184 | foreach ($options as $name => $des) {
185 | if (!$name = trim($name, ', ')) {
186 | continue;
187 | }
188 |
189 | // start with '--', padding length equals to '-h, '
190 | if (isset($name[1]) && $name[1] === '-') {
191 | $name = ' ' . $name;
192 | } else {
193 | $name = str_replace([',-'], [', -'], $name);
194 | }
195 |
196 | $formatted[$name] = $des;
197 | }
198 |
199 | return $formatted;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/FlagsParser.php:
--------------------------------------------------------------------------------
1 | '',
65 | 'desc' => '',
66 | 'type' => FlagType::STRING,
67 | 'helpType' => '', // use for render help
68 | // 'index' => 0, // only for argument
69 | 'required' => false,
70 | 'envVar' => '', // support read value from ENV var
71 | 'default' => null,
72 | 'shorts' => [], // only for option. ['a', 'b']
73 | 'aliases' => [], // only for option. ['cd', 'ef']
74 | 'hidden' => false, // only for option
75 | // value validator
76 | 'validator' => null,
77 | // 'category' => null
78 | ];
79 |
80 | /**
81 | * If locked, cannot add option and argument
82 | *
83 | * @var bool
84 | */
85 | protected bool $locked = false;
86 |
87 | /**
88 | * @var bool Mark option is parsed
89 | */
90 | protected bool $parsed = false;
91 |
92 | /**
93 | * @var int
94 | */
95 | protected int $parseStatus = self::STATUS_OK;
96 |
97 | /**
98 | * The input flags
99 | *
100 | * @var string[]
101 | */
102 | protected array $flags = [];
103 |
104 | /**
105 | * The raw args, after option parsed from {@see $flags}
106 | *
107 | * @var string[]
108 | */
109 | protected array $rawArgs = [];
110 |
111 | /**
112 | * The overage raw args, after argument parsed from {@see $rawArgs}
113 | *
114 | * @var string[]
115 | */
116 | protected array $remainArgs = [];
117 |
118 | /**
119 | * The required option names.
120 | *
121 | * @var string[]
122 | */
123 | protected array $requiredOpts = [];
124 |
125 | // -------------------- settings for show help --------------------
126 |
127 | /**
128 | * The description. use for show help
129 | *
130 | * @var string
131 | */
132 | protected string $desc = '';
133 |
134 | /**
135 | * The bin script name. use for show help
136 | *
137 | * @var string
138 | */
139 | protected string $scriptName = '';
140 |
141 | /**
142 | * The bin script file. use for show help
143 | *
144 | * @var string
145 | */
146 | protected string $scriptFile = '';
147 |
148 | /**
149 | * settings and metadata information
150 | *
151 | * @var array
152 | */
153 | protected array $settings = [
154 | 'hasShorts' => false,
155 | // some setting for render help
156 | 'argNameLen' => 12,
157 | 'optNameLen' => 12,
158 | 'descNlOnOptLen' => self::OPT_MAX_WIDTH,
159 | // more settings
160 | 'exampleHelp' => '',
161 | 'moreHelp' => '',
162 | ];
163 |
164 | /**
165 | * Delay call validators after parsed. TODO
166 | *
167 | * @var bool
168 | */
169 | // protected $delayValidate = false;
170 |
171 | // -------------------- settings for parse option --------------------
172 |
173 | /**
174 | * Special short option style
175 | *
176 | * - gnu: `-abc` will expand: `-a -b -c`
177 | * - posix: `-abc` will expand: `-a=bc`
178 | *
179 | * @var string
180 | */
181 | protected string $shortStyle = self::SHORT_STYLE_GUN;
182 |
183 | /**
184 | * Stop parse option on found first argument.
185 | *
186 | * - Useful for support multi commands. eg: `top --opt ... sub --opt ...`
187 | *
188 | * @var bool
189 | */
190 | protected bool $stopOnFistArg = true;
191 |
192 | /**
193 | * Skip on found undefined option.
194 | *
195 | * - FALSE will throw FlagException error.
196 | * - TRUE will skip it and collect as raw arg, then continue parse next.
197 | *
198 | * @var bool
199 | */
200 | protected bool $skipOnUndefined = false;
201 |
202 | // -------------------- settings for parse argument --------------------
203 |
204 | /**
205 | * Whether auto bind remaining args after option parsed
206 | *
207 | * @var bool
208 | */
209 | protected bool $autoBindArgs = true;
210 |
211 | /**
212 | * Strict match args number.
213 | * if exist unbind args, will throw FlagException
214 | *
215 | * @var bool
216 | */
217 | protected bool $strictMatchArgs = false;
218 |
219 | /**
220 | * Has array argument
221 | *
222 | * @var bool
223 | */
224 | protected bool $arrayArg = false;
225 |
226 | /**
227 | * Has optional argument
228 | *
229 | * @var bool
230 | */
231 | protected bool $optionalArg = false;
232 |
233 | /**
234 | * Class constructor.
235 | *
236 | * @param array $config
237 | */
238 | public function __construct(array $config = [])
239 | {
240 | Obj::init($this, $config);
241 | }
242 |
243 | /**
244 | * @param string $cmdline
245 | * @param bool $hasBin
246 | *
247 | * @return bool
248 | */
249 | public function parseCmdline(string $cmdline, bool $hasBin = true): bool
250 | {
251 | $flags = LineParser::parseIt($cmdline);
252 |
253 | if ($hasBin && $flags) {
254 | $sFile = array_shift($flags);
255 | $this->setScriptFile($sFile);
256 | }
257 |
258 | return $this->parse($flags);
259 | }
260 |
261 | /**
262 | * @param array|null $flags
263 | *
264 | * @return bool
265 | */
266 | public function parse(?array $flags = null): bool
267 | {
268 | if ($this->parsed) {
269 | return $this->parseStatus === self::STATUS_OK;
270 | }
271 |
272 | $this->parsed = true;
273 | $this->rawArgs = [];
274 |
275 | if ($flags === null) {
276 | $flags = $_SERVER['argv'];
277 | $sFile = array_shift($flags);
278 | $this->setScriptFile($sFile);
279 | } else {
280 | $flags = array_values($flags);
281 | }
282 |
283 | $this->flags = $flags;
284 | return $this->doParse($flags);
285 | }
286 |
287 | /**
288 | * @param array $flags
289 | *
290 | * @return bool
291 | */
292 | abstract protected function doParse(array $flags): bool;
293 |
294 | /**
295 | * @param array $rawArgs
296 | *
297 | * @return array
298 | */
299 | protected function parseRawArgs(array $rawArgs): array
300 | {
301 | $args = [];
302 |
303 | // parse arguments
304 | foreach ($rawArgs as $arg) {
305 | // value specified inline (=)
306 | if (strpos($arg, '=') > 0) {
307 | [$name, $value] = explode('=', $arg, 2);
308 |
309 | // ensure is valid name.
310 | if (FlagUtil::isValidName($name)) {
311 | $args[$name] = $value;
312 | } else {
313 | $args[] = $arg;
314 | }
315 | } else {
316 | $args[] = $arg;
317 | }
318 | }
319 |
320 | return $args;
321 | }
322 |
323 | /**
324 | * @param mixed|null $default
325 | *
326 | * @return mixed
327 | */
328 | public function getFirstArg(mixed $default = null): mixed
329 | {
330 | return $this->getArg(0, $default);
331 | }
332 |
333 | public function resetResults(): void
334 | {
335 | // clear match results
336 | $this->parsed = false;
337 | $this->rawArgs = $this->flags = [];
338 | }
339 |
340 | /**
341 | * @return bool
342 | */
343 | public function isEmpty(): bool
344 | {
345 | return !$this->isNotEmpty();
346 | }
347 |
348 | /**
349 | * @return bool
350 | */
351 | public function hasShortOpts(): bool
352 | {
353 | return $this->countAlias() > 0;
354 | }
355 |
356 | /****************************************************************
357 | * build and render help
358 | ***************************************************************/
359 |
360 | /**
361 | * display help messages
362 | */
363 | public function displayHelp(): void
364 | {
365 | if ($fn = $this->helpRenderer) {
366 | $fn($this);
367 | return;
368 | }
369 |
370 | Cli::println($this->buildHelp());
371 | }
372 |
373 | /**
374 | * @return string
375 | */
376 | public function toString(): string
377 | {
378 | return $this->buildHelp();
379 | }
380 |
381 | /**
382 | * @return string
383 | */
384 | public function __toString(): string
385 | {
386 | return $this->buildHelp();
387 | }
388 |
389 | /**
390 | * @param bool $withColor
391 | *
392 | * @return string
393 | */
394 | abstract public function buildHelp(bool $withColor = true): string;
395 |
396 | /**
397 | * @param string $name
398 | * @param string $sep
399 | *
400 | * @return string[]
401 | */
402 | public function getOptStrAsArray(string $name, string $sep = ','): array
403 | {
404 | $str = $this->getOpt($name);
405 |
406 | return $str ? Str::toNoEmptyArray($str, $sep) : [];
407 | }
408 |
409 | /**
410 | * @param string $name
411 | * @param string $sep
412 | *
413 | * @return int[]
414 | */
415 | public function getOptStrAsInts(string $name, string $sep = ','): array
416 | {
417 | $str = $this->getOpt($name);
418 |
419 | return $str ? Str::toInts($str, $sep) : [];
420 | }
421 |
422 | /****************************************************************
423 | * getter/setter methods
424 | ***************************************************************/
425 |
426 | /**
427 | * @return array
428 | */
429 | public function getRequiredOpts(): array
430 | {
431 | return $this->requiredOpts;
432 | }
433 |
434 | /**
435 | * @return string
436 | */
437 | public function getName(): string
438 | {
439 | return $this->getScriptName();
440 | }
441 |
442 | /**
443 | * @param string $name
444 | */
445 | public function setName(string $name): void
446 | {
447 | $this->setScriptName($name);
448 | }
449 |
450 | /**
451 | * @return string
452 | */
453 | public function getDesc(): string
454 | {
455 | return $this->desc;
456 | }
457 |
458 | /**
459 | * @param string $desc
460 | * @param bool $setOnEmpty only set on desc is empty
461 | */
462 | public function setDesc(string $desc, bool $setOnEmpty = false): void
463 | {
464 | // only set on desc is empty
465 | if ($setOnEmpty && $this->desc) {
466 | return;
467 | }
468 |
469 | if ($desc) {
470 | $this->desc = $desc;
471 | }
472 | }
473 |
474 | /**
475 | * @return array
476 | */
477 | public function getFlags(): array
478 | {
479 | return $this->flags;
480 | }
481 |
482 | /**
483 | * @return array
484 | */
485 | public function getRawArgs(): array
486 | {
487 | return $this->rawArgs;
488 | }
489 |
490 | /**
491 | * @return string[]
492 | */
493 | public function getRemainArgs(): array
494 | {
495 | return $this->remainArgs;
496 | }
497 |
498 | /**
499 | * @return string
500 | */
501 | public function popFirstRawArg(): string
502 | {
503 | return array_shift($this->rawArgs);
504 | }
505 |
506 | /**
507 | * @param bool $more
508 | *
509 | * @return array
510 | */
511 | public function getInfo(bool $more = false): array
512 | {
513 | $info = [
514 | 'driver' => static::class,
515 | 'flags' => $this->flags,
516 | 'rawArgs' => $this->rawArgs,
517 | 'remainArgs' => $this->remainArgs,
518 | 'opts' => $this->getOpts(),
519 | 'args' => $this->getArgs(),
520 | ];
521 |
522 | if ($more) {
523 | $info['optRules'] = $this->getOptRules();
524 | $info['argRules'] = $this->getArgRules();
525 | $info['aliases'] = $this->getAliases();
526 | }
527 |
528 | return $info;
529 | }
530 |
531 | /**
532 | * @return bool
533 | */
534 | public function isLocked(): bool
535 | {
536 | return $this->locked;
537 | }
538 |
539 | public function lock(): void
540 | {
541 | $this->locked = true;
542 | }
543 |
544 | public function unlock(): void
545 | {
546 | $this->locked = false;
547 | }
548 |
549 | /**
550 | * @param bool $locked
551 | */
552 | public function setLocked(bool $locked): void
553 | {
554 | $this->locked = $locked;
555 | }
556 |
557 | /**
558 | * @return bool
559 | */
560 | public function isParsed(): bool
561 | {
562 | return $this->parsed;
563 | }
564 |
565 | /**
566 | * @return int
567 | */
568 | public function getParseStatus(): int
569 | {
570 | return $this->parseStatus;
571 | }
572 |
573 | /**
574 | * @return bool
575 | */
576 | public function isStopOnFistArg(): bool
577 | {
578 | return $this->stopOnFistArg;
579 | }
580 |
581 | /**
582 | * @param bool $stopOnFistArg
583 | */
584 | public function setStopOnFistArg(bool $stopOnFistArg): void
585 | {
586 | $this->stopOnFistArg = $stopOnFistArg;
587 | }
588 |
589 | /**
590 | * @return bool
591 | */
592 | public function isSkipOnUndefined(): bool
593 | {
594 | return $this->skipOnUndefined;
595 | }
596 |
597 | /**
598 | * @param bool $skipOnUndefined
599 | */
600 | public function setSkipOnUndefined(bool $skipOnUndefined): void
601 | {
602 | $this->skipOnUndefined = $skipOnUndefined;
603 | }
604 |
605 | /**
606 | * @return bool
607 | */
608 | public function hasOptionalArg(): bool
609 | {
610 | return $this->optionalArg;
611 | }
612 |
613 | /**
614 | * @return bool
615 | */
616 | public function hasArrayArg(): bool
617 | {
618 | return $this->arrayArg;
619 | }
620 |
621 | /**
622 | * @return bool
623 | */
624 | public function isAutoBindArgs(): bool
625 | {
626 | return $this->autoBindArgs;
627 | }
628 |
629 | /**
630 | * @param bool $autoBindArgs
631 | */
632 | public function setAutoBindArgs(bool $autoBindArgs): void
633 | {
634 | $this->autoBindArgs = $autoBindArgs;
635 | }
636 |
637 | /**
638 | * @return bool
639 | */
640 | public function isStrictMatchArgs(): bool
641 | {
642 | return $this->strictMatchArgs;
643 | }
644 |
645 | /**
646 | * @param bool $strictMatchArgs
647 | */
648 | public function setStrictMatchArgs(bool $strictMatchArgs): void
649 | {
650 | $this->strictMatchArgs = $strictMatchArgs;
651 | }
652 |
653 | /**
654 | * @return string
655 | */
656 | public function getScriptFile(): string
657 | {
658 | return $this->scriptFile;
659 | }
660 |
661 | /**
662 | * @param string $scriptFile
663 | */
664 | public function setScriptFile(string $scriptFile): void
665 | {
666 | if ($scriptFile) {
667 | $this->scriptFile = $scriptFile;
668 | $this->scriptName = basename($scriptFile);
669 | }
670 | }
671 |
672 | /**
673 | * @param string $key
674 | * @param mixed $value
675 | */
676 | public function set(string $key, mixed $value): void
677 | {
678 | $this->settings[$key] = $value;
679 | }
680 |
681 | /**
682 | * @return array
683 | */
684 | public function getSettings(): array
685 | {
686 | return $this->settings;
687 | }
688 |
689 | /**
690 | * @param array $settings
691 | */
692 | public function setSettings(array $settings): void
693 | {
694 | $this->settings = array_merge($this->settings, $settings);
695 | }
696 |
697 | /**
698 | * @return string
699 | */
700 | public function getScriptName(): string
701 | {
702 | return $this->scriptName ?: FlagUtil::getBinName();
703 | }
704 |
705 | /**
706 | * @param string $scriptName
707 | */
708 | public function setScriptName(string $scriptName): void
709 | {
710 | $this->scriptName = $scriptName;
711 | }
712 | }
713 |
--------------------------------------------------------------------------------
/src/Helper/ValueBinding.php:
--------------------------------------------------------------------------------
1 | checkInput($value, $name);
28 | }
29 |
30 | /**
31 | * @param mixed $value
32 | * @param string $name
33 | *
34 | * @return bool
35 | */
36 | abstract public function checkInput(mixed $value, string $name): bool;
37 | }
38 |
--------------------------------------------------------------------------------
/src/Validator/CondValidator.php:
--------------------------------------------------------------------------------
1 | condFn;
41 | if ($condFn && !$condFn($this->fs)) {
42 | return true;
43 | }
44 |
45 | return $this->checkInput($value, $name);
46 | }
47 |
48 | /**
49 | * @return FlagsParser
50 | */
51 | public function getFs(): FlagsParser
52 | {
53 | return $this->fs;
54 | }
55 |
56 | /**
57 | * @param FlagsParser $fs
58 | */
59 | public function setFs(FlagsParser $fs): void
60 | {
61 | $this->fs = $fs;
62 | }
63 |
64 | /**
65 | * @param mixed $condFn
66 | *
67 | * @return static
68 | */
69 | public function setCondFn(mixed $condFn): self
70 | {
71 | $this->condFn = $condFn;
72 | return $this;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Validator/EmptyValidator.php:
--------------------------------------------------------------------------------
1 | enums = $enums;
56 | }
57 |
58 | /**
59 | * @param mixed $value
60 | * @param string $name
61 | *
62 | * @return bool
63 | */
64 | public function checkInput(mixed $value, string $name): bool
65 | {
66 | if (in_array($value, $this->enums, true)) {
67 | return true;
68 | }
69 |
70 | throw new FlagException("flag '$name' value must be in: " . implode(',', $this->enums));
71 | }
72 |
73 | /**
74 | * @return string
75 | */
76 | public function __toString(): string
77 | {
78 | return 'Allow: ' . implode(',', $this->enums);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Validator/FuncValidator.php:
--------------------------------------------------------------------------------
1 | func;
36 |
37 | return $fn($value, $name);
38 | }
39 |
40 | /**
41 | * @return string
42 | */
43 | public function __toString(): string
44 | {
45 | return $this->tipMsg;
46 | }
47 |
48 | /**
49 | * @param callable $func
50 | *
51 | * @return FuncValidator
52 | */
53 | public function setFunc(callable $func): self
54 | {
55 | $this->func = $func;
56 | return $this;
57 | }
58 |
59 | /**
60 | * @param string $tipMsg
61 | *
62 | * @return FuncValidator
63 | */
64 | public function setTipMsg(string $tipMsg): self
65 | {
66 | $this->tipMsg = $tipMsg;
67 | return $this;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Validator/LenValidator.php:
--------------------------------------------------------------------------------
1 | min = $min;
55 | $this->max = $max;
56 | }
57 |
58 | /**
59 | * @param mixed $value
60 | * @param string $name
61 | *
62 | * @return bool
63 | */
64 | public function checkInput(mixed $value, string $name): bool
65 | {
66 | if (is_string($value)) {
67 | $len = strlen(trim($value));
68 | } elseif (is_array($value)) {
69 | $len = count($value);
70 | } else {
71 | return false;
72 | }
73 |
74 | // if ($this->min !== null && $this->max !== null) {
75 | // return sprintf('Len: %d - %d', $this->min, $this->max);
76 | // }
77 | //
78 | // if ($this->min !== null) {
79 | // return sprintf('Len: >= %d', $this->min);
80 | // }
81 | //
82 | // if ($this->max !== null) {
83 | // return sprintf('Len: <= %d', $this->max);
84 | // }
85 |
86 | // if (empty($value)) {
87 | // throw new FlagException("flag '$name' value cannot be empty");
88 | // }
89 |
90 | return true;
91 | }
92 |
93 | /**
94 | * @return string
95 | */
96 | public function __toString(): string
97 | {
98 | if ($this->min !== null && $this->max !== null) {
99 | return sprintf('Len: %d - %d', $this->min, $this->max);
100 | }
101 |
102 | if ($this->min !== null) {
103 | return sprintf('Len: >= %d', $this->min);
104 | }
105 |
106 | if ($this->max !== null) {
107 | return sprintf('Len: <= %d', $this->max);
108 | }
109 |
110 | // not limit
111 | return '';
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/Validator/MultiValidator.php:
--------------------------------------------------------------------------------
1 | validators = $validators;
42 | }
43 |
44 | /**
45 | * @param mixed $value
46 | * @param string $name
47 | *
48 | * @return bool
49 | */
50 | public function checkInput(mixed $value, string $name): bool
51 | {
52 | foreach ($this->validators as $validator) {
53 | $ok = $validator($value, $name);
54 | if ($ok === false) {
55 | return false;
56 | }
57 | }
58 |
59 | return true;
60 | }
61 |
62 | /**
63 | * @return string
64 | */
65 | public function __toString(): string
66 | {
67 | return '';
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Validator/NameValidator.php:
--------------------------------------------------------------------------------
1 | setRegex($regex);
48 | }
49 |
50 | /**
51 | * Validate input value
52 | *
53 | * @param mixed $value
54 | * @param string $name
55 | *
56 | * @return bool
57 | */
58 | public function checkInput(mixed $value, string $name): bool
59 | {
60 | $regex = $this->regex;
61 | if (is_string($value) && preg_match("/$regex/", $value)) {
62 | return true;
63 | }
64 |
65 | throw new FlagException("flag '$name' value should match: $regex");
66 | }
67 |
68 | /**
69 | * @return string
70 | */
71 | public function __toString(): string
72 | {
73 | // return 'should match: ' . $this->regex;
74 | return '';
75 | }
76 |
77 | /**
78 | * @param string $regex
79 | */
80 | public function setRegex(string $regex): void
81 | {
82 | $this->regex = $regex;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/test/BaseFlagsTestCase.php:
--------------------------------------------------------------------------------
1 | assertTrue($has, $message ?: $msg);
52 | }
53 |
54 | /**
55 | * @param Closure $testFunc
56 | */
57 | protected function runTestsWithParsers(Closure $testFunc): void
58 | {
59 | echo '- tests by use the parser: ', Flags::class, "\n";
60 | $fs = Flags::new(['name' => 'flags']);
61 | $testFunc($fs);
62 |
63 | echo '- tests by use the parser: ', SFlags::class, "\n";
64 | $sfs = SFlags::new(['name' => 'simple-flags']);
65 | $testFunc($sfs);
66 | }
67 |
68 | protected function createParsers(): array
69 | {
70 | $fs = Flags::new(['name' => 'flags']);
71 | $sfs = SFlags::new(['name' => 'simple-flags']);
72 |
73 | return [$fs, $sfs];
74 | // return [$sfs];
75 | }
76 |
77 | protected function bindingOptsAndArgs(FlagsParser $fs): void
78 | {
79 | $optRules = [
80 | 'int-opt' => 'int;an int option',
81 | 'int-opt1' => 'int;an int option with shorts;false;;i,g',
82 | 'str-opt' => 'an string option',
83 | 'str-opt1' => "string;an int option with required,\nand has multi line desc;true",
84 | 'str-opt2' => 'string;an string option with default;false;inhere',
85 | 'bool-opt' => 'bool;an int option with an short;false;;b',
86 | '-a, --bool-opt1' => 'bool;an int option with an short',
87 | 's' => 'string;an string option only short name',
88 | ];
89 | $argRules = [
90 | 'int-arg' => 'int;an int argument',
91 | 'str-arg' => "an string argument,\nand has multi line desc",
92 | ];
93 |
94 | $fs->addOptsByRules($optRules);
95 | $fs->addArgsByRules($argRules);
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/test/Cases/DemoCmdHandler.php:
--------------------------------------------------------------------------------
1 | 'demo',
31 | 'desc' => 'desc for demo command handler',
32 | ];
33 | }
34 |
35 | /**
36 | * @param FlagsParser $fs
37 | *
38 | * @return void
39 | */
40 | public function configure(FlagsParser $fs): void
41 | {
42 | $fs->addOptsByRules([
43 | 'opt1' => 'string;a string opt1 for command test2, and is required;true',
44 | 'opt2' => 'int;a int opt2 for command test2',
45 | ]);
46 | }
47 |
48 | /**
49 | * @param FlagsParser $fs
50 | * @param CliApp $app
51 | *
52 | * @return mixed
53 | */
54 | public function execute(FlagsParser $fs, CliApp $app): mixed
55 | {
56 | vdump(__METHOD__);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/test/Cases/RuleParser.php:
--------------------------------------------------------------------------------
1 | parseRule($rule, $name, $index, $isOption);
37 | }
38 |
39 | /**
40 | * @param array|string $rule
41 | * @param string $name
42 | * @param int $index
43 | *
44 | * @return array
45 | */
46 | public function parseArg(array|string $rule, string $name = '', int $index = 0): array
47 | {
48 | return $this->parseRule($rule, $name, $index, false);
49 | }
50 |
51 | /**
52 | * @param array|string $rule
53 | * @param string $name
54 | *
55 | * @return array
56 | */
57 | public function parseOpt(array|string $rule, string $name): array
58 | {
59 | return $this->parseRule($rule, $name, 0, true);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/CliAppTest.php:
--------------------------------------------------------------------------------
1 | add('test1', fn () => $buf->set('key', 'in test1'));
29 |
30 | $app->addCommands([
31 | 'test2' => [
32 | 'desc' => 'desc for test2 command',
33 | 'handler' => function () use ($buf): void {
34 | $buf->set('key', 'in test2');
35 | },
36 | 'options' => [
37 | 'opt1' => 'string;a string opt1 for command test2',
38 | 'opt2' => 'int;a int opt2 for command test2',
39 | ],
40 | ],
41 | ]);
42 |
43 | $app->addHandler(DemoCmdHandler::class);
44 |
45 | return $buf;
46 | }
47 |
48 | public function testCliApp_basic(): void
49 | {
50 | $app = CliApp::global();
51 | $app->setScriptFile('/path/myapp');
52 |
53 | $this->assertEquals('/path/myapp', $app->getScriptFile());
54 | $this->assertEquals('myapp', $app->getBinName());
55 | $this->assertEquals('myapp', $app->getScriptName());
56 | $this->assertFalse($app->hasCommand('test1'));
57 |
58 | $buf = $this->initApp($app);
59 |
60 | $this->assertTrue($app->hasCommand('test1'));
61 | $this->assertTrue($app->hasCommand('test2'));
62 | $this->assertTrue($app->hasCommand('demo'));
63 |
64 | $app->runByArgs(['test1']);
65 | $this->assertEquals('in test1', $buf->get('key'));
66 | }
67 |
68 | public function testCliApp_showHelp(): void
69 | {
70 | $app = new CliApp();
71 | $this->initApp($app);
72 |
73 | $this->assertTrue($app->hasCommand('test1'));
74 | $this->assertTrue($app->hasCommand('test2'));
75 |
76 | $app->runByArgs(['-h']);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/test/Concern/RuleParserTest.php:
--------------------------------------------------------------------------------
1 | parseOpt('string;flag desc;true;inhere;a,b', 'username');
25 |
26 | $this->assertNotEmpty($define);
27 | $this->assertSame('string', $define['type']);
28 | $this->assertSame('username', $define['name']);
29 | $this->assertSame('flag desc', $define['desc']);
30 | $this->assertSame('inhere', $define['default']);
31 | $this->assertSame(['a', 'b'], $define['shorts']);
32 | $this->assertTrue($define['required']);
33 |
34 | $define = $p->parseOpt('strings;this is an array, allow multi value;;[ab,cd]', 'names');
35 | $this->assertFalse($define['required']);
36 | $this->assertEmpty($define['shorts']);
37 | $this->assertSame(['ab', 'cd'], $define['default']);
38 |
39 | $define = $p->parseOpt('ints;this is an array, allow multi value;no;[23,45];', 'ids');
40 | $this->assertFalse($define['required']);
41 | $this->assertEmpty($define['shorts']);
42 | $this->assertSame([23, 45], $define['default']);
43 |
44 | $define = $p->parseOpt('array;this is an array, allow multi value;no;[23,45];', 'ids');
45 | $this->assertFalse($define['required']);
46 | $this->assertEmpty($define['shorts']);
47 | $this->assertSame(['23', '45'], $define['default']);
48 | }
49 |
50 | public function testParseRule_string_hasAliases(): void
51 | {
52 | $p = RuleParser::new();
53 |
54 | $define = $p->parseOpt('this is an string', '-t, --tpl, --tpl-file');
55 |
56 | $this->assertEquals('tpl-file', $define['name']);
57 | $this->assertEquals(['tpl'], $define['aliases']);
58 | $this->assertEquals(['t'], $define['shorts']);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/test/Flag/ArgumentTest.php:
--------------------------------------------------------------------------------
1 | assertSame(FlagType::STRING, $arg->getType());
27 | $this->assertFalse($arg->hasDefault());
28 |
29 | $arg->setDefault(89);
30 | $this->assertSame(89, $arg->getDefault());
31 | $this->assertTrue($arg->hasDefault());
32 | $this->assertFalse($arg->hasValue());
33 | $this->assertNull($arg->getValue());
34 |
35 | $arg->init();
36 | $this->assertSame('89', $arg->getValue());
37 | $this->assertTrue($arg->hasValue());
38 | $this->assertSame('89', $arg->getDefault());
39 | }
40 |
41 | public function testValidate(): void
42 | {
43 | $arg = Argument::new('name');
44 | $arg->setValidator(EmptyValidator::new());
45 |
46 | $arg->setValue('inhere');
47 | $this->assertSame('inhere', $arg->getValue());
48 |
49 | $this->expectException(FlagException::class);
50 | $this->expectExceptionMessage("flag 'name' value cannot be empty");
51 | $arg->setValue('');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/test/Flag/OptionTest.php:
--------------------------------------------------------------------------------
1 | assertSame(FlagType::STRING, $opt->getType());
25 | $this->assertFalse($opt->hasDefault());
26 | $this->assertFalse($opt->hasValue());
27 |
28 | $opt->setAliases(['n1', 'n2']);
29 | $this->assertSame(['n1', 'n2'], $opt->getAliases());
30 | $this->assertSame('--n1, --n2, --name', $opt->getHelpName());
31 |
32 | $opt->setShortcut('n');
33 | $this->assertEquals('-n, --n1, --n2, --name', $opt->getHelpName());
34 |
35 | $opt->setDefault(89);
36 | $this->assertTrue($opt->hasDefault());
37 | $this->assertFalse($opt->hasValue());
38 | $this->assertNull($opt->getValue());
39 | $this->assertSame(89, $opt->getDefault());
40 |
41 | $opt->init();
42 | $this->assertSame('89', $opt->getValue());
43 | $this->assertTrue($opt->hasValue());
44 | $this->assertSame('89', $opt->getDefault());
45 | }
46 |
47 | public function testShortcut(): void
48 | {
49 | $opt = Option::newByArray('name', [
50 | 'desc' => 'option name',
51 | ]);
52 |
53 | $tests = [
54 | 'a,b',
55 | '-a,b',
56 | '-a,-b',
57 | '-a, -b',
58 | ];
59 | foreach ($tests as $test) {
60 | $opt->setShortcut($test);
61 | $this->assertSame(['a', 'b'], $opt->getShorts());
62 | $this->assertSame('-a, -b', $opt->getShortcut());
63 | }
64 |
65 | $this->assertSame('-a, -b, --name', $opt->getHelpName());
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/FlagUtilTest.php:
--------------------------------------------------------------------------------
1 | 'a',
20 | '-a=value' => 'a=value',
21 | '--long' => 'long',
22 | '--long=value' => 'long=value',
23 | // invalid
24 | '-' => '',
25 | '- ' => '',
26 | '--' => '',
27 | '--9' => '',
28 | '--89' => '',
29 | 'arg0' => '',
30 | 'a89' => '',
31 | ];
32 | foreach ($tests as $case => $want) {
33 | $this->assertSame($want, FlagUtil::filterOptionName($case));
34 | }
35 |
36 | $this->assertSame('', FlagUtil::filterOptionName('-9'));
37 | $this->assertSame('', FlagUtil::filterOptionName('89'));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/test/FlagsParserTest.php:
--------------------------------------------------------------------------------
1 | runTestsWithParsers(function (FlagsParser $fs): void {
27 | $this->doCheckBasic($fs);
28 | });
29 | }
30 |
31 | private function doCheckBasic(FlagsParser $fs): void
32 | {
33 | $this->assertTrue($fs->isEmpty());
34 | $this->assertFalse($fs->isNotEmpty());
35 | $this->assertFalse($fs->hasShortOpts());
36 | $this->assertFalse($fs->hasArg('github'));
37 |
38 | $fs->setArgRules([
39 | 'github' => 'an string argument'
40 | ]);
41 | $this->assertFalse($fs->isEmpty());
42 | $this->assertTrue($fs->hasArg('github'));
43 | $this->assertFalse($fs->hasArg('not-exist'));
44 | $this->assertTrue($fs->isNotEmpty());
45 | $this->assertFalse($fs->hasShortOpts());
46 | $this->assertNotEmpty($fs->getArgDefine('github'));
47 |
48 | $fs->setOptRules([
49 | '-n,--name' => 'an string option'
50 | ]);
51 | $this->assertFalse($fs->isEmpty());
52 | $this->assertTrue($fs->isNotEmpty());
53 | $this->assertTrue($fs->hasShortOpts());
54 | $this->assertTrue($fs->hasOpt('name'));
55 | $this->assertFalse($fs->hasInputOpt('name'));
56 | $this->assertFalse($fs->hasOpt('not-exist'));
57 | $this->assertNotEmpty($fs->getOptDefine('name'));
58 |
59 | $fs->parseCmdline('bin/app --name inhere http://github.com/inhere');
60 | $this->assertSame('bin/app', $fs->getScriptFile());
61 | $this->assertSame('app', $fs->getScriptName());
62 | $this->assertSame('inhere', $fs->getOpt('name'));
63 | $this->assertSame('http://github.com/inhere', $fs->getArg('github'));
64 | }
65 |
66 | // public function testOption_aliases(): void
67 | // {
68 | // $this->runTestsWithParsers(function (FlagsParser $fs) {
69 | // // $fs->addOptByRule('', $rule)
70 | // });
71 | // }
72 |
73 | public function testGetOptAndGetArg(): void
74 | {
75 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
76 | $this->bindingOptsAndArgs($fs);
77 | $this->doTestGetOptAndGetArg($fs);
78 | });
79 | }
80 |
81 | private function doTestGetOptAndGetArg(FlagsParser $fs): void
82 | {
83 | // int type
84 | $ok = $fs->parse(['--str-opt1', 'val1', '--int-opt', '335', '233']);
85 | $this->assertTrue($ok);
86 | $this->assertSame(335, $fs->getOpt('int-opt'));
87 | $this->assertSame(335, $fs->getMustOpt('int-opt'));
88 | $this->assertSame(233, $fs->getArg('int-arg'));
89 | $this->assertSame(233, $fs->getMustArg('int-arg'));
90 | $fs->resetResults();
91 |
92 | // getMustOpt
93 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
94 | $fs->getMustOpt('str-opt');
95 | }, $fs);
96 |
97 | $this->assertSame(InvalidArgumentException::class, get_class($e));
98 | $this->assertSame("The option 'str-opt' is required", $e->getMessage());
99 |
100 | // getMustArg
101 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
102 | $fs->getMustArg('str-arg');
103 | }, $fs);
104 |
105 | $this->assertSame(InvalidArgumentException::class, get_class($e));
106 | $this->assertSame("The argument '#1(str-arg)' is required", $e->getMessage());
107 | }
108 |
109 | public function testParse_specialArg(): void
110 | {
111 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
112 | $fs->addOpt('a', '', 'an string opt');
113 | $fs->addArg('num', 'an int arg', 'int');
114 |
115 | $ok = $fs->parse(['-a', 'val0', '-9']);
116 | $this->assertTrue($ok);
117 | $this->assertSame([-9], $fs->getArgs());
118 | $fs->resetResults();
119 |
120 | $ok = $fs->parse(['-a', 'val0', '-90']);
121 | $this->assertTrue($ok);
122 | $this->assertSame([-90], $fs->getArgs());
123 | $fs->resetResults();
124 | });
125 | }
126 |
127 | public function testStopOnTwoHl(): void
128 | {
129 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
130 | $this->doCheckStopOnTwoHl($fs);
131 | });
132 | }
133 |
134 | private function doCheckStopOnTwoHl(FlagsParser $fs): void
135 | {
136 | $fs->addOpt('name', '', 'desc');
137 | $fs->addArg('arg0', 'desc');
138 | $this->assertFalse($fs->isStrictMatchArgs());
139 |
140 | $ok = $fs->parse(['--name', 'inhere', 'val0']);
141 | $this->assertTrue($ok);
142 | $this->assertSame('val0', $fs->getArg('arg0'));
143 | $fs->resetResults();
144 |
145 | $ok = $fs->parse(['--name', 'inhere', '--', '--val0']);
146 | $this->assertTrue($ok);
147 | $this->assertSame('--val0', $fs->getArg('arg0'));
148 | $fs->resetResults();
149 |
150 | $ok = $fs->parse(["--", "-e", "dev", "-v", "port=3455"]);
151 | $this->assertTrue($ok);
152 | $this->assertNotEmpty($fs->getArgs());
153 | $this->assertSame('-e', $fs->getArg('arg0'));
154 |
155 | $otherArgs = $fs->getRemainArgs();
156 | $this->assertArrayHasValue('port=3455', $otherArgs);
157 | $fs->resetResults();
158 | }
159 |
160 | public function testStopOnFirstArg(): void
161 | {
162 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
163 | $this->runStopOnFirstArg($fs);
164 | });
165 | }
166 |
167 | private function runStopOnFirstArg(FlagsParser $fs): void
168 | {
169 | $fs->addOptsByRules([
170 | 'name' => 'string',
171 | 'age' => 'int',
172 | ]);
173 | $flags = ['--name', 'inhere', '--age', '90', 'arg0', 'arg1'];
174 | // move an arg in middle
175 | $flags1 = ['--name', 'inhere', 'arg0', '--age', '90', 'arg1'];
176 |
177 | // ----- stopOnFirstArg=true
178 | $this->assertTrue($fs->isStopOnFistArg());
179 |
180 | $fs->parse($flags);
181 | $this->assertCount(2, $fs->getRawArgs());
182 | $this->assertSame(['arg0', 'arg1'], $fs->getRawArgs());
183 | $this->assertSame(['name' => 'inhere', 'age' => 90], $fs->getOpts());
184 | $fs->resetResults();
185 |
186 | // will stop parse on found 'arg0'
187 | $fs->parse($flags1);
188 | $this->assertCount(4, $fs->getRawArgs());
189 | $this->assertSame(['arg0', '--age', '90', 'arg1'], $fs->getRawArgs());
190 | $this->assertSame(['name' => 'inhere'], $fs->getOpts());
191 | $fs->resetResults();
192 |
193 | // ----- set stopOnFirstArg=false
194 | $fs->setStopOnFistArg(false);
195 | $this->assertFalse($fs->isStopOnFistArg());
196 |
197 | $fs->parse($flags);
198 | $this->assertCount(2, $fs->getRawArgs());
199 | $this->assertSame(['arg0', 'arg1'], $fs->getRawArgs());
200 | $this->assertSame(['name' => 'inhere', 'age' => 90], $fs->getOpts());
201 | $fs->resetResults();
202 |
203 | // will skip 'arg0' and continue parse '--age', '90'
204 | $fs->parse($flags1);
205 | $this->assertCount(2, $fs->getRawArgs());
206 | $this->assertSame(['arg0', 'arg1'], $fs->getRawArgs());
207 | $this->assertSame(['name' => 'inhere', 'age' => 90], $fs->getOpts());
208 | $fs->reset();
209 | }
210 |
211 | public function testSkipOnUndefined(): void
212 | {
213 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
214 | $this->runSkipOnUndefined_false($fs);
215 | });
216 |
217 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
218 | $this->runSkipOnUndefined_true($fs);
219 | });
220 | }
221 |
222 | private function runSkipOnUndefined_false(FlagsParser $fs): void
223 | {
224 | $fs->addOptsByRules([
225 | 'name' => 'string',
226 | 'age' => 'int',
227 | ]);
228 |
229 | // ----- skipOnUndefined=false
230 | $this->assertFalse($fs->isSkipOnUndefined());
231 |
232 | $this->expectException(FlagException::class);
233 | $this->expectExceptionMessage('flag option provided but not defined: --not-exist');
234 | $flags = ['--name', 'inhere', '--not-exist', '--age', '90', 'arg0', 'arg1'];
235 | $fs->parse($flags);
236 | }
237 |
238 | /**
239 | * @param FlagsParser $fs
240 | */
241 | private function runSkipOnUndefined_true(FlagsParser $fs): void
242 | {
243 | $fs->addOptsByRules([
244 | 'name' => 'string',
245 | 'age' => 'int',
246 | ]);
247 |
248 | // ----- skipOnUndefined=true
249 | $fs->setSkipOnUndefined(true);
250 | $this->assertTrue($fs->isSkipOnUndefined());
251 |
252 | $flags = ['--name', 'inhere', '--not-exist', '--age', '90', 'arg0', 'arg1'];
253 | $fs->parse($flags);
254 | // vdump($fs->toArray());
255 | $this->assertCount(3, $fs->getRawArgs());
256 | $this->assertSame(['--not-exist', 'arg0', 'arg1'], $fs->getRawArgs());
257 | $this->assertSame(['name' => 'inhere', 'age' => 90], $fs->getOpts());
258 | }
259 |
260 | public function testRenderHelp_showTypeOnHelp(): void
261 | {
262 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
263 | $this->bindingOptsAndArgs($fs);
264 | $this->renderFlagsHelp($fs);
265 | });
266 | }
267 |
268 | public function testRenderHelp_showTypeOnHelp_false(): void
269 | {
270 | $this->runTestsWithParsers(function (FlagsParser $fs): void {
271 | $fs->setShowTypeOnHelp(false);
272 | $this->bindingOptsAndArgs($fs);
273 | $this->renderFlagsHelp($fs);
274 | });
275 | }
276 |
277 | private function renderFlagsHelp(FlagsParser $fs): void
278 | {
279 | $ok = $fs->parse(['-h']);
280 | $this->assertFalse($ok);
281 | $this->assertSame(FlagsParser::STATUS_HELP, $fs->getParseStatus());
282 | }
283 |
284 | public function testException_RepeatName(): void
285 | {
286 | foreach ($this->createParsers() as $fs) {
287 | $this->doCheckRepeatName($fs);
288 | }
289 | }
290 |
291 | protected function doCheckRepeatName(FlagsParser $fs): void
292 | {
293 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
294 | $fs->addOptsByRules([
295 | '--name' => 'an string',
296 | 'name' => 'an string',
297 | ]);
298 | }, $fs);
299 |
300 | $this->assertEquals(FlagException::class, get_class($e));
301 |
302 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
303 | $fs->addArgsByRules([
304 | 'name' => 'an string',
305 | ]);
306 | $fs->addArg('name', 'an string');
307 | }, $fs);
308 |
309 | $this->assertSame(FlagException::class, get_class($e));
310 | }
311 |
312 | public function testException_addOpt(): void
313 | {
314 | foreach ($this->createParsers() as $fs) {
315 | $this->doCheckErrorOnAddOpt($fs);
316 | }
317 | }
318 |
319 | private function doCheckErrorOnAddOpt(FlagsParser $fs): void
320 | {
321 | // empty name
322 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
323 | $fs->addOpt('', '', 'an desc');
324 | }, $fs);
325 |
326 | $this->assertSame(FlagException::class, get_class($e));
327 | $this->assertSame('invalid flag option name: ', $e->getMessage());
328 |
329 | // invalid name
330 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
331 | $fs->addOpt('name=+', '', 'an desc');
332 | }, $fs);
333 |
334 | $this->assertSame(FlagException::class, get_class($e));
335 | $this->assertSame('invalid flag option name: name=+', $e->getMessage());
336 |
337 | // invalid type
338 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
339 | $fs->addOpt('name', '', 'an desc', 'invalid');
340 | }, $fs);
341 |
342 | $this->assertSame(FlagException::class, get_class($e));
343 | $this->assertSame("invalid flag type 'invalid', option: name", $e->getMessage());
344 | }
345 |
346 | public function testException_addArg(): void
347 | {
348 | foreach ($this->createParsers() as $fs) {
349 | $this->doCheckErrorOnAddArg($fs);
350 | }
351 | }
352 |
353 | private function doCheckErrorOnAddArg(FlagsParser $fs): void
354 | {
355 | // invalid name
356 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
357 | $fs->addArg('name=+', 'an desc');
358 | }, $fs);
359 |
360 | $this->assertSame(FlagException::class, get_class($e));
361 | $this->assertSame('invalid flag argument name: #0(name=+)', $e->getMessage());
362 |
363 | // invalid type
364 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
365 | $fs->addArg('name', 'an desc', 'invalid');
366 | }, $fs);
367 |
368 | $this->assertSame(FlagException::class, get_class($e));
369 | $this->assertSame("invalid flag type 'invalid', argument: #0(name)", $e->getMessage());
370 | }
371 |
372 | public function testSetOptAndSetArg(): void
373 | {
374 | foreach ($this->createParsers() as $fs) {
375 | $this->bindingOptsAndArgs($fs);
376 | $this->doCheckSetOptAndSetArg($fs);
377 | }
378 | }
379 |
380 | private function doCheckSetOptAndSetArg($fs): void
381 | {
382 | $this->assertSame(0, $fs->getOpt('int-opt'));
383 | $this->assertSame('', $fs->getOpt('str-opt'));
384 | $this->assertSame('', $fs->getArg('str-arg'));
385 |
386 | // test set
387 | $fs->setOpt('int-opt', '22');
388 | $fs->setOpt('str-opt', 'value');
389 | $fs->setArg('str-arg', 'value1');
390 |
391 | $this->assertSame(22, $fs->getOpt('int-opt'));
392 | $this->assertSame('value', $fs->getOpt('str-opt'));
393 | $this->assertSame('value1', $fs->getArg('str-arg'));
394 |
395 | // test set trust
396 | $fs->setTrustedOpt('int-opt', '33'); // will not format, validate value.
397 | $fs->setTrustedOpt('str-opt', 'trust-value');
398 | $fs->setTrustedArg('str-arg', 'trust-value1');
399 |
400 | $this->assertSame('33', $fs->getOpt('int-opt'));
401 | $this->assertSame('trust-value', $fs->getOpt('str-opt'));
402 | $this->assertSame('trust-value1', $fs->getArg('str-arg'));
403 |
404 | // test error
405 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
406 | $fs->setOpt('not-exist-opt', '22');
407 | }, $fs);
408 |
409 | $this->assertSame(FlagException::class, get_class($e));
410 | $this->assertSame("flag option 'not-exist-opt' is undefined", $e->getMessage());
411 |
412 | $e = $this->runAndGetException(function (FlagsParser $fs): void {
413 | $fs->setArg('not-exist-arg', '22');
414 | }, $fs);
415 |
416 | $this->assertSame(FlagException::class, get_class($e));
417 | $this->assertSame("flag argument 'not-exist-arg' is undefined", $e->getMessage());
418 | }
419 |
420 | public function testParse_arrayArg(): void
421 | {
422 | foreach ($this->createParsers() as $fs) {
423 | $this->doTestParse_arrayArg($fs);
424 | }
425 | }
426 |
427 | public function doTestParse_arrayArg(FlagsParser $fs): void
428 | {
429 | $fs->addOptsByRules([
430 | 'env, e' => [
431 | 'type' => FlagType::STRING,
432 | 'required' => true,
433 | ],
434 | ]);
435 | $fs->addArgByRule('files', 'array;a array arg');
436 |
437 | $flags = ['-e', 'dev', 'abc'];
438 | $fs->parse($flags);
439 |
440 | $this->assertNotEmpty($fs->getOpts());
441 | $this->assertNotEmpty($fs->getArgs());
442 | $this->assertEquals(['abc'], $fs->getArg('files'));
443 | $fs->resetResults();
444 |
445 | $flags = ['-e', 'dev', 'abc', 'def'];
446 | $fs->parse($flags);
447 |
448 | $this->assertNotEmpty($fs->getOpts());
449 | $this->assertNotEmpty($fs->getArgs());
450 | $this->assertEquals(['abc', 'def'], $fs->getArg('files'));
451 | $fs->resetResults();
452 | }
453 |
454 | public function testParse_optValueIsKV(): void
455 | {
456 | foreach ($this->createParsers() as $fs) {
457 | $this->doTestParse_optValueIsKV($fs);
458 | }
459 | }
460 |
461 | public function doTestParse_optValueIsKV(FlagsParser $fs): void
462 | {
463 | $fs->addOptsByRules([
464 | 'env, e' => [
465 | 'type' => FlagType::STRING,
466 | 'required' => true,
467 | ],
468 | 'vars,var,v' => [
469 | 'type' => FlagType::ARRAY,
470 | 'desc' => 'can append some extra vars, format: KEY=VALUE',
471 | ],
472 | ]);
473 |
474 | $flags = ['-e', 'dev', '--var', 'key0=val0', '-v', 'port=3445'];
475 | $fs->parse($flags);
476 |
477 | $this->assertNotEmpty($fs->getOpts());
478 | $this->assertNotEmpty($fs->getInfo(true));
479 | $this->assertEquals('vars', $fs->resolveAlias('v'));
480 | $this->assertEquals('vars', $fs->resolveAlias('var'));
481 | $this->assertEquals(['key0=val0', 'port=3445'], $fs->getOpt('vars'));
482 | }
483 |
484 | public function testRenderHelp_withValidator(): void
485 | {
486 | foreach ($this->createParsers() as $fs) {
487 | $this->doTestRenderHelp_withValidator($fs);
488 | }
489 | }
490 |
491 | public function doTestRenderHelp_withValidator(FlagsParser $fs): void
492 | {
493 | $fs->addOptsByRules([
494 | 'env, e' => [
495 | 'type' => FlagType::STRING,
496 | 'required' => true,
497 | 'desc' => 'the env name, eg: qa',
498 | 'validator' => $v1 = new EnumValidator(['testing', 'qa']),
499 | ],
500 | ]);
501 |
502 | $str = $fs->toString();
503 | $this->assertStringContainsString('Allow: testing,qa', $str);
504 | $this->assertStringContainsString((string)$v1, $str);
505 | }
506 | }
507 |
--------------------------------------------------------------------------------
/test/FlagsTest.php:
--------------------------------------------------------------------------------
1 | addOption(Option::new('name'));
28 | $fs->addOpt('age', '', 'age desc', FlagType::INT);
29 | $fs->addOpt('int1', '', 'opt1 desc', FlagType::INT, false, '89');
30 |
31 | $int1 = $fs->getDefinedOption('int1');
32 | $this->assertNotEmpty($int1);
33 | $this->assertSame(89, $int1->getDefault());
34 | $this->assertSame(89, $int1->getValue());
35 |
36 | self::assertTrue($fs->hasDefined('name'));
37 | self::assertFalse($fs->hasMatched('name'));
38 |
39 | $flags = ['--name', 'inhere', 'arg0', 'arg1'];
40 | $fs->parse($flags);
41 |
42 | self::assertTrue($fs->hasMatched('name'));
43 | $this->assertNotEmpty($fs->getOption('name'));
44 | $this->assertSame('inhere', $fs->getOpt('name'));
45 | $this->assertSame(0, $fs->getOpt('age', 0));
46 | $this->assertSame(89, $fs->getOpt('int1'));
47 | $this->assertSame(['arg0', 'arg1'], $fs->getRawArgs());
48 |
49 | $fs->reset();
50 | $flags = ['--name', 'inhere', '-s', 'sv', '-f'];
51 | $this->expectException(FlagException::class);
52 | $this->expectExceptionMessage('flag option provided but not defined: -s');
53 | $fs->parse($flags);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/test/SFlagsTest.php:
--------------------------------------------------------------------------------
1 | assertFalse($fs->isParsed());
27 | $this->assertTrue($fs->isStopOnFistArg());
28 |
29 | // string
30 | $flags = ['--name', 'inhere', 'arg0', 'arg1'];
31 | $fs->parseDefined($flags, [
32 | 'name', // string
33 | ]);
34 |
35 | $this->assertTrue($fs->isParsed());
36 | $this->assertCount(2, $fs->getRawArgs());
37 | $this->assertSame('inhere', $fs->getOption('name'));
38 | $this->assertNotEmpty($fs->getOptRules());
39 | $this->assertNotEmpty($fs->getOptDefines());
40 | $this->assertEmpty($fs->getArgRules());
41 | $this->assertEmpty($fs->getArgDefines());
42 |
43 | $fs->reset();
44 | $this->assertFalse($fs->isParsed());
45 | // vdump($fs);
46 |
47 | // int
48 | $flags = ['-n', 'inhere', '--age', '99'];
49 | $fs->parseDefined($flags, [
50 | // 'name,n' => FlagType::STRING, // add an alias
51 | 'n,name' => FlagType::STRING, // add an alias
52 | 'age' => FlagType::INT,
53 | ]);
54 | // vdump($fs);
55 | $this->assertSame('inhere', $fs->getOption('name'));
56 | $this->assertSame(99, $fs->getOption('age'));
57 | $this->assertCount(0, $fs->getRawArgs());
58 | $this->assertTrue($fs->hasAlias('n'));
59 | $this->assertSame('name', $fs->resolveAlias('n'));
60 |
61 | $fs->reset();
62 | $this->assertFalse($fs->isParsed());
63 |
64 | // bool
65 | $flags = ['--name', 'inhere', '-f', 'arg0'];
66 | $fs->parseDefined($flags, [
67 | 'name', // string
68 | 'f' => FlagType::BOOL,
69 | ]);
70 | $this->assertTrue($fs->getOpt('f'));
71 | $this->assertSame('inhere', $fs->getOption('name'));
72 | $this->assertCount(1, $fs->getRawArgs());
73 |
74 | $fs->reset();
75 | $this->assertFalse($fs->isParsed());
76 |
77 | // array
78 | $flags = ['--name', 'inhere', '--tags', 'php', '-t', 'go', '--tags', 'java', '-f', 'arg0'];
79 | $fs->parseDefined($flags, [
80 | 'name', // string
81 | 'tags,t' => FlagType::ARRAY,
82 | 'f' => FlagType::BOOL,
83 | ]);
84 | // vdump($fs);
85 | $this->assertTrue($fs->getOpt('f'));
86 | $this->assertSame('inhere', $fs->getOption('name'));
87 | // [php, go, java]
88 | $this->assertIsArray($tags = $fs->getOption('tags'));
89 | $this->assertCount(3, $tags);
90 | $this->assertCount(1, $rArgs = $fs->getRawArgs());
91 | $this->assertCount(0, $fs->getArgs());
92 | $this->assertSame('arg0', $rArgs[0]);
93 | // vdump($rArgs, $fs->getOpts());
94 |
95 | $fs->reset();
96 | $this->assertFalse($fs->isParsed());
97 |
98 | // ints
99 | $flags = ['--id', '23', '--id', '45'];
100 | $fs->parseDefined($flags, [
101 | 'id' => FlagType::INTS,
102 | ]);
103 | // [23, 45]
104 | $this->assertIsArray($ids = $fs->getOption('id'));
105 | $this->assertCount(2, $ids);
106 | $this->assertSame([23, 45], $ids);
107 | $this->assertCount(0, $fs->getRawArgs());
108 | // vdump($fs->getOpts(), $fs->getArgs());
109 |
110 | $fs->reset();
111 | $this->assertFalse($fs->isParsed());
112 |
113 | // parse undefined
114 | $flags = ['--name', 'inhere'];
115 | $this->expectException(FlagException::class);
116 | $this->expectExceptionMessage('flag option provided but not defined: --name');
117 | $fs->parseDefined($flags, []);
118 | }
119 |
120 | public function testOptRule_required(): void
121 | {
122 | $fs = SFlags::new();
123 | $this->assertFalse($fs->isParsed());
124 | $this->assertTrue($fs->isStopOnFistArg());
125 |
126 | $flags = ['--name', 'inhere'];
127 | $fs->parseDefined($flags, [
128 | 'name' => 'string;;required',
129 | ]);
130 | $this->assertNotEmpty($req = $fs->getRequiredOpts());
131 | $this->assertCount(1, $req);
132 | $this->assertSame('inhere', $fs->getOpt('name'));
133 | $fs->reset();
134 |
135 | $this->expectException(FlagException::class);
136 | $this->expectExceptionMessage("flag option 'name' is required");
137 | $fs->setOptRules([
138 | 'name' => 'string;;required',
139 | ]);
140 | $fs->parse([]);
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/test/ValidatorTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($v('inhere', 'test'));
25 |
26 | $this->expectException(FlagException::class);
27 | $this->expectExceptionMessage("flag 'test' value should match: ^\w+$");
28 | $v(' inhere ', 'test');
29 | }
30 |
31 | public function testNameValidator(): void
32 | {
33 | $v = NameValidator::new();
34 | $this->assertTrue($v('inhere', 'test'));
35 | $this->assertEmpty((string)$v);
36 |
37 | $v->setRegex('');
38 | $this->assertTrue($v('inhere', 'test'));
39 |
40 | $v = new NameValidator;
41 | $this->assertTrue($v('inhere', 'test'));
42 |
43 | $this->expectException(FlagException::class);
44 | $this->expectExceptionMessage("flag 'test' value should match: " . NameValidator::DEFAULT_REGEX);
45 | $v(' inhere ', 'test');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/bootstrap.php:
--------------------------------------------------------------------------------
1 | $libDir . '/test/',
16 | 'Toolkit\PFlag\\' => $libDir . '/src/',
17 | ];
18 |
19 | spl_autoload_register(static function ($class) use ($npMap): void {
20 | foreach ($npMap as $np => $dir) {
21 | $file = $dir . str_replace('\\', '/', substr($class, strlen($np))) . '.php';
22 |
23 | if (file_exists($file)) {
24 | include $file;
25 | }
26 | }
27 | });
28 |
29 | if (is_file(dirname(__DIR__, 3) . '/autoload.php')) {
30 | require dirname(__DIR__, 3) . '/autoload.php';
31 | } elseif (is_file(dirname(__DIR__) . '/vendor/autoload.php')) {
32 | require dirname(__DIR__) . '/vendor/autoload.php';
33 | }
34 |
--------------------------------------------------------------------------------
/test/testdata/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/php-toolkit/pflag/ffbd245d5e4b2e570cd6960de35d3d5add8c2283/test/testdata/.keep
--------------------------------------------------------------------------------