├── .github ├── changelog.yml ├── dependabot.yml └── workflows │ ├── php.yml │ └── release.yml ├── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── LICENSE ├── README.en.md ├── README.md ├── TODO.md ├── _config.yml ├── composer.json ├── docs ├── catalog.md ├── cli-color.md ├── controller.md ├── input-output.md ├── intro.md ├── screenshots │ ├── alone-command-help.jpg │ ├── app-command-list.png │ ├── cli-php-file-highlight.jpg │ ├── cli-text-progress.gif │ ├── command-help.png │ ├── error-handle-display.png │ ├── fmt-help-panel.png │ ├── fmt-list.png │ ├── fmt-multi-list.png │ ├── fmt-panel.png │ ├── group-command-list.png │ ├── interact-limited-ask.png │ ├── interactive-ask.png │ ├── interactive-limited-ask.png │ ├── output-color-text.png │ ├── output-format-msg.png │ ├── progress-demo.png │ ├── show-dynamic-text.gif │ ├── show-progress-bar.gif │ ├── show-progress-txt.gif │ ├── table-show.png │ └── use-definition-args.png ├── show-ascii-font.md ├── something.md ├── sprintf.md └── v4-run-workflow.puml ├── examples ├── Command │ ├── CorCommand.php │ ├── DemoCommand.php │ └── TestCommand.php ├── Controller │ ├── Attach │ │ └── DemoSubCommand.php │ ├── HomeController.php │ ├── InteractController.php │ ├── ProcessController.php │ └── ShowController.php ├── alone ├── alone-app ├── app ├── commands.php ├── demo │ ├── cli-spinner.php │ ├── color.php │ ├── constants.php │ ├── progress_bar.php │ ├── progress_bar1.php │ ├── progress_bar3.php │ ├── sf2_color.php │ └── spinner.php └── tmp │ └── .gitkeep ├── phar.build.inc ├── phpunit.xml ├── psalm.xml ├── resource ├── art-fonts │ ├── 404.txt │ ├── 404_italic.txt │ ├── 500.txt │ ├── 500_italic.txt │ ├── app-name.txt │ ├── app-name_italic.txt │ ├── error.txt │ ├── error_italic.txt │ ├── no.txt │ ├── no_italic.txt │ ├── ok.txt │ ├── ok_italic.txt │ ├── somefonts.txt │ ├── success.txt │ ├── success_italic.txt │ ├── yes.txt │ └── yes_italic.txt ├── cmd │ ├── README.md │ └── SetEscChar.cmd ├── exe │ ├── README.md │ └── hiddeninput.exe ├── refer │ ├── shell-completion-doc.md │ └── zsh-complete-doc.md └── templates │ ├── bash-completion.tpl │ └── zsh-completion.tpl ├── sami.doc.inc ├── src ├── AbstractApplication.php ├── Annotate │ ├── AnnotateRules.php │ ├── Attr │ │ ├── CmdArgument.php │ │ ├── CmdOption.php │ │ ├── RuleArg.php │ │ └── RuleOpt.php │ └── DocblockRules.php ├── Application.php ├── BuiltIn │ ├── DevServerCommand.php │ ├── PharController.php │ ├── README.md │ └── SelfUpdateCommand.php ├── Command.php ├── Component │ ├── Animation │ │ └── Animation.php │ ├── CompletionDumper.php │ ├── ConsoleAppIShell.php │ ├── ConsoleRenderer.php │ ├── ErrorHandler.php │ ├── Formatter │ │ ├── Alert.php │ │ ├── Block.php │ │ ├── Body.php │ │ ├── HelpPanel.php │ │ ├── JSONPretty.php │ │ ├── MultiList.php │ │ ├── Padding.php │ │ ├── Panel.php │ │ ├── Section.php │ │ ├── SingleList.php │ │ ├── Table.php │ │ ├── Title.php │ │ └── Tree.php │ ├── Interact │ │ ├── AbstractSelect.php │ │ ├── Checkbox.php │ │ ├── Choose.php │ │ ├── Confirm.php │ │ ├── IShell.php │ │ ├── LimitedAsk.php │ │ ├── MultiSelect.php │ │ ├── ParamDefinition │ │ │ ├── AbstractParam.php │ │ │ ├── ChoiceParam.php │ │ │ └── StringParam.php │ │ ├── Password.php │ │ ├── Question.php │ │ ├── SingleSelect.php │ │ ├── Terminal.php │ │ └── ValuesCollector.php │ ├── MessageFormatter.php │ ├── NotifyMessage.php │ ├── PharCompiler.php │ ├── Progress │ │ ├── Bar.php │ │ ├── CounterText.php │ │ ├── DynamicText.php │ │ ├── Pending.php │ │ ├── SimpleBar.php │ │ ├── SimpleTextBar.php │ │ ├── Spinner.php │ │ └── TextBar.php │ ├── ReadlineCompleter.php │ ├── Router.php │ └── Symbol │ │ ├── ArtFont.php │ │ ├── Char.php │ │ ├── Emoji.php │ │ └── GitEmoji.php ├── Concern │ ├── AbstractInput.php │ ├── AbstractOutput.php │ ├── AbstractQuestion.php │ └── InteractiveHandle.php ├── Console.php ├── ConsoleConst.php ├── ConsoleEvent.php ├── Contract │ ├── ApplicationInterface.php │ ├── CommandHandlerInterface.php │ ├── CommandInterface.php │ ├── ControllerInterface.php │ ├── ErrorHandlerInterface.php │ ├── FormatterInterface.php │ ├── InputInterface.php │ ├── OutputInterface.php │ └── RouterInterface.php ├── Controller.php ├── Decorate │ ├── ApplicationHelpTrait.php │ ├── AttachApplicationTrait.php │ ├── CommandHelpTrait.php │ ├── ControllerHelpTrait.php │ ├── FormatOutputAwareTrait.php │ ├── InputOutputAwareTrait.php │ ├── RuntimeProfileTrait.php │ ├── SimpleEventAwareTrait.php │ ├── StyledOutputAwareTrait.php │ ├── SubCommandsWareTrait.php │ └── UserInteractAwareTrait.php ├── Exception │ ├── ConsoleException.php │ └── PromptException.php ├── GlobalOption.php ├── Handler │ ├── AbstractHandler.php │ ├── CallableCommand.php │ └── CommandWrapper.php ├── IO │ ├── Input.php │ ├── Input │ │ ├── ArrayInput.php │ │ ├── StreamInput.php │ │ └── StringInput.php │ ├── Output.php │ └── Output │ │ ├── BufferedOutput.php │ │ ├── MemoryOutput.php │ │ ├── StreamOutput.php │ │ └── TempOutput.php └── Util │ ├── ConsoleUtil.php │ ├── FormatUtil.php │ ├── Helper.php │ ├── Interact.php │ ├── PhpDevServe.php │ ├── ProgressBar.php │ └── Show.php └── test ├── Annotate └── DocblockRulesTest.php ├── ApplicationTest.php ├── BaseTestCase.php ├── CommandTest.php ├── ControllerTest.php ├── IO └── InputTest.php ├── TestCommand.php ├── TestController.php └── bootstrap.php /.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 | # if empty will auto fetch by git remote 7 | #repo_url: https://github.com/gookit/gitw 8 | 9 | filters: 10 | # message length should >= 12 11 | - name: msg_len 12 | min_len: 12 13 | # message words should >= 3 14 | - name: words_len 15 | min_len: 3 16 | - name: keyword 17 | keyword: format code 18 | exclude: true 19 | - name: keywords 20 | keywords: format code, action test 21 | exclude: true 22 | 23 | # group match rules 24 | # not matched will use 'Other' group. 25 | rules: 26 | - name: Refactor 27 | start_withs: [refactor, break] 28 | contains: ['refactor:'] 29 | - name: Fixed 30 | start_withs: [fix] 31 | contains: ['fix:'] 32 | - name: Feature 33 | start_withs: [feat, new] 34 | contains: ['feat:'] 35 | - name: Update 36 | start_withs: [update] 37 | contains: ['update:', 'up:'] 38 | -------------------------------------------------------------------------------- /.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 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | - 'composer.json' 8 | - '**.yml' 9 | 10 | jobs: 11 | test: 12 | name: Test on php ${{ matrix.php}} 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | php: [8.1, 8.2, 8.3, 8.4] # 7.3, 7.4, 19 | # os: [ubuntu-latest] # windows-latest, 20 | # include: 21 | # - os: 'ubuntu-latest' 22 | # php: '7.2' 23 | # phpunit: '8.5.13' 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Set ENV vars 30 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 31 | run: | 32 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 33 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 34 | 35 | - name: Display Env 36 | run: env 37 | 38 | # usage refer https://github.com/shivammathur/setup-php 39 | - name: Setup PHP 40 | timeout-minutes: 5 41 | uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: ${{ matrix.php}} 44 | tools: pecl, php-cs-fixer, phpunit:${{ matrix.phpunit }} 45 | extensions: mbstring, dom, fileinfo, mysql, openssl, igbinary, redis # , swoole-4.4.19 #optional, setup extensions 46 | ini-values: post_max_size=56M, short_open_tag=On #optional, setup php.ini configuration 47 | coverage: none #optional, setup coverage driver: xdebug, none 48 | 49 | - name: Install dependencies 50 | run: composer install --no-progress 51 | 52 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 53 | # Docs: https://getcomposer.org/doc/articles/scripts.md 54 | 55 | - name: Run unit tests 56 | run: | 57 | phpunit 58 | php examples/alone -h 59 | php examples/app --help 60 | 61 | - name: Test build PHAR 62 | run: | 63 | php -d phar.readonly=0 examples/app phar pack --no-progress -o=myapp.phar 64 | php myapp.phar -h 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: New 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 | # if: startsWith(github.ref, 'refs/tags/') 36 | with: 37 | name: ${{ env.RELEASE_TAG }} 38 | tag_name: ${{ env.RELEASE_TAG }} 39 | body_path: changelog.md 40 | # files: kite-${{ env.RELEASE_TAG }}.phar 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### OSX template 3 | *.DS_Store 4 | 5 | .idea/ 6 | .phpintel/ 7 | .phpdoc/ 8 | docs/api/ 9 | caches/ 10 | vendor/ 11 | !README.md 12 | !.gitkeep 13 | composer.lock 14 | *.log 15 | *.swp 16 | *.swo 17 | *.zip 18 | *.phar 19 | .DS_Store 20 | .interactive_history 21 | *.bash 22 | *.zsh 23 | *.cache 24 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | true, 13 | 'array_syntax' => [ 14 | 'syntax' => 'short' 15 | ], 16 | 'list_syntax' => [ 17 | 'syntax' => 'short' 18 | ], 19 | 'class_attributes_separation' => true, 20 | 'declare_strict_types' => true, 21 | 'global_namespace_import' => [ 22 | 'import_constants' => true, 23 | 'import_functions' => true, 24 | ], 25 | 'header_comment' => [ 26 | 'comment_type' => 'PHPDoc', 27 | 'header' => $header, 28 | 'separate' => 'bottom' 29 | ], 30 | 'no_unused_imports' => true, 31 | 'return_type_declaration' => [ 32 | 'space_before' => 'none', 33 | ], 34 | 'single_quote' => true, 35 | 'standardize_not_equals' => true, 36 | 'void_return' => true, // add :void for method 37 | ]; 38 | 39 | $finder = PhpCsFixer\Finder::create() 40 | ->exclude('docs') 41 | ->exclude('vendor') 42 | ->exclude('resource') 43 | ->in(__DIR__); 44 | 45 | return (new PhpCsFixer\Config) 46 | ->setRiskyAllowed(true) 47 | ->setRules($rules) 48 | ->setFinder($finder) 49 | ->setUsingCache(false); 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v4.1.x 4 | 5 | > require php 8.0+ 6 | 7 | - feat: allow attach multi level sub-commands to command,group 8 | - feat: support add flags config for add alone Closure command 9 | 10 | ## v4.0.x 11 | 12 | > require php 7.3+ 13 | > begin at: 2020.08.21, branch `master` 14 | 15 | ## v3.1.21 16 | 17 | ## v3.1.20 18 | 19 | **update** 20 | 21 | - update some for error tips and single list 22 | - add before/after methods on run controller method. 23 | - support controller object cache on repeat fetch 24 | - enhance built in php serve util logic 25 | 26 | ## v3.1.19 27 | 28 | > publish at 2021.04.20 29 | 30 | - up: use `Str::padByWidth` support zh-CN words on render table/list/panel 31 | 32 | ## v3.0.x 33 | 34 | > publish at: 2019.01.03 35 | 36 | - modify some dir structure 37 | - remove some helper methods, use deps lib instead 38 | - fix some bugs, format code 39 | 40 | ## v2.4.0 41 | 42 | > publish at: 2018.07.03 43 | 44 | - **update deps**: add dep toolkit/cli-utils and toolkit/sys-utils 45 | - fix: no des when display alone command help 46 | - add new interactive method: `Interact::answerIsYes()` 47 | - update PharCompiler.php, some bug fixed 48 | - adjust help information display 49 | - remove some invalid classes 50 | 51 | ## v2.3.3 52 | 53 | > publish at: 2018.03.15 54 | 55 | **update** 56 | 57 | - prompt password input use sh instead bash 58 | - command help display modify 59 | - modify error display 60 | - Update README.md 61 | - add more param type declare 62 | 63 | **new** 64 | 65 | - add running sub-command support 66 | - add disable command support in controller 67 | - add a built in command for run php dev server 68 | 69 | **bug fixed** 70 | 71 | - token_get_all not exist for Highlighter 72 | - fix some errors for phar build 73 | 74 | ## v2.3.2 75 | 76 | > publish at: 2018.01.26 77 | 78 | - now can disable a controller or command by method `isEnabled()` 79 | - fixed: should not display 'isAlone' command in a controller 80 | - format codes, add more param type define 81 | - some update for process util 82 | - phar compiler can only pack changed files(by git status) 83 | - group/command allow define alias by aliases() in class 84 | - support run command by coroutine, base on swoole. 85 | - update demo classes. add changelog file 86 | - Update README.md 87 | 88 | ## v2.3.1 89 | 90 | - fixed for alone command description message dispaly on use `-h` 91 | - add global options for a method help info 92 | - method annotation format update 93 | - complete phar package tool. add a example controller for pack phar 94 | - you can run: `php examples/app phar:pack` to see demo 95 | 96 | 97 | ## v2.3.0 98 | 99 | - move Style to Components dir 100 | - move some demo files and tool class to `inhere/console-components` 101 | 102 | ## v2.2.5 103 | 104 | - add a profiler tool class 105 | - add new show method: `pointing`, `tree`, `splitLine` 106 | - some update for params parse. add 'arrayValues' option 107 | - add new method `writeStyle()` for output message 108 | 109 | ## v2.2.4 110 | 111 | - add console code Highlighter tool, from 'jakub-onderka/php-console-highlighter' 112 | - complete a simple template generator, add a new input class 113 | - update readme 114 | 115 | ## v2.2.3 116 | 117 | - simple ascii font display 118 | - add a simple process util 119 | - annotation var support in others tag: `options`, `arguments` 120 | - add new notify message method: `Show::spinner()` `Show::pending()` 121 | - add new screenshots images, other update ... 122 | 123 | ## v2.2.2 124 | 125 | - some modify for user interactive method. bug fixed for `write()` need quit 126 | - new feature: alias support for command(app command alias and controller command alias) 127 | - add new interactive : `multi` `select` 128 | - Update README.md 129 | 130 | ## .... 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Noah Buscher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - [x] Add more events supports 4 | - [x] Add nested command supports 5 | - [ ] Simpler bash command completion support 6 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inhere/console", 3 | "type": "library", 4 | "description": "php console library, provide console argument parse, console controller/command run, color style, user interactive, information show.", 5 | "keywords": [ 6 | "cli", 7 | "phar", 8 | "color", 9 | "console", 10 | "console-color", 11 | "command", 12 | "command-line", 13 | "console-application" 14 | ], 15 | "homepage": "https://github.com/inhere/php-console", 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "inhere", 20 | "email": "in.798@qq.com", 21 | "homepage": "https://github.com/inhere" 22 | } 23 | ], 24 | "require": { 25 | "php": ">8.1", 26 | "toolkit/cli-utils": "~2.0", 27 | "toolkit/fsutil": "~2.0", 28 | "toolkit/pflag": "~2.0", 29 | "toolkit/stdlib": "^2.0", 30 | "toolkit/sys-utils": "~2.0" 31 | }, 32 | "require-dev": {}, 33 | "autoload": { 34 | "psr-4": { 35 | "Inhere\\Console\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Inhere\\ConsoleTest\\": "test/" 41 | } 42 | }, 43 | "suggest": { 44 | "symfony/process": "php process operation", 45 | "padraic/phar-updater": "A thing to make PHAR self-updating easy and secure.", 46 | "text/template": "Single-Class string template engine for PHP5/7 supporting if/loops/filters" 47 | }, 48 | "scripts": { 49 | "check-cs": "./php-cs-fixer fix --dry-run --diff --diff-format=udiff", 50 | "cs-fix": "./php-cs-fixer fix" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/catalog.md: -------------------------------------------------------------------------------- 1 | # php 命令行应用库 2 | 3 | [简介和安装](intro.md) 4 | -------------------------------------------------------------------------------- /docs/cli-color.md: -------------------------------------------------------------------------------- 1 | # cli color 2 | 3 | ``` 4 | 5 | \033[0m 关闭所有属性 6 | \033[1m 设置高亮度 7 | \033[4m 下划线 8 | \033[5m 闪烁 9 | \033[7m 反显 10 | \033[8m 消隐 11 | \033[30m 至 \033[37m 设置前景色 12 | \033[40m 至 \033[47m 设置背景色 13 | \033[nA 光标上移n行 14 | \033[nB 光标下移n行 15 | \033[nC 光标右移n行 16 | \033[nD 光标左移n行 17 | \033[y;xH设置光标位置 18 | \033[2J 清屏 19 | \033[K 清除从光标到行尾的内容 20 | \033[s 保存光标位置 21 | \033[u 恢复光标位置 22 | \033[?25l 隐藏光标 23 | \033[?25h 显示光标 24 | ``` 25 | 26 | ### 各数字所代表的颜色如下: 27 | 28 | - 背景颜色范围:40----49 29 | 30 | ``` 31 | 40:黑 32 | 41:深红 33 | 42:绿 34 | 43:黄色 35 | 44:蓝色 36 | 45:紫色 37 | 46:深绿 38 | 47:白色 39 | ``` 40 | 41 | - 字颜色:30-----------39 42 | 43 | ``` 44 | 30:黑 45 | 31:红 46 | 32:绿 47 | 33:黄 48 | 34:蓝色 49 | 35:紫色 50 | 36:深绿 51 | 37:白色 52 | ``` 53 | 54 | 另外,同类的多种设置项可以组合在一起,中间用分号(`;`)隔开。如下: 55 | 56 | ``` 57 | echo -e "\033[20;1H\033[1;4;32mHello,world\033[0m" 58 | ``` -------------------------------------------------------------------------------- /docs/controller.md: -------------------------------------------------------------------------------- 1 | # 注册命令 2 | 3 | ### 注册独立命令 4 | ### 注册组命令 5 | ### 设置命令名称 6 | ### 设置命令描述 7 | 8 | ## 独立命令 9 | 10 | ## 组命令(controller) 11 | 12 | ## 输入定义(InputDefinition) 13 | 14 | 15 | ## 设置参数 16 | 17 | ### 使用名称设置参数 18 | 19 | ### 根据位置设置参数 20 | 21 | ``` 22 | $ php examples/app demo john male 43 --opt1 value1 -y 23 | hello, this in Inhere\Console\examples\DemoCommand::execute 24 | this is argument and option example: 25 | the opt1's value 26 | option: opt1 | 27 | | | 28 | php examples/app demo john male 43 --opt1 value1 -y 29 | | | | | | | 30 | script command | | |______ option: yes, it use shortcat: y, and it is a Input::OPT_BOOLEAN, so no value. 31 | | |___ | 32 | argument: name | argument: age 33 | argument: sex 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/input-output.md: -------------------------------------------------------------------------------- 1 | # input and output 2 | 3 | ## input 4 | 5 | ## output 6 | 7 | ### output buffer 8 | 9 | how tu use 10 | 11 | - use `Output` 12 | 13 | ```php 14 | // open buffer 15 | $this->output->startBuffer(); 16 | 17 | $this->output->write('message 0'); 18 | $this->output->write('message 1'); 19 | // .... 20 | $this->output->write('message n'); 21 | 22 | // stop and output buffer 23 | $this->output->stopBuffer(); 24 | ``` 25 | 26 | - use `Show` 27 | 28 | ```php 29 | // open buffer 30 | Show::startBuffer(); 31 | 32 | Show::write('message 0'); 33 | Show::write('message 1'); 34 | // .... 35 | Show::write('message n'); 36 | 37 | // stop and output buffer 38 | Show::stopBuffer(); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 简洁、功能全面的php命令行应用库。提供控制台参数解析, 颜色风格输出, 用户信息交互, 特殊格式信息显示。 4 | 5 | > 无其他库依赖,可以方便的整合到任何已有项目中。 6 | 7 | - 功能全面的命令行的选项参数解析(命名参数,短选项,长选项 ...) 8 | - 命令行应用, 命令行的 `controller`, `command` 解析运行 9 | - 命令行中功能强大的 `input`, `output` 管理、使用 10 | - 消息文本的多种颜色风格输出支持(`info`, `comment`, `success`, `danger`, `error` ... ...) 11 | - 丰富的特殊格式信息显示(`section`, `panel`, `padding`, `help-panel`, `table`, `title`, `list`, `progressBar`) 12 | - 常用的用户信息交互支持(`select`, `confirm`, `ask/question`) 13 | - 命令方法注释自动解析(提取为参数 `arguments` 和 选项 `options` 等信息) 14 | - 类似 `symfony/console` 的预定义参数定义支持(按位置赋予参数值) 15 | - 输出是 windows,linux 兼容的,不支持颜色的环境会自动去除相关CODE 16 | 17 | > 下面所有的特性,效果都是运行 `examples/` 中的示例代码 `php examples/app` 展示出来的。下载后可以直接测试体验 18 | 19 | 20 | ## 项目地址 21 | 22 | - **github** https://github.com/inhere/php-console.git 23 | - **git@osc** https://git.oschina.net/inhere/php-console.git 24 | 25 | **注意:** 26 | 27 | - master 分支是要求 `php >= 7` 的(推荐使用)。 28 | - php5 分支是支持 php5 `php >= 5.5` 的代码分支。 29 | 30 | ## 安装 31 | 32 | - 使用 composer 命令 33 | 34 | ```bash 35 | composer require inhere/console 36 | ``` 37 | 38 | - 使用 composer.json 39 | 40 | 编辑 `composer.json`,在 `require` 添加 41 | 42 | ``` 43 | "inhere/console": "dev-master", 44 | 45 | // "inhere/console": "^2.0", // 指定稳定版本 46 | // "inhere/console": "dev-php5", // for php5 47 | ``` 48 | 49 | 然后执行: `composer update` 50 | 51 | - 直接拉取 52 | 53 | ``` 54 | git clone https://git.oschina.net/inhere/php-console.git // git@osc 55 | git clone https://github.com/inhere/php-console.git // github 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/screenshots/alone-command-help.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/alone-command-help.jpg -------------------------------------------------------------------------------- /docs/screenshots/app-command-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/app-command-list.png -------------------------------------------------------------------------------- /docs/screenshots/cli-php-file-highlight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/cli-php-file-highlight.jpg -------------------------------------------------------------------------------- /docs/screenshots/cli-text-progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/cli-text-progress.gif -------------------------------------------------------------------------------- /docs/screenshots/command-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/command-help.png -------------------------------------------------------------------------------- /docs/screenshots/error-handle-display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/error-handle-display.png -------------------------------------------------------------------------------- /docs/screenshots/fmt-help-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/fmt-help-panel.png -------------------------------------------------------------------------------- /docs/screenshots/fmt-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/fmt-list.png -------------------------------------------------------------------------------- /docs/screenshots/fmt-multi-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/fmt-multi-list.png -------------------------------------------------------------------------------- /docs/screenshots/fmt-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/fmt-panel.png -------------------------------------------------------------------------------- /docs/screenshots/group-command-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/group-command-list.png -------------------------------------------------------------------------------- /docs/screenshots/interact-limited-ask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/interact-limited-ask.png -------------------------------------------------------------------------------- /docs/screenshots/interactive-ask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/interactive-ask.png -------------------------------------------------------------------------------- /docs/screenshots/interactive-limited-ask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/interactive-limited-ask.png -------------------------------------------------------------------------------- /docs/screenshots/output-color-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/output-color-text.png -------------------------------------------------------------------------------- /docs/screenshots/output-format-msg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/output-format-msg.png -------------------------------------------------------------------------------- /docs/screenshots/progress-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/progress-demo.png -------------------------------------------------------------------------------- /docs/screenshots/show-dynamic-text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/show-dynamic-text.gif -------------------------------------------------------------------------------- /docs/screenshots/show-progress-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/show-progress-bar.gif -------------------------------------------------------------------------------- /docs/screenshots/show-progress-txt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/show-progress-txt.gif -------------------------------------------------------------------------------- /docs/screenshots/table-show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/table-show.png -------------------------------------------------------------------------------- /docs/screenshots/use-definition-args.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/docs/screenshots/use-definition-args.png -------------------------------------------------------------------------------- /docs/show-ascii-font.md: -------------------------------------------------------------------------------- 1 | # show cli font 2 | 3 | ```php 4 | 5 | $name = '404'; 6 | ArtFont::create()->show($name, ArtFont::INTERNAL_GROUP,[ 7 | 'type' => $this->input->getBoolOpt('italic') ? 'italic' : '', 8 | 'style' => $this->input->getOpt('style'), 9 | ]); 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/something.md: -------------------------------------------------------------------------------- 1 | # some idea 2 | 3 | ## controller 4 | 5 | ```php 6 | protected function commandConfigure($definition) 7 | { 8 | // old: own create. 9 | $this->createDefinition()->addArgument(); 10 | 11 | // maybe: get by argument. 12 | $definition->addArgument(); 13 | 14 | // .... 15 | } 16 | ``` 17 | 18 | ```php 19 | /** 20 | * the group controller metadata. to define name, description 21 | * @return array 22 | */ 23 | public static function metadata() 24 | { 25 | return [ 26 | 'name' => 'model', 27 | 'description' => 'some console command handle for model user data.', 28 | 29 | // for command 30 | 'aliases' => [ 31 | 'i', 'in', 32 | ], 33 | 34 | // for controller 35 | 'aliases' => [ 36 | 'i' => 'install', 37 | 'up' => 'update', 38 | ] 39 | ]; 40 | } 41 | ``` -------------------------------------------------------------------------------- /docs/sprintf.md: -------------------------------------------------------------------------------- 1 | # sprintf(format,arg1,arg2,arg++) 2 | 3 | ```php 4 | sprintf($format, $arg1, $arg2, $arg++) 5 | ``` 6 | 7 | - `$format` 8 | 9 | 必需。规定字符串以及如何格式化其中的变量。 10 | 11 | 可能的格式值: 12 | 13 | ``` 14 | %% - 返回一个百分号 % 15 | %b - 二进制数 16 | %c - ASCII 值对应的字符 17 | %d - 包含正负号的十进制数(负数、0、正数) 18 | %e - 使用小写的科学计数法(例如 1.2e+2) 19 | %E - 使用大写的科学计数法(例如 1.2E+2) 20 | %u - 不包含正负号的十进制数(大于等于 0) 21 | %f - 浮点数(本地设置) 22 | %F - 浮点数(非本地设置) 23 | %g - 较短的 %e 和 %f 24 | %G - 较短的 %E 和 %f 25 | %o - 八进制数 26 | %s - 字符串 27 | %x - 十六进制数(小写字母) 28 | %X - 十六进制数(大写字母) 29 | ``` 30 | 31 | 附加的格式值。必需放置在 `%` 和字母之间(例如 `%.2f`): 32 | 33 | ``` 34 | + (在数字前面加上 + 或 - 来定义数字的正负性。默认情况下,只有负数才做标记,正数不做标记) 35 | ' (规定使用什么作为填充,默认是空格。它必须与宽度指定器一起使用。例如:%'x20s(使用 "x" 作为填充)) 36 | - (左调整变量值) 37 | [0-9] (规定变量值的最小宽度) 38 | .[0-9] (规定小数位数或最大字符串长度) 39 | ``` 40 | 41 | > 注释:如果使用多个上述的格式值,它们必须按照以上顺序使用。 42 | -------------------------------------------------------------------------------- /docs/v4-run-workflow.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | start 3 | :input command line - Entry; 4 | 5 | :create app: new Application(); 6 | 7 | :create Input/Output instance; 8 | 9 | partition AppInit { 10 | :save app instance to Console::$app; 11 | :load application config; 12 | :register command/group commands; 13 | } 14 | 15 | :run: app->run(); 16 | 17 | partition AppRun { 18 | :match global flags: --debug; 19 | :match global help flag: -h|--help; 20 | if (TRUE) then(Yes) 21 | :display commands and exit; 22 | endif 23 | :match global flags: -V|--version; 24 | } 25 | 26 | stop 27 | @enduml -------------------------------------------------------------------------------- /examples/Command/CorCommand.php: -------------------------------------------------------------------------------- 1 | aList([ 45 | 'support coroutine?' => Helper::isSupportCoroutine() ? 'Y' : 'N', 46 | 'open coroutine running?' => self::isCoroutine() ? 'Y' : 'N', 47 | 'running in coroutine?' => Helper::inCoroutine() ? 'Y' : 'N', 48 | ], 'some information'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/Command/DemoCommand.php: -------------------------------------------------------------------------------- 1 | getFlags() 34 | ->addArg('name', 'description for the argument [name], is required', FlagType::STRING, true) 35 | ->addArg('sex', 'description for the argument [sex], is optional') 36 | ->addArg('age', 'description for the argument [age], is optional', FlagType::INT) 37 | ->addOpt('yes', 'y', 'description for the option [yes], is boolean', FlagType::BOOL) 38 | ->addOpt('opt1', '', 'description for the option [opt1], is required', FlagType::STRING, true) 39 | ->addOpt('opt2', '', 'description for the option [opt2], is optional') 40 | ->setExample($this->parseCommentsVars('{script} {command} john male 43 --opt1 value1')) 41 | ; 42 | } 43 | 44 | /** 45 | * description text by annotation. it is invalid when configure() is exists 46 | * @param Input $input 47 | * @param Output $output 48 | * @return int|void 49 | */ 50 | public function execute(Input $input, Output $output) 51 | { 52 | $output->write('hello, this in ' . __METHOD__); 53 | // $name = $input->getArg('name'); 54 | 55 | $output->write( 56 | << CommandWrapper::new(static function ($fs, $out): void { 31 | $out->println('hello, this is an sub command of test.'); 32 | }, [ 33 | 'desc' => 'sub command of test command' 34 | ]), 35 | ]; 36 | } 37 | 38 | /** 39 | * test text 40 | * 41 | * @usage {name} test message 42 | * @arguments 43 | * arg1 argument description 1 44 | * arg2 argument description 2 45 | * 46 | * @options 47 | * --long,-s option description 1 48 | * --opt option description 2 49 | * 50 | * @param Input $input 51 | * @param Output $output 52 | */ 53 | public function execute(Input $input, Output $output): void 54 | { 55 | $output->write('hello, this in ' . __METHOD__); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/Controller/Attach/DemoSubCommand.php: -------------------------------------------------------------------------------- 1 | 'string option1', 24 | 's2,str2' => 'string option2', 25 | ]; 26 | } 27 | 28 | /** 29 | * Do execute command 30 | * 31 | * @param Input $input 32 | * @param Output $output 33 | * 34 | * @return void|mixed 35 | */ 36 | protected function execute(Input $input, Output $output) 37 | { 38 | vdump(__METHOD__); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/Controller/ProcessController.php: -------------------------------------------------------------------------------- 1 | 'childProcess', 32 | 'mpr' => 'multiProcess', 33 | 'dr' => 'daemonRun', 34 | 'rs' => 'runScript', 35 | 'rb' => 'runInBackground', 36 | ]; 37 | } 38 | 39 | /** 40 | * simple process example for child-process 41 | */ 42 | public function runScriptCommand(): void 43 | { 44 | /*$script = '';*/ 45 | $script = ''; 46 | 47 | // $tmpDir = CliUtil::getTempDir(); 48 | // $tmpFile = $tmpDir . '/' . md5($script) . '.php'; 49 | // file_put_contents($tmpFile, $script); 50 | 51 | $descriptorSpec = [ 52 | 0 => ['pipe', 'r'], // 标准输入,子进程从此管道中读取数据 53 | 1 => ['pipe', 'w'], // 标准输出,子进程向此管道中写入数据 54 | 2 => ['file', $this->app->getRootPath() . '/examples/tmp/error-output.log', 'a'] // 标准错误,写入到一个文件 55 | ]; 56 | 57 | $process = proc_open('php', $descriptorSpec, $pipes); 58 | 59 | if (is_resource($process)) { 60 | // $pipes 现在看起来是这样的: 61 | // 0 => 可以向子进程标准输入写入的句柄 62 | // 1 => 可以从子进程标准输出读取的句柄 63 | // 错误输出将被追加到文件 error-output.txt 64 | 65 | fwrite($pipes[0], $script); 66 | fclose($pipes[0]); 67 | 68 | $result = stream_get_contents($pipes[1]); 69 | 70 | fclose($pipes[1]); 71 | 72 | $this->write("RESULT:\n" . $result); 73 | 74 | // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。 75 | $retVal = proc_close($process); 76 | 77 | echo "command returned $retVal\n"; 78 | } 79 | } 80 | 81 | /** 82 | * simple process example for child-process 83 | */ 84 | public function childProcessCommand(): void 85 | { 86 | $ret = ProcessUtil::create(function ($pid): void { 87 | echo "print in process $pid"; 88 | 89 | sleep(5); 90 | }); 91 | 92 | if ($ret === false) { 93 | $this->output->liteError('current env is not support process create.'); 94 | } 95 | } 96 | 97 | /** 98 | * simple process example for daemon run 99 | * @throws RuntimeException 100 | */ 101 | public function daemonRunCommand(): void 102 | { 103 | $ret = ProcessUtil::daemonRun(function ($pid): void { 104 | $this->output->info("will running background by new process: $pid"); 105 | }); 106 | 107 | if ($ret === 0) { 108 | $this->output->liteError('current env is not support process create.'); 109 | } 110 | } 111 | 112 | /** 113 | * simple process example for run In Background 114 | */ 115 | public function runInBackgroundCommand(): void 116 | { 117 | $script = ''; 118 | $ret = Sys::execInBackground("php $script"); 119 | 120 | if ($ret === false) { 121 | $this->output->liteError('current env is not support process create.'); 122 | } 123 | } 124 | 125 | /** 126 | * simple process example for multi-process 127 | * @options 128 | * 129 | */ 130 | public function multiProcessCommand(): void 131 | { 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /examples/alone: -------------------------------------------------------------------------------- 1 | #!/usr/env/php 2 | setDetached(); 22 | $ctrl->run($input->getFlags()); 23 | } catch (Throwable $e) { 24 | $message = Color::apply('error', $e->getMessage()); 25 | 26 | echo sprintf("%s\nFile %s:%d\nTrace:\n%s\n", 27 | $message, $e->getFile(), $e->getLine(), $e->getTraceAsString() 28 | ); 29 | } 30 | 31 | // can also: see './alone-app' file 32 | -------------------------------------------------------------------------------- /examples/alone-app: -------------------------------------------------------------------------------- 1 | #!/usr/env/php 2 | true, 20 | 'rootPath' => BASE_PATH, 21 | ], $input); 22 | 23 | $app->controller('home', HomeController::class); 24 | 25 | $subCmd = $input->getCommand(); 26 | $app->dispatch('home:' . $subCmd, []); 27 | } catch (Throwable $e) { 28 | $message = Color::apply('error', $e->getMessage()); 29 | 30 | echo sprintf("%s\nFile %s:%d\nTrace:\n%s\n", 31 | $message, $e->getFile(), $e->getLine(), $e->getTraceAsString() 32 | ); 33 | } 34 | 35 | // can also: see './alone' file 36 | -------------------------------------------------------------------------------- /examples/app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | true, 18 | 'rootPath' => dirname(__DIR__), 19 | 'desc' => 'This is demo console application', 20 | ]); 21 | 22 | $app->setLogo(" 23 | ________ ____ ___ ___ __ _ 24 | / ____/ / / _/ / | ____ ____ / (_)________ _/ /_(_)___ ____ 25 | / / / / / / / /| | / __ \/ __ \/ / / ___/ __ `/ __/ / __ \/ __ \ 26 | / /___/ /____/ / / ___ |/ /_/ / /_/ / / / /__/ /_/ / /_/ / /_/ / / / / 27 | \____/_____/___/ /_/ |_/ .___/ .___/_/_/\___/\____/\__/_/\____/_/ /_/ 28 | /_/ /_/ 29 | ", 'success'); 30 | 31 | require __DIR__ . '/commands.php'; 32 | 33 | // run 34 | $app->run(); 35 | -------------------------------------------------------------------------------- /examples/commands.php: -------------------------------------------------------------------------------- 1 | command(DemoCommand::class); 25 | $app->command('exam', function (Input $in, Output $out): void { 26 | $cmd = $in->getCommand(); 27 | 28 | $out->info('hello, this is a test command: ' . $cmd); 29 | }, [ 30 | 'desc' => 'a description message', 31 | ]); 32 | 33 | $app->command('test', TestCommand::class, [ 34 | 'aliases' => ['t'] 35 | ]); 36 | 37 | $app->command(SelfUpdateCommand::class, null, [ 38 | 'aliases' => ['selfUpdate'] 39 | ]); 40 | 41 | $app->command(CorCommand::class); 42 | 43 | $app->controller('home', HomeController::class); 44 | 45 | $app->controller(ProcessController::class, null, [ 46 | 'aliases' => 'prc' 47 | ]); 48 | $app->controller(PharController::class); 49 | $app->controller(ShowController::class); 50 | $app->controller(InteractController::class); 51 | 52 | // add alias for a group command. 53 | $app->addAliases('home:test', 'h-test'); 54 | -------------------------------------------------------------------------------- /examples/demo/cli-spinner.php: -------------------------------------------------------------------------------- 1 | strlen($chars) - 1) { 39 | $spinner = 0; 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Uses `stty` to hide input/output completely. 46 | * 47 | * @param boolean $hidden Will hide/show the next data. Defaults to true. 48 | */ 49 | public static function hide(bool $hidden = true): void 50 | { 51 | system('stty ' . ($hidden? '-echo' : 'echo')); 52 | } 53 | 54 | /** 55 | * Prompts the user for input. Optionally masking it. 56 | * 57 | * @param string $prompt The prompt to show the user 58 | * @param bool $masked If true, the users input will not be shown. e.g. password input 59 | * @param int $limit The maximum amount of input to accept 60 | * 61 | * @return string 62 | */ 63 | public static function prompt(string $prompt, bool $masked, int $limit=100): string 64 | { 65 | echo "$prompt: "; 66 | if ($masked) { 67 | `stty -echo`; // disable shell echo 68 | } 69 | $buffer = ''; 70 | $char = ''; 71 | $f = fopen('php://stdin', 'rb'); 72 | while (strlen($buffer) < $limit) { 73 | $char = fread($f, 1); 74 | if ($char === "\n" || $char === "\r") { 75 | break; 76 | } 77 | $buffer.= $char; 78 | } 79 | if ($masked) { 80 | `stty echo`; // enable shell echo 81 | echo "\n"; 82 | } 83 | return $buffer; 84 | } 85 | } 86 | 87 | Status::hide(); 88 | echo 'Password: '; 89 | $input = fgets(STDIN); 90 | Status::hide(false); 91 | echo $input; 92 | die; 93 | 94 | /** @noinspection PhpUnreachableStatementInspection */ 95 | $total = random_int(5000, 10000); 96 | for ($x=1; $x<=$total; $x++) { 97 | Status::spinner(); 98 | usleep(50); 99 | } 100 | 101 | Status::clearLine(); 102 | 103 | // 104 | // $answer = Status::prompt("What is the secret word?", 0); 105 | // if ($answer == "secret") { 106 | // echo "Yay! You got it!"; 107 | // } else { 108 | // echo "Boo! That is wrong!"; 109 | // } 110 | -------------------------------------------------------------------------------- /examples/demo/color.php: -------------------------------------------------------------------------------- 1 | ', $length, $tfKb, $i * 10); 27 | // mac is not support there are chars. 28 | // printf("\r[%-100s] %d%% (%2d/%2d kb)", str_repeat(chr(22), $length) . '>', $length, $tfKb, $i * 10); // ■ 29 | // printf("\r[%-100s] %d%% (%2d/%2d kb)", str_repeat(chr(1), $length) . '>', $length, $tfKb, $i * 10);// ☺ 30 | // printf("\r[%-100s] %d%% (%2d/%2d kb)", str_repeat(chr(2), $length) . '>', $length, $tfKb, $i * 10);// ☻ 31 | printf("\r[%-100s] %d%% (%2d/%2d kb)", str_repeat(chr(3), $length) . '>', $length, $tfKb, $i * 10);// ♥ 32 | 33 | usleep(50000); 34 | $i++; 35 | } 36 | 37 | echo "end\n"; 38 | -------------------------------------------------------------------------------- /examples/demo/progress_bar1.php: -------------------------------------------------------------------------------- 1 | start($total); 20 | while ($i <= $total) { 21 | $bar->advance(); 22 | usleep(50000); 23 | $i++; 24 | } 25 | $bar->finish(); 26 | var_dump($bar); 27 | -------------------------------------------------------------------------------- /examples/demo/progress_bar3.php: -------------------------------------------------------------------------------- 1 | = 100) { 30 | $progress = 100; 31 | $finished = true; 32 | } 33 | 34 | printf( 35 | "\r[%-100s] %d%% %s", 36 | str_repeat($char, $progress) . ($finished ? '' : '>'), 37 | $progress, 38 | $msg 39 | );// ♥ 40 | 41 | if ($finished) { 42 | echo "\n"; 43 | break; 44 | } 45 | } 46 | } 47 | 48 | $i = 0; 49 | $total = 120; 50 | $bar = progress_bar($total, 'Msg Text', '#'); 51 | echo "progress:\n"; 52 | while ($i <= $total) { 53 | $bar->send($i); 54 | usleep(50000); 55 | $i++; 56 | } 57 | //var_dump($bar->valid()); 58 | -------------------------------------------------------------------------------- /examples/demo/spinner.php: -------------------------------------------------------------------------------- 1 | rewind(); 42 | sleep(4); 43 | $y->send(false); 44 | -------------------------------------------------------------------------------- /examples/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/examples/tmp/.gitkeep -------------------------------------------------------------------------------- /phar.build.inc: -------------------------------------------------------------------------------- 1 | stripComments(false) 14 | ->setShebang(true) 15 | ->addExclude([ 16 | 'demo/', 17 | 'docs/', 18 | 'example/', 19 | 'runtime/', 20 | 'node_modules/', 21 | 'test/', 22 | 'tmp/', 23 | '/console/resource/', 24 | ]) 25 | ->addFile([ 26 | 'LICENSE', 27 | 'composer.json', 28 | 'README.md', 29 | 'test/bootstrap.php', 30 | ]) 31 | ->setCliIndex('examples/app') 32 | // ->setWebIndex('web/index.php') 33 | // ->setVersionFile('config/config.php') 34 | ; 35 | 36 | // Console 下的 Command Controller 命令类不去除注释,注释上是命令帮助信息 37 | $compiler->setStripFilter(static function ($file) { 38 | /** @var SplFileInfo $file */ 39 | $name = $file->getFilename(); 40 | 41 | return !str_contains($name, 'Command.php') && !str_contains($name, 'Controller.php'); 42 | }); 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resource/art-fonts/404.txt: -------------------------------------------------------------------------------- 1 | _ _ ___ _ _ 2 | | || | / _ \| || | 3 | | || |_| | | | || |_ 4 | |__ _| | | |__ _| 5 | | | | |_| | | | 6 | |_| \___/ |_| -------------------------------------------------------------------------------- /resource/art-fonts/404_italic.txt: -------------------------------------------------------------------------------- 1 | __ __ ____ __ __ 2 | / // / / __ \/ // / 3 | / // /_/ / / / // /_ 4 | /__ __/ /_/ /__ __/ 5 | /_/ \____/ /_/ 6 | -------------------------------------------------------------------------------- /resource/art-fonts/500.txt: -------------------------------------------------------------------------------- 1 | _____ ___ ___ 2 | | ____|/ _ \ / _ \ 3 | | |__ | | | | | | | 4 | |___ \| | | | | | | 5 | ___) | |_| | |_| | 6 | |____/ \___/ \___/ -------------------------------------------------------------------------------- /resource/art-fonts/500_italic.txt: -------------------------------------------------------------------------------- 1 | __________ ____ 2 | / ____/ __ \/ __ \ 3 | /___ \/ / / / / / / 4 | ____/ / /_/ / /_/ / 5 | /_____/\____/\____/ 6 | -------------------------------------------------------------------------------- /resource/art-fonts/app-name.txt: -------------------------------------------------------------------------------- 1 | ____ _ ___ _ _ _ _ _ 2 | / ___| | |_ _| / \ _ __ _ __ | (_) ___ __ _| |_(_) ___ _ __ 3 | | | | | | | / _ \ | '_ \| '_ \| | |/ __/ _` | __| |/ _ \| '_ \ 4 | | |___| |___ | | / ___ \| |_) | |_) | | | (_| (_| | |_| | (_) | | | | 5 | \____|_____|___| /_/ \_\ .__/| .__/|_|_|\___\____|\__|_|\___/|_| |_| 6 | |_| |_| -------------------------------------------------------------------------------- /resource/art-fonts/app-name_italic.txt: -------------------------------------------------------------------------------- 1 | ________ ____ ___ ___ __ _ 2 | / ____/ / / _/ / | ____ ____ / (_)________ _/ /_(_)___ ____ 3 | / / / / / / / /| | / __ \/ __ \/ / / ___/ __ `/ __/ / __ \/ __ \ 4 | / /___/ /____/ / / ___ |/ /_/ / /_/ / / / /__/ /_/ / /_/ / /_/ / / / / 5 | \____/_____/___/ /_/ |_/ .___/ .___/_/_/\___/\____/\__/_/\____/_/ /_/ 6 | /_/ /_/ -------------------------------------------------------------------------------- /resource/art-fonts/error.txt: -------------------------------------------------------------------------------- 1 | ______ 2 | | ____| 3 | | |__ _ __ _ __ ___ _ __ 4 | | __| | '__| '__/ _ \| '__| 5 | | |____| | | | | (_) | | 6 | |______|_| |_| \___/|_| 7 | -------------------------------------------------------------------------------- /resource/art-fonts/error_italic.txt: -------------------------------------------------------------------------------- 1 | ______ 2 | / ____/_____________ _____ 3 | / __/ / ___/ ___/ __ \/ ___/ 4 | / /___/ / / / / /_/ / / 5 | /_____/_/ /_/ \____/_/ 6 | -------------------------------------------------------------------------------- /resource/art-fonts/no.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | | \ | | 3 | | \| | ___ 4 | | . ` |/ _ \ 5 | | |\ | (_) | 6 | |_| \_|\___/ 7 | -------------------------------------------------------------------------------- /resource/art-fonts/no_italic.txt: -------------------------------------------------------------------------------- 1 | _ __ 2 | / | / /___ 3 | / |/ / __ \ 4 | / /| / /_/ / 5 | /_/ |_/\____/ 6 | -------------------------------------------------------------------------------- /resource/art-fonts/ok.txt: -------------------------------------------------------------------------------- 1 | ____ _ 2 | / __ \| | 3 | | | | | | __ 4 | | | | | |/ / 5 | | |__| | < 6 | \____/|_|\_\ 7 | -------------------------------------------------------------------------------- /resource/art-fonts/ok_italic.txt: -------------------------------------------------------------------------------- 1 | ____ __ 2 | / __ \/ /__ 3 | / / / / //_/ 4 | / /_/ / .< 5 | \____/_/|_| 6 | -------------------------------------------------------------------------------- /resource/art-fonts/somefonts.txt: -------------------------------------------------------------------------------- 1 | standard: 2 | ____ _ 3 | / ___| __ _ _ __ ___ _ __ | | ___ 4 | \___ \ / _` | '_ ` _ \| '_ \| |/ _ \ 5 | ___) | (_| | | | | | | |_) | | __/ 6 | |____/ \__,_|_| |_| |_| .__/|_|\___| 7 | |_| 8 | 9 | small: 10 | 11 | ___ _ 12 | / __| __ _ _ __ _ __| |___ 13 | \__ \/ _` | ' \| '_ \ / -_) 14 | |___/\__,_|_|_|_| .__/_\___| 15 | |_| 16 | 17 | big: 18 | 19 | _____ _ 20 | / ____| | | 21 | | (___ __ _ _ __ ___ _ __ | | ___ 22 | \___ \ / _` | '_ ` _ \| '_ \| |/ _ \ 23 | ____) | (_| | | | | | | |_) | | __/ 24 | |_____/ \__,_|_| |_| |_| .__/|_|\___| 25 | | | 26 | |_| 27 | 28 | larry3d: 29 | 30 | ____ ___ 31 | /\ _`\ __ /\_ \ 32 | \ \,\L\_\/\_\ ___ ___ _____\//\ \ __ 33 | \/_\__ \\/\ \ /' __` __`\/\ '__`\\ \ \ /'__`\ 34 | /\ \L\ \ \ \/\ \/\ \/\ \ \ \L\ \\_\ \_/\ __/ 35 | \ `\____\ \_\ \_\ \_\ \_\ \ ,__//\____\ \____\ 36 | \/_____/\/_/\/_/\/_/\/_/\ \ \/ \/____/\/____/ 37 | \ \_\ 38 | \/_/ 39 | 40 | rounded: 41 | 42 | ______ _ 43 | / _____) | | 44 | ( (____ _____ ____ ____ | | _____ 45 | \____ \(____ | \| _ \| || ___ | 46 | _____) ) ___ | | | | |_| | || ____| 47 | (______/\_____|_|_|_| __/ \_)_____) 48 | |_| 49 | 50 | slant: 51 | 52 | _____ __ 53 | / ___/____ _____ ___ ____ / /__ 54 | \__ \/ __ `/ __ `__ \/ __ \/ / _ \ 55 | ___/ / /_/ / / / / / / /_/ / / __/ 56 | /____/\__,_/_/ /_/ /_/ .___/_/\___/ 57 | /_/ 58 | 59 | -------------------------------------------------------------------------------- /resource/art-fonts/success.txt: -------------------------------------------------------------------------------- 1 | _____ 2 | / ____| 3 | | (___ _ _ ___ ___ ___ ___ ___ 4 | \___ \| | | |/ __/ __/ _ \/ __/ __| 5 | ____) | |_| | (_| (_| __/\__ \__ \ 6 | |_____/ \____|\___\___\___||___/___/ 7 | -------------------------------------------------------------------------------- /resource/art-fonts/success_italic.txt: -------------------------------------------------------------------------------- 1 | _____ 2 | / ___/__ _______________ __________ 3 | \__ \/ / / / ___/ ___/ _ \/ ___/ ___/ 4 | ___/ / /_/ / /__/ /__/ __(__ |__ ) 5 | /____/\____/\___/\___/\___/____/____/ 6 | -------------------------------------------------------------------------------- /resource/art-fonts/yes.txt: -------------------------------------------------------------------------------- 1 | __ __ 2 | \ \ / / 3 | \ \_/ /__ ___ 4 | \ / _ \/ __| 5 | | | __/\__ \ 6 | |_|\___||___/ 7 | -------------------------------------------------------------------------------- /resource/art-fonts/yes_italic.txt: -------------------------------------------------------------------------------- 1 | __ __ 2 | \ \/ /__ _____ 3 | \ / _ \/ ___/ 4 | / / __(__ ) 5 | /_/\___/____/ 6 | -------------------------------------------------------------------------------- /resource/cmd/README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 3 | 一些辅助命令及脚本文件。 4 | 5 | 来自于: 6 | 7 | - ConEmu https://github.com/Maximus5/ConEmu 8 | 9 | ## 使用 10 | 11 | ### 在cmd里输出颜色: 12 | 13 | 需要设置一个环境变量 `set ESC=`, 对应的 ASCII `\x1B` 14 | 15 | 先运行: 16 | 17 | ```bash 18 | call SetEscChar.cmd 19 | ``` 20 | 21 | 然后执行: 22 | 23 | ```bash 24 | echo %ESC%[1;33;40m Yellow on black %ESC%[0m 25 | ``` 26 | 27 | 成功的话,就会看见黄色的文字 Yellow on black 28 | 29 | ### 设置cmd中的字符集为 `utf-8` 30 | 31 | ```bash 32 | chcp 65001 & cmd 33 | ``` 34 | 35 | 36 | 37 | ## 参考 38 | 39 | - https://conemu.github.io/en/AnsiEscapeCodes.html -------------------------------------------------------------------------------- /resource/cmd/SetEscChar.cmd: -------------------------------------------------------------------------------- 1 | @rem This file helps you using ANSI in your cmd scripts 2 | @rem Just call this file and you can type in your prompt smth like 3 | @rem 4 | @rem call SetEscChar 5 | @rem echo %ESC%[1;33;40m Yellow on black %ESC%[0m 6 | @rem 7 | 8 | @set ESC= 9 | -------------------------------------------------------------------------------- /resource/exe/README.md: -------------------------------------------------------------------------------- 1 | # exe file 2 | 3 | ## Hidden Input 4 | 5 | From https://github.com/Seldaek/hidden-input -------------------------------------------------------------------------------- /resource/exe/hiddeninput.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-console/d8be9b366efb369e5148ef0f649f6346605e36e6/resource/exe/hiddeninput.exe -------------------------------------------------------------------------------- /resource/refer/shell-completion-doc.md: -------------------------------------------------------------------------------- 1 | # shell completion 2 | 3 | ## bash 4 | 5 | https://github.com/nosarthur/gita/blob/master/.gita-completion.bash 6 | 7 | ## zsh 8 | 9 | https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh 10 | 11 | 12 | -------------------------------------------------------------------------------- /resource/refer/zsh-complete-doc.md: -------------------------------------------------------------------------------- 1 | # zsh complete 2 | 3 | 参考补全代码仓库提供的文档: 4 | 5 | https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org 6 | 7 | 补全代码示例: 8 | 9 | https://github.com/zsh-users/zsh-completions/blob/master/src/_golang 10 | 11 | 重新加载定义的补全函数: 12 | 13 | ```bash 14 | unfunction _pandoc && autoload -U _pandoc 15 | ``` 16 | -------------------------------------------------------------------------------- /resource/templates/bash-completion.tpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # ------------------------------------------------------------------------------ 3 | # DATE: {{datetime}} 4 | # FILE: {{filename}} 5 | # AUTHOR: inhere (https://github.com/inhere) 6 | # VERSION: {{version}} 7 | # HOMEPAGE: https://github.com/inhere/php-console 8 | # DESCRIPTION: bash shell complete for console app: {{binName}} 9 | # ------------------------------------------------------------------------------ 10 | # 11 | # temp usage: 12 | # source {{filename}} 13 | # add to ~/.bashrc: 14 | # source path/to/{{filename}} 15 | # run 'complete' to see registered complete function. 16 | 17 | _complete_for_{{fmtBinName}} () { 18 | local cur prev 19 | commands="{{commands}}" 20 | COMPREPLY=($(compgen -W "$commands" -- "$cur")) 21 | } 22 | 23 | complete -F _complete_for_{{fmtBinName}} {{binName}} 24 | -------------------------------------------------------------------------------- /resource/templates/zsh-completion.tpl: -------------------------------------------------------------------------------- 1 | #compdef {{binName}} 2 | # ------------------------------------------------------------------------------ 3 | # DATE: {{datetime}} 4 | # FILE: {{filename}} 5 | # AUTHOR: inhere (https://github.com/inhere) 6 | # VERSION: {{version}} 7 | # HOMEPAGE: https://github.com/inhere/php-console 8 | # DESCRIPTION: zsh shell complete for console app: {{binName}} 9 | # ------------------------------------------------------------------------------ 10 | # 11 | # temp usage: 12 | # source {{filename}} 13 | # add to ~/.zshrc: 14 | # source path/to/{{filename}} 15 | 16 | _complete_for_{{fmtBinName}} () { 17 | local -a commands 18 | IFS=$'\n' 19 | commands+=( 20 | {{commands}} 21 | ) 22 | 23 | _describe 'commands' commands 24 | } 25 | 26 | compdef _complete_for_{{fmtBinName}} {{binName}} 27 | -------------------------------------------------------------------------------- /sami.doc.inc: -------------------------------------------------------------------------------- 1 | files() 9 | ->name('*.php') 10 | ->notName('routes.php') 11 | ->exclude(['test', 'example']) 12 | ->in([ 13 | __DIR__ . '/src/' 14 | ]); 15 | 16 | $versions = GitVersionCollection::create(__DIR__); 17 | 18 | return new Sami($iterator, [ 19 | // 'theme' => 'enhanced', 20 | // 'versions' => $versions, 21 | 'title' => 'Inhere Console Classes Documentation', 22 | 'build_dir' => __DIR__ . '/classes/%version%', 23 | 'cache_dir' => __DIR__ . '/caches/%version%', 24 | 'default_opened_level' => 1, 25 | // 'store' => new MyArrayStore, 26 | ]); 27 | 28 | /** 29 | * usage: php sami.phar update --force sami.doc.inc 30 | */ 31 | -------------------------------------------------------------------------------- /src/Annotate/AnnotateRules.php: -------------------------------------------------------------------------------- 1 | allow multi tags 24 | 'desc' => false, 25 | 'usage' => false, 26 | 'argument' => true, 27 | 'option' => true, 28 | 'example' => false, 29 | 'help' => false, 30 | ]; 31 | 32 | /** 33 | * @return array 34 | */ 35 | public static function getAllowedTags(): array 36 | { 37 | return self::$allowedTags; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Annotate/Attr/CmdArgument.php: -------------------------------------------------------------------------------- 1 | setEnvVar($envVar); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Annotate/Attr/CmdOption.php: -------------------------------------------------------------------------------- 1 | setEnvVar($envVar); 33 | $this->setShortcut($shortcut); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Annotate/Attr/RuleArg.php: -------------------------------------------------------------------------------- 1 | public) 45 | * -H,--host The server host address(127.0.0.1) 46 | * -p,--port The server port number(8552) 47 | * -b,--php-bin The php binary file(php) 48 | * 49 | * @arguments 50 | * file The entry file for server. e.g web/index.php 51 | * 52 | * @param Input $input 53 | * @param Output $output 54 | * 55 | * @return void 56 | * @throws Exception 57 | * @example 58 | * {command} -S 127.0.0.1:8552 web/index.php 59 | */ 60 | 61 | #[CmdOption('dev-serve', 'start a php built-in http server for developmentd')] 62 | public function execute(Input $input, Output $output): void 63 | { 64 | $serveAddr = $this->flags->getOpt('addr'); 65 | if (!$serveAddr) { 66 | $serveAddr = $this->flags->getOpt('host'); 67 | } 68 | 69 | $port = $this->flags->getOpt('port'); 70 | if ($port && !str_contains($serveAddr, ':')) { 71 | $serveAddr .= ':' . $port; 72 | } 73 | 74 | $docRoot = $this->flags->getOpt('doc-root'); 75 | $hceFile = $this->flags->getOpt('hce-file'); 76 | $hceEnv = $this->flags->getOpt('hce-env'); 77 | $phpBin = $this->flags->getOpt('php-bin'); 78 | 79 | $entryFile = $this->flags->getArg('file'); 80 | 81 | $pds = PhpDevServe::new($serveAddr, $docRoot, $entryFile); 82 | $pds->setPhpBin($phpBin); 83 | 84 | if ($hceEnv && $hceFile) { 85 | $pds->loadHceFile($hceFile); 86 | $pds->useHceEnv($hceEnv); 87 | } 88 | 89 | $pds->listen(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/BuiltIn/README.md: -------------------------------------------------------------------------------- 1 | # something 2 | 3 | 4 | ## art font 5 | 6 | - sublime plugin: `Figlet Big ASCII Text` 7 | - php implement: [povils/figlet](https://github.com/povils/figlet) 8 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | 'type;desc', 49 | * ] 50 | * 51 | * @return array 52 | */ 53 | protected function getArguments(): array 54 | { 55 | return []; 56 | } 57 | 58 | /** 59 | * @param FlagsParser $fs 60 | */ 61 | protected function beforeInitFlagsParser(FlagsParser $fs): void 62 | { 63 | $fs->addArgsByRules($this->getArguments()); 64 | // $fs->setStopOnFistArg(false); 65 | } 66 | 67 | /** 68 | * @param FlagsParser $fs 69 | * 70 | * @throws ReflectionException 71 | */ 72 | protected function afterInitFlagsParser(FlagsParser $fs): void 73 | { 74 | $this->debugf('cmd: %s - load command flags configure, class: %s', $this->getRealCName(), static::class); 75 | $this->configure(); 76 | $this->configFlags($fs); 77 | 78 | $isEmpty = $this->flags->isEmpty(); 79 | 80 | // load built in options 81 | $fs->addOptsByRules(GlobalOption::getAloneOptions()); 82 | 83 | // not config flags. load rules from method doc-comments 84 | if ($isEmpty) { 85 | $this->loadRulesByDocblock(self::METHOD, $fs); 86 | } 87 | } 88 | 89 | /** 90 | * @param array $args 91 | * 92 | * @return mixed 93 | */ 94 | protected function doRun(array $args): mixed 95 | { 96 | // if input sub-command name 97 | if (isset($args[0])) { 98 | $first = $args[0]; 99 | $rName = $this->resolveAlias($first); 100 | 101 | if ($this->isSub($rName)) { 102 | array_shift($args); 103 | return $this->dispatchSub($rName, $args); 104 | } 105 | } 106 | 107 | return parent::doRun($args); 108 | } 109 | 110 | /** 111 | * Show help information 112 | * 113 | * @return bool 114 | */ 115 | protected function showHelp(): bool 116 | { 117 | $aliases = $this->getAliases(); 118 | 119 | $this->logf(Console::VERB_CRAZY, 'display help info for the command: %s', $this->commandName); 120 | 121 | // $execMethod = self::METHOD; 122 | // return $this->showHelpByAnnotations($execMethod, '', $aliases) !== 0; 123 | return $this->showHelpByFlagsParser($this->flags, $aliases) !== 0; 124 | } 125 | 126 | /** 127 | * @return string 128 | */ 129 | public function getRealCName(): string 130 | { 131 | return self::getName(); 132 | } 133 | 134 | /** 135 | * Get the group 136 | * 137 | * @return Controller|null 138 | */ 139 | public function getGroup(): ?Controller 140 | { 141 | return $this->group; 142 | } 143 | 144 | /** 145 | * Set the value of group 146 | * 147 | * @param Controller $group 148 | */ 149 | public function setGroup(Controller $group): void 150 | { 151 | $this->group = $group; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Component/Animation/Animation.php: -------------------------------------------------------------------------------- 1 | formatter; 44 | } 45 | 46 | /** 47 | * @param FormatterInterface $formatter 48 | */ 49 | public function setFormatter(FormatterInterface $formatter): void 50 | { 51 | $this->formatter = $formatter; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Component/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | getMessage()); 72 | return; 73 | } 74 | 75 | $class = get_class($e); 76 | 77 | // open debug, throw exception 78 | if ($this->isDebug()) { 79 | $tpl = << Error %s 81 | 82 | At File %s line %d 83 | Exception class is $class 84 | Code View:\n\n%s 85 | Code Trace:\n\n%s\n 86 | ERR; 87 | $line = $e->getLine(); 88 | $file = $e->getFile(); 89 | $prev = $e->getPrevious(); 90 | 91 | // var_dump($e); 92 | $snippet = Highlighter::create()->snippet(file_get_contents($file), $line, 3, 3); 93 | $message = sprintf( 94 | $tpl, // $e->getCode(), 95 | $e->getMessage(), 96 | $file, 97 | $line, // __METHOD__, 98 | $snippet, 99 | $e->getTraceAsString() . ($prev ? "\nPrevious: {$prev->getMessage()}\n" . $prev->getTraceAsString() : '') 100 | ); 101 | 102 | if ($this->hideRootPath && ($rootPath = $this->rootPath)) { 103 | $message = str_replace($rootPath, '{ROOT}', $message); 104 | } 105 | 106 | Cli::write($message, false); 107 | return; 108 | } 109 | 110 | // simple output 111 | Cli::error($e->getMessage() ?: 'unknown error'); 112 | Cli::write("\nYou can use '--debug 4' to see error details."); 113 | } 114 | 115 | /** 116 | * @return bool 117 | */ 118 | public function isDebug(): bool 119 | { 120 | return $this->debug; 121 | } 122 | 123 | /** 124 | * @param bool $debug 125 | */ 126 | public function setDebug(bool $debug): void 127 | { 128 | $this->debug = $debug; 129 | } 130 | 131 | /** 132 | * @return string 133 | */ 134 | public function getRootPath(): string 135 | { 136 | return $this->rootPath; 137 | } 138 | 139 | /** 140 | * @param string $rootPath 141 | */ 142 | public function setRootPath(string $rootPath): void 143 | { 144 | $this->rootPath = $rootPath; 145 | } 146 | 147 | /** 148 | * @return bool 149 | */ 150 | public function isHideRootPath(): bool 151 | { 152 | return $this->hideRootPath; 153 | } 154 | 155 | /** 156 | * @param bool $hideRootPath 157 | */ 158 | public function setHideRootPath(bool $hideRootPath): void 159 | { 160 | $this->hideRootPath = $hideRootPath; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Component/Formatter/Alert.php: -------------------------------------------------------------------------------- 1 | '<{@style}>{@message}', 23 | 'theme1' => '<{@style}>[{@type}] {@message}', 24 | 'lite' => '[<{@style}>{@type}] {@message}', 25 | ]; 26 | 27 | public static function simple(string $message, string $style = 'info', array $opts = []): void 28 | { 29 | } 30 | 31 | public static function block(string $message, string $style = 'info', array $opts = []): void 32 | { 33 | $opts = array_merge([ 34 | 'paddingX' => 1, // line 35 | 'paddingY' => 1, // space 36 | 37 | 'icon' => '', 38 | 'theme' => 'default', 39 | 'template' => '' 40 | ], $opts); 41 | } 42 | 43 | public static function lite(string $message, string $style = 'info'): void 44 | { 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Component/Formatter/Block.php: -------------------------------------------------------------------------------- 1 | [ 29 | * 'name' => 'value text', 30 | * 'name2' => 'value text 2', 31 | * ], 32 | * 'list2 title' => [ 33 | * 'name' => 'value text', 34 | * 'name2' => 'value text 2', 35 | * ], 36 | * ... ... 37 | * ]; 38 | * 39 | * MultiList::show($data); 40 | * ``` 41 | * 42 | * @param array $data 43 | * @param array $opts 44 | * 45 | * @psalm-param array{beforeWrite: callable, lastNewline: bool} $opts 46 | */ 47 | public static function show(array $data, array $opts = []): void 48 | { 49 | $stringList = []; 50 | $ignoreEmpty = $opts['ignoreEmpty'] ?? true; 51 | $lastNewline = true; 52 | 53 | $opts['returned'] = true; 54 | if (isset($opts['lastNewline'])) { 55 | $lastNewline = $opts['lastNewline']; 56 | unset($opts['lastNewline']); 57 | } 58 | 59 | $beforeWrite = null; 60 | if (isset($opts['beforeWrite'])) { 61 | $beforeWrite = $opts['beforeWrite']; 62 | unset($opts['beforeWrite']); 63 | } 64 | 65 | foreach ($data as $title => $list) { 66 | if ($ignoreEmpty && !$list) { 67 | continue; 68 | } 69 | 70 | $stringList[] = SingleList::show($list, (string)$title, $opts); 71 | } 72 | 73 | $str = implode("\n", $stringList); 74 | 75 | // before write handler 76 | if ($beforeWrite) { 77 | $str = $beforeWrite($str); 78 | } 79 | 80 | Console::write($str, $lastNewline); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Component/Formatter/Padding.php: -------------------------------------------------------------------------------- 1 | '$1.99', 32 | * 'Oatmeal' => '$4.99', 33 | * 'Bacon' => '$2.99', 34 | * ]; 35 | * ``` 36 | * 37 | * @param array $data 38 | * @param string $title 39 | * @param array $opts 40 | */ 41 | public static function show(array $data, string $title = '', array $opts = []): void 42 | { 43 | if (!$data) { 44 | return; 45 | } 46 | 47 | $string = $title ? ColorTag::wrap(ucfirst($title), 'comment') . ":\n" : ''; 48 | $opts = array_merge([ 49 | 'char' => '.', 50 | 'indent' => ' ', 51 | 'padding' => 10, 52 | 'valueStyle' => 'info', 53 | ], $opts); 54 | 55 | $keyMaxLen = ArrayHelper::getKeyMaxWidth($data); 56 | $paddingLen = max($keyMaxLen, $opts['padding']); 57 | 58 | foreach ($data as $label => $value) { 59 | $value = ColorTag::wrap((string)$value, $opts['valueStyle']); 60 | $string .= $opts['indent'] . Str::pad($label, $paddingLen, $opts['char']) . " $value\n"; 61 | } 62 | 63 | Console::write(trim($string)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Component/Formatter/Section.php: -------------------------------------------------------------------------------- 1 | 80, 39 | 'char' => self::CHAR_HYPHEN, 40 | 'titlePos' => self::POS_LEFT, 41 | 'indent' => 2, 42 | 'topBorder' => true, 43 | 'bottomBorder' => true, 44 | ], $opts); 45 | 46 | // list($sW, $sH) = Helper::getScreenSize(); 47 | $width = (int)$opts['width']; 48 | $char = trim($opts['char']); 49 | $indent = (int)$opts['indent'] >= 0 ? $opts['indent'] : 2; 50 | $indentStr = Str::pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); 51 | 52 | $title = Str::ucwords(trim($title)); 53 | $tLength = Str::len($title); 54 | $width = $width > 10 ? $width : 80; 55 | 56 | // title position 57 | if ($tLength >= $width) { 58 | $titleIndent = Str::pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); 59 | } elseif ($opts['titlePos'] === self::POS_RIGHT) { 60 | $titleIndent = Str::pad(self::CHAR_SPACE, ceil($width - $tLength) + $indent, self::CHAR_SPACE); 61 | } elseif ($opts['titlePos'] === self::POS_MIDDLE) { 62 | $titleIndent = Str::pad(self::CHAR_SPACE, ceil(($width - $tLength) / 2) + $indent, self::CHAR_SPACE); 63 | } else { 64 | $titleIndent = Str::pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); 65 | } 66 | 67 | $template = "%s\n%s%s\n%s";// title topBorder body bottomBorder 68 | $topBorder = $bottomBorder = ''; 69 | $titleLine = "$titleIndent$title"; 70 | 71 | $showTBorder = (bool)$opts['topBorder']; 72 | $showBBorder = (bool)$opts['bottomBorder']; 73 | 74 | if ($showTBorder || $showBBorder) { 75 | $border = Str::pad($char, $width, $char); 76 | 77 | if ($showTBorder) { 78 | $topBorder = "$indentStr$border\n"; 79 | } 80 | 81 | if ($showBBorder) { 82 | $bottomBorder = "$indentStr$border\n"; 83 | } 84 | } 85 | 86 | $body = is_array($body) ? implode(PHP_EOL, $body) : $body; 87 | $body = FormatUtil::wrapText($body, 4, $opts['width']); 88 | 89 | Console::writef($template, $titleLine, $topBorder, $body, $bottomBorder); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Component/Formatter/SingleList.php: -------------------------------------------------------------------------------- 1 | 'value text', 35 | * 'name2' => 'value text 2', 36 | * ]; 37 | * ``` 38 | * 39 | * @param array|mixed $data 40 | * @param string $title 41 | * @param array $opts More {@see FormatUtil::spliceKeyValue()} 42 | * 43 | * @return int|string 44 | */ 45 | public static function show(mixed $data, string $title = 'Information', array $opts = []): int|string 46 | { 47 | $string = ''; 48 | $opts = array_merge([ 49 | 'leftChar' => ' ', 50 | // 'sepChar' => ' ', 51 | 'keyStyle' => 'info', 52 | 'keyMinWidth' => 8, 53 | 'titleStyle' => 'comment', 54 | 'ucFirst' => false, 55 | 'returned' => false, 56 | 'ucTitleWords' => true, 57 | 'lastNewline' => true, 58 | ], $opts); 59 | 60 | // title 61 | if ($title) { 62 | $title = $opts['ucTitleWords'] ? Str::ucwords(trim($title)) : $title; 63 | $string .= ColorTag::wrap($title, $opts['titleStyle']) . PHP_EOL; 64 | } 65 | 66 | // handle item list 67 | $string .= FormatUtil::spliceKeyValue((array)$data, $opts); 68 | 69 | // return formatted string. 70 | if ($opts['returned']) { 71 | return $string; 72 | } 73 | 74 | return Console::write($string, $opts['lastNewline']); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Component/Formatter/Title.php: -------------------------------------------------------------------------------- 1 | 80, 35 | 'char' => self::CHAR_EQUAL, 36 | 'titlePos' => self::POS_LEFT, 37 | 'titleStyle' => 'bold', 38 | 'indent' => 0, 39 | 'ucWords' => true, 40 | 'showBorder' => true, 41 | ], $opts); 42 | 43 | // list($sW, $sH) = Helper::getScreenSize(); 44 | $width = (int)$opts['width']; 45 | $char = trim($opts['char']); 46 | $indent = (int)$opts['indent'] >= 0 ? (int)$opts['indent'] : 0; 47 | 48 | $indentStr = ''; 49 | if ($indent > 0) { 50 | $indentStr = Str::pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); 51 | } 52 | 53 | $title = $opts['ucWords'] ? Str::ucwords(trim($title)) : trim($title); 54 | $tLength = Str::len($title); 55 | $width = $width > 10 ? $width : 80; 56 | 57 | [$sw,] = Sys::getScreenSize(); 58 | if ($sw > $width) { 59 | $width = $sw; 60 | } 61 | 62 | // title position 63 | $titleIndent = ''; 64 | if ($tLength >= $width) { 65 | $titleIndent = Str::pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); 66 | } elseif ($opts['titlePos'] === self::POS_RIGHT) { 67 | $titleIndent = Str::pad(self::CHAR_SPACE, ceil($width - $tLength) + $indent, self::CHAR_SPACE); 68 | } elseif ($opts['titlePos'] === self::POS_MIDDLE) { 69 | $titleIndent = Str::pad(self::CHAR_SPACE, ceil(($width - $tLength) / 2) + $indent, self::CHAR_SPACE); 70 | } elseif ($indent > 0) { 71 | $titleIndent = Str::pad(self::CHAR_SPACE, $indent, self::CHAR_SPACE); 72 | } 73 | 74 | $titleText = ColorTag::wrap($title, $opts['titleStyle']); 75 | $titleLine = "$titleIndent$titleText\n"; 76 | 77 | $border = $indentStr . Str::pad($char, $width, $char); 78 | 79 | Console::write($titleLine . $border); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Component/Formatter/Tree.php: -------------------------------------------------------------------------------- 1 | Cli::isSupportColor() ? '─' : '-', // —— 51 | 'char' => '-', 52 | 'prefix' => Cli::isSupportColor() ? '├' : '|', 53 | 'leftPadding' => '', 54 | ], $opts); 55 | 56 | $opts['_level'] = 1; 57 | $opts['_is_main'] = true; 58 | 59 | Console::startBuffer(); 60 | } 61 | 62 | foreach ($data as $key => $value) { 63 | if (is_scalar($value)) { 64 | $counter++; 65 | $leftString = $opts['leftPadding'] . str_pad($opts['prefix'], $opts['_level'] + 1, $opts['char']); 66 | 67 | Console::write($leftString . ' ' . Std::toString($value)); 68 | } elseif (is_array($value)) { 69 | $newOpts = $opts; 70 | $newOpts['_is_main'] = false; 71 | $newOpts['_level']++; 72 | 73 | self::show($value, $newOpts); 74 | } 75 | } 76 | 77 | if ($opts['_is_main']) { 78 | Console::write('node count: ' . $counter); 79 | // var_dump('f'); 80 | Console::flushBuffer(); 81 | 82 | // reset. 83 | $counter = $started = 0; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Component/Interact/AbstractSelect.php: -------------------------------------------------------------------------------- 1 | allowExit; 35 | } 36 | 37 | /** 38 | * @param bool $allowExit 39 | */ 40 | public function setAllowExit(bool $allowExit): void 41 | { 42 | $this->allowExit = $allowExit; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Component/Interact/Checkbox.php: -------------------------------------------------------------------------------- 1 | $description"; 57 | foreach ($options as $key => $value) { 58 | $text .= "\n $key) $value"; 59 | } 60 | 61 | Console::write($text); 62 | $defText = $default !== null ? "[default:$default]" : ''; 63 | $filter = static function ($val) use ($options) { 64 | return $val !== 'q' && isset($options[$val]); 65 | }; 66 | 67 | beginChoice: 68 | $r = Console::readln("Your choice$defText : "); 69 | $r = $r !== '' ? str_replace(' ', '', trim($r, $sep)) : ''; 70 | 71 | // empty 72 | if ($r === '') { 73 | goto beginChoice; 74 | } 75 | 76 | // exit 77 | if ($r === 'q') { 78 | Console::write("\n Quit,ByeBye.", true, true); 79 | } 80 | 81 | $rs = strpos($r, $sep) ? array_filter(explode($sep, $r), $filter) : [$r]; 82 | 83 | // error, try again 84 | if (!$rs) { 85 | goto beginChoice; 86 | } 87 | 88 | return $rs; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Component/Interact/Choose.php: -------------------------------------------------------------------------------- 1 | value 35 | * '1' => 'chengdu', 36 | * '2' => 'beijing' 37 | * ] 38 | * 39 | * @param string $description 40 | * @param array|string $options Option data 41 | * @param int|string|null $default Default option 42 | * @param bool $allowExit 43 | * @param array $opts 44 | * 45 | * @psalm-param array{returnVal: bool, retFilter: callable} $opts 46 | * 47 | * @return string 48 | */ 49 | public static function one(string $description, array|string $options, int|string|null $default = null, bool $allowExit = true, array $opts = []): string 50 | { 51 | if (!$description = trim($description)) { 52 | Show::error('Please provide a description text!', 1); 53 | } 54 | 55 | $options = is_array($options) ? $options : explode(',', $options); 56 | 57 | // If default option is error 58 | if (null !== $default && !isset($options[$default])) { 59 | Show::error("The default option [$default] don't exists.", true); 60 | } 61 | 62 | if ($allowExit) { 63 | $options['q'] = 'quit'; 64 | } 65 | 66 | $text = "$description"; 67 | foreach ($options as $key => $value) { 68 | $text .= "\n $key) $value"; 69 | } 70 | 71 | $defaultText = $default !== null ? "[default:$default]" : ''; 72 | Console::write($text); 73 | 74 | beginChoice: 75 | $r = Console::readln("Your choice$defaultText : "); 76 | 77 | if ($r === '' && $default !== null) { 78 | return $default; 79 | } 80 | 81 | // error, allow try again once. 82 | if (!array_key_exists($r, $options)) { 83 | Cli::warn('[WARN] input must in the list: ' . implode(',', array_keys($options))); 84 | goto beginChoice; 85 | } 86 | 87 | // exit 88 | if ($r === 'q') { 89 | Console::write("\n Quit,ByeBye.", true, true); 90 | } 91 | 92 | // return value 93 | if ($opts['returnVal'] ?? false) { 94 | $r = $options[$r]; 95 | } 96 | 97 | if ($retFn = $opts['retFilter'] ?? null) { 98 | $r = $retFn($r); 99 | } 100 | 101 | return $r; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Component/Interact/Confirm.php: -------------------------------------------------------------------------------- 1 | $question ?\nPlease confirm (yes|no)[default:$defText]: "; 44 | 45 | while (true) { 46 | $answer = Console::readChar($message); 47 | if ('' === $answer) { 48 | return $default; 49 | } 50 | 51 | if (0 === stripos($answer, 'y')) { 52 | return true; 53 | } 54 | 55 | if (0 === stripos($answer, 'n')) { 56 | return false; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Component/Interact/LimitedAsk.php: -------------------------------------------------------------------------------- 1 | 100) { 45 | * Interact::error('Allow the input range is 1-100'); 46 | * return false; 47 | * } 48 | * return true; 49 | * } ); 50 | * 51 | * // has default value 52 | * Interact::limitedAsk('please entry you age?', 89, function($age) 53 | * { 54 | * if ($age<1 || $age>100) { 55 | * Interact::error('Allow the input range is 1-100'); 56 | * return false; 57 | * } 58 | * return true; 59 | * } ); 60 | * ``` 61 | * 62 | */ 63 | public static function ask( 64 | string $question, 65 | string $default = '', 66 | ?Closure $validator = null, 67 | int $times = 3 68 | ): string { 69 | if (!$question = trim($question)) { 70 | Show::error('Please provide a question text!', 1); 71 | } 72 | 73 | $answer = ''; 74 | $back = $times = ($times > 6 || $times < 1) ? 3 : $times; 75 | 76 | $question = ucfirst($question); 77 | $hasDefault = '' !== $default; 78 | 79 | if ($hasDefault) { 80 | $message = "$question(default: $default) "; 81 | } else { 82 | $message = "$question"; 83 | Console::write($message); 84 | } 85 | 86 | while ($times--) { 87 | if ($hasDefault) { 88 | $answer = Console::readln($message); 89 | 90 | if ('' === $answer) { 91 | $answer = $default; 92 | break; 93 | } 94 | } else { 95 | $num = $times + 1; 96 | $answer = Console::readln(sprintf('(You have [%s] chances to enter!) ', $num)); 97 | } 98 | 99 | // If setting verify callback 100 | if ($validator && true === $validator($answer)) { 101 | break; 102 | } 103 | 104 | // no setting verify callback 105 | if (!$validator && $answer !== '') { 106 | break; 107 | } 108 | } 109 | 110 | if ('' !== $answer) { 111 | return $answer; 112 | } 113 | 114 | if ($hasDefault) { 115 | return $default; 116 | } 117 | 118 | Console::write("\n You've entered incorrectly $back times in a row. exit!", true, true); 119 | return ''; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Component/Interact/MultiSelect.php: -------------------------------------------------------------------------------- 1 | selected; 42 | } 43 | 44 | /** 45 | * @return string[] 46 | */ 47 | public function getSelectedVals(): array 48 | { 49 | return $this->selectedVals; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getDefaults(): string 56 | { 57 | return $this->defaults; 58 | } 59 | 60 | /** 61 | * @param string $defaults 62 | */ 63 | public function setDefaults(string $defaults): void 64 | { 65 | $this->defaults = $defaults; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Component/Interact/ParamDefinition/AbstractParam.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 48 | return $this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Component/Interact/ParamDefinition/ChoiceParam.php: -------------------------------------------------------------------------------- 1 | $default)" : ''; 72 | $message = '' . ucfirst($question) . "$defText "; 73 | 74 | askQuestion: 75 | $answer = Console::readln($message); 76 | 77 | if ('' === $answer) { 78 | if ('' === $default) { 79 | Show::error('A value is required.'); 80 | goto askQuestion; 81 | } 82 | 83 | return $default; 84 | } 85 | 86 | // has answer validator 87 | if ($validator) { 88 | $error = null; 89 | 90 | if ($validator($answer, $error)) { 91 | return $answer; 92 | } 93 | 94 | if ($error) { 95 | Show::warning($error); 96 | } 97 | 98 | goto askQuestion; 99 | } 100 | 101 | return $answer; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Component/Interact/SingleSelect.php: -------------------------------------------------------------------------------- 1 | default; 42 | } 43 | 44 | /** 45 | * @param string $default 46 | */ 47 | public function setDefault(string $default): void 48 | { 49 | $this->default = $default; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getSelected(): string 56 | { 57 | return $this->selected; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getSelectedVal(): string 64 | { 65 | return $this->selectedVal; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Component/Interact/Terminal.php: -------------------------------------------------------------------------------- 1 | 'title', // param name 18 | * 'type' => 'StringParam', // String Parameter Definition 19 | * 'desc' => 'description', 20 | * 'default' => null, // default value 21 | * ], 22 | * [ 23 | * 'name' => 'projects', 24 | * 'type' => 'ChoiceParam', // Choice Parameter Definition 25 | * 'desc' => 'description', 26 | * 'default' => null, // default value 27 | * ], 28 | * ] 29 | */ 30 | public array $propDefinitions; 31 | 32 | /** 33 | * @param array $propDefinitions 34 | * 35 | * @return $this 36 | */ 37 | public function new(array $propDefinitions = []): self 38 | { 39 | return new self($propDefinitions); 40 | } 41 | 42 | /** 43 | * Class constructor. 44 | * 45 | * @param array $propDefinitions 46 | */ 47 | public function __construct(array $propDefinitions = []) 48 | { 49 | $this->propDefinitions = $propDefinitions; 50 | } 51 | 52 | /** 53 | * @param FlagsParser $fs 54 | */ 55 | public function collect(FlagsParser $fs): void 56 | { 57 | // for ($fs->getOptDefine($name)) 58 | Assert::notEmpty($this->propDefinitions); 59 | foreach ($this->propDefinitions as $definition) { 60 | 61 | } 62 | } 63 | 64 | /** 65 | * @param array $propDefinitions 66 | * 67 | * @return ValuesCollector 68 | */ 69 | public function setPropDefinitions(array $propDefinitions): self 70 | { 71 | $this->propDefinitions = $propDefinitions; 72 | return $this; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Component/MessageFormatter.php: -------------------------------------------------------------------------------- 1 | config = $config; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function __toString() 67 | { 68 | return $this->toString(); 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function format(): string 75 | { 76 | throw new RuntimeException('Please implement the method on sub-class'); 77 | } 78 | 79 | /** 80 | * Format and output message to steam. 81 | * 82 | * @return int 83 | */ 84 | public function render(): int 85 | { 86 | return $this->display(); 87 | } 88 | 89 | /** 90 | * Format and output message to steam. 91 | * 92 | * @return int 93 | */ 94 | public function display(): int 95 | { 96 | return Console::write($this->toString()); 97 | } 98 | 99 | /** 100 | * @return string 101 | */ 102 | public function toString(): string 103 | { 104 | return $this->format(); 105 | } 106 | 107 | /** 108 | * @return array 109 | */ 110 | public function getConfig(): array 111 | { 112 | return $this->config; 113 | } 114 | 115 | /** 116 | * @return callable 117 | */ 118 | public function getBeforeWrite(): callable 119 | { 120 | return $this->beforeWrite; 121 | } 122 | 123 | /** 124 | * @param callable $beforeWrite 125 | */ 126 | public function setBeforeWrite(callable $beforeWrite): void 127 | { 128 | $this->beforeWrite = $beforeWrite; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Component/NotifyMessage.php: -------------------------------------------------------------------------------- 1 | speed; 31 | } 32 | 33 | /** 34 | * @param int $speed 35 | */ 36 | public function setSpeed(int $speed): void 37 | { 38 | $this->speed = (int)$speed; 39 | } 40 | 41 | public function display(): void 42 | { 43 | throw new RuntimeException('Please implement the method on sub-class'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Component/Progress/Bar.php: -------------------------------------------------------------------------------- 1 | write('Counter:'); 31 | * while ($total - 1) { 32 | * $ctt->send(1); 33 | * usleep(30000); 34 | * $total--; 35 | * } 36 | * // end of the counter. 37 | * $ctt->send(-1); 38 | * ``` 39 | * 40 | * @param string $msg 41 | * @param string $doneMsg 42 | * 43 | * @return Generator 44 | */ 45 | public static function gen(string $msg, string $doneMsg = ''): Generator 46 | { 47 | $counter = 0; 48 | $finished = false; 49 | 50 | $tpl = (Cli::isSupportColor() ? "\x0D\x1B[2K" : "\x0D\r") . '%d %s'; 51 | $msg = Console::style()->render($msg); 52 | 53 | $doneMsg = $doneMsg ? Console::style()->render($doneMsg) : ''; 54 | 55 | while (true) { 56 | if ($finished) { 57 | break; 58 | } 59 | 60 | $step = yield; 61 | 62 | if ((int)$step <= 0) { 63 | $counter++; 64 | $finished = true; 65 | $msg = $doneMsg ?: $msg; 66 | } else { 67 | $counter += $step; 68 | } 69 | 70 | printf($tpl, $counter, $msg); 71 | 72 | if ($finished) { 73 | echo "\n"; 74 | break; 75 | } 76 | } 77 | 78 | yield false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Component/Progress/DynamicText.php: -------------------------------------------------------------------------------- 1 | render($fixedMsg); 40 | } 41 | 42 | $template .= '%s'; 43 | $doneMsg = $doneMsg ? Console::style()->render($doneMsg) : ''; 44 | 45 | while (true) { 46 | if ($finished) { 47 | break; 48 | } 49 | 50 | $msg = yield; 51 | 52 | if ($msg === false) { 53 | $msg = $doneMsg ?: ''; 54 | $counter++; 55 | $finished = true; 56 | } 57 | 58 | printf($template, $msg); 59 | 60 | if ($finished) { 61 | echo "\n"; 62 | break; 63 | } 64 | } 65 | 66 | yield $counter; 67 | } 68 | 69 | public function display(): void 70 | { 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Component/Progress/Pending.php: -------------------------------------------------------------------------------- 1 | 'Msg Text', 34 | * 'doneChar' => '#' 35 | * ]); 36 | * echo "progress:\n"; 37 | * while ($i <= $total) { 38 | * $bar->send(1); // 发送步进长度,通常是 1 39 | * usleep(50000); 40 | * $i++; 41 | * } 42 | * ``` 43 | * 44 | * @param int $total 45 | * @param array $opts 46 | * 47 | * @return Generator 48 | * @internal int $current 49 | */ 50 | public static function gen(int $total, array $opts = []): Generator 51 | { 52 | $current = 0; 53 | $finished = false; 54 | $tplPrefix = Cli::isSupportColor() ? "\x0D\x1B[2K" : "\x0D\r"; 55 | 56 | $opts = array_merge([ 57 | 'doneChar' => '=', 58 | 'waitChar' => ' ', 59 | 'signChar' => '>', 60 | 'msg' => '', 61 | 'doneMsg' => '', 62 | ], $opts); 63 | $msg = Console::style()->render($opts['msg']); 64 | 65 | $doneMsg = Console::style()->render($opts['doneMsg']); 66 | $waitChar = $opts['waitChar']; 67 | 68 | while (true) { 69 | if ($finished) { 70 | break; 71 | } 72 | 73 | $step = yield; 74 | if ((int)$step <= 0) { 75 | $step = 1; 76 | } 77 | 78 | $current += $step; 79 | $percent = (int)ceil(($current / $total) * 100); 80 | 81 | if ($percent >= 100) { 82 | $msg = $doneMsg ?: $msg; 83 | $percent = 100; 84 | $finished = true; 85 | } 86 | 87 | /** 88 | * \r, \x0D 回车,到行首 89 | * \x1B ESC 90 | * 2K 清除本行 91 | */ // printf("\r[%'--100s] %d%% %s", 92 | // printf("\x0D\x1B[2K[%'{$waitChar}-100s] %d%% %s", 93 | printf( 94 | "%s[%'$waitChar-100s] %' 3d%% %s", 95 | $tplPrefix, 96 | str_repeat($opts['doneChar'], $percent) . ($finished ? '' : $opts['signChar']), 97 | $percent, 98 | $msg 99 | );// ♥ ■ ☺ ☻ = # 100 | 101 | if ($finished) { 102 | echo "\n"; 103 | break; 104 | } 105 | } 106 | 107 | yield false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Component/Progress/SimpleTextBar.php: -------------------------------------------------------------------------------- 1 | render($msg); 39 | $doneMsg = $doneMsg ? Console::style()->render($doneMsg) : ''; 40 | 41 | while (true) { 42 | if ($finished) { 43 | break; 44 | } 45 | 46 | $step = yield; 47 | 48 | if ((int)$step <= 0) { 49 | $step = 1; 50 | } 51 | 52 | $current += $step; 53 | $percent = ceil(($current / $total) * 100); 54 | 55 | if ($percent >= 100) { 56 | $percent = 100; 57 | $finished = true; 58 | $msg = $doneMsg ?: $msg; 59 | } 60 | 61 | // printf("\r%d%% %s", $percent, $msg); 62 | // printf("\x0D\x2K %d%% %s", $percent, $msg); 63 | // printf("\x0D\r%'2d%% %s", $percent, $msg); 64 | printf($tpl, $percent, $msg); 65 | 66 | if ($finished) { 67 | echo "\n"; 68 | break; 69 | } 70 | } 71 | 72 | yield false; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Component/Progress/Spinner.php: -------------------------------------------------------------------------------- 1 | completer = $completer; 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function listHistory(): array 60 | { 61 | return Readline::listHistory(); 62 | } 63 | 64 | /** 65 | * @return bool 66 | */ 67 | public function loadHistory(): bool 68 | { 69 | if ($this->historyFile) { 70 | return Readline::loadHistory($this->historyFile); 71 | } 72 | 73 | return false; 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function dumpHistory(): bool 80 | { 81 | if ($this->historyFile) { 82 | return Readline::dumpHistory($this->historyFile); 83 | } 84 | 85 | return false; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getHistoryFile(): string 92 | { 93 | return $this->historyFile; 94 | } 95 | 96 | /** 97 | * @param string $historyFile 98 | */ 99 | public function setHistoryFile(string $historyFile): void 100 | { 101 | $this->historyFile = $historyFile; 102 | } 103 | 104 | /** 105 | * @return int 106 | */ 107 | public function getHistorySize(): int 108 | { 109 | return $this->historySize; 110 | } 111 | 112 | /** 113 | * @param int $historySize 114 | */ 115 | public function setHistorySize(int $historySize): void 116 | { 117 | $this->historySize = $historySize; 118 | } 119 | 120 | /** 121 | * @return callable 122 | */ 123 | public function getCompleter(): callable 124 | { 125 | return $this->completer; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Component/Symbol/Char.php: -------------------------------------------------------------------------------- 1 | value, 98 | * ... 99 | * ] 100 | */ 101 | private static array $constants; 102 | 103 | /** 104 | * @return array 105 | */ 106 | public static function getConstants(): array 107 | { 108 | if (!self::$constants) { 109 | $objClass = new ReflectionClass(__CLASS__); 110 | 111 | self::$constants = $objClass->getConstants(); 112 | } 113 | 114 | return self::$constants; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Component/Symbol/Emoji.php: -------------------------------------------------------------------------------- 1 | value, 131 | * ... 132 | * ] 133 | */ 134 | private static array $constants; 135 | 136 | /** 137 | * @return array 138 | */ 139 | public static function getConstants(): array 140 | { 141 | if (!self::$constants) { 142 | $objClass = new ReflectionClass(__CLASS__); 143 | 144 | // 此处获取类中定义的全部常量 返回的是 [key=>value,...] 的数组 145 | // key是常量名 value是常量值 146 | self::$constants = $objClass->getConstants(); 147 | } 148 | 149 | return self::$constants; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Concern/AbstractOutput.php: -------------------------------------------------------------------------------- 1 | answer = StrValue::new($str)->trim(); 32 | } 33 | 34 | /** 35 | * @return StrValue 36 | */ 37 | public function getAnswer(): StrValue 38 | { 39 | return $this->answer; 40 | } 41 | 42 | /** 43 | * @return int 44 | */ 45 | public function getInt(): int 46 | { 47 | return $this->answer->toInt(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Concern/InteractiveHandle.php: -------------------------------------------------------------------------------- 1 | setValidator(function (string $line) { 26 | * // check input 27 | * if (!$line) { 28 | * throw new InvalidArgumentException('argument is required'); 29 | * } 30 | * return $line; 31 | * }); 32 | * ``` 33 | * 34 | * @var callable 35 | */ 36 | protected $validator; 37 | 38 | /** 39 | * @var callable 40 | */ 41 | protected $ansFilter; 42 | 43 | /** 44 | * Class constructor. 45 | * 46 | * @param array $options 47 | */ 48 | public function __construct(array $options = []) 49 | { 50 | Obj::init($this, $options); 51 | } 52 | 53 | /** 54 | * @param callable $validator 55 | * 56 | * @return self 57 | */ 58 | public function setValidator(callable $validator): self 59 | { 60 | $this->validator = $validator; 61 | return $this; 62 | } 63 | 64 | /** 65 | * @return callable 66 | */ 67 | public function getAnsFilter(): callable 68 | { 69 | return $this->ansFilter; 70 | } 71 | 72 | /** 73 | * @param callable $ansFilter 74 | */ 75 | public function setAnsFilter(callable $ansFilter): void 76 | { 77 | $this->ansFilter = $ansFilter; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ConsoleConst.php: -------------------------------------------------------------------------------- 1 | {name} 27 | public const ANNOTATION_VAR = '{%s}'; // '{$%s}'; 28 | 29 | // {$%s} name -> {name} 30 | public const HELP_VAR_LEFT = '{'; 31 | 32 | public const HELP_VAR_RIGHT = '}'; 33 | 34 | /** 35 | * Run command 36 | * 37 | * @param array $args 38 | * 39 | * @return mixed return int is exit code. other is command exec result. 40 | */ 41 | public function run(array $args): mixed; 42 | 43 | /** 44 | * @return Application 45 | */ 46 | public function getApp(): Application; 47 | 48 | /** 49 | * The input group name. 50 | * 51 | * @return string 52 | */ 53 | public function getGroupName(): string; 54 | 55 | /** 56 | * The real group or command name. Alias of the getName() 57 | * 58 | * @return string 59 | */ 60 | public function getRealName(): string; 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getRealDesc(): string; 66 | 67 | /** 68 | * The real group name. 69 | * 70 | * @return string 71 | */ 72 | public function getRealGName(): string; 73 | 74 | /** 75 | * The real command name. 76 | * 77 | * @return string 78 | */ 79 | public function getRealCName(): string; 80 | 81 | /** 82 | * The input command/subcommand name. 83 | * 84 | * @return string 85 | */ 86 | public function getCommandName(): string; 87 | 88 | /** 89 | * @param bool $useReal 90 | * 91 | * @return string 92 | */ 93 | public function getCommandId(bool $useReal = true): string; 94 | 95 | /** 96 | * @return string 97 | */ 98 | public static function getName(): string; 99 | 100 | /** 101 | * @return string 102 | */ 103 | public static function getDesc(): string; 104 | } 105 | -------------------------------------------------------------------------------- /src/Contract/CommandInterface.php: -------------------------------------------------------------------------------- 1 | 1, // 1 group 2 command 55 | * handler => handler class/object/func ... 56 | * config => [ 57 | * desc => '', 58 | * aliases => [], 59 | * ], 60 | * ] 61 | * ``` 62 | * 63 | * @param string $name The input command name 64 | * 65 | * @return array{name:string, cmdId: string, config: array, handler: mixed} return route info. If not found, will return empty array. 66 | */ 67 | public function match(string $name): array; 68 | } 69 | -------------------------------------------------------------------------------- /src/Decorate/AttachApplicationTrait.php: -------------------------------------------------------------------------------- 1 | app; 45 | } 46 | 47 | /** 48 | * @param Application|null $app 49 | */ 50 | public function setApp(Application|null $app): void 51 | { 52 | if ($app !== null) { 53 | $this->app = $app; 54 | 55 | // auto setting $attached 56 | $this->attached = true; 57 | } 58 | } 59 | 60 | /** 61 | * @return bool 62 | */ 63 | public function isAttached(): bool 64 | { 65 | return $this->attached; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | public function isDetached(): bool 72 | { 73 | return $this->attached === false; 74 | } 75 | 76 | /** 77 | * Detached running 78 | */ 79 | public function setDetached(): void 80 | { 81 | $this->attached = false; 82 | } 83 | 84 | /** 85 | * @return bool 86 | */ 87 | public function isInteractive(): bool 88 | { 89 | if ($this->app) { 90 | return $this->app->isInteractive(); 91 | } 92 | 93 | // $value = $this->input->getOpt(GlobalOption::NO_INTERACTIVE); 94 | return $this->input->isInteractive(); 95 | } 96 | 97 | /** 98 | * Get current debug level value 99 | * 100 | * @return int 101 | */ 102 | public function getVerbLevel(): int 103 | { 104 | if ($this->app) { 105 | return $this->app->getVerbLevel(); 106 | } 107 | 108 | // return (int)$this->input->getLongOpt('debug', Console::VERB_ERROR); 109 | return Console::getLevelFromENV(); 110 | } 111 | 112 | /** 113 | * @param int $level 114 | * 115 | * @return bool 116 | */ 117 | public function isDebug(int $level = Console::VERB_DEBUG): bool 118 | { 119 | if ($this->app) { 120 | return $this->app->isDebug(); 121 | } 122 | 123 | $setVal = Console::getLevelFromENV(); 124 | return $level <= $setVal; 125 | } 126 | 127 | /** 128 | * @param string $format 129 | * @param mixed ...$args 130 | */ 131 | public function debugf(string $format, ...$args): void 132 | { 133 | if ($this->getVerbLevel() < Console::VERB_DEBUG) { 134 | return; 135 | } 136 | 137 | Console::logf(Console::VERB_DEBUG, $format, ...$args); 138 | } 139 | 140 | /** 141 | * @param int $level 142 | * @param string $format 143 | * @param mixed ...$args 144 | */ 145 | public function logf(int $level, string $format, ...$args): void 146 | { 147 | if ($this->getVerbLevel() < $level) { 148 | return; 149 | } 150 | 151 | Console::logf($level, $format, ...$args); 152 | } 153 | 154 | /** 155 | * @param int $level 156 | * @param string $message 157 | * @param array $extra 158 | */ 159 | public function log(int $level, string $message, array $extra = []): void 160 | { 161 | if ($this->getVerbLevel() < $level) { 162 | return; 163 | } 164 | 165 | Console::log($level, $message, $extra); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Decorate/InputOutputAwareTrait.php: -------------------------------------------------------------------------------- 1 | input->getScriptFile(); 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getWorkdir(): string 50 | { 51 | return $this->input->getWorkDir(); 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getScriptName(): string 58 | { 59 | return $this->input->getScriptName(); 60 | } 61 | 62 | /** 63 | * @param string $question 64 | * @param bool $nl 65 | * 66 | * @return string 67 | */ 68 | public function readln(string $question = '', bool $nl = false): string 69 | { 70 | return $this->input->readln($question, $nl); 71 | } 72 | 73 | /** 74 | * @param mixed $message 75 | * 76 | * @return int 77 | */ 78 | public function write(mixed $message): int 79 | { 80 | return $this->output->write($message); 81 | } 82 | 83 | /** 84 | * @param mixed $message 85 | * 86 | * @return int 87 | */ 88 | public function writeln(mixed $message): int 89 | { 90 | return $this->output->writeln($message); 91 | } 92 | 93 | /** 94 | * @return Input 95 | */ 96 | public function getInput(): Input 97 | { 98 | return $this->input; 99 | } 100 | 101 | /** 102 | * @param Input $input 103 | */ 104 | public function setInput(Input $input): void 105 | { 106 | $this->input = $input; 107 | } 108 | 109 | /** 110 | * @return Output 111 | */ 112 | public function getOutput(): Output 113 | { 114 | return $this->output; 115 | } 116 | 117 | /** 118 | * @param Output $output 119 | */ 120 | public function setOutput(Output $output): void 121 | { 122 | $this->output = $output; 123 | } 124 | 125 | /** 126 | * @param Input $input 127 | * @param Output $output 128 | * 129 | * @return static 130 | */ 131 | public function setInputOutput(Input $input, Output $output): static 132 | { 133 | $this->input = $input; 134 | $this->output = $output; 135 | return $this; 136 | } 137 | 138 | /** 139 | * @return FlagsParser 140 | */ 141 | public function getFlags(): FlagsParser 142 | { 143 | return $this->flags; 144 | } 145 | 146 | /** 147 | * @param FlagsParser $flags 148 | */ 149 | public function setFlags(FlagsParser $flags): void 150 | { 151 | $this->flags = $flags; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Decorate/RuntimeProfileTrait.php: -------------------------------------------------------------------------------- 1 | [ 58 | 'startTime' => microtime(true), 59 | 'startMem' => memory_get_usage(), 60 | ], 61 | '_profile_start' => $context, 62 | '_profile_end' => null, 63 | '_profile_msg' => null, 64 | ]; 65 | 66 | $profileKey = $category . '|' . $name; 67 | 68 | if (in_array($profileKey, self::$keyQueue, true)) { 69 | throw new InvalidArgumentException("Your added profile name [$name] have been exists!"); 70 | } 71 | 72 | self::$keyQueue[] = $profileKey; 73 | self::$profiles[$category][$name] = $data; 74 | } 75 | 76 | /** 77 | * mark data analysis end 78 | * 79 | * @param string|null $msg 80 | * @param array $context 81 | * 82 | * @return bool|array 83 | */ 84 | public static function profileEnd(?string $msg = null, array $context = []): bool|array 85 | { 86 | if (!$latestKey = array_pop(self::$keyQueue)) { 87 | return false; 88 | } 89 | 90 | [$category, $name] = explode('|', $latestKey); 91 | 92 | if (isset(self::$profiles[$category][$name])) { 93 | $data = self::$profiles[$category][$name]; 94 | 95 | $old = $data['_profile_stats']; 96 | $data['_profile_stats'] = PhpHelper::runtime($old['startTime'], $old['startMem']); 97 | $data['_profile_end'] = $context; 98 | $data['_profile_msg'] = $msg; 99 | 100 | // $title = $category . ' - ' . ($title ?: $name); 101 | 102 | self::$profiles[$category][$name] = $data; 103 | // self::$log(Logger::DEBUG, $title, $data); 104 | 105 | return $data; 106 | } 107 | 108 | return false; 109 | } 110 | 111 | /** 112 | * @param null|string $name 113 | * @param string $category 114 | * 115 | * @return array 116 | */ 117 | public static function getProfileData(?string $name = null, string $category = 'application'): array 118 | { 119 | if ($name) { 120 | return self::$profiles[$category][$name] ?? []; 121 | } 122 | 123 | if ($category) { 124 | return self::$profiles[$category] ?? []; 125 | } 126 | 127 | return self::$profiles; 128 | } 129 | 130 | public function clearProfileData(): void 131 | { 132 | self::$profiles = []; 133 | self::$keyQueue = []; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Exception/ConsoleException.php: -------------------------------------------------------------------------------- 1 | collectInfo($args); 44 | } 45 | 46 | /** 47 | * @return resource 48 | */ 49 | public function getInputStream() 50 | { 51 | return $this->stream; 52 | } 53 | 54 | public function resetInputStream(): void 55 | { 56 | $this->stream = Cli::getInputStream(); 57 | } 58 | 59 | /*********************************************************************************** 60 | * getter/setter 61 | ***********************************************************************************/ 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getBinWithCommand(): string 67 | { 68 | return $this->scriptName . ' ' . $this->command; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function getFullCommand(): string 75 | { 76 | return $this->scriptFile . ' ' . $this->command; 77 | } 78 | 79 | /** 80 | * @param string ...$names 81 | * 82 | * @return string 83 | */ 84 | public function buildCmdPath(string... $names): string 85 | { 86 | return $this->scriptName . ' ' . implode(' ', $names); 87 | } 88 | 89 | /** 90 | * @param string ...$names 91 | * 92 | * @return string 93 | */ 94 | public function buildFullCmd(string... $names): string 95 | { 96 | return $this->scriptFile . ' ' . implode(' ', $names); 97 | } 98 | 99 | /** 100 | * Get command ID e.g `http:start` 101 | * 102 | * @return string 103 | */ 104 | public function getCommandId(): string 105 | { 106 | return $this->commandId; 107 | } 108 | 109 | /** 110 | * Set command ID e.g `http:start` 111 | * 112 | * @param string $commandId e.g `http:start` 113 | * 114 | * @return void 115 | */ 116 | public function setCommandId(string $commandId): void 117 | { 118 | $this->commandId = $commandId; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/IO/Input/ArrayInput.php: -------------------------------------------------------------------------------- 1 | $val) { 31 | if (!is_int($key)) { 32 | $args[] = $key; 33 | } 34 | 35 | $args[] = $val; 36 | } 37 | 38 | parent::__construct($args); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/IO/Input/StreamInput.php: -------------------------------------------------------------------------------- 1 | setStream($stream); 40 | } 41 | 42 | /** 43 | * @param bool $blocking 44 | * 45 | * @return string 46 | */ 47 | public function readAll(bool $blocking = true): string 48 | { 49 | return File::streamReadAll($this->stream, $blocking); 50 | } 51 | 52 | /** 53 | * @param string $question 54 | * @param bool $nl 55 | * 56 | * @return string 57 | */ 58 | public function read(string $question = '', bool $nl = false): string 59 | { 60 | if ($question) { 61 | fwrite($this->stream, $question . ($nl ? "\n" : '')); 62 | } 63 | 64 | return File::streamFgets($this->stream); 65 | } 66 | 67 | /** 68 | * @param string $question 69 | * @param bool $nl 70 | * 71 | * @return string 72 | */ 73 | public function readln(string $question = '', bool $nl = false): string 74 | { 75 | if ($question) { 76 | fwrite($this->stream, $question . ($nl ? "\n" : '')); 77 | } 78 | 79 | return File::streamFgets($this->stream); 80 | } 81 | 82 | /** 83 | * @return resource 84 | */ 85 | public function getStream() 86 | { 87 | return $this->stream; 88 | } 89 | 90 | /** 91 | * @param resource $stream 92 | */ 93 | protected function setStream($stream): void 94 | { 95 | File::assertStream($stream); 96 | 97 | $meta = stream_get_meta_data($stream); 98 | if (!str_contains($meta['mode'], 'r') && !str_contains($meta['mode'], '+')) { 99 | throw new InvalidArgumentException('Expected a readable stream'); 100 | } 101 | 102 | $this->stream = $stream; 103 | } 104 | 105 | /** 106 | * Whether the stream is an interactive terminal 107 | * 108 | * @return bool 109 | */ 110 | public function isInteractive(): bool 111 | { 112 | return OS::isInteractive($this->stream); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/IO/Input/StringInput.php: -------------------------------------------------------------------------------- 1 | buffer; 38 | 39 | if ($reset) { 40 | $this->buffer = ''; 41 | } 42 | 43 | return $str; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function toString(): string 50 | { 51 | return $this->fetch(); 52 | } 53 | 54 | public function reset(): void 55 | { 56 | $this->buffer = ''; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function __toString(): string 63 | { 64 | return $this->fetch(); 65 | } 66 | 67 | /** 68 | * @param mixed $messages 69 | * @param bool $nl 70 | * @param bool $quit 71 | * @param array $opts 72 | * 73 | * @return int 74 | */ 75 | public function write($messages, $nl = true, $quit = false, array $opts = []): int 76 | { 77 | if (is_array($messages)) { 78 | $str = implode($nl ? PHP_EOL : '', $messages); 79 | } else { 80 | $str = (string)$messages; 81 | } 82 | 83 | if ($nl) { 84 | $str .= PHP_EOL; 85 | } 86 | 87 | $this->buffer .= $str; 88 | return strlen($str); 89 | } 90 | 91 | /** 92 | * @param mixed $messages 93 | * @param bool $quit 94 | * @param array $opts 95 | * 96 | * @return int 97 | */ 98 | public function writeln($messages, bool $quit = false, array $opts = []): int 99 | { 100 | return $this->write($messages, true, $quit, $opts); 101 | } 102 | 103 | /** 104 | * Write a message to output with format. 105 | * 106 | * @param string $format 107 | * @param mixed ...$args 108 | * 109 | * @return int 110 | */ 111 | public function writef(string $format, ...$args): int 112 | { 113 | return $this->write(sprintf($format, ...$args)); 114 | } 115 | 116 | /** 117 | * start buffering 118 | */ 119 | public function startBuffer(): void 120 | { 121 | } 122 | 123 | /** 124 | * clear buffering 125 | */ 126 | public function clearBuffer(): void 127 | { 128 | $this->reset(); 129 | } 130 | 131 | /** 132 | * stop buffering and flush buffer text 133 | * 134 | * @param bool $flush 135 | * @param bool $nl 136 | * @param bool $quit 137 | * @param array{quitCode:int} $opts 138 | * 139 | * @see Console::stopBuffer() 140 | */ 141 | public function stopBuffer(bool $flush = true, bool $nl = false, bool $quit = false, array $opts = []): void 142 | { 143 | 144 | } 145 | 146 | /** 147 | * stop buffering and flush buffer text 148 | * 149 | * @param bool $nl 150 | * @param bool $quit 151 | * @param array $opts 152 | */ 153 | public function flush(bool $nl = false, bool $quit = false, array $opts = []): void 154 | { 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/IO/Output/MemoryOutput.php: -------------------------------------------------------------------------------- 1 | stream); 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getBuffer(): string 48 | { 49 | return $this->fetch(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/IO/Output/StreamOutput.php: -------------------------------------------------------------------------------- 1 | stream, $content); 43 | } 44 | 45 | /** 46 | * @param string $format 47 | * @param mixed ...$args 48 | * 49 | * @return int 50 | */ 51 | public function writef(string $format, ...$args): int 52 | { 53 | $content = sprintf($format, ...$args); 54 | 55 | return File::streamWrite($this->stream, $content); 56 | } 57 | 58 | /** 59 | * @param string $content 60 | * @param bool $quit 61 | * @param array $opts 62 | * 63 | * @return int 64 | */ 65 | public function writeln($content, bool $quit = false, array $opts = []): int 66 | { 67 | $content = DataHelper::toString($content); 68 | 69 | return File::streamWrite($this->stream, $content . PHP_EOL); 70 | } 71 | 72 | /** 73 | * @return resource 74 | */ 75 | public function getStream() 76 | { 77 | return $this->stream; 78 | } 79 | 80 | /** 81 | * @param resource $stream 82 | */ 83 | public function setStream($stream): void 84 | { 85 | File::assertStream($stream); 86 | 87 | $meta = stream_get_meta_data($stream); 88 | if (!str_contains($meta['mode'], 'w') && !str_contains($meta['mode'], '+')) { 89 | throw new InvalidArgumentException('Expected a writeable stream'); 90 | } 91 | 92 | $this->stream = $stream; 93 | } 94 | 95 | /** 96 | * Whether the stream is an interactive terminal 97 | * 98 | * @return bool 99 | */ 100 | public function isInteractive(): bool 101 | { 102 | return OS::isInteractive($this->stream); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/IO/Output/TempOutput.php: -------------------------------------------------------------------------------- 1 | stream); 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getBuffer(): string 51 | { 52 | return $this->fetch(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Util/ConsoleUtil.php: -------------------------------------------------------------------------------- 1 | assertSame('test1', $c::getName()); 28 | $this->assertSame('test1', $c->getRealName()); 29 | $this->assertStringContainsString('description', $c::getDesc()); 30 | $this->assertStringContainsString('description', $c->getRealDesc()); 31 | } 32 | 33 | public function testCommand_alone_run(): void 34 | { 35 | $c = new TestCommand(new Input(), new Output()); 36 | 37 | $str = $c->run([]); 38 | $this->assertEquals('Inhere\ConsoleTest\TestCommand::execute', $str); 39 | } 40 | 41 | public function testCommand_sub_run(): void 42 | { 43 | $c = new TestCommand(new Input(), Output::new()); 44 | 45 | $str = $c->run(['sub1']); 46 | $this->assertEquals('at TestCommand::sub1', $str); 47 | } 48 | 49 | public function testCommand_sub_help(): void 50 | { 51 | $c = new TestCommand(new Input(), $buf = Output\BufferedOutput::new()); 52 | $this->assertNotEmpty($c); 53 | 54 | $c->run(['sub1', '-h']); 55 | vdump($buf->toString()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/ControllerTest.php: -------------------------------------------------------------------------------- 1 | assertSame('test', $c::getName()); 26 | $this->assertStringContainsString('desc', $c::getDesc()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/IO/InputTest.php: -------------------------------------------------------------------------------- 1 | assertSame('./bin/app', $in->getScriptFile()); 27 | $this->assertSame('app', $in->getScriptName()); 28 | // $this->assertSame('cmd', $in->getCommand()); 29 | $this->assertEquals('cmd val0 val1', $in->getFullScript()); 30 | $s = $in->toString(); 31 | $this->assertStringContainsString('bin/app', $s); 32 | $this->assertStringContainsString('cmd val0 val1', $s); 33 | // $this->assertEquals("'./bin/app' cmd val0 val1", $in->toString()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/TestCommand.php: -------------------------------------------------------------------------------- 1 | withConfig([ 34 | 'name' => 'sub1', 35 | 'desc' => 'desc for sub1 in test1', 36 | ]), 37 | ]; 38 | } 39 | 40 | /** 41 | * do execute command 42 | * 43 | * @param Input $input 44 | * @param Output $output 45 | * 46 | * @return mixed 47 | */ 48 | protected function execute(Input $input, Output $output): mixed 49 | { 50 | return __METHOD__; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/TestController.php: -------------------------------------------------------------------------------- 1 |