├── .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](https://img.shields.io/packagist/l/toolkit/pflag.svg?style=flat-square)](LICENSE) 4 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/php-toolkit/pflag)](https://github.com/php-toolkit/pflag) 5 | [![Actions Status](https://github.com/php-toolkit/pflag/workflows/Unit-Tests/badge.svg)](https://github.com/php-toolkit/pflag/actions) 6 | [![Php Version Support](https://img.shields.io/packagist/php-v/toolkit/pflag)](https://packagist.org/packages/toolkit/pflag) 7 | [![Latest Stable Version](http://img.shields.io/packagist/v/toolkit/pflag.svg)](https://packagist.org/packages/toolkit/pflag) 8 | [![Coverage Status](https://coveralls.io/repos/github/php-toolkit/pflag/badge.svg?branch=main)](https://coveralls.io/github/php-toolkit/pflag?branch=main) 9 | [![zh-CN readme](https://img.shields.io/badge/中文-Readme-brightgreen.svg?style=for-the-badge&maxAge=2592000)](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 | ![flags-demo](example/images/flags-demo.png) 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 | ![cmd-demo-help](example/images/cli-cmd-help.png) 287 | 288 | - Run command: 289 | 290 | ![cmd-demo-run](example/images/cli-cmd-run.png) 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 | ![cli-app-help](example/images/cli-app-help.png) 348 | 349 | - Command help: 350 | 351 | ![cli-app-cmd-help](example/images/cli-app-cmd-help.png) 352 | 353 | - Run command: 354 | 355 | ![cli-app-cmd-run](example/images/cli-app-cmd-run.png) 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](https://img.shields.io/packagist/l/toolkit/pflag.svg?style=flat-square)](LICENSE) 4 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/php-toolkit/pflag)](https://github.com/php-toolkit/pflag) 5 | [![Actions Status](https://github.com/php-toolkit/pflag/workflows/Unit-Tests/badge.svg)](https://github.com/php-toolkit/pflag/actions) 6 | [![Php Version Support](https://img.shields.io/packagist/php-v/toolkit/pflag)](https://packagist.org/packages/toolkit/pflag) 7 | [![Latest Stable Version](http://img.shields.io/packagist/v/toolkit/pflag.svg)](https://packagist.org/packages/toolkit/pflag) 8 | [![Coverage Status](https://coveralls.io/repos/github/php-toolkit/pflag/badge.svg?branch=main)](https://coveralls.io/github/php-toolkit/pflag?branch=main) 9 | [![English readme](https://img.shields.io/badge/English-Readme-brightgreen.svg?style=for-the-badge&maxAge=2592000)](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 | ![flags-demo](example/images/flags-demo.png) 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 | ![cmd-demo-help](example/images/cli-cmd-help.png) 414 | 415 | - 运行命令: 416 | 417 | ![cmd-demo-run](example/images/cli-cmd-run.png) 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 | ![cli-app-help](example/images/cli-app-help.png) 472 | 473 | - 显示子命令帮助: 474 | 475 | ![cli-app-cmd-help](example/images/cli-app-cmd-help.png) 476 | 477 | - 运行一个命令: 478 | 479 | ![cli-app-cmd-run](example/images/cli-app-cmd-run.png) 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\n", $nameTag, $hName, $nameTag); 138 | $buf->writef(" %s%s\n", Str::repeat(' ', $nameLen), $desc); 139 | } else { 140 | $buf->writef(" <%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\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 --------------------------------------------------------------------------------