├── .coveralls.yml ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.xml ├── cliframework.ini ├── composer.json ├── example ├── action-logger.php ├── ask.php ├── chooser.php ├── demo ├── password.php ├── progress-bar.php ├── table-simple.php └── table.php ├── package.ini ├── phpdox.xml ├── phprelease.ini ├── phpunit.xml ├── phpunit.xml.dist ├── scripts └── phar-debug.php ├── snippets └── zsh │ ├── _describe_complex_array │ ├── _meta_command │ └── array_expansion ├── src ├── Ansi │ ├── Colors.php │ └── CursorControl.php ├── Application.php ├── ArgInfo.php ├── ArgInfoList.php ├── ArgumentEditor │ └── ArgumentEditor.php ├── Autoload │ └── ComposerAutoloadGenerator.php ├── Buffer.php ├── ChainedCommand.php ├── Chooser.php ├── Command.php ├── Command │ ├── ArchiveCommand.php │ ├── BashCompletionCommand.php │ ├── BuildGitHubWikiTopicsCommand.php │ ├── CompileCommand.php │ ├── HelpCommand.php │ ├── MetaCommand.php │ └── ZshCompletionCommand.php ├── CommandAutoloader.php ├── CommandBase.php ├── CommandException.php ├── CommandGroup.php ├── CommandInterface.php ├── CommandLoader.php ├── Completion │ ├── BashGenerator.php │ ├── Utils.php │ └── ZshGenerator.php ├── CompletionUtils.php ├── Component │ ├── Progress │ │ ├── ETACalculator.php │ │ ├── ProgressBar.php │ │ ├── ProgressBarStyle.php │ │ ├── ProgressReporter.php │ │ ├── ProgressStar.php │ │ └── SharpProgressBarStyle.php │ └── Table │ │ ├── BorderlessTableStyle.php │ │ ├── CellAttribute.php │ │ ├── CompactTableStyle.php │ │ ├── CurrencyFormatCell.php │ │ ├── DateFormatCell.php │ │ ├── DurationFormatCell.php │ │ ├── MarkdownTableStyle.php │ │ ├── NumberFormatCell.php │ │ ├── PercentFormatCell.php │ │ ├── SpellOutNumberFormatCell.php │ │ ├── Table.php │ │ └── TableStyle.php ├── Config │ └── GlobalConfig.php ├── ConsoleInfo │ ├── ConsoleInfoFactory.php │ ├── ConsoleInfoInterface.php │ ├── EnvConsoleInfo.php │ └── TputConsoleInfo.php ├── Corrector.php ├── Debug │ ├── ConsoleDebug.php │ └── LineIndicator.php ├── Exception │ ├── CommandArgumentNotEnoughException.php │ ├── CommandBaseException.php │ ├── CommandClassNotFoundException.php │ ├── CommandNotFoundException.php │ ├── ExecuteMethodNotDefinedException.php │ ├── ExtensionException.php │ └── InvalidCommandArgumentException.php ├── ExceptionPrinter │ ├── DevelopmentExceptionPrinter.php │ └── ProductionExceptionPrinter.php ├── Extension │ ├── ApplicationExtension.php │ ├── CommandExtension.php │ ├── DaemonExtension.php │ ├── Extension.php │ └── ExtensionBase.php ├── Formatter.php ├── IO │ ├── Console.php │ ├── EchoWriter.php │ ├── NullStty.php │ ├── ReadlineConsole.php │ ├── StandardConsole.php │ ├── StreamWriter.php │ ├── Stty.php │ ├── UnixStty.php │ └── Writer.php ├── LevenshteinCorrector.php ├── Logger.php ├── Logger │ ├── ActionLogger.php │ └── Logger.php ├── OptionPrinter.php ├── PharKit │ ├── PharGenerator.php │ └── PharURI.php ├── Prompter.php ├── ServiceContainer.php ├── Testing │ ├── CommandTestCase.php │ ├── ConsoleTestCase.php │ └── Parser.php ├── Topic │ ├── BaseTopic.php │ └── GitHubTopic.php ├── Utils.php ├── ValueCollection.php └── ValueGroup.php ├── tests ├── CLIFramework │ ├── Ansi │ │ └── ColorsTest.php │ ├── ApplicationTest.php │ ├── ArgumentEditor │ │ └── ArgumentEditorTest.php │ ├── ArgumentInfoListTest.php │ ├── ArgumentInfoTest.php │ ├── Autoload │ │ └── ComposerAutoloadGeneratorTest.php │ ├── BufferTest.php │ ├── Command │ │ └── ZshCompletionCommandTest.php.bak │ ├── CommandLoaderTest.php │ ├── CommandTest.php │ ├── CompletionUtilsTest.php │ ├── Component │ │ ├── Progress │ │ │ └── ETACalculatorTest.php │ │ └── Table │ │ │ ├── DateFormatCellTest.php │ │ │ └── TableTest.php │ ├── Config │ │ └── GlobalConfigTest.php │ ├── ConsoleInfo │ │ ├── EnvConsoleInfoTest.php │ │ └── TputConsoleInfoTest.php │ ├── Extension │ │ └── DaemonExtensionTest.php │ ├── IO │ │ ├── EchoWriterTest.php │ │ ├── NullSttyTest.php │ │ ├── ReadlineConsoleTest.php │ │ ├── StandardConsoleTest.php │ │ └── StreamWriterTest.php │ ├── LoggerTest.php │ ├── TestAppCommandTest.php │ ├── Testing │ │ └── ParserTest.php │ ├── UtilsTest.php │ ├── ValueCollectionTest.php │ └── ValueGroupTest.php ├── DemoApp │ ├── Application.php │ ├── Command │ │ ├── AddCommand.php │ │ ├── CommitCommand.php │ │ ├── FooCommand.php │ │ ├── FooCommand │ │ │ └── SubFooCommand.php │ │ └── ServerCommand.php │ ├── GitHubBuildTopicCommandTest.php │ ├── HelpCommandTest.php │ ├── MetaCommandTest.php │ └── Topic │ │ └── BasicTopic.php ├── TestApp │ ├── Application.php │ ├── Command │ │ ├── ArginfoCommand.php │ │ ├── ListCommand.php │ │ ├── ListCommand │ │ │ ├── ExtraArgumentTestCommand.php │ │ │ └── FooCommand.php │ │ ├── SimpleCommand.php │ │ └── Test1Command.php │ └── Topic │ │ ├── FaqTopic.php │ │ ├── InstallTopic.php │ │ ├── ListTopic.php │ │ └── SetupTopic.php ├── bootstrap.php ├── data │ ├── default-table-2.txt │ ├── default-table-cell-attribute.txt │ ├── default-table-column-cell-attribute.txt │ ├── default-table-footer.txt │ ├── default-table-number-column-cell-attribute.txt │ ├── default-table-row-separator.txt │ ├── default-table.txt │ ├── markdown-table.txt │ └── sample.ini ├── fixture │ └── composer.json.phar-test ├── helpers.php └── script │ └── CLIFramework │ └── IO │ ├── ReadlineConsoleReadLine.php │ ├── ReadlineConsoleReadPassword.php │ ├── StandardConsoleReadLine.php │ └── StandardConsoleReadPassword.php └── todo.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: gYGZtvChRhyhfkdzV9T4h6PidNbxcvXK1 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [ubuntu-latest] 11 | php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1'] 12 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v1 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | extensions: mbstring, intl, zip, xml 23 | coverage: none 24 | 25 | - name: Install dependencies 26 | run: composer install -n 27 | 28 | - name: Run test suite 29 | run: vendor/bin/phpunit 30 | 31 | - name: Run demo script 32 | run: example/demo meta --zsh commit arg 0 suggestions && example/demo meta --zsh commit arg 1 valid-values && example/demo zsh --bind demo > zsh 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | src/PHPBrew/ 3 | build/ 4 | .onion 5 | .hhconfig 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.6' 4 | - '7.0' 5 | - '7.1' 6 | - '7.2' 7 | matrix: 8 | fast_finish: true 9 | allow_failure: 10 | - php: '5.6' 11 | install: 12 | - phpenv rehash 13 | - composer install 14 | script: 15 | - phpunit -c phpunit.xml.dist 16 | - example/demo meta --zsh commit arg 0 suggestions 17 | - example/demo meta --zsh commit arg 1 valid-values 18 | - example/demo zsh --bind demo > zsh 19 | after_success: 20 | - php vendor/bin/coveralls -v 21 | cache: 22 | apt: true 23 | directories: 24 | - vendor 25 | notifications: 26 | email: 27 | on_success: change 28 | on_failure: change 29 | slack: 30 | secure: O3CKTxa+uoi0TXc2xZWAR3oaodIuG6L7eEeYt+lPe2Ghc7AcN9UQJZrmaN/TTUg6X6mV6KHNJz9qcVk7Tg3MdsDPS/DWsjoolQiGH2FZ5iMdJtS0N38w5KhtVVkO7ecfpu26UnHaBv6zs/3JFF2T1ZnlRA2l+euv+I/EPyf7LSQ= 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # dev-master 2 | 3 | - Chooser component update to support associative arrays properly. 4 | 5 | Version 3.0.0 6 | 7 | - Added Progress Component. 8 | 9 | Version 2.8.1 10 | 11 | Several bug fixes: 12 | 13 | - class loader generator. 14 | - table component column width calculation. 15 | - fix archive command app-boostrap option 16 | 17 | Version 2.8.0 18 | 19 | - Added debug tools. 20 | - Fix table component bug. 21 | 22 | Version 2.7.2 23 | 24 | - Merged Commit bc318ab: Merge pull request #85 from marcioAlmada/fix/compile 25 | 26 | Fix Compile Command 27 | 28 | Version 2.7.0 29 | 30 | - Rewrite the whole compile command with composer.json support. 31 | 32 | Version 2.6.3 33 | 34 | - Use universal package that is newer than 1.4 35 | 36 | Version 2.6.2 37 | 38 | - Fixed column header width checking 39 | 40 | Version 2.6.1 41 | 42 | - Fixed column width for empty cell. 43 | 44 | Version 2.6 45 | 46 | - Added CommandExtension support. @shinnya++ 47 | - Added Event system. (Universal/Event/PhpEvent) 48 | 49 | Version 2.5 50 | 51 | - Added ANSI Color definition class. 52 | - Added show/hide cursor ansi code to CursorControl. 53 | - Moved all singleton object builders to `CLIFramework\ServiceContainer`. 54 | - Added more logger methods. 55 | - Added powerful text table generator. 56 | - Added `compile` command to compile console application into phar file. 57 | - #64 - Abstract IO layers (stdin/stdout, tty) by @shinnya++ 58 | - #66 - Support password prompt by @shinnya++ 59 | - #65 - Global configuration file support by @shinnya++ 60 | - #63 - Add logException method support by @shinnya++ 61 | - #51 - New correct: use `similar_text` instead of `levenshtein` by @dh3014++ 62 | - #49 - Command autoloading feature by @dh3014++ 63 | - #10 - Allow raw output even if terminal emulator support colors by @marcioAlmada++ 64 | - Several bugfixes by @marcioAlmada++ 65 | 66 | Version 2.2 - Wed Dec 31 11:27:50 2014 67 | 68 | - Added ConsoleInfo classes for detecting the dimension of the console. 69 | 70 | Version 1.7 - Mon Jun 30 11:41:33 2014 71 | 72 | - Added zsh completion generator. 73 | - Added ArgInfo class. 74 | - Added arginfo() method to command class. 75 | - Improved help information generator. 76 | 77 | Version 1.3.1 - 三 3/14 18:10:37 2012 78 | 79 | - Added Chooser component. 80 | - Added Prompter component. 81 | - Refactor formatter methods. 82 | 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Yo-An Lin 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. All advertising materials mentioning features or use of this software 12 | must display the following acknowledgement: 13 | This product includes software developed by the University of 14 | California, Berkeley and its contributors. 15 | 4. Neither the name of the University nor the names of its contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 25 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 26 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 | SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /cliframework.ini: -------------------------------------------------------------------------------- 1 | [core] 2 | verbose = false 3 | debug = false 4 | pid_dir = /tmp 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "corneltek/cliframework", 3 | "homepage": "http://github.com/c9s/CLIFramework", 4 | "description": "Command-line framework for PHP", 5 | "keywords": [ 6 | "command", 7 | "command-line", 8 | "completion", 9 | "zsh", 10 | "framework", 11 | "getopt" 12 | ], 13 | "require": { 14 | "php": ">=7.2.0", 15 | "corneltek/getoptionkit": "~2.6.1", 16 | "universal/universal": "2.0.x-dev", 17 | "corneltek/codegen": "4.0.x-dev", 18 | "pimple/pimple": "*", 19 | "symfony/finder": "~2.8|~3.0|^5.3.4", 20 | "symfony/class-loader": "~2.8|~3.0|~3.2" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", 24 | "satooshi/php-coveralls": "^1", 25 | "ext-intl": "*" 26 | }, 27 | "license": "MIT", 28 | "authors": [ 29 | { "name": "Yo-An Lin", "email": "yoanlin93@gmail.com" } 30 | ], 31 | "autoload": { 32 | "psr-4": { 33 | "CLIFramework\\": "src/" 34 | } 35 | }, 36 | "extra": { "branch-alias": { "dev-master": "4.1.x-dev" } } 37 | } 38 | -------------------------------------------------------------------------------- /example/action-logger.php: -------------------------------------------------------------------------------- 1 | newAction($title, 'Update schema class files...'); 12 | foreach (['checking', 'updating', 'pulling'] as $status) { 13 | $logAction->setStatus($status); 14 | sleep(1); 15 | } 16 | $logAction->done(); 17 | } 18 | -------------------------------------------------------------------------------- /example/ask.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | require 'vendor/autoload.php'; 12 | 13 | $prompter = new CLIFramework\Prompter(); 14 | $line = $prompter->ask('Your Name:',array('John','Mary')); 15 | echo "input value: "; 16 | var_dump($line); 17 | -------------------------------------------------------------------------------- /example/chooser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | require 'vendor/autoload.php'; 12 | 13 | $app = new CLIFramework\Prompter(); 14 | $line = $app->ask('Your Name:',array('John','Mary')); 15 | echo "input value: "; 16 | var_dump($line); 17 | $app = new CLIFramework\Chooser(); 18 | $val = $app->choose('Your versions:' , array( 19 | 'php-5.4.0' => '5.4.0', 20 | 'php-5.4.1' => '5.4.1', 21 | 'system' => '5.3.0', 22 | )); 23 | var_dump($val); 24 | -------------------------------------------------------------------------------- /example/demo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * 12 | * 13 | * 14 | * php example/demo meta commit opt C valid-values 15 | * 16 | * php example/demo meta commit opt c valid-values 17 | * 18 | * php example/demo meta commit opt author suggestions 19 | * 20 | * php example/demo meta commit arg 0 valid-values 21 | * 22 | */ 23 | $loader = require 'vendor/autoload.php'; 24 | $loader->add('DemoApp','tests'); 25 | 26 | require 'tests/DemoApp/Application.php'; 27 | require 'tests/DemoApp/Command/CommitCommand.php'; 28 | require 'tests/DemoApp/Command/FooCommand.php'; 29 | require 'tests/DemoApp/Command/AddCommand.php'; 30 | require 'tests/DemoApp/Command/FooCommand/SubFooCommand.php'; 31 | require 'tests/DemoApp/Command/ServerCommand.php'; 32 | 33 | $app = new DemoApp\Application; 34 | 35 | 36 | 37 | 38 | // $app->runWithTry($argv); 39 | $app->run($argv); 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/password.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | require 'vendor/autoload.php'; 12 | 13 | $prompter = new CLIFramework\Prompter(); 14 | $line = $prompter->password('Your Password:'); 15 | echo "input value: "; 16 | var_dump($line); 17 | -------------------------------------------------------------------------------- /example/progress-bar.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | require 'vendor/autoload.php'; 12 | 13 | $progress = new CLIFramework\Component\Progress\ProgressBar(STDERR); 14 | $progress->setUnit('bytes'); 15 | $progress->start('downloading file'); 16 | $total = 512; 17 | for ($i = 0; $i <= $total; $i++) { 18 | usleep(5 * 10000); 19 | $progress->updateLayout(); 20 | $progress->update($i, $total); 21 | } 22 | $progress->finish(); 23 | -------------------------------------------------------------------------------- /example/table-simple.php: -------------------------------------------------------------------------------- 1 | setHeaders([ 'Published Date', 'Title', 'Description' ]); 7 | $table->addRow(array( 8 | "September 16, 2014", 9 | "Zero to One: Notes on Startups, or How to Build the Future", 10 | "If you want to build a better future, you must believe in secrets. 11 | The great secret of our time is that there are still uncharted frontiers to explore and new inventions to create. In Zero to One, legendary entrepreneur and investor Peter Thiel shows how we can find singular ways to create those new things. ", 12 | 29.5 13 | )); 14 | $table->addRow(array( 15 | "November 4, 2014", 16 | "Hooked: How to Build Habit-Forming Products", 17 | ["Why do some products capture widespread attention while others flop? What makes us engage with certain products out of sheer habit? Is there a pattern underlying how technologies hook us? " 18 | , "Nir Eyal answers these questions (and many more) by explaining the Hook Model—a four-step process embedded into the products of many successful companies to subtly encourage customer behavior. Through consecutive “hook cycles,” these products reach their ultimate goal of bringing users back again and again without depending on costly advertising or aggressive messaging."], 19 | 99, 20 | )); 21 | echo $table->render(); 22 | -------------------------------------------------------------------------------- /example/table.php: -------------------------------------------------------------------------------- 1 | setBackgroundColor('blue'); 12 | 13 | $redhighlight = new CellAttribute; 14 | $redhighlight->setBackgroundColor('red'); 15 | 16 | $priceCell = new CurrencyFormatCell('fr', 'EUR'); 17 | 18 | $table = new Table; 19 | $table->setColumnCellAttribute(0, $bluehighlight); 20 | $table->setColumnCellAttribute(3, $priceCell); 21 | $table->setHeaders([ 'Published Date', 'Title', 'Description' ]); 22 | // $table->setStyle(new MarkdownTableStyle); 23 | $table->addRow(array( 24 | "September 16, 2014", 25 | [$redhighlight, "Zero to One: Notes on Startups, or How to Build the Future"], 26 | "If you want to build a better future, you must believe in secrets. 27 | The great secret of our time is that there are still uncharted frontiers to explore and new inventions to create. In Zero to One, legendary entrepreneur and investor Peter Thiel shows how we can find singular ways to create those new things. ", 28 | 29.5 29 | )); 30 | $table->addRow(array( 31 | "November 4, 2014", 32 | "Hooked: How to Build Habit-Forming Products", 33 | 34 | "Why do some products capture widespread attention while others flop? What makes us engage with certain products out of sheer habit? Is there a pattern underlying how technologies hook us? " 35 | . "Nir Eyal answers these questions (and many more) by explaining the Hook Model—a four-step process embedded into the products of many successful companies to subtly encourage customer behavior. Through consecutive “hook cycles,” these products reach their ultimate goal of bringing users back again and again without depending on costly advertising or aggressive messaging.\n", 36 | 99, 37 | )); 38 | echo $table->render(); 39 | -------------------------------------------------------------------------------- /package.ini: -------------------------------------------------------------------------------- 1 | [package] 2 | name = CLIFramework 3 | desc = A Powerful Command Line Application Framework 4 | version = 1.5.10 5 | author = Yo-An Lin 6 | channel = pear.corneltek.com 7 | stability = stable 8 | 9 | [require] 10 | php = 5.3 11 | pearinstaller = 1.4 12 | 13 | ; packages 14 | pear.corneltek.com/GetOptionKit = 1.1.2 15 | pear.corneltek.com/Universal = 0 16 | 17 | [resources] 18 | git = git://github.com/c9s/CLIFramework.git 19 | -------------------------------------------------------------------------------- /phpdox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /phprelease.ini: -------------------------------------------------------------------------------- 1 | Steps = BumpVersion, GitCommit, GitTag, GitPush, GitPushTags 2 | VersionFrom = src/CLIFramework/Application.php 3 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | tests/CLIFramework/Component 22 | 23 | 24 | 25 | tests/DemoApp 26 | 27 | 28 | 29 | tests/TestApp 30 | 31 | 32 | 33 | 34 | 35 | 36 | ./src 37 | 38 | ./src/CLIFramework/Testing 39 | ./src/PHPBrew 40 | ./build 41 | ./composer 42 | ./tests 43 | ./travis 44 | ./vendor 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | tests/DemoApp 20 | 21 | 22 | 23 | tests/TestApp 24 | 25 | 26 | 27 | 28 | 29 | 30 | ./src 31 | 32 | ./src/CLIFramework/Testing 33 | ./src/PHPBrew 34 | ./build 35 | ./composer 36 | ./tests 37 | ./travis 38 | ./vendor 39 | 40 | 41 | 42 | 43 | 44 | 45 | 53 | 54 | 56 | 57 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /scripts/phar-debug.php: -------------------------------------------------------------------------------- 1 | isDir()) { 10 | echo "D "; 11 | } else { 12 | echo "F "; 13 | } 14 | echo $file->getPathname(), PHP_EOL; 15 | } 16 | echo "Stub:\n"; 17 | echo $phar->getStub(); 18 | -------------------------------------------------------------------------------- /snippets/zsh/_describe_complex_array: -------------------------------------------------------------------------------- 1 | 2 | _foo () { 3 | local curcontext=$curcontext state line 4 | typeset -A val_args 5 | declare -A opt_args 6 | local ret=1 7 | 8 | eval "$(php example/demo meta --zsh commit arg 0 valid-values)" 9 | for k in ${(k)groups} ; do 10 | values=(${(z)${groups[$k]}}) 11 | label=${labels[$k]} 12 | if [[ -z $label ]] ; then 13 | label=$k 14 | fi 15 | _describe -t $k $label values && ret=0 16 | done 17 | 18 | # _describe -t commits commit commits && ret=0 19 | # commitdesca=("a:a" "b:b" "c:c") 20 | # commitdesca=(${(z)a:a b:b c:c}) 21 | # commitdescb=("z:a" "x:b" "y:c") 22 | # _describe -t commitb commitb commitdescb && ret=0 23 | return ret 24 | } 25 | compdef _foo foo 26 | -------------------------------------------------------------------------------- /snippets/zsh/_meta_command: -------------------------------------------------------------------------------- 1 | # demo zsh completion script generated by CLIFramework 2 | # Web: http://github.com/c9s/php-CLIFramework 3 | # THIS IS AN AUTO-GENERATED FILE, PLEASE DON'T MODIFY THIS FILE DIRECTLY. 4 | 5 | (( $+functions[__foometa] )) || 6 | __foometa () { 7 | local curcontext=$curcontext state line ret=1 8 | declare -A opt_args 9 | local ret=1 10 | declare -a values 11 | 12 | desc=$1 13 | cmd=$2 14 | valtype=$3 15 | pos=$4 16 | completion=$5 17 | 18 | # $1 = -M, $2 = m:{[:lower:][:upper:]}={[:upper:][:lower:]}, $3 = -J, $4= option--author-1 19 | echo "example/demo meta $4" 20 | # values=$(example/demo meta $cmd $valtype $pos $completion) 21 | values=($(example/demo meta commit $valtype $pos $completion)) 22 | 23 | # =values to expand values 24 | _values $desc ${=values} && ret=0 25 | # _arguments $desc ${=values} && ret=0 26 | return ret 27 | } 28 | 29 | # (( $+functions[__foo_commit] )) || 30 | __foo_commit () { 31 | local curcontext=$curcontext state line ret=1 32 | declare -A opt_args 33 | declare -a lines 34 | local ret=1 35 | lines=("#values" "c7a559c" "f135f61" "2f662" "572e9a7a31cb03e9caa97") 36 | 37 | lines=( 38 | '#descriptions' 39 | aaa:'aaa' 40 | bbb:'bbb' 41 | ccc:'ccc' 42 | ddd:'ddd' 43 | eee:'eee' 44 | ) 45 | 46 | if [[ $lines[1] == "#values" ]] ; then 47 | declare -a commits 48 | commits=(${lines:1}) 49 | _values "commits" ${=commits} && ret=0 50 | elif [[ $lines[1] == "#descriptions" ]] ; then 51 | declare -a commits 52 | commits=(${lines:1}) 53 | # _describe -t commits commit commits && ret=0 54 | _describe commit commits && ret=0 55 | fi 56 | return ret 57 | } 58 | 59 | _foo () { 60 | local curcontext=$curcontext state line 61 | # typeset -A opt_args 62 | typeset -A val_args 63 | # typeset -A values 64 | declare -A opt_args 65 | local ret=1 66 | _arguments -w -C -S -s \ 67 | '--author=[Override the commit author. Specify an explicit author using the standard A U Thor format.]:author name:{__foometa dddd commit arg 1 valid-values}' \ 68 | '--commit=[commit with description]:commit:{ __foo_commit }' \ 69 | '--test=[commit with description]:test:(("foo\:foo desc" "bar\:bar desc"))' \ 70 | && ret=0 71 | return ret 72 | } 73 | compdef _foo foo 74 | -------------------------------------------------------------------------------- /snippets/zsh/array_expansion: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | declare -A values 3 | values[foo]='"111" "222" "333"' 4 | values[bar]="222" 5 | 6 | echo "keys: " ${(@k)values} 7 | echo "values: " ${(@v)values} 8 | echo "keys and values: " ${(@kv)values} 9 | echo "index foo: " ${=values[foo]} 10 | echo "index bar: " ${values[bar]} 11 | -------------------------------------------------------------------------------- /src/Ansi/Colors.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Colors 13 | { 14 | const CODE_PATTERN = '#\033\[\d{0,1}[,;]?\d*(?:;\d2)?m#x'; 15 | 16 | protected static $foregroundColors = array( 17 | 'black' => '0;30', 18 | 'dark_gray' => '1;30', 19 | 'blue' => '0;34', 20 | 'light_blue' => '1;34', 21 | 'green' => '0;32', 22 | 'light_green' => '1;32', 23 | 'cyan' => '0;36', 24 | 'light_cyan' => '1;36', 25 | 'red' => '0;31', 26 | 'light_red' => '1;31', 27 | 'purple' => '0;35', 28 | 'light_purple' => '1;35', 29 | 'brown' => '0;33', 30 | 'yellow' => '1;33', 31 | 'light_gray' => '0;37', 32 | 'white' => '1;37', 33 | ); 34 | 35 | protected static $backgroundColors = array( 36 | 'black' => '40', 37 | 'red' => '41', 38 | 'green' => '42', 39 | 'yellow' => '43', 40 | 'blue' => '44', 41 | 'magenta' => '45', 42 | 'cyan' => '46', 43 | 'light_gray' => '47', 44 | ); 45 | 46 | protected static $attributes = array( 47 | 'bold' => 1, 48 | 'dim' => 2, 49 | 'underline' => 4, 50 | 'blink' => 5, 51 | 'reverse' => 7, 52 | 'hidden' => 8, 53 | ); 54 | 55 | public static function stripAnsiEscapeCode($str) 56 | { 57 | return preg_replace(self::CODE_PATTERN, '', $str); 58 | } 59 | 60 | public static function strlenWithoutAnsiEscapeCode($str) 61 | { 62 | $plain = preg_replace(self::CODE_PATTERN, '', $str); 63 | 64 | return mb_strlen($plain); 65 | } 66 | 67 | public static function reset() 68 | { 69 | return "\033[0m"; 70 | } 71 | 72 | public static function attribute($text, $attribute) 73 | { 74 | if (!isset(self::$attributes[$attribute])) { 75 | throw new InvalidArgumentException("Undefined attribute $attribute"); 76 | } 77 | $str = "\033[".self::$attributes[$attribute].'m'; 78 | $str .= $text; 79 | $str = "\033[0m"; 80 | 81 | return $str; 82 | } 83 | 84 | // Returns colored string 85 | public static function decorate($string, $fg = null, $bg = null, $attribute = null) 86 | { 87 | $coloredString = ''; 88 | 89 | // Check if given foreground color found 90 | if ($fg && isset(self::$foregroundColors[$fg])) { 91 | $coloredString .= "\033[".self::$foregroundColors[$fg].'m'; 92 | } 93 | // Check if given background color found 94 | if ($bg && isset(self::$backgroundColors[$bg])) { 95 | $coloredString .= "\033[".self::$backgroundColors[$bg].'m'; 96 | } 97 | 98 | if ($attribute) { 99 | $coloredString .= "\033[".self::$attributes[$attribute].'m'; 100 | } 101 | 102 | // Add string and end coloring 103 | $coloredString .= $string; 104 | 105 | if ($fg || $bg) { 106 | $coloredString .= "\033[0m"; 107 | } 108 | 109 | return $coloredString; 110 | } 111 | 112 | // Returns all foreground color names 113 | public static function getForegroundColors() 114 | { 115 | return array_keys(self::$foregroundColors); 116 | } 117 | 118 | // Returns all background color names 119 | public static function getBackgroundColors() 120 | { 121 | return array_keys(self::$backgroundColors); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Ansi/CursorControl.php: -------------------------------------------------------------------------------- 1 | fd = $fd ?: fopen('php://stderr', 'w'); 15 | } 16 | 17 | /** 18 | * Sets the cursor position where subsequent text will begin. If no 19 | * row/column parameters are provided (ie. [H), the cursor will move 20 | * to the home position, at the upper left of the screen. 21 | */ 22 | public function home($row, $col) 23 | { 24 | fwrite($this->fd, "\e[{$row};{$column}H"); 25 | } 26 | 27 | public function up($count = 1) 28 | { 29 | fwrite($this->fd, "\e[{$count}A"); 30 | } 31 | 32 | public function down($count = 1) 33 | { 34 | fwrite($this->fd, "\e[{$count}B"); 35 | } 36 | 37 | public function forward($count = 1) 38 | { 39 | fwrite($this->fd, "\e[{$count}C"); 40 | } 41 | 42 | public function backward($count = 1) 43 | { 44 | fwrite($this->fd, "\e[{$count}D"); 45 | } 46 | 47 | /** 48 | * Force Cursor Position. 49 | * 50 | * Identical to Cursor Home. 51 | */ 52 | public function position($row, $column) 53 | { 54 | fwrite($this->fd, "\e[{$row},{$column}f"); 55 | } 56 | 57 | /** 58 | * Save Cursor & Attrs. 59 | * 60 | * Save current cursor position. 61 | */ 62 | public function save($attrs = true) 63 | { 64 | if ($attrs) { 65 | fwrite($this->fd, "\e7"); 66 | } 67 | fwrite($this->fd, "\e[s"); 68 | } 69 | 70 | /** 71 | * Restore Cursor & Attrs. 72 | * 73 | * Restores cursor position after a Save Cursor. 74 | */ 75 | public function restore($attrs = true) 76 | { 77 | if ($attrs) { 78 | fwrite($this->fd, "\e8"); 79 | } 80 | fwrite($this->fd, "\e[u"); 81 | } 82 | 83 | public function hide() 84 | { 85 | fwrite($this->fd, "\e[?25l"); 86 | } 87 | 88 | public function show() 89 | { 90 | fwrite($this->fd, "\e[?25h"); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ArgInfo.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | if ($desc) { 29 | $this->desc = $desc; 30 | } 31 | } 32 | 33 | public function isa($isa) 34 | { 35 | $this->isa = $isa; 36 | return $this; 37 | } 38 | 39 | public function desc($desc) 40 | { 41 | $this->desc = $desc; 42 | return $this; 43 | } 44 | 45 | public function optional($optional = true) 46 | { 47 | $this->optional = $optional; 48 | return $this; 49 | } 50 | 51 | public function multiple($a = true) 52 | { 53 | $this->multiple = $a; 54 | return $this; 55 | } 56 | 57 | public function validValues($val) 58 | { 59 | $this->validValues = $val; 60 | return $this; 61 | } 62 | 63 | public function validator($cb) 64 | { 65 | $this->validator = $cb; 66 | return $this; 67 | } 68 | 69 | /** 70 | * Assign suggestions 71 | * 72 | * @param string[]|string|Closure $value 73 | * 74 | * $value can be an array of strings, or a closure 75 | * 76 | * If $value is string, the prefix "zsh:" will be translated into zsh function call. 77 | */ 78 | public function suggestions($values) 79 | { 80 | $this->suggestions = $values; 81 | return $this; 82 | } 83 | 84 | 85 | /** 86 | * Specify argument glob pattern 87 | */ 88 | public function glob($g) 89 | { 90 | $this->glob = $g; 91 | return $this; 92 | } 93 | 94 | 95 | public function getSuggestions() 96 | { 97 | if ($this->suggestions) { 98 | if (is_callable($this->suggestions)) { 99 | return call_user_func($this->suggestions); 100 | } 101 | return $this->suggestions; 102 | } 103 | } 104 | 105 | 106 | public function getValidValues() 107 | { 108 | if ($this->validValues) { 109 | if (is_callable($this->validValues)) { 110 | return call_user_func($this->validValues); 111 | } 112 | return $this->validValues; 113 | } 114 | } 115 | 116 | /** 117 | * Test a value if it match the spec 118 | */ 119 | public function validate($value) 120 | { 121 | if ($this->isa) { 122 | switch ($this->isa) { 123 | case "number": 124 | return is_numeric($value); 125 | case "boolean": 126 | return is_bool($value); 127 | case "string": 128 | return is_string($value); 129 | } 130 | } 131 | $validValues = $this->getValidValues(); 132 | if ($validValues && $validValues instanceof ValueCollection) { 133 | return $validValues->containsValue($value); 134 | } elseif (is_array($validValues)) { 135 | return in_array($value, $validValues); 136 | } 137 | if ($this->validator) { 138 | return call_user_func($this->validator); 139 | } 140 | return true; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ArgInfoList.php: -------------------------------------------------------------------------------- 1 | append($arginfo); 16 | return $arginfo; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ArgumentEditor/ArgumentEditor.php: -------------------------------------------------------------------------------- 1 | args = $args; 12 | } 13 | 14 | public function append($arg) 15 | { 16 | $args = func_get_args(); 17 | foreach ($args as $arg) { 18 | $this->args[] = trim($arg); 19 | } 20 | 21 | return $this; 22 | } 23 | 24 | public function remove($arg) 25 | { 26 | $p = 0; 27 | $removed = 0; 28 | while ($p !== false) { 29 | // search next 30 | $p = array_search($arg, $this->args); 31 | if ($p !== false) { 32 | array_splice($this->args, $p); 33 | ++$removed; 34 | } 35 | } 36 | 37 | return $removed; 38 | } 39 | 40 | public function replace($needle, $newarg) 41 | { 42 | $p = array_search($needle, $this->args); 43 | if ($p !== false) { 44 | $spliced = array_splice($this->args, $p, 1, $newarg); 45 | 46 | return $spliced[0]; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | public function replaceRegExp($regexp, $newarg) 53 | { 54 | $regexp = '/'.preg_quote($regexp, '/').'/'; 55 | $this->args = preg_replace($regexp, $newarg, $this->args); 56 | } 57 | 58 | /** 59 | * Remove arguments by regular expression pattern. 60 | * 61 | * @param string $regexp 62 | */ 63 | public function removeRegExp($regexp) 64 | { 65 | $regexp = '/'.preg_quote($regexp, '/').'/'; 66 | $this->args = preg_grep($regexp, $this->args, PREG_GREP_INVERT); 67 | } 68 | 69 | /** 70 | * Filter all arguments through a closure. 71 | * 72 | * @param Closue 73 | */ 74 | public function filter($callback) 75 | { 76 | $this->args = array_map($callback, $this->args); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Run escape fitler to current arguments. 83 | */ 84 | public function escape() 85 | { 86 | $this->args = array_map(function ($arg) { 87 | return escapeshellarg($arg); 88 | }, $this->args); 89 | } 90 | 91 | /** 92 | * Output current argument to string. 93 | */ 94 | public function __toString() 95 | { 96 | return implode(' ', $this->args); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/ChainedCommand.php: -------------------------------------------------------------------------------- 1 | getChainedCommands(); 22 | foreach ($cmds as $cmd) { 23 | $cmd->options($opts); 24 | } 25 | } 26 | 27 | public function getChainedCommands() 28 | { 29 | $cmds = array(); 30 | foreach ($this->commands as $command) { 31 | $cmd = new $command($this->application); 32 | $cmd->logger = $this->logger; 33 | $cmd->parent = $this; 34 | $cmds[] = $cmd; 35 | } 36 | return $cmds; 37 | } 38 | 39 | public function execute() 40 | { 41 | $this->logger->info('Executing chained commands: ' . join(',', $this->commands)); 42 | $args = func_get_args(); 43 | $cmds = $this->getChainedCommands(); 44 | foreach ($cmds as $cmd) { 45 | $cmd->options = $this->options; 46 | $cmd->executeWrapper($args); 47 | } 48 | $this->logger->info('Done'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Chooser.php: -------------------------------------------------------------------------------- 1 | formatter = new Formatter; 20 | } 21 | 22 | /** 23 | * set prompt style 24 | */ 25 | public function setStyle($style) 26 | { 27 | return $this->style = $style; 28 | } 29 | 30 | /** 31 | * 32 | * 33 | */ 34 | public function choose($prompt, $choices) 35 | { 36 | echo $prompt . ": \n"; 37 | 38 | $choicesMap = array(); 39 | $isAssoc = (array_keys($choices) !== range(0, count($choices) - 1)); 40 | 41 | $i = 0; 42 | if ($isAssoc) { 43 | foreach ($choices as $choice => $value) { 44 | $i++; 45 | $choicesMap[ $i ] = $value; 46 | echo "\t$i: " . $choice . " => " . $value . "\n"; 47 | } 48 | } else { 49 | //is sequential 50 | foreach ($choices as $choice) { 51 | $i++; 52 | $choicesMap[ $i ] = $choice; 53 | echo "\t$i: $choice\n"; 54 | } 55 | } 56 | 57 | if ($this->style) { 58 | echo $this->formatter->getStartMark($this->style); 59 | } 60 | 61 | $completionItems = array_keys($choicesMap); 62 | $choosePrompt = "Please Choose 1-$i > "; 63 | while (1) { 64 | if (extension_loaded('readline')) { 65 | $success = readline_completion_function(function ($string, $index) use ($completionItems) { 66 | return $completionItems; 67 | }); 68 | $answer = readline($choosePrompt); 69 | readline_add_history($answer); 70 | } else { 71 | echo $choosePrompt; 72 | $answer = rtrim(fgets(STDIN), "\n"); 73 | } 74 | 75 | $answer = (int) trim($answer); 76 | if (is_integer($answer)) { 77 | if (isset($choicesMap[$answer])) { 78 | if ($this->style) { 79 | echo $this->formatter->getClearMark(); 80 | } 81 | 82 | return $choicesMap[$answer]; 83 | } else { 84 | continue; 85 | } 86 | } 87 | break; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace CLIFramework; 11 | 12 | use Exception; 13 | use CLIFramework\CommandInterface; 14 | use CLIFramework\Exception\CommandClassNotFoundException; 15 | use CLIFramework\Application; 16 | 17 | /** 18 | * abstract command class 19 | * 20 | */ 21 | abstract class Command extends CommandBase implements CommandInterface 22 | { 23 | /** 24 | * @var CLIFramework\Application Application object. 25 | */ 26 | public $application; 27 | 28 | public $name; 29 | 30 | public function __construct(CommandBase $parent = null) 31 | { 32 | parent::__construct($parent); 33 | } 34 | 35 | public function setApplication(Application $application) 36 | { 37 | $this->application = $application; 38 | } 39 | 40 | 41 | /** 42 | * Get the main application object from parents 43 | * 44 | * @return Application 45 | */ 46 | public function getApplication() 47 | { 48 | if ($this->application) { 49 | return $this->application; 50 | } 51 | $p = $this->parent; 52 | while (true) { 53 | if (! $p) { 54 | return null; 55 | } 56 | if ($p instanceof Application) { 57 | return $p; 58 | } 59 | $p = $p->parent; 60 | } 61 | } 62 | 63 | public function hasApplication() 64 | { 65 | return $this->getApplication() !== null; 66 | } 67 | 68 | public function setName($name) 69 | { 70 | $this->name = $name; 71 | } 72 | 73 | /** 74 | * Translate current class name to command name. 75 | * 76 | * @return string command name 77 | */ 78 | public function getName() 79 | { 80 | if ($this->name) { 81 | return $this->name; 82 | } 83 | 84 | // Extract command name from the class name. 85 | $class = get_class($this); 86 | // strip command suffix 87 | $parts = explode('\\', $class); 88 | $class = end($parts); 89 | $class = preg_replace('/Command$/', '', $class); 90 | return strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '-\1', $class)); 91 | } 92 | 93 | 94 | /** 95 | * Returns logger object. 96 | * 97 | * @return CLIFramework\Logger 98 | */ 99 | public function getLogger() 100 | { 101 | return $this->getApplication()->getLogger(); 102 | } 103 | 104 | 105 | 106 | /** 107 | * Returns text style formatter. 108 | * 109 | * @return CLIFramework\Formatter 110 | */ 111 | public function getFormatter() 112 | { 113 | return $this->getApplication()->getFormatter(); 114 | } 115 | 116 | /** 117 | * User may register their aliases 118 | */ 119 | public function aliases() 120 | { 121 | return array(); 122 | } 123 | 124 | /** 125 | * Provide a shorthand property for retrieving logger object. 126 | * 127 | * @param string $k property name 128 | */ 129 | public function __get($k) 130 | { 131 | if ($k === 'logger') { 132 | return $this->getLogger(); 133 | } elseif ($k === 'formatter') { 134 | return $this->getFormatter(); 135 | } 136 | throw new Exception("$k is not defined."); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Command/BashCompletionCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | */ 12 | 13 | namespace CLIFramework\Command; 14 | 15 | use CLIFramework\Command; 16 | use CLIFramework\CommandInterface; 17 | use CLIFramework\Completion\BashGenerator; 18 | 19 | class BashCompletionCommand extends Command implements CommandInterface 20 | { 21 | public function brief() 22 | { 23 | return 'This command generate a bash completion script automatically'; 24 | } 25 | 26 | public function options($opts) 27 | { 28 | $opts->add('bind:', 'bind complete to command'); 29 | $opts->add('program:', 'programe name'); 30 | } 31 | 32 | public function execute() 33 | { 34 | $programName = $this->options->program ?: $this->getApplication()->getProgramName(); 35 | $bind = $this->options->bind ?: $programName; 36 | $compName = '_'.preg_replace('#\W+#', '_', $programName); 37 | $generator = new BashGenerator($this->getApplication(), $programName, $bind, $compName); 38 | echo $generator->output(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Command/ZshCompletionCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | */ 12 | 13 | namespace CLIFramework\Command; 14 | 15 | use CLIFramework\Command; 16 | use CLIFramework\CommandInterface; 17 | use CLIFramework\Completion\ZshGenerator; 18 | 19 | class ZshCompletionCommand extends Command implements CommandInterface 20 | { 21 | public function brief() 22 | { 23 | return 'This function generate a zsh-completion script automatically'; 24 | } 25 | 26 | public function options($opts) 27 | { 28 | $opts->add('bind:', 'bind complete to command'); 29 | $opts->add('program:', 'programe name'); 30 | } 31 | 32 | public function execute() 33 | { 34 | $programName = $this->options->program ?: $this->getApplication()->getProgramName(); 35 | $bind = $this->options->bind ?: $programName; 36 | $compName = '_'.preg_replace('#\W+#', '_', $programName); 37 | $generator = new ZshGenerator($this->getApplication(), $programName, $bind, $compName); 38 | echo $generator->output(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CommandAutoloader.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 24 | } 25 | 26 | /** 27 | * Add all commands in a directory to parent command 28 | * 29 | * @param string|null $path if string is given, add the commands in given 30 | * path. If null is given, use current command's path. 31 | * @return void 32 | */ 33 | public function autoload($path = null) 34 | { 35 | if (is_null($path)) { 36 | $path = $this->getCurrentCommandDirectory(); 37 | } 38 | $commands = $this->scanCommandsInPath($path); 39 | $this->addCommandsForParent($commands); 40 | } 41 | 42 | private function getCurrentCommandDirectory() 43 | { 44 | $reflector = new \ReflectionClass(get_class($this->parent)); 45 | $classDir = dirname($reflector->getFileName()); 46 | 47 | /* 48 | * Commands to be autoloaded must located at specific directory. 49 | * If parent is Application, commands must be whthin App/Command/ directory. 50 | * If parent is another command named FooCommand, sub-commands must 51 | * within App/Command/FooCommand/ directory. 52 | */ 53 | $commandDirectoryBase= $this->parent->isApplication() 54 | ? 'Command' 55 | : $reflector->getShortName(); 56 | return $classDir . DIRECTORY_SEPARATOR . $commandDirectoryBase; 57 | } 58 | 59 | private function scanCommandsInPath($path) 60 | { 61 | if (!is_dir($path)) { 62 | return array(); 63 | } 64 | $files = scandir($path); 65 | return $this->translateFileNamesToCommands($files); 66 | } 67 | 68 | private function translateFileNamesToCommands(array $fileNames) 69 | { 70 | $commands = array_map( 71 | array($this, 'translateFileNameToCommand'), 72 | $fileNames 73 | ); 74 | return array_filter( 75 | $commands, 76 | function ($command) { 77 | return $command !== false; 78 | } 79 | ); 80 | } 81 | 82 | private function translateFileNameToCommand($fileName) 83 | { 84 | $extensions = explode(',', spl_autoload_extensions()); 85 | $isCommandClassFile = ($fileName[0] !== '.' 86 | and preg_match('/(^.*Command)(\..*)$/', $fileName, $matches) === 1 87 | and in_array($matches[2], $extensions)); 88 | return $isCommandClassFile 89 | ? $this->parent->getLoader()->inverseTranslate($matches[1]) 90 | : false; 91 | } 92 | 93 | private function addCommandsForParent($commands) 94 | { 95 | foreach ($commands as $command) { 96 | $this->parent->addCommand($command); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/CommandException.php: -------------------------------------------------------------------------------- 1 | name = $groupName; 22 | $this->commands = $commands; 23 | } 24 | 25 | public function setId($id) 26 | { 27 | $this->id = $id; 28 | return $this; 29 | } 30 | 31 | public function getId() 32 | { 33 | return $this->id ?: $this->getName(); 34 | } 35 | 36 | public function getName() 37 | { 38 | return $this->name; 39 | } 40 | 41 | public function addCommand($name, CommandBase $command) 42 | { 43 | $this->commands[$name] = $command; 44 | return $this; 45 | } 46 | 47 | public function getCommands() 48 | { 49 | return $this->commands; 50 | } 51 | 52 | public function getCommandNames() 53 | { 54 | return array_keys($this->commands); 55 | } 56 | 57 | /** 58 | * Set group description 59 | * 60 | * @param string $desc 61 | * @return CommandGroup 62 | */ 63 | public function setDesc($desc) 64 | { 65 | $this->desc = $desc; 66 | return $this; 67 | } 68 | 69 | /** 70 | * Get the group description 71 | */ 72 | public function getDesc() 73 | { 74 | return $this->desc; 75 | } 76 | 77 | public function hidden() 78 | { 79 | $this->isHidden = true; 80 | return $this; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/CommandInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace CLIFramework; 12 | 13 | interface CommandInterface 14 | { 15 | public function getLogger(); 16 | public function getFormatter(); 17 | } 18 | -------------------------------------------------------------------------------- /src/CommandLoader.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace CLIFramework; 12 | 13 | use CLIFramework\Exception\CommandClassNotFoundException; 14 | use Exception; 15 | 16 | class CommandLoader 17 | { 18 | public $namespaces = array(); 19 | 20 | public function addNamespace($ns) 21 | { 22 | $nss = (array) $ns; 23 | foreach ($nss as $n) { 24 | $this->namespaces[] = $n; 25 | } 26 | } 27 | 28 | /** 29 | * Translate command name to class name. 30 | * 31 | * This method convert "foo-bar" to "FooBar", so if you have command name like "foo-bar", 32 | * this method returns FooBar class. e.g., 33 | * 34 | * list => ListCommand 35 | * list-all => ListAllCommand 36 | * 37 | * @param string $command command name. 38 | * @return string class name. 39 | */ 40 | public function translate($command) 41 | { 42 | $args = explode('-', $command); 43 | foreach ($args as & $a) { 44 | $a = ucfirst($a); 45 | } 46 | return join('', $args) . 'Command'; 47 | } 48 | 49 | /** 50 | * Translate class name to command name 51 | * 52 | * This method is inverse of self::translate() 53 | * 54 | * HelpCommand => help 55 | * SuchALongCommand => such-a-long 56 | * 57 | * @param string $className class name. 58 | * @return string translated command name. 59 | */ 60 | public function inverseTranslate($className) 61 | { 62 | if (substr($className, -7) !== 'Command') { 63 | throw new \InvalidArgumentException("Command class name need to end with 'Command'"); 64 | } 65 | // remove the suffix 'Command', then lower case the first letter 66 | $className = lcfirst(substr($className, 0, -7)); 67 | return preg_replace_callback( 68 | '/[A-Z]/', 69 | function ($matches) { 70 | return '-' . strtolower($matches[0]); 71 | }, 72 | $className 73 | ); 74 | } 75 | 76 | /** 77 | * load command class: 78 | * 79 | * @param string $command command name 80 | * @return boolean 81 | **/ 82 | public function load($command) 83 | { 84 | $subclass = $this->translate($command); 85 | return $this->loadClass($subclass); 86 | } 87 | 88 | /** 89 | * Load command class/subclass 90 | * 91 | * @param string $class 92 | * @return string loaded class name 93 | */ 94 | public function loadClass($class) 95 | { 96 | if (class_exists($class)) { 97 | return $class; 98 | } 99 | 100 | // for subcommand class name (under any subcommand namespace) 101 | // has application command class ? 102 | foreach ($this->namespaces as $ns) { 103 | $fullclass = $ns . '\\' . $class; 104 | if (class_exists($fullclass)) { 105 | return $fullclass; 106 | } 107 | } 108 | throw new CommandClassNotFoundException($class, $this->namespaces); 109 | } 110 | 111 | 112 | /** 113 | * load subcommand class from command name 114 | * 115 | * @param $command 116 | * @param $parent parent command class 117 | * 118 | * */ 119 | public function loadSubcommand($subcommand, $parent) 120 | { 121 | $parent_class = get_class($parent); 122 | $class = '\\' . $parent_class . '\\' . $this->translate($subcommand); 123 | return $this->loadClass($class); 124 | } 125 | 126 | public static function getInstance() 127 | { 128 | static $instance; 129 | if ($instance) { 130 | return $instance; 131 | } 132 | return $instance = new self; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Completion/Utils.php: -------------------------------------------------------------------------------- 1 | 0 ? $proceeded / $secondDiff : 0; 10 | $remaining = $total - $proceeded; 11 | if ($speed > 0) { 12 | $remainingSeconds = $remaining / $speed; 13 | return $remainingSeconds; 14 | } 15 | } 16 | 17 | public static function calculateEstimatedPeriod($proceeded, $total, $start, $now) 18 | { 19 | $str = '--'; 20 | if ($remainingSeconds = self::calculateRemainingSeconds($proceeded, $total, $start, $now)) { 21 | $str = ''; 22 | 23 | $days = 0; 24 | $hours = 0; 25 | $minutes = 0; 26 | if ($remainingSeconds > (3600 * 24)) { 27 | $days = ceil($remainingSeconds / (3600 * 24)); 28 | $remainingSeconds = $remainingSeconds % (3600 * 24); 29 | } 30 | 31 | if ($remainingSeconds > 3600) { 32 | $hours = ceil($remainingSeconds / 3600); 33 | $remainingSeconds = $remainingSeconds % 3600; 34 | } 35 | 36 | if ($remainingSeconds > 60) { 37 | $minutes = ceil($remainingSeconds / 60); 38 | $remainingSeconds = $remainingSeconds % 60; 39 | } 40 | 41 | if ($days > 0) { 42 | $str .= $days . 'd'; 43 | } 44 | if ($hours) { 45 | $str .= $hours . 'h'; 46 | } 47 | if ($minutes) { 48 | $str .= $minutes . 'm'; 49 | } 50 | if ($remainingSeconds > 0) { 51 | $str .= intval($remainingSeconds) . 's'; 52 | } 53 | } 54 | return $str; 55 | } 56 | 57 | public static function calculateEstimatedTime($proceeded, $total, $start, $now) 58 | { 59 | if ($remainingSeconds = self::calculateRemainingSeconds($proceeded, $total, $start, $now)) { 60 | return $now + $remainingSeconds; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Component/Progress/ProgressBar.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 55 | 56 | if ($container) { 57 | $this->formatter = $container['formatter']; 58 | if (isset($container['consoleInfo'])) { 59 | $this->console = $container['consoleInfo']; 60 | } 61 | } else { 62 | $this->formatter = new Formatter; 63 | $this->console = ConsoleInfoFactory::create(); 64 | $this->updateLayout(); 65 | } 66 | } 67 | 68 | public function updateLayout() 69 | { 70 | if ($this->console) { 71 | $this->terminalWidth = $this->console->getColumns(); 72 | } 73 | } 74 | 75 | public function setTitle($title) 76 | { 77 | $this->title = $title; 78 | } 79 | 80 | public function setUnit($unit) 81 | { 82 | $this->unit = $unit; 83 | } 84 | 85 | public function start($title = null) 86 | { 87 | if ($title) { 88 | $this->setTitle($title); 89 | } 90 | $this->start = microtime(true); 91 | } 92 | 93 | public function update($finished, $total) 94 | { 95 | $percentage = $total > 0 ? round($finished / $total, 2) : 0.0; 96 | $trigger = $finished % 3; 97 | 98 | if ($trigger) { 99 | $this->etaTime = date('H:i', ETACalculator::calculateEstimatedTime($finished, $total, $this->start, microtime(true))); 100 | $this->etaPeriod = ETACalculator::calculateEstimatedPeriod($finished, $total, $this->start, microtime(true)); 101 | } 102 | $desc = str_replace([ 103 | '%finished%', '%total%', '%unit%', '%percentage%', '%eta_time%', '%eta_period%', 104 | ], [ 105 | $finished, 106 | $total, 107 | $this->unit, 108 | ($percentage * 100) . '%', 109 | 'ETA: ' . $this->etaTime, 110 | 'ETA: ' . $this->etaPeriod, 111 | ], $this->descFormat); 112 | 113 | $barSize = $this->terminalWidth 114 | - mb_strlen($desc) 115 | - mb_strlen($this->leftDecorator) 116 | - mb_strlen($this->rightDecorator) 117 | - mb_strlen($this->columnDecorator) 118 | ; 119 | 120 | if ($this->title) { 121 | $barSize -= (mb_strlen($this->title) + mb_strlen($this->columnDecorator)); 122 | } 123 | 124 | $sharps = ceil($barSize * $percentage); 125 | 126 | fwrite($this->stream, "\r" 127 | . ($this->title ? $this->title . $this->columnDecorator : "") 128 | . Colors::decorate($this->leftDecorator, $trigger ? 'purple' : 'light_purple') 129 | . Colors::decorate(str_repeat($this->barCharacter, $sharps), $trigger ? 'purple' : 'light_purple') 130 | . str_repeat(' ', max($barSize - $sharps, 0)) 131 | . Colors::decorate($this->rightDecorator, $trigger ? 'purple' : 'light_purple') 132 | . $this->columnDecorator 133 | . Colors::decorate($desc, $trigger ? 'light_gray' : 'white') 134 | ); 135 | 136 | // hide cursor 137 | // fputs($this->stream, "\033[?25l"); 138 | 139 | // show cursor 140 | // fputs($this->stream, "\033[?25h"); 141 | } 142 | 143 | public function finish($title = null) 144 | { 145 | if ($title) { 146 | $this->setTitle($title); 147 | } 148 | fwrite($this->stream, PHP_EOL); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Component/Progress/ProgressBarStyle.php: -------------------------------------------------------------------------------- 1 | 1000000) { 17 | return round($bytes / 1000000, 2) . 'M'; 18 | } elseif ($bytes > 1000) { 19 | return round($bytes / 1000, 2) . 'K'; 20 | } 21 | return round($bytes, 2) . 'B'; 22 | } 23 | 24 | public function reset() 25 | { 26 | $this->done = false; 27 | } 28 | 29 | public function setUrl($url) 30 | { 31 | $this->url = $url; 32 | } 33 | 34 | public function curlCallback($ch, $downloadSize, $downloaded, $uploadSize, $uploaded) 35 | { 36 | /* 4kb */ 37 | if ($this->done || $downloadSize == 0) { 38 | return; 39 | } 40 | 41 | // printf("%s % 4d%%", $s , $percent ); 42 | if ($downloadSize != 0 && $downloadSize === $downloaded) { 43 | $this->done = true; 44 | printf("\r\t%-60s \n", $this->url); 45 | } else { 46 | $percent = ($downloaded > 0 ? (float) ($downloaded / $downloadSize) : 0.0); 47 | if (++$this->i > 3) { 48 | $this->i = 0; 49 | } 50 | 51 | /* 8 + 1 + 60 + 1 + 1 + 1 + 6 = */ 52 | printf("\r\tFetching %-60s %s % -3.1f%% %s", $this->url, 53 | $this->stars[ $this->i ], 54 | $percent * 100, $this->prettySize($downloaded)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Component/Progress/SharpProgressBarStyle.php: -------------------------------------------------------------------------------- 1 | style = $style; 37 | } 38 | 39 | public function setStyle(TableStyle $style) 40 | { 41 | $this->style = $style; 42 | } 43 | */ 44 | 45 | public function setAlignment($alignment) 46 | { 47 | $this->alignment = $alignment; 48 | } 49 | 50 | public function setFormatter($formatter) 51 | { 52 | $this->formatter = $formatter; 53 | } 54 | 55 | public function getFormatter() 56 | { 57 | return $this->formatter; 58 | } 59 | 60 | public function setTextOverflow($overflowType) 61 | { 62 | $this->textOverflow = $overflowType; 63 | } 64 | 65 | /** 66 | * The default cell text formatter 67 | */ 68 | public function format($cell) 69 | { 70 | if ($this->formatter) { 71 | return call_user_func($this->formatter, $cell); 72 | } 73 | return $cell; 74 | } 75 | 76 | public function setBackgroundColor($color) 77 | { 78 | $this->backgroundColor = $color; 79 | } 80 | 81 | public function setForegroundColor($color) 82 | { 83 | $this->foregroundColor = $color; 84 | } 85 | 86 | public function getForegroundColor() 87 | { 88 | return $this->foregroundColor; // TODO: fallback to table style 89 | } 90 | 91 | public function getBackgroundColor() 92 | { 93 | return $this->backgroundColor; // TODO: fallback to table style 94 | } 95 | 96 | /** 97 | * When inserting rows, we pre-explode the lines to extra rows from Table 98 | * hence this method is separated for pre-processing.. 99 | */ 100 | public function handleTextOverflow($cell, $maxWidth) 101 | { 102 | $lines = explode("\n", $cell); 103 | if ($this->textOverflow == self::WRAP) { 104 | $maxLineWidth = max(array_map('mb_strlen', $lines)); 105 | if ($maxLineWidth > $maxWidth) { 106 | $cell = wordwrap($cell, $maxWidth, "\n"); 107 | // Re-explode the lines 108 | $lines = explode("\n", $cell); 109 | } 110 | } elseif ($this->textOverflow == self::ELLIPSIS) { 111 | if (mb_strlen($lines[0]) > $maxWidth) { 112 | $lines = array(mb_substr($lines[0], 0, $maxWidth - 2) . '..'); 113 | } 114 | } elseif ($this->textOverflow == self::CLIP) { 115 | if (mb_strlen($lines[0]) > $maxWidth) { 116 | $lines = array(mb_substr($lines[0], 0, $maxWidth)); 117 | } 118 | } 119 | return $lines; 120 | } 121 | 122 | public function renderCell($cell, $width, $style) 123 | { 124 | $out = ''; 125 | $out .= str_repeat($style->cellPaddingChar, $style->cellPadding); 126 | /* 127 | if ($this->backgroundColor || $this->foregroundColor) { 128 | $decoratedCell = Colors::decorate($cell, $this->foregroundColor, $this->backgroundColor); 129 | $width += mb_strlen($decoratedCell) - mb_strlen($cell); 130 | $cell = $decoratedCell; 131 | } 132 | */ 133 | 134 | if ($this->alignment === CellAttribute::ALIGN_LEFT) { 135 | $out .= str_pad($cell, $width, ' '); // default alignment = LEFT 136 | } elseif ($this->alignment === CellAttribute::ALIGN_RIGHT) { 137 | $out .= str_pad($cell, $width, ' ', STR_PAD_LEFT); 138 | } elseif ($this->alignment === CellAttribute::ALIGN_CENTER) { 139 | $out .= str_pad($cell, $width, ' ', STR_PAD_BOTH); 140 | } else { 141 | $out .= str_pad($cell, $width, ' '); // default alignment 142 | } 143 | 144 | $out .= str_repeat($style->cellPaddingChar, $style->cellPadding); 145 | 146 | if ($this->backgroundColor || $this->foregroundColor) { 147 | return Colors::decorate($out, $this->foregroundColor, $this->backgroundColor); 148 | } 149 | return $out; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Component/Table/CompactTableStyle.php: -------------------------------------------------------------------------------- 1 | currency = $currency; 15 | } 16 | 17 | public function format($cell) 18 | { 19 | return $this->formatter->formatCurrency($cell, $this->currency); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Component/Table/DateFormatCell.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 25 | $this->formatter = new IntlDateFormatter($locale, $datetype, $timetype, $timezone, $calendar); 26 | } 27 | 28 | public function format($cell) 29 | { 30 | if ($cell instanceof DateTime) { 31 | return $this->formatter->formatObject($cell); 32 | } 33 | return $this->formatter->format($cell); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Component/Table/DurationFormatCell.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 12 | $this->formatter = new NumberFormatter($locale, NumberFormatter::DURATION); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Component/Table/MarkdownTableStyle.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 16 | $this->formatter = new NumberFormatter($locale, NumberFormatter::DECIMAL); 17 | } 18 | 19 | public function format($cell) 20 | { 21 | if (is_numeric($cell)) { 22 | return $this->formatter->format($cell); 23 | } 24 | return $cell; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Component/Table/PercentFormatCell.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 12 | $this->formatter = new NumberFormatter($locale, NumberFormatter::PERCENT); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Component/Table/SpellOutNumberFormatCell.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 12 | $this->formatter = new NumberFormatter($locale, NumberFormatter::SPELLOUT); 13 | $this->formatter->setTextAttribute(NumberFormatter::DEFAULT_RULESET, "%financial"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Component/Table/TableStyle.php: -------------------------------------------------------------------------------- 1 | cellPadding = $padding; 28 | } 29 | 30 | public function setCellPaddingChar($c) 31 | { 32 | $this->cellPaddingChar = $c; 33 | } 34 | 35 | public function setVerticalBorderChar($c) 36 | { 37 | $this->verticalBorderChar = $c; 38 | } 39 | 40 | public function setRowSeparatorCrossChar($c) 41 | { 42 | $this->rowSeparatorCrossChar = $c; 43 | } 44 | 45 | public function setRowSeparatorRightmostCrossChar($c) 46 | { 47 | $this->rowSeparatorRightmostCrossChar = $c; 48 | } 49 | 50 | public function setRowSeparatorLeftmostCrossChar($c) 51 | { 52 | $this->rowSeparatorLeftmostCrossChar = $c; 53 | } 54 | 55 | public function setRowSeparatorBorderChar($c) 56 | { 57 | $this->rowSeparatorBorderChar = $c; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Config/GlobalConfig.php: -------------------------------------------------------------------------------- 1 | config = $config; 29 | } 30 | 31 | /** 32 | * Returns true if verbose option is enabled. 33 | * @return boolean 34 | */ 35 | public function isVerbose() 36 | { 37 | if (isset($this->config['core']['verbose'])) { 38 | $this->isVerbose = $this->config['core']['verbose'] === '1'; 39 | } 40 | return $this->isVerbose; 41 | } 42 | 43 | /** 44 | * Returns true if debug option is enabled. 45 | * @return boolean 46 | */ 47 | public function isDebug() 48 | { 49 | if (isset($this->config['core']['debug'])) { 50 | $this->isDebug = $this->config['core']['debug'] === '1'; 51 | } 52 | return $this->isDebug; 53 | } 54 | 55 | /** 56 | * Returns the directory of pid files. 57 | */ 58 | public function getPidDirectory() 59 | { 60 | if (isset($this->config['core']['pid_dir'])) { 61 | $this->pidDir = $this->config['core']['pid_dir']; 62 | } 63 | return $this->pidDir; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ConsoleInfo/ConsoleInfoFactory.php: -------------------------------------------------------------------------------- 1 | possibleTokens = $possibleTokens; 23 | } 24 | 25 | /** 26 | * Given user's input, ask user to correct it. 27 | * 28 | * @param string $input user's input 29 | * @return string corrected input 30 | */ 31 | public function correct($input) 32 | { 33 | $guess = $this->match($input); 34 | if ($guess === $input) { 35 | return $guess; 36 | } else { 37 | return $this->askForGuess($guess) ? $guess : $input; 38 | } 39 | } 40 | 41 | /** 42 | * Given user's input, return the best match among candidates. 43 | * 44 | * @param string $input @see self::correct() 45 | * @return string best matched string or raw input if no candidates provided 46 | */ 47 | public function match($input) 48 | { 49 | if (empty($this->possibleTokens)) { 50 | return $input; 51 | } 52 | 53 | $bestSimilarity = -1; 54 | $bestGuess = $input; 55 | foreach ($this->possibleTokens as $possibleToken) { 56 | similar_text($input, $possibleToken, $similarity); 57 | if ($similarity > $bestSimilarity) { 58 | $bestSimilarity = $similarity; 59 | $bestGuess = $possibleToken; 60 | } 61 | } 62 | return $bestGuess; 63 | } 64 | 65 | private function askForGuess($guess) 66 | { 67 | $prompter = new Prompter; 68 | $answer = $prompter->ask("Did you mean '$guess'?", array('Y','n'), 'Y'); 69 | return !$answer || strtolower($answer) == 'y'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Debug/ConsoleDebug.php: -------------------------------------------------------------------------------- 1 | getMessage() . '".'; 23 | $output[] = $indicator->indicateFile($e->getFile(), $e->getLine()); 24 | 25 | $output[] = "Exception Stack Trace"; 26 | $output[] = "====================="; 27 | $output[] = ""; 28 | $output[] = $e->getTraceAsString(); 29 | return join(PHP_EOL, $output); 30 | } 31 | 32 | 33 | /** 34 | * Dump Record Collection 35 | */ 36 | public static function dumpCollection(BaseCollection $collection, array $options = array()) 37 | { 38 | return self::dumpRows($collection->toArray(), $options); 39 | } 40 | 41 | 42 | public static function dumpRows(array $array, array $options = array()) 43 | { 44 | $table = new Table; 45 | 46 | $keys = null; 47 | if (isset($options['keys'])) { 48 | $keys = $options['keys']; 49 | } elseif (isset($array[0])) { 50 | $keys = array_keys($array[0]); 51 | } 52 | 53 | if ($keys) { 54 | $table->setHeaders($keys); 55 | } 56 | 57 | if (empty($array)) { 58 | return '0 rows.' . PHP_EOL; 59 | } 60 | 61 | foreach ($array as $item) { 62 | $values = []; 63 | foreach ($keys as $key) { 64 | $values[] = $item[$key]; 65 | } 66 | $table->addRow($values); 67 | } 68 | return $table->render() . PHP_EOL 69 | . count($array) . ' rows.' . PHP_EOL; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Debug/LineIndicator.php: -------------------------------------------------------------------------------- 1 | % 4d| %s"; 9 | 10 | protected $contextLineFormat = " % 4d| %s"; 11 | 12 | public function __construct() 13 | { 14 | } 15 | 16 | /** 17 | * 18 | * @param string $file 19 | * @param integer|integer[] $line 20 | */ 21 | public function indicateFile($file, $line) 22 | { 23 | $lines = file($file); 24 | $fromIndex = max($line - 1 - $this->contextLines, 0); 25 | $toIndex = min($line - 1 + $this->contextLines, count($lines)); 26 | 27 | if ($fromIndex === $toIndex) { 28 | $indexRange = [ $fromIndex ]; 29 | } else { 30 | $indexRange = range($fromIndex, $toIndex); 31 | } 32 | 33 | $output = []; 34 | $output[] = "$file @ line " . join(',', (array) $line); 35 | $output[] = str_repeat('=', strlen($output[0])); 36 | foreach ($indexRange as $index) { 37 | if ((is_integer($line) && $index + 1 == $line) || (is_array($line) && in_array($index + 1, $line))) { 38 | $output[] = sprintf($this->indicatedLineFormat, $index + 1, rtrim($lines[$index])); 39 | } else { 40 | $output[] = sprintf($this->contextLineFormat, $index + 1, rtrim($lines[$index])); 41 | } 42 | } 43 | return join(PHP_EOL, $output) . PHP_EOL; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Exception/CommandArgumentNotEnoughException.php: -------------------------------------------------------------------------------- 1 | given = $given; 16 | $this->required = $required; 17 | parent::__construct($command, "Insufficient arguments for command '{$command->getName()}', which requires $required arguments, $given given."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exception/CommandBaseException.php: -------------------------------------------------------------------------------- 1 | command = $command; 14 | parent::__construct($message, $code, $previous); 15 | } 16 | 17 | public function getCommand() 18 | { 19 | return $this->command; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/CommandClassNotFoundException.php: -------------------------------------------------------------------------------- 1 | class = $class; 17 | $this->registeredNamespaces = $registeredNamespaces; 18 | 19 | $this->possibleClasses[] = $class; 20 | foreach ($registeredNamespaces as $ns) { 21 | $this->possibleClasses[] = $ns . '\\' . ltrim($class, '\\'); 22 | } 23 | 24 | $desc = "Command $class not found."; 25 | if (!empty($this->registeredNamespaces)) { 26 | $desc .= "\nRegistered namespaces: [" . join(',', $this->registeredNamespaces) . "]"; 27 | } 28 | if (!empty($this->possibleClasses)) { 29 | $desc .= "\nPossible classnames: [" . join(',', $this->possibleClasses) . "]"; 30 | } 31 | parent::__construct($desc); 32 | } 33 | 34 | public function getRegisteredNamespaces() 35 | { 36 | return $this->registeredNamespaces; 37 | } 38 | 39 | public function getPossibleClasses() 40 | { 41 | return $this->possibleClasses; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Exception/CommandNotFoundException.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | parent::__construct($command, "Command $name not found."); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/ExecuteMethodNotDefinedException.php: -------------------------------------------------------------------------------- 1 | extension = $extension; 16 | } 17 | 18 | public function getExtension() 19 | { 20 | return $this->extension; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/InvalidCommandArgumentException.php: -------------------------------------------------------------------------------- 1 | argIndex = $argIndex; 17 | $this->arg = $arg; 18 | parent::__construct($command, "Invalid '{$command->getName()}' command argument '$arg' at position $argIndex"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ExceptionPrinter/DevelopmentExceptionPrinter.php: -------------------------------------------------------------------------------- 1 | $i) { 32 | $out .= $k . ' => ' . output_var($i); 33 | } 34 | $out .= ']'; 35 | return $out; 36 | } 37 | } elseif (is_scalar($a)) { 38 | return var_export($a, true); 39 | } elseif (is_object($a)) { 40 | if (method_exists($a, '__toString')) { 41 | return $a->__toString(); 42 | } else { 43 | return get_class($a); 44 | } 45 | } else { 46 | return '...'; 47 | } 48 | } 49 | 50 | class DevelopmentExceptionPrinter 51 | { 52 | public $reportUrl; 53 | 54 | public $logger; 55 | 56 | public function __construct(Logger $logger) 57 | { 58 | $this->logger = $logger; 59 | } 60 | 61 | public function dumpVar($var) 62 | { 63 | return output_var($var); 64 | } 65 | 66 | public function dumpArgs(array $args) 67 | { 68 | if (empty($args)) { 69 | return ''; 70 | } 71 | 72 | $desc = array(); 73 | foreach ($args as $a) { 74 | $desc[] = output_var($a); 75 | } 76 | return join(', ', $desc); 77 | } 78 | 79 | public function dumpTraceInPhar(Exception $e) 80 | { 81 | $this->logger->info("Trace:\n"); 82 | $trace = $e->getTrace(); 83 | foreach ($trace as $idx => $entry) { 84 | $argDesc = $this->dumpArgs($entry['args']); 85 | $this->logger->info(sprintf(" %d) %s%s%s(%s)", $idx, @$entry['class'], @$entry['type'], $entry['function'], $argDesc)); 86 | } 87 | $this->logger->newline(); 88 | } 89 | 90 | public function dumpTrace(Exception $e) 91 | { 92 | $this->logger->info("Trace:\n"); 93 | $trace = $e->getTrace(); 94 | foreach ($trace as $idx => $entry) { 95 | $argDesc = $this->dumpArgs($entry['args']); 96 | 97 | $this->logger->info(sprintf(" %d) %s%s%s(%s)", $idx, @$entry['class'], @$entry['type'], $entry['function'], $argDesc)); 98 | 99 | if (isset($entry['file'])) { 100 | $this->logger->info(sprintf(" from %s: %d", @$entry['file'], @$entry['line'])); 101 | } 102 | 103 | $this->logger->newline(); 104 | } 105 | $this->logger->newline(); 106 | } 107 | 108 | public function dumpCodeBlock(Exception $e) 109 | { 110 | $line = $e->getLine(); 111 | $file = $e->getFile(); 112 | $this->logger->info("Thrown from $file at line $line:\n"); 113 | 114 | $lines = file($file); 115 | $indexRange = range(max($line - 4, 0), min($line + 3, count($lines))); 116 | foreach ($indexRange as $index) { 117 | if ($index == ($line - 1)) { 118 | $this->logger->warn(sprintf("> % 3d", $index + 1) . rtrim($lines[$index])); 119 | } else { 120 | $this->logger->info(sprintf(" % 3d", $index + 1) . rtrim($lines[$index])); 121 | } 122 | } 123 | 124 | $this->logger->newline(); 125 | } 126 | 127 | public function dumpBrief(Exception $e) 128 | { 129 | $logger = $this->logger; 130 | $code = $e->getCode(); 131 | $message = $e->getMessage(); 132 | 133 | $file = $e->getFile(); 134 | $line = $e->getLine(); 135 | 136 | $class = get_class($e); 137 | 138 | if ($code) { 139 | $logger->error("$class: ($code) $message"); 140 | } else { 141 | $logger->error("$class: $message"); 142 | } 143 | } 144 | 145 | public function dump(Exception $e) 146 | { 147 | $this->dumpBrief($e); 148 | $this->dumpCodeBlock($e); 149 | $this->dumpTrace($e); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/ExceptionPrinter/ProductionExceptionPrinter.php: -------------------------------------------------------------------------------- 1 | dumpBrief($e); 15 | $this->dumpTraceInPhar($e); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Extension/ApplicationExtension.php: -------------------------------------------------------------------------------- 1 | command = $command; 17 | $this->options($command->getOptionCollection()); 18 | // $this->arguments( ); 19 | 20 | $this->config = $command->getApplication()->getGlobalConfig(); 21 | $this->setServiceContainer($command->getApplication()->getService()); 22 | $this->init(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Extension/Extension.php: -------------------------------------------------------------------------------- 1 | container = $container; 23 | } 24 | 25 | 26 | /** 27 | * init method is called when the extension is added to the pool. 28 | */ 29 | public function init() 30 | { 31 | } 32 | 33 | public static function isSupported() 34 | { 35 | return true; 36 | } 37 | 38 | public function isAvailable() 39 | { 40 | return true; 41 | } 42 | 43 | public function options($opts) 44 | { 45 | } 46 | 47 | public function arguments($args) 48 | { 49 | } 50 | 51 | public function prepare() 52 | { 53 | } 54 | 55 | public function execute() 56 | { 57 | } 58 | 59 | public function finish() 60 | { 61 | } 62 | 63 | public function __get($accessor) 64 | { 65 | if (isset($this->container[$accessor])) { 66 | return $this->container[$accessor]; 67 | } 68 | throw new LogicException("Undefined accessor '$accessor'"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Formatter.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace CLIFramework; 12 | 13 | /** 14 | * Console output formatter class 15 | * 16 | * 17 | * $formatter = new Formatter; 18 | * $text = $formatter->format( 'text', 'styleName' ); 19 | * $text = $formatter->format( 'text', 'red' ); 20 | * $text = $formatter->format( 'text', 'green' ); 21 | * 22 | */ 23 | class Formatter 24 | { 25 | 26 | // Refactor style builder out. 27 | protected $styles = array( 28 | 'dim' => array('dim' => 1), 29 | 'red' => array('fg' => 'red'), 30 | 'green' => array('fg' => 'green'), 31 | 'white' => array('fg' => 'white'), 32 | 'yellow' => array('fg' => 'yellow'), 33 | 'strong_red' => array('fg' => 'red', 'bold' => 1), 34 | 'strong_green' => array('fg' => 'green', 'bold' => 1), 35 | 'strong_white' => array('fg' => 'white', 'bold' => 1), 36 | 'ask' => array('fg' => 'white', 'bold' => 1 , 'underline' => 1 ), 37 | 'choose' => array('fg' => 'white', 'bold' => 1 , 'underline' => 1 ), 38 | 39 | 'bold' => array('fg' => 'white', 'bold' => 1 ), 40 | 'underline' => array( 'fg' => 'white' , 'underline' => 1 ), 41 | 42 | // generic styles for logger 43 | 'info' => array('fg' => 'white', 'bold' => 1 ), 44 | 'debug' => array('fg' => 'white' ), 45 | 'notice' => array('fg' => 'yellow' ), 46 | 'warn' => array('fg' => 'red' ), 47 | 'error' => array('fg' => 'red', 'bold' => 1 ), 48 | 49 | 'done' => array('fg' => 'black', 'bg' => 'green' ), 50 | 'success' => array('fg' => 'black', 'bg' => 'green' ), 51 | 'fail' => array('fg' => 'black', 'bg' => 'red' ), 52 | 53 | 'action' => array('fg' => 'white', 'bg' => 'green' ), 54 | ); 55 | 56 | protected $options = array( 57 | 'bold' => 1, 58 | 'dim' => 2, 59 | 'underline' => 4, 60 | 'blink' => 5, 61 | 'reverse' => 7, 62 | 'conceal' => 8 63 | ); 64 | 65 | protected $foreground = array( 66 | 'black' => 30, 67 | 'red' => 31, 68 | 'green' => 32, 69 | 'yellow' => 33, 70 | 'blue' => 34, 71 | 'magenta' => 35, 72 | 'cyan' => 36, 73 | 'white' => 37 74 | ); 75 | 76 | protected $background = array( 77 | 'black' => 40, 78 | 'red' => 41, 79 | 'green' => 42, 80 | 'yellow' => 43, 81 | 'blue' => 44, 82 | 'magenta' => 45, 83 | 'cyan' => 46, 84 | 'white' => 47 85 | ); 86 | 87 | protected $supportsColors; 88 | 89 | public function __construct() 90 | { 91 | $this->supportsColors = DIRECTORY_SEPARATOR != '\\' 92 | && function_exists('posix_isatty') && @posix_isatty(STDOUT); 93 | } 94 | 95 | public function preferRawOutput() 96 | { 97 | $this->supportsColors = false; 98 | } 99 | 100 | public function addStyle($name, $style) 101 | { 102 | $this->styles[ $name ] = $style; 103 | } 104 | 105 | public function hasStyle($name) 106 | { 107 | return isset($this->styles[ $name ]); 108 | } 109 | 110 | public function getStartMark($style) 111 | { 112 | if (!$this->supportsColors) { 113 | return; 114 | } 115 | 116 | if ($style == 'none' || ! isset($this->styles[$style])) { 117 | return ''; 118 | } 119 | 120 | $parameters = $this->styles[$style]; 121 | $codes = array(); 122 | 123 | if (isset($parameters['fg'])) { 124 | $codes[] = $this->foreground[$parameters['fg']]; 125 | } 126 | 127 | if (isset($parameters['bg'])) { 128 | $codes[] = $this->background[$parameters['bg']]; 129 | } 130 | 131 | foreach ($this->options as $option => $value) { 132 | if (isset($parameters[$option]) && $parameters[$option]) { 133 | $codes[] = $value; 134 | } 135 | } 136 | 137 | return "\033[".implode(';', $codes).'m'; 138 | } 139 | 140 | public function getClearMark() 141 | { 142 | if (! $this->supportsColors) { 143 | return ''; 144 | } 145 | return "\033[0m"; 146 | } 147 | 148 | /** 149 | * Formats a text according to the given style or parameters. 150 | * 151 | * @param string $text The text to style 152 | * @param string $style A style name 153 | * 154 | * @return string The styled text 155 | */ 156 | public function format($text = '', $style = 'none') 157 | { 158 | return $this->getStartMark($style) . $text . $this->getClearMark(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/IO/Console.php: -------------------------------------------------------------------------------- 1 | write(call_user_func_array('sprintf', $args)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/IO/NullStty.php: -------------------------------------------------------------------------------- 1 | stty = $stty; 18 | } 19 | 20 | public static function isAvailable() 21 | { 22 | return extension_loaded('readline'); 23 | } 24 | 25 | public function readLine($prompt) 26 | { 27 | $line = readline($prompt); 28 | readline_add_history($line); 29 | return $line; 30 | } 31 | 32 | public function readPassword($prompt) 33 | { 34 | return $this->noEcho(function () use ($prompt) { 35 | return readline($prompt); 36 | }); 37 | } 38 | 39 | public function noEcho(\Closure $callback) 40 | { 41 | return $this->stty->withoutEcho($callback); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/IO/StandardConsole.php: -------------------------------------------------------------------------------- 1 | stty = $stty; 18 | } 19 | 20 | public function readLine($prompt) 21 | { 22 | echo $prompt; 23 | 24 | return $this->read(); 25 | } 26 | 27 | public function readPassword($prompt) 28 | { 29 | echo $prompt; 30 | 31 | return $this->noEcho(function () use ($prompt) { 32 | return $this->read(); 33 | }); 34 | } 35 | 36 | public function noEcho(\Closure $callback) 37 | { 38 | return $this->stty->withoutEcho($callback); 39 | } 40 | 41 | private function read() 42 | { 43 | return rtrim(fgets(STDIN), "\n"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/IO/StreamWriter.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 20 | } 21 | 22 | public function write($text) 23 | { 24 | fwrite($this->stream, $text); 25 | } 26 | 27 | public function writeln($text) 28 | { 29 | fwrite($this->stream, $text."\n"); 30 | } 31 | 32 | public function writef($format) 33 | { 34 | $args = func_get_args(); 35 | $this->write(call_user_func_array('sprintf', $args)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/IO/Stty.php: -------------------------------------------------------------------------------- 1 | dump(); 25 | // don't display characters from user input. 26 | $this->disableEcho(); 27 | $result = null; 28 | 29 | try { 30 | $result = $callback(); 31 | $this->restoreStyle($oldStyle); 32 | } catch (\Exception $e) { 33 | $this->restoreStyle($oldStyle); 34 | throw $e; 35 | } 36 | 37 | return $result; 38 | } 39 | 40 | private function restoreStyle($style) 41 | { 42 | if (is_null($style)) { 43 | return; 44 | } 45 | 46 | shell_exec('stty '.$style); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/IO/Writer.php: -------------------------------------------------------------------------------- 1 | possibleTokens as $word) { 13 | 14 | // calculate the distance between the input word, 15 | // and the current word 16 | $lev = levenshtein($input, $word); 17 | 18 | // check for an exact match 19 | if ($lev == 0) { 20 | 21 | // closest word is this one (exact match) 22 | $closest = $word; 23 | $shortest = 0; 24 | 25 | // break out of the loop; we've found an exact match 26 | break; 27 | } 28 | 29 | // if this distance is less than the next found shortest 30 | // distance, OR if a next shortest word has not yet been found 31 | if ($lev <= $shortest || $shortest < 0) { 32 | // set the closest match, and shortest distance 33 | $closest = $word; 34 | $shortest = $lev; 35 | } 36 | } 37 | return $closet; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Logger/ActionLogger.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 25 | $this->title = $title; 26 | $this->desc = $desc; 27 | $this->status = $status; 28 | 29 | $this->cursorControl = new CursorControl($this->logger->fd); 30 | $this->cursorControl->hide(); 31 | } 32 | 33 | public function setStatus($status, $style = 'green') 34 | { 35 | $this->status = $status; 36 | $this->update($style); 37 | } 38 | 39 | public function setActionColumnWidth($width) 40 | { 41 | $this->actionColumnWidth = $width; 42 | } 43 | 44 | protected function update($style = 'green') 45 | { 46 | $padding = max($this->actionColumnWidth - strlen($this->title), 1); 47 | $buf = sprintf(' %s % -20s', 48 | $this->logger->formatter->format(sprintf('%s', $this->title), $style).str_repeat(' ', $padding), 49 | $this->status 50 | ); 51 | fwrite($this->logger->fd, $buf."\r"); 52 | fflush($this->logger->fd); 53 | } 54 | 55 | public function finalize() 56 | { 57 | fwrite($this->logger->fd, "\n"); 58 | fflush($this->logger->fd); 59 | $this->cursorControl->show(); 60 | } 61 | 62 | public function done() 63 | { 64 | $this->setStatus('done'); 65 | $this->finalize(); 66 | } 67 | } 68 | 69 | class ActionLogger 70 | { 71 | public $fd; 72 | 73 | public $formatter; 74 | 75 | public function __construct($fd = null, $formatter = null) 76 | { 77 | $this->fd = $fd ?: fopen('php://stderr', 'w'); 78 | $this->formatter = $formatter ?: new Formatter(); 79 | } 80 | 81 | public function newAction($title, $desc = '', $status = 'waiting') 82 | { 83 | $logAction = new LogAction($this, $title, $desc); 84 | return $logAction; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Logger/Logger.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace CLIFramework; 12 | 13 | use GetOptionKit\OptionCollection; 14 | use GetOptionKit\Option; 15 | use GetOptionKit\OptionPrinter\OptionPrinter as OptionPrinterInterface; 16 | use CLIFramework\Formatter; 17 | 18 | class OptionPrinter implements OptionPrinterInterface 19 | { 20 | public $screenWidth = 78; 21 | 22 | public $formatter; 23 | 24 | public function __construct() 25 | { 26 | $this->formatter = new Formatter; 27 | } 28 | 29 | /** 30 | * Render readable spec 31 | */ 32 | public function renderOption(Option $opt) 33 | { 34 | $columns = array(); 35 | if ($opt->short) { 36 | $columns[] = $this->formatter->format(sprintf('-%s', $opt->short), 'strong_white') 37 | . $this->renderOptionValueHint($opt, false); 38 | } 39 | if ($opt->long) { 40 | $columns[] = $this->formatter->format(sprintf('--%s', $opt->long), 'strong_white') 41 | . $this->renderOptionValueHint($opt, true); 42 | } 43 | return join(', ', $columns); 44 | } 45 | 46 | public function renderOptionValueHint(Option $opt, $assign = true) 47 | { 48 | $n = 'value'; 49 | if ($opt->valueName) { 50 | $n = $opt->valueName; 51 | } elseif ($opt->isa) { 52 | $n = $opt->isa; 53 | } 54 | 55 | if ($opt->isRequired()) { 56 | return sprintf('%s<%s>', $assign ? '=' : ' ', $this->formatter->format($n, 'underline')); 57 | } elseif ($opt->isOptional()) { 58 | return sprintf('%s[<%s>]', $assign ? '=' : ' ', $this->formatter->format($n, 'underline')); 59 | } 60 | 61 | return ''; 62 | } 63 | 64 | /** 65 | * render option descriptions 66 | * 67 | * @return string output 68 | */ 69 | public function render(OptionCollection $options) 70 | { 71 | # echo "* Available options:\n"; 72 | $lines = array(); 73 | foreach ($options as $option) { 74 | $c1 = $this->renderOption($option); 75 | $lines[] = "\t" . $c1; 76 | $lines[] = wordwrap("\t\t" . $option->desc, $this->screenWidth, "\n\t\t"); # wrap text 77 | $lines[] = ""; 78 | } 79 | return join("\n", $lines); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/PharKit/PharGenerator.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 34 | $this->options = $options; 35 | $this->pharFile = $pharFile; 36 | 37 | if ($alias) { 38 | $this->alias = $alias; 39 | } else { 40 | $this->alias = basename($pharFile); 41 | } 42 | 43 | $this->phar = new Phar($this->pharFile, 0, $this->alias); 44 | $this->phar->setSignatureAlgorithm(Phar::SHA1); 45 | } 46 | 47 | public function shellbang($shellbang) 48 | { 49 | $this->shellbang = $shellbang; 50 | } 51 | 52 | public function getPhar() 53 | { 54 | return $this->phar; 55 | } 56 | 57 | public function generate() 58 | { 59 | // $this->phar->startBuffering(); 60 | 61 | 62 | // Finish building... 63 | $this->phar->stopBuffering(); 64 | 65 | $compressType = Phar::GZ; 66 | if ($this->options->{'no-compress'}) { 67 | $compressType = null; 68 | } elseif ($type = $this->options->compress) { 69 | switch ($type) { 70 | case 'gz': 71 | $compressType = Phar::GZ; 72 | break; 73 | case 'bz2': 74 | $compressType = Phar::BZ2; 75 | break; 76 | default: 77 | throw new Exception("Phar compression: $type is not supported, valid values are gz, bz2"); 78 | break; 79 | } 80 | } 81 | if ($compressType) { 82 | $this->logger->info("Compressing phar files..."); 83 | $this->phar->compressFiles($compressType); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/PharKit/PharURI.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 15 | $this->localPath = $localPath; 16 | } 17 | 18 | public function getLocalPath() 19 | { 20 | return $this->localPath; 21 | } 22 | 23 | public function getAlias() 24 | { 25 | return $this->alias; 26 | } 27 | 28 | 29 | /** 30 | * 'render' is a method of Renderable interface 31 | */ 32 | public function render(array $args = array()) 33 | { 34 | return $this->__toString(); 35 | } 36 | 37 | 38 | public function __toString() 39 | { 40 | // $stmt = new RequireStatement("phar://$pharFile/" . $localPath); 41 | return var_export('phar://' . $this->alias . '/' . $this->localPath, true); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Prompter.php: -------------------------------------------------------------------------------- 1 | formatter = $container['formatter']; 19 | $this->console = $container['console']; 20 | } 21 | 22 | /** 23 | * set prompt style 24 | */ 25 | public function setStyle($style) 26 | { 27 | return $this->style = $style; 28 | } 29 | 30 | /** 31 | * show prompt with message 32 | */ 33 | public function ask($prompt, $validAnswers = null, $default = null) 34 | { 35 | if ($validAnswers) { 36 | $prompt .= ' [' . join('/', $validAnswers) . ']'; 37 | } 38 | $prompt .= ' '; 39 | 40 | if ($this->style) { 41 | echo $this->formatter->getStartMark($this->style); 42 | // $prompt = $this->formatter->getStartMark( $this->style ) . $prompt . $this->formatter->getClearMark(); 43 | } 44 | 45 | $answer = null; 46 | while (1) { 47 | $answer = trim($this->console->readLine($prompt)); 48 | if ($validAnswers) { 49 | if (in_array($answer, $validAnswers)) { 50 | break; 51 | } else { 52 | if (trim($answer) === "" && $default) { 53 | $answer = $default; 54 | break; 55 | } 56 | continue; 57 | } 58 | } 59 | break; 60 | } 61 | if ($this->style) { 62 | echo $this->formatter->getClearMark(); 63 | } 64 | return $answer; 65 | } 66 | 67 | /** 68 | * Show password prompt with a message. 69 | */ 70 | public function password($prompt) 71 | { 72 | if ($this->style) { 73 | echo $this->formatter->getStartMark($this->style); 74 | } 75 | 76 | $result = $this->console->readPassword($prompt); 77 | 78 | if ($this->style) { 79 | echo $this->formatter->getClearMark(); 80 | } 81 | 82 | return $result; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ServiceContainer.php: -------------------------------------------------------------------------------- 1 | isWindows()) { 73 | // TODO support Windows 74 | return new NullStty(); 75 | } 76 | return new UnixStty(); 77 | }; 78 | $this['console'] = function ($c) { 79 | if (ReadlineConsole::isAvailable()) { 80 | return new ReadlineConsole($c['console.stty']); 81 | } 82 | return new StandardConsole($c['console.stty']); 83 | }; 84 | $this['command_loader'] = function ($c) { 85 | return CommandLoader::getInstance(); 86 | }; 87 | parent::__construct(); 88 | } 89 | 90 | public function isWindows() 91 | { 92 | return preg_match('/^Win/', PHP_OS); 93 | } 94 | 95 | public static function getInstance() 96 | { 97 | static $instance; 98 | 99 | if (!$instance) { 100 | $instance = new static; 101 | } 102 | 103 | return $instance; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Testing/CommandTestCase.php: -------------------------------------------------------------------------------- 1 | app; 17 | } 18 | 19 | protected function setUp(): void 20 | { 21 | if ($this->outputBufferingActive) { 22 | ob_start(); 23 | } 24 | $this->app = static::setupApplication(); 25 | } 26 | 27 | protected function tearDown(): void 28 | { 29 | $this->app = null; 30 | if ($this->outputBufferingActive) { 31 | ob_end_clean(); 32 | } 33 | } 34 | 35 | public function runCommand($args) 36 | { 37 | if (is_string($args)) { 38 | $args = Parser::getArguments($args); 39 | } 40 | return $this->app->run($args); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Testing/ConsoleTestCase.php: -------------------------------------------------------------------------------- 1 | array('pipe', 'r'), // stdin 10 | 1 => array('pipe', 'w'), // stdout 11 | 2 => array('pipe', 'w') // stderr 12 | ); 13 | 14 | $pipes = array(); 15 | 16 | 17 | // $_ENV is only populated if php.ini allows it, which it doesn't seem to 18 | // do by default, at least not in the default WAMP server installation. 19 | // 20 | // 21 | $php = PHP_BINARY; 22 | $command = "$php $path"; 23 | 24 | $process = proc_open($command, $descriptors, $pipes); 25 | // $process = proc_open($command, $descriptors, $pipes, NULL, [ 'PATH' => getenv('PATH') ]); 26 | // $process = proc_open($command, $descriptors, $pipes, NULL, $_ENV); 27 | // $process = proc_open($command, $descriptors, $pipes, NULL, [ 'PATH' => getenv('PATH') ]); 28 | 29 | 30 | if ($process === false) { 31 | throw new \RuntimeException("failed to proc_open '$command'"); 32 | } 33 | 34 | $this->assertTrue(is_resource($process), 'The returned value should be resource'); 35 | 36 | fwrite($pipes[0], $input); 37 | fflush($pipes[0]); 38 | fclose($pipes[0]); 39 | 40 | $content = stream_get_contents($pipes[1]); 41 | fclose($pipes[1]); 42 | fclose($pipes[2]); 43 | 44 | $callback($content); 45 | $code = proc_close($process); 46 | if ($code !== 0) { 47 | throw new \RuntimeException("proc_close failed '$command', code: $code"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Testing/Parser.php: -------------------------------------------------------------------------------- 1 | url; 15 | } 16 | public function getId() 17 | { 18 | return $this->id; 19 | } 20 | 21 | public function getTitle() 22 | { 23 | return $this->title; 24 | } 25 | public function getContent() 26 | { 27 | return ''; 28 | } 29 | public function getFooter() 30 | { 31 | if ($url = $this->getUrl()) { 32 | return "\tYou may view this topic at " . $url . "\n"; 33 | } 34 | return ''; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Topic/GitHubTopic.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace CLIFramework; 12 | 13 | use ReflectionClass; 14 | 15 | class Utils 16 | { 17 | 18 | /** 19 | * translate command name to class name 20 | * 21 | * so something like: to-list will be ToListCommand 22 | * 23 | * */ 24 | public static function translateCommandClassName($command) 25 | { 26 | $args = explode('-', $command); 27 | foreach ($args as & $a) { 28 | $a = ucfirst($a); 29 | } 30 | $subclass = join('', $args) . 'Command'; 31 | 32 | return $subclass; 33 | } 34 | 35 | public static function getClassPath($class, $baseDir = null) 36 | { 37 | $refclass = new ReflectionClass($class); 38 | $path = $refclass->getFilename(); 39 | if ($path && $baseDir) { 40 | return str_replace( 41 | rtrim(realpath($baseDir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, 42 | '', 43 | $path); 44 | } 45 | return $path; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ValueCollection.php: -------------------------------------------------------------------------------- 1 | group('id', 'ID', [ 'a', 'b', 'c' ]); 25 | * ->group('id', 'ID', [ 'label' => 'desc' ]); 26 | * 27 | */ 28 | public function group($groupId, $label, $values) 29 | { 30 | // for indexed array 31 | if (is_array($values)) { 32 | if (!isset($this->groups[ $groupId ])) { 33 | $this->groups[$groupId] = $values; 34 | } else { 35 | $this->groups[ $groupId ] = array_merge( 36 | $this->groups[ $groupId ], $values); 37 | } 38 | } else { 39 | $this->groups[ $groupId ][] = $values; 40 | } 41 | $this->setGroupLabel($groupId, $label); 42 | } 43 | 44 | 45 | 46 | public function getGroups() 47 | { 48 | return $this->groups; 49 | } 50 | 51 | 52 | public function setGroup($groupId, $values) 53 | { 54 | $this->groups[ $groupId ] = $values; 55 | } 56 | 57 | public function getGroup($groupId) 58 | { 59 | return $this->groups[ $groupId ]; 60 | } 61 | 62 | public function setGroupLabel($groupId, $label) 63 | { 64 | $this->labels[ $groupId ] = $label; 65 | } 66 | 67 | public function getGroupLabel($groupId) 68 | { 69 | if (isset($this->labels[ $groupId ])) { 70 | return $this->labels[ $groupId ]; 71 | } 72 | } 73 | 74 | public function containsValue($value) 75 | { 76 | foreach ($this->groups as $groupId => $values) { 77 | if (in_array($value, $values)) { 78 | return true; 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | public function getGroupLabels() 85 | { 86 | return $this->labels; 87 | } 88 | 89 | public function toJson() 90 | { 91 | return json_encode($this->groups); 92 | } 93 | 94 | public function getIterator() 95 | { 96 | return new ArrayIterator($this->groups); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/ValueGroup.php: -------------------------------------------------------------------------------- 1 | getArrayCopy()); 11 | } 12 | 13 | public function keys() 14 | { 15 | return array_keys($this->getArrayCopy()); 16 | } 17 | 18 | public function append($val) 19 | { 20 | parent::append($val); 21 | return $this; 22 | } 23 | 24 | 25 | public function add($val) 26 | { 27 | parent::append($val); 28 | return $this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/CLIFramework/Ansi/ColorsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(strlen($input), $len); 36 | } 37 | 38 | /** 39 | * @dataProvider stringProvider 40 | */ 41 | public function testStripAnsiEscapeCode($input, $fg, $bg) 42 | { 43 | $str = Colors::decorate($input, $fg, $bg); 44 | $output = Colors::stripAnsiEscapeCode($str); 45 | $this->assertEquals($input, $output); 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /tests/CLIFramework/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework; 12 | use TestApp\Application; 13 | use PHPUnit\Framework\TestCase; 14 | 15 | class ApplicationTest extends TestCase 16 | { 17 | public function testGlobalVars() 18 | { 19 | $app = new Application; 20 | $argv = explode(' ','app -v -d list foo arg1 arg2 arg3'); 21 | $ret = $app->run($argv); 22 | $this->assertTrue($ret); 23 | 24 | $logger = $app->getLogger(); 25 | 26 | global $_prepare; 27 | global $_execute; 28 | global $_finish; 29 | $this->assertNotNull( $_prepare ); 30 | $this->assertNotNull( $_execute ); 31 | $this->assertNotNull( $_finish ); 32 | } 33 | 34 | public function testPostOptionParsing() 35 | { 36 | $app = new Application; 37 | $argv = explode(' ','app -v -d test1 ARG1 ARG2 --as AS'); 38 | $ret = $app->run($argv); 39 | $this->assertTrue($ret); 40 | } 41 | 42 | public function testOptionParsing() 43 | { 44 | $app = new Application; 45 | $argv = explode(' ','app -v -d test1 --as AS ARG1 ARG2'); 46 | $ret = $app->run($argv); 47 | $this->assertTrue($ret); 48 | } 49 | 50 | public function testExtraArguments() 51 | { 52 | $app = new Application; 53 | $argv = explode(' ','app -v -d list extra --as AS ARG1 ARG2'); 54 | $ret = $app->run($argv); 55 | $this->assertTrue( $ret ); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tests/CLIFramework/ArgumentEditor/ArgumentEditorTest.php: -------------------------------------------------------------------------------- 1 | append('--enable-zip'); 11 | $this->assertEquals("./configure --enable-debug --enable-zip", $editor->__toString() ); 12 | 13 | 14 | $editor->remove('--enable-zip'); 15 | $this->assertEquals("./configure --enable-debug", $editor->__toString() ); 16 | 17 | $editor->append('--with-sqlite','--with-postgres'); 18 | $this->assertEquals("./configure --enable-debug --with-sqlite --with-postgres", $editor->__toString() ); 19 | } 20 | 21 | public function testRemoveRegExp() { 22 | $editor = new ArgumentEditor(array('./configure','--enable-debug')); 23 | $editor->append('--enable-zip'); 24 | $editor->removeRegExp('--enable'); 25 | } 26 | 27 | public function testReplaceRegExp() { 28 | $editor = new ArgumentEditor(array('./configure','--enable-debug','--enable-zip')); 29 | $editor->replaceRegExp('--enable', '--with'); 30 | $this->assertEquals("./configure --with-debug --with-zip", $editor->__toString() ); 31 | } 32 | 33 | public function testFilter() { 34 | $editor = new ArgumentEditor(array('./configure','--enable-debug','--enable-zip')); 35 | $editor->filter(function($arg) { 36 | return escapeshellarg($arg); 37 | }); 38 | $this->assertEquals("'./configure' '--enable-debug' '--enable-zip'", $editor->__toString() ); 39 | } 40 | 41 | 42 | 43 | public function testReplace() { 44 | $editor = new ArgumentEditor(array('./configure','--enable-debug')); 45 | $old = $editor->replace('--enable-debug','--enable-foo'); 46 | $this->assertEquals('--enable-debug', $old); 47 | $this->assertEquals("./configure --enable-foo", $editor->__toString() ); 48 | } 49 | 50 | public function testEscape() { 51 | $editor = new ArgumentEditor(array('./configure','--enable-debug')); 52 | $editor->escape(); 53 | $this->assertEquals("'./configure' '--enable-debug'", $editor->__toString() ); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /tests/CLIFramework/ArgumentInfoListTest.php: -------------------------------------------------------------------------------- 1 | add('x'); 13 | $this->assertInstanceOf('CLIFramework\ArgInfo', $a1); 14 | $this->assertEquals('x' , $a1->name ); 15 | 16 | $a2 = $arguments->add('y'); 17 | $this->assertInstanceOf('CLIFramework\ArgInfo', $a2); 18 | $this->assertEquals('y' , $a2->name ); 19 | 20 | $this->assertNotNull( $arguments[0] ); 21 | $this->assertNotNull( $arguments[1] ); 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /tests/CLIFramework/ArgumentInfoTest.php: -------------------------------------------------------------------------------- 1 | isa('number'); 12 | $this->assertTrue($info->validate('123')); 13 | 14 | $this->assertFalse($info->validate('foo')); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/CLIFramework/Autoload/ComposerAutoloadGeneratorTest.php: -------------------------------------------------------------------------------- 1 | setQuiet(); 13 | $workingDir = new SplFileInfo(getcwd()); 14 | $vendorDirName = 'vendor'; 15 | $autoloadGenerator = new ComposerAutoloadGenerator($logger); 16 | $autoloadGenerator->setVendorDir('vendor'); 17 | $autoloadGenerator->setWorkingDir($workingDir->getPathname()); 18 | $autoloadGenerator->scanComposerJsonFiles($workingDir . DIRECTORY_SEPARATOR . $vendorDirName); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /tests/CLIFramework/BufferTest.php: -------------------------------------------------------------------------------- 1 | appendLine('foo'); 12 | $buf->appendLine('bar'); 13 | 14 | $buf->indent(); 15 | $buf->appendLine('inner bar'); 16 | $buf->unindent(); 17 | $buf->unindent(); 18 | $this->assertEquals(0, $buf->indent); 19 | 20 | $this->assertNotNull($buf->__toString()); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /tests/CLIFramework/Command/ZshCompletionCommandTest.php.bak: -------------------------------------------------------------------------------- 1 | add('a|all','Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.'); 9 | 10 | $opts->add('p|patch','Use the interactive patch selection interface to chose which changes to commit. See git-add(1) for details.'); 11 | 12 | $opts->add('C|reuse-message:','Take an existing commit object, and reuse the log message and the authorship information (including the timestamp) when creating the commit.') 13 | ->isa('string') 14 | ->validValues(array( '50768ab', 'c2efdc2', 'ed5ba6a', 'cf0b1eb')) 15 | ; 16 | 17 | $opts->add('c|reedit-message:','like -C, but with -c the editor is invoked, so that the user can further edit the commit message.') 18 | ->isa('string') 19 | ->validValues(array( '50768ab', 'c2efdc2', 'ed5ba6a', 'cf0b1eb')) 20 | ; 21 | 22 | $opts->add('author:', 'Override the commit author. Specify an explicit author using the standard A U Thor format.') 23 | ->suggestions(array( 'c9s', 'foo' , 'bar' )) 24 | ->valueName('author name') 25 | ; 26 | 27 | $opts->add('output:', 'Output file') 28 | ->isa('file') 29 | ; 30 | } 31 | 32 | public function arguments($args) { 33 | $args->add('user') 34 | ->validValues(array('c9s','bar','foo')) 35 | ; 36 | 37 | $args->add('repo') 38 | ->validValues(array('CLIFramework','GetOptionKit')) 39 | ; 40 | 41 | $args->add('file') 42 | ->isa('file') 43 | ->glob('*.php') 44 | ; 45 | } 46 | 47 | public function execute($user,$repo) { 48 | $this->getLogger()->notice('executing bar command.'); 49 | } 50 | } 51 | class FooCommand extends CLIFramework\Command { 52 | 53 | public function brief() { return 'brief of foo'; } 54 | 55 | public function init() { 56 | $this->addCommand('subfoo','SubFooCommand'); 57 | } 58 | 59 | public function execute() { 60 | $this->getLogger()->info('executing foo command.'); 61 | } 62 | } 63 | 64 | class SubFooCommand extends CLIFramework\Command { 65 | 66 | public function brief() { return 'brief of subfoo'; } 67 | 68 | public function options($opts) { 69 | $opts->add('x', 'x desc'); 70 | $opts->add('y', 'y desc'); 71 | $opts->add('z', 'z desc'); 72 | } 73 | 74 | public function arguments($args) { 75 | $args->add('p1'); 76 | $args->add('p2'); 77 | } 78 | 79 | public function execute() { 80 | $this->getLogger()->info('executing subfoo command.'); 81 | } 82 | } 83 | 84 | class ExampleApplication extends CLIFramework\Application { 85 | 86 | public function init() 87 | { 88 | parent::init(); 89 | $this->command('foo','FooCommand'); 90 | $this->command('commit','CommitCommand'); 91 | } 92 | } 93 | 94 | 95 | class ZshCompletionCommandTest extends TestCase 96 | { 97 | 98 | public function test() 99 | { 100 | $this->expectOutputRegex('/compdef _demo demo/'); 101 | $app = ExampleApplication::getInstance(); 102 | $app->run(array('demo','zsh', '--program' , 'demo', '--bind' , 'demo')); 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /tests/CLIFramework/CommandLoaderTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | use PHPUnit\Framework\TestCase; 12 | 13 | 14 | class CommandLoaderTest extends TestCase 15 | { 16 | public function test() 17 | { 18 | $command = new TestApp\Command\SimpleCommand(new TestApp\Application); 19 | $text = $command->getFormattedHelpText(); 20 | 21 | // tODO: use string format assertion API to verify this 22 | $this->assertNotNull($text); 23 | 24 | $return = $command->execute(123); 25 | $this->assertNotNull( $return ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/CLIFramework/CommandTest.php: -------------------------------------------------------------------------------- 1 | command = new CommandTestCommand(); 15 | } 16 | 17 | public function testHasApplicationWhenApplicationIsNotSet() 18 | { 19 | $this->assertFalse($this->command->hasApplication()); 20 | } 21 | 22 | public function testHasApplicationWhenApplicationIsSet() 23 | { 24 | $this->command->setApplication(new Application); 25 | $this->assertTrue($this->command->hasApplication()); 26 | } 27 | } 28 | 29 | class CommandTestCommand extends Command 30 | { 31 | public function execute() 32 | { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/CLIFramework/CompletionUtilsTest.php: -------------------------------------------------------------------------------- 1 | assertCount(3, $words); 12 | } 13 | 14 | public function testPaths() { 15 | $paths = CompletionUtils::paths("src"); 16 | $this->assertTrue(is_array($paths)); 17 | } 18 | 19 | public function testClassnames() { 20 | $classes = CompletionUtils::classnames(); 21 | $this->assertTrue( is_array($classes) ); 22 | 23 | $classes = CompletionUtils::classnames('/CLI/'); 24 | $this->assertTrue( is_array($classes) ); 25 | } 26 | 27 | 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /tests/CLIFramework/Component/Progress/ETACalculatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(is_double($seconds)); 13 | } 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /tests/CLIFramework/Component/Table/DateFormatCellTest.php: -------------------------------------------------------------------------------- 1 | format(0); 12 | 13 | // older icu does not output "at" 14 | $this->assertRegExp('/Wednesday, December \d+, \d+( at)? \d:00:00 PM Pacific Standard Time/', $str); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tests/CLIFramework/Config/GlobalConfigTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework\Config; 12 | 13 | use CLIFramework\Config\GlobalConfig; 14 | 15 | class GlobalConfigTest extends \PHPUnit\Framework\TestCase 16 | { 17 | /** 18 | * @test 19 | */ 20 | public function testDefaultValues() 21 | { 22 | $config = new GlobalConfig(array()); 23 | $this->assertFalse($config->isVerbose()); 24 | $this->assertFalse($config->isDebug()); 25 | } 26 | 27 | /** 28 | * @test 29 | * @dataProvider provideSampleConfig 30 | */ 31 | public function testIsVerbose($sampleConfig) 32 | { 33 | $config = new GlobalConfig($sampleConfig); 34 | $this->assertTrue($config->isVerbose()); 35 | } 36 | 37 | /** 38 | * @test 39 | * @dataProvider provideSampleConfig 40 | */ 41 | public function testIsDebug($sampleConfig) 42 | { 43 | $config = new GlobalConfig($sampleConfig); 44 | $this->assertTrue($config->isDebug()); 45 | } 46 | 47 | /** 48 | * @test 49 | * @dataProvider provideSampleConfig 50 | */ 51 | public function testGetPidDirectory($sampleConfig) 52 | { 53 | $config = new GlobalConfig($sampleConfig); 54 | $this->assertSame('/var/run', $config->getPidDirectory()); 55 | } 56 | 57 | public function provideSampleConfig() 58 | { 59 | return array( 60 | array(parse_ini_file(__DIR__ . '/../../data/sample.ini', true)) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/CLIFramework/ConsoleInfo/EnvConsoleInfoTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('env console info is not supported.'); 12 | } 13 | $info = new EnvConsoleInfo; 14 | $this->assertNotNull($info->getColumns()); 15 | $this->assertNotNull($info->getRows()); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /tests/CLIFramework/ConsoleInfo/TputConsoleInfoTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('tput is not supported.'); 12 | } 13 | $info = new TputConsoleInfo; 14 | $this->assertNotNull($info->getColumns()); 15 | $this->assertNotNull($info->getRows()); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /tests/CLIFramework/Extension/DaemonExtensionTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework\Extension; 12 | use CLIFramework\Extension\DaemonExtension; 13 | use CLIFramework\Command; 14 | use CLIFramework\Application; 15 | use CLIFramework\ServiceContainer; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class DaemonExtensionTest extends TestCase 19 | { 20 | private $extension; 21 | 22 | private $command; 23 | 24 | protected function setUp(): void 25 | { 26 | $extension = new DaemonExtensionForTest; 27 | if (!$extension->isAvailable()) { 28 | $this->markTestSkipped('DaemonExtension is not available.'); 29 | } 30 | 31 | $this->command = new DaemonExtensionTestCommand(); 32 | 33 | // Setup a new application 34 | $this->command->setApplication(new Application()); 35 | $this->command->init(); 36 | } 37 | 38 | public function testRun() 39 | { 40 | $this->command->executeWrapper(array()); 41 | } 42 | 43 | protected function tearDown(): void 44 | { 45 | } 46 | } 47 | 48 | class DaemonExtensionForTest extends DaemonExtension 49 | { 50 | protected $detach = false; 51 | } 52 | 53 | class DaemonExtensionTestCommand extends Command 54 | { 55 | public function init() 56 | { 57 | $this->extension(new DaemonExtensionForTest); 58 | } 59 | 60 | public function execute() 61 | { 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/CLIFramework/IO/EchoWriterTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework\IO; 12 | 13 | use CLIFramework\IO\EchoWriter; 14 | 15 | class EchoWriterTest extends \PHPUnit\Framework\TestCase 16 | { 17 | private $writer; 18 | 19 | protected function setUp(): void 20 | { 21 | $this->writer = new EchoWriter(); 22 | } 23 | 24 | function testWrite() 25 | { 26 | $this->expectOutputString("test"); 27 | $this->writer->write("test"); 28 | } 29 | 30 | function testWriteln() 31 | { 32 | $this->writer->writeln("test"); 33 | $this->expectOutputString("test\n"); 34 | } 35 | 36 | function testWritef() 37 | { 38 | $this->writer->writef("%s:%s", "test", "writef"); 39 | $this->expectOutputString("test:writef"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/CLIFramework/IO/NullSttyTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework\IO; 12 | 13 | use CLIFramework\IO\NullStty; 14 | 15 | class NullSttyTest extends \PHPUnit\Framework\TestCase 16 | { 17 | private $stty; 18 | 19 | protected function setUp(): void 20 | { 21 | $this->stty = new NullStty(); 22 | } 23 | 24 | function testEnableEcho() 25 | { 26 | $this->stty->enableEcho(); 27 | } 28 | 29 | function testDisableEcho() 30 | { 31 | $this->stty->disableEcho(); 32 | } 33 | 34 | function testDump() 35 | { 36 | $this->assertSame('', $this->stty->dump()); 37 | } 38 | 39 | function testWithoutEcho() 40 | { 41 | $this->assertSame('echo', $this->stty->withoutEcho(function() { 42 | return 'echo'; 43 | })); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /tests/CLIFramework/IO/ReadlineConsoleTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework\IO; 12 | 13 | use CLIFramework\IO\ReadlineConsole; 14 | use CLIFramework\Testing\ConsoleTestCase; 15 | 16 | class ReadlineConsoleTest extends ConsoleTestCase 17 | { 18 | public function testReadLine() 19 | { 20 | $this->markTestSkipped('there is a bug in the php7 readline extension '); 21 | 22 | 23 | if (!ReadlineConsole::isAvailable()) { 24 | $this->markTestSkipped('readline is not available.'); 25 | } 26 | 27 | $script = __DIR__ . '/../../script/CLIFramework/IO/ReadlineConsoleReadLine.php'; 28 | $self = $this; 29 | $this->runScript($script, "foo\n", function($line) use($self) { 30 | $self->assertEquals("foo", $line); 31 | }); 32 | } 33 | 34 | function testReadPassword() 35 | { 36 | $this->markTestSkipped('there is a bug in the php7 readline extension '); 37 | 38 | if (!ReadlineConsole::isAvailable()) { 39 | $this->markTestSkipped('readline is not available.'); 40 | } 41 | 42 | $script = __DIR__ . '/../../script/CLIFramework/IO/ReadlineConsoleReadPassword.php'; 43 | $self = $this; 44 | $this->runScript($script, "test\n", function($line) use($self) { 45 | $self->assertEquals("test", $line); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/CLIFramework/IO/StandardConsoleTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework\IO; 12 | 13 | use CLIFramework\IO\StandardConsole; 14 | use CLIFramework\Testing\ConsoleTestCase; 15 | 16 | class StandardConsoleTest extends ConsoleTestCase 17 | { 18 | public function testReadLine() 19 | { 20 | if (!extension_loaded('readline')) { 21 | $this->markTestSkipped("readline is required."); 22 | } 23 | $script = __DIR__ . '/../../script/CLIFramework/IO/StandardConsoleReadLine.php'; 24 | $self = $this; 25 | $this->runScript($script, "test\n", function($line) use($self) { 26 | $self->assertSame('test', $line); 27 | }); 28 | } 29 | 30 | public function testReadPassword() 31 | { 32 | if (!extension_loaded('readline')) { 33 | $this->markTestSkipped("readline is required."); 34 | } 35 | $script = __DIR__ . '/../../script/CLIFramework/IO/StandardConsoleReadPassword.php'; 36 | $self = $this; 37 | $this->runScript($script, "test\n", function($line) use($self) { 38 | $self->assertSame('test', $line); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/CLIFramework/IO/StreamWriterTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace tests\CLIFramework\IO; 12 | 13 | use CLIFramework\IO\StreamWriter; 14 | 15 | class StreamWriterTest extends \PHPUnit\Framework\TestCase 16 | { 17 | private $writer; 18 | private $stream; 19 | 20 | protected function setUp(): void 21 | { 22 | $this->stream = fopen('php://memory', 'rw'); 23 | $this->writer = new StreamWriter($this->stream); 24 | } 25 | 26 | protected function tearDown(): void 27 | { 28 | fclose($this->stream); 29 | } 30 | 31 | function testWrite() 32 | { 33 | $this->writer->write("test"); 34 | $this->assertStreamSame("test"); 35 | } 36 | 37 | function testWriteln() 38 | { 39 | $this->writer->writeln("test"); 40 | $this->assertStreamSame("test\n"); 41 | } 42 | 43 | function testWritef() 44 | { 45 | $this->writer->writef("%s:%s", "test", "writef"); 46 | $this->assertStreamSame("test:writef"); 47 | } 48 | 49 | function assertStreamSame($expected) 50 | { 51 | fseek($this->stream, 0); 52 | $this->assertSame($expected, stream_get_contents($this->stream)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/CLIFramework/LoggerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | */ 12 | use PHPUnit\Framework\TestCase; 13 | use CLIFramework\IO\EchoWriter; 14 | 15 | class LoggerTest extends TestCase 16 | { 17 | private $logger; 18 | 19 | protected function setUp(): void 20 | { 21 | $this->logger = new \CLIFramework\Logger; 22 | } 23 | 24 | function testRawOutput() 25 | { 26 | $this->logger->getFormatter()->preferRawOutput(); 27 | $this->logger->info('test'); 28 | $this->logger->debug('test'); 29 | 30 | $this->expectOutputString("test\n"); 31 | } 32 | 33 | 34 | function testLogException() 35 | { 36 | $this->logger->logException(new \Exception('exception')); 37 | $this->expectOutputString("exception\n"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/CLIFramework/TestAppCommandTest.php: -------------------------------------------------------------------------------- 1 | init(); 12 | 13 | $argInfos = $command->getArgInfoList(); 14 | $this->assertNotEmpty($argInfos); 15 | $this->assertCount(1, $argInfos); 16 | $this->assertEquals('var', $argInfos[0]->name); 17 | } 18 | 19 | public function testArginfoCommand() { 20 | $cmd = new TestApp\Command\ArginfoCommand(new Application); 21 | $cmd->init(); 22 | 23 | $argInfos = $cmd->getArgInfoList(); 24 | $this->assertNotEmpty($argInfos); 25 | $this->assertCount(3, $argInfos); 26 | 27 | foreach( $argInfos as $arginfo ) { 28 | $this->assertInstanceOf('CLIFramework\ArgInfo', $arginfo); 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /tests/CLIFramework/Testing/ParserTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expect, $result); 15 | } 16 | 17 | public function testGetArguments_TwoArguments() 18 | { 19 | $command = "program arg1 arg2"; 20 | $expect = array("program", "arg1", "arg2"); 21 | 22 | $result = Parser::getArguments($command); 23 | $this->assertEquals($expect, $result); 24 | } 25 | 26 | public function testGetArguments_ArgumentWithSpaces() 27 | { 28 | $command = "program arg1 \"arg2.1 arg2.2\" arg3"; 29 | $expect = array("program", "arg1", "arg2.1 arg2.2", "arg3"); 30 | 31 | $result = Parser::getArguments($command); 32 | $this->assertEquals($expect, $result); 33 | } 34 | } 35 | ?> 36 | -------------------------------------------------------------------------------- /tests/CLIFramework/UtilsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('vendor/universal/universal/src/ClassLoader/ClassLoader.php', $path); 12 | $this->assertFileExists($path); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /tests/CLIFramework/ValueCollectionTest.php: -------------------------------------------------------------------------------- 1 | group('extension-commands', 'Extension Commands', array( 'install', 'enable', 'disable' )); 11 | $groups->group('version-related', 'Version Related Commands', array('use', 'switch', 'off' )); 12 | 13 | foreach( $groups as $groupId => $values) { 14 | $this->assertNotNull($values); 15 | } 16 | 17 | $values = $groups->getGroup('extension-commands'); 18 | $this->assertNotEmpty($values); 19 | $this->assertTrue(is_array($values)); 20 | 21 | $this->assertTrue($groups->containsValue('disable')); 22 | 23 | $this->assertFalse($groups->containsValue('foobar')); 24 | $json = $groups->toJson(); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /tests/CLIFramework/ValueGroupTest.php: -------------------------------------------------------------------------------- 1 | add('aaa') 12 | ->add('bbb') 13 | ->add('bar') 14 | ->add('zoo'); 15 | 16 | $keys = $group->keys(); 17 | $this->assertNotEmpty($keys); 18 | $this->assertCount(4, $group); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /tests/DemoApp/Application.php: -------------------------------------------------------------------------------- 1 | command('foo'); 13 | $this->command('add'); 14 | $this->command('commit'); 15 | $this->command('server'); 16 | $this->topic('basic'); 17 | } 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/DemoApp/Command/AddCommand.php: -------------------------------------------------------------------------------- 1 | add('f|force', 'Allow adding otherwise ignored files.'); 12 | $opts->add('i|interactive', 'Add modified contents in the working tree interactively to the index.' 13 | . 'Optional path arguments may be supplied to limit operation to a subset of the working tree. See "Interactive mode" for details.'); 14 | } 15 | 16 | public function arguments($args) 17 | { 18 | # XXX: Add a DSL here to support zsh/bash function completion 19 | $args->add('file'); 20 | } 21 | 22 | 23 | public function execute() { 24 | $this->getLogger()->info('executing add command.'); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /tests/DemoApp/Command/CommitCommand.php: -------------------------------------------------------------------------------- 1 | add('a|all','Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.'); 13 | 14 | $opts->add('p|patch','Use the interactive patch selection interface to chose which changes to commit. See git-add(1) for details.'); 15 | 16 | $opts->add('C|reuse-message:','Take an existing commit object, and reuse the log message and the authorship information (including the timestamp) when creating the commit.') 17 | ->isa('string') 18 | ->valueName('commit hash') 19 | // ->validValues([ 'static-50768ab', 'static-c2efdc2', 'static-ed5ba6a', 'static-cf0b1eb']) 20 | ->validValues(function() { 21 | $output = array(); 22 | exec("git rev-list --abbrev-commit HEAD -n 20", $output); 23 | return $output; 24 | }) 25 | ; 26 | 27 | $opts->add('c|reedit-message:','like -C, but with -c the editor is invoked, so that the user can further edit the commit message.') 28 | ->isa('string') 29 | ->valueName('commit hash') 30 | ->validValues(function() { 31 | // exec("git log -n 10 --pretty=format:%H:%s", $output); 32 | exec("git log -n 10 --pretty=format:%H:%s", $output); 33 | return array_map(function($line) { 34 | list($key,$val) = explode(':',$line); 35 | $val = preg_replace('/\W/',' ', $val); 36 | return array($key, $val); 37 | }, $output); 38 | }) 39 | ; 40 | 41 | $opts->add('author:', 'Override the commit author. Specify an explicit author using the standard A U Thor format.') 42 | ->suggestions(array( 'c9s', 'foo' , 'bar' )) 43 | ->valueName('author name') 44 | ; 45 | 46 | $opts->add('output:', 'Output file') 47 | ->isa('file') 48 | ; 49 | } 50 | 51 | public function arguments($args) { 52 | $args->add('user') 53 | ->validValues(function() { 54 | $values = new ValueCollection; 55 | $values->group('authors', 'Authors', array( 56 | 'abba' => 'ABBA', 57 | 'michael' => 'Michael Jackson', 58 | 'adele' => 'Adele', 59 | 'air' => 'Air', 60 | 'alicia' => 'Alicia Keys', 61 | 'andras' => 'Andras Schiff', 62 | )); 63 | $values->group('admins', 'Administrators', array( 'admin1', 'admin2' , 'admin3' )); 64 | $values->group('users', 'Users', array( 'userA', 'userB' , 'userC' )); 65 | $values->group('extension', 'PHP Extensions', get_loaded_extensions()); 66 | 67 | $funcs = get_defined_functions(); 68 | $values->group('internal-functions', 'PHP Internal Functions', $funcs['internal']); 69 | $values->group('user-functions', 'PHP User-defined Functions', $funcs['user']); 70 | return $values; 71 | }) 72 | ; 73 | 74 | $args->add('repo') 75 | ->validValues(array('CLIFramework','GetOptionKit', 'PHPBrew', 'AssetKit', 'ActionKit')) 76 | ; 77 | 78 | $args->add('file') 79 | ->isa('file') 80 | ->glob('*.php') 81 | ->multiple() 82 | ; 83 | } 84 | 85 | public function execute($user,$repo) { 86 | $this->logger->notice('executing bar command.'); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /tests/DemoApp/Command/FooCommand.php: -------------------------------------------------------------------------------- 1 | command('subfoo','DemoApp\\Command\\FooCommand\\SubFooCommand'); 10 | } 11 | 12 | public function execute() { 13 | $this->getLogger()->info('executing foo command.'); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /tests/DemoApp/Command/FooCommand/SubFooCommand.php: -------------------------------------------------------------------------------- 1 | add('x', 'x desc'); 10 | $opts->add('y', 'y desc'); 11 | $opts->add('z', 'z desc'); 12 | } 13 | 14 | public function arguments($args) { 15 | $args->add('p1'); 16 | $args->add('p2'); 17 | } 18 | 19 | public function execute() { 20 | $this->logger->info('executing subfoo command.'); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /tests/DemoApp/Command/ServerCommand.php: -------------------------------------------------------------------------------- 1 | addExtension(new DaemonExtension); 20 | } 21 | } 22 | 23 | public function execute($host, $port) 24 | { 25 | 26 | $server = stream_socket_server("tcp://$host:$port", $errno, $errorMessage); 27 | 28 | if ($server === false) { 29 | throw new \RuntimeException("Could not bind to socket: $errorMessage"); 30 | } 31 | 32 | for (;;) { 33 | $socket = @stream_socket_accept($server); 34 | 35 | if ($socket) { 36 | $text = fread($socket, 1024); 37 | $this->getLogger()->writeln($text); 38 | fclose($socket); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/DemoApp/GitHubBuildTopicCommandTest.php: -------------------------------------------------------------------------------- 1 | cleanUp("tmp"); 22 | $this->cleanUp($outputDir); 23 | 24 | $this->expectOutputRegex("!Creating .*?/.*?/Topic/ContributionTopic.php!xs"); 25 | $this->runCommand("example/demo github:build-topics --ns PHPBrew:Topic --dir $outputDir phpbrew phpbrew"); 26 | $this->cleanUp($outputDir); 27 | } 28 | 29 | public function cleanUp($path) { 30 | if (!file_exists($path)) { 31 | return; 32 | } 33 | $directoryIterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); 34 | $iterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::CHILD_FIRST); 35 | foreach($iterator as $file) { 36 | if (is_file($file)) { 37 | unlink($file); 38 | } else if (is_dir($file)) { 39 | rmdir($file); 40 | } 41 | } 42 | } 43 | } 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/DemoApp/HelpCommandTest.php: -------------------------------------------------------------------------------- 1 | expectOutputRegex("/A simple demo command/"); 14 | $this->assertTrue( $this->runCommand('example/demo help') ); 15 | } 16 | 17 | public function testHelpTopicCommand() { 18 | $this->expectOutputRegex("/A bare repository is normally an appropriately/"); 19 | $this->assertTrue( $this->runCommand('example/demo help basic') ); 20 | } 21 | 22 | } 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/DemoApp/MetaCommandTest.php: -------------------------------------------------------------------------------- 1 | setQuiet(); 15 | $app = new \DemoApp\Application($service); 16 | return $app; 17 | } 18 | 19 | public function testMetaArgValidValuesGroups() 20 | { 21 | $this->expectOutputRegex("/#groups/"); 22 | $this->runCommand('example/demo meta --zsh commit arg 0 valid-values'); 23 | } 24 | 25 | public function testMetaArgSimpleValidValues() 26 | { 27 | $this->expectOutputString("#values 28 | CLIFramework 29 | GetOptionKit 30 | PHPBrew 31 | AssetKit 32 | ActionKit 33 | "); 34 | $this->assertTrue( $this->runCommand('example/demo meta --zsh commit arg 1 valid-values')); 35 | } 36 | 37 | public function testOptValidValues() { 38 | ob_start(); 39 | $this->assertTrue( $this->runCommand('example/demo meta --zsh commit opt reuse-message valid-values')); 40 | $output = ob_get_contents(); 41 | ob_end_clean(); 42 | $lines = explode("\n",trim($output)); 43 | 44 | $this->assertEquals('#values',$lines[0]); 45 | array_shift($lines); 46 | foreach($lines as $line) { 47 | $this->assertRegExp('/^\w{7}$/', $line); 48 | } 49 | } 50 | 51 | public function testGenerateZshCompletion() { 52 | $this->expectOutputRegex("!compdef _demo demo!"); 53 | $this->assertTrue( $this->runCommand('example/demo zsh --program demo --bind demo') ); 54 | } 55 | 56 | public function testCommandNotFound() 57 | { 58 | $this->expectException(CommandNotFoundException::class); 59 | $this->runCommand('example/demo --no-interact zzz'); 60 | } 61 | 62 | public function testArgument() 63 | { 64 | $this->expectException(CommandArgumentNotEnoughException::class); 65 | $this->runCommand('example/demo commit'); 66 | } 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /tests/DemoApp/Topic/BasicTopic.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace TestApp; 12 | use CLIFramework\Application as CLIApplication; 13 | 14 | class Application extends CLIApplication 15 | { 16 | 17 | public function options($getopt) 18 | { 19 | $getopt->add('c|color','Color message'); 20 | parent::options($getopt); 21 | } 22 | 23 | public function init() 24 | { 25 | parent::init(); 26 | // $this->addCommand('list'); 27 | // $this->addCommand('test1'); 28 | $this->CommandGroup('Daily Basic', array('list', 'test1')); 29 | $this->topic('list'); 30 | $this->topics(array('setup','install')); 31 | } 32 | 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/TestApp/Command/ArginfoCommand.php: -------------------------------------------------------------------------------- 1 | add('name'); 11 | $args->add('email'); 12 | $args->add('phone')->optional(); 13 | } 14 | 15 | public function execute($name, $email, $phone = null) { } 16 | } 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/TestApp/Command/ListCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace TestApp\Command; 12 | 13 | use CLIFramework\Command; 14 | 15 | class ListCommand extends Command 16 | { 17 | 18 | public function brief() { 19 | return 'brief message'; 20 | } 21 | 22 | public function usage() { 23 | return 'app list [arguments]'; 24 | } 25 | 26 | public function init() { 27 | $this->command('foo'); 28 | $this->command('extra', 'TestApp\Command\ListCommand\ExtraArgumentTestCommand'); 29 | } 30 | 31 | public function execute() { 32 | 33 | } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /tests/TestApp/Command/ListCommand/ExtraArgumentTestCommand.php: -------------------------------------------------------------------------------- 1 | add('as:','required a value'); 11 | } 12 | 13 | function execute() 14 | { 15 | if( null === $this->options->as ) 16 | throw new Exception( '--as option is required.' ); 17 | $args = func_get_args(); 18 | if( empty($args) ) 19 | throw new Exception( 'command argument is required' ); 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/TestApp/Command/ListCommand/FooCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | namespace TestApp\Command\ListCommand; 12 | use CLIFramework\Command; 13 | 14 | /** Test process stage **/ 15 | class FooCommand extends Command 16 | { 17 | 18 | function prepare() 19 | { 20 | global $_prepare; 21 | $_prepare = 1; 22 | } 23 | 24 | function execute() 25 | { 26 | global $_execute; 27 | $_execute = 1; 28 | } 29 | 30 | function finish() 31 | { 32 | global $_finish; 33 | $_finish = 1; 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/TestApp/Command/SimpleCommand.php: -------------------------------------------------------------------------------- 1 | Info Style 13 | 14 | Bold Text 15 | Bold Text 16 | HELP; 17 | } 18 | 19 | public function arguments($args) 20 | { 21 | $args->add('var'); 22 | } 23 | 24 | public function execute($var) 25 | { 26 | return $var; 27 | } 28 | } 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/TestApp/Command/Test1Command.php: -------------------------------------------------------------------------------- 1 | add('as:', 'as name'); 12 | } 13 | 14 | public function execute() 15 | { 16 | $args = func_get_args(); 17 | if (empty($args)) { 18 | throw new Exception('empty args'); 19 | } 20 | if (! $this->options->as) { 21 | throw new Exception('The value of option --as is empty.'); 22 | } 23 | return $this->options->as; 24 | } 25 | 26 | 27 | } 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/TestApp/Topic/FaqTopic.php: -------------------------------------------------------------------------------- 1 | add('TestApp','tests'); 4 | $loader->add('DemoApp','tests'); 5 | $container = \CLIFramework\ServiceContainer::getInstance(); 6 | // $container['logger']->setQuiet(); 7 | -------------------------------------------------------------------------------- /tests/data/default-table-2.txt: -------------------------------------------------------------------------------- 1 | +--------------------+--------------------------------+--------------------------------+ 2 | | September 16, 2014 | Zero to One: Notes on Startu.. | If you want to build a bette.. | 3 | +--------------------+--------------------------------+--------------------------------+ 4 | -------------------------------------------------------------------------------- /tests/data/default-table-cell-attribute.txt: -------------------------------------------------------------------------------- 1 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 2 | | September 16, 2014 | Zero to One: Notes on Startups, or How to Build | If you want to build a better future, you must | 3 | | | the Future | believe in secrets. | 4 | | | | The great secret of our time is that | 5 | | | | there are still uncharted frontiers to explore and | 6 | | | | new inventions to create. In Zero to One, | 7 | | | | legendary entrepreneur and investor Peter Thiel | 8 | | | | shows how we can find singular ways to create | 9 | | | | those new things. | 10 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 11 | -------------------------------------------------------------------------------- /tests/data/default-table-column-cell-attribute.txt: -------------------------------------------------------------------------------- 1 | +-----+--------------------------------------+ 2 | | AAA | ASCII adjust AL after addition | 3 | | AAD | ASCII adjust AX before division | 4 | | AAM | ASCII adjust AX after multiplication | 5 | +-----+--------------------------------------+ 6 | -------------------------------------------------------------------------------- /tests/data/default-table-footer.txt: -------------------------------------------------------------------------------- 1 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 2 | | Published Date | Title | Description | 3 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 4 | | September 16, 2014 | Zero to One: Notes on Startups, or How to Build | If you want to build a better future, you must | 5 | | | the Future | believe in secrets. | 6 | | | | The great secret of our time is that | 7 | | | | there are still uncharted frontiers to explore and | 8 | | | | new inventions to create. In Zero to One, | 9 | | | | legendary entrepreneur and investor Peter Thiel | 10 | | | | shows how we can find singular ways to create | 11 | | | | those new things. | 12 | | November 4, 2014 | Hooked: How to Build Habit-Forming Products | Why do some products capture widespread attention | 13 | | | | while others flop? What makes us engage with | 14 | | | | certain products out of sheer habit? Is there a | 15 | | | | pattern underlying how technologies hook us? Nir | 16 | | | | Eyal answers these questions (and many more) by | 17 | | | | explaining the Hook Model—a four-step process | 18 | | | | embedded into the products of many successful | 19 | | | | companies to subtly encourage customer behavior. | 20 | | | | Through consecutive “hook cycles,” these | 21 | | | | products reach their ultimate goal of bringing | 22 | | | | users back again and again without depending on | 23 | | | | costly advertising or aggressive messaging. | 24 | | | | | 25 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 26 | | Found 3 books... | 27 | +---------------------------------------------------------------------------------------------------------------------------+ 28 | -------------------------------------------------------------------------------- /tests/data/default-table-number-column-cell-attribute.txt: -------------------------------------------------------------------------------- 1 | +-----+--------------------------------------+--------+ 2 | | AAA | ASCII adjust AL after addition | 123 | 3 | | AAD | ASCII adjust AX before division | 222 | 4 | | AAM | ASCII adjust AX after multiplication | 12,909 | 5 | +-----+--------------------------------------+--------+ 6 | -------------------------------------------------------------------------------- /tests/data/default-table-row-separator.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------------------------------------------------------- 2 | September 16, 2014 Zero to One: Notes on Startups, or How to Build If you want to build a better future, you must 3 | the Future believe in secrets. 4 | The great secret of our time is that 5 | there are still uncharted frontiers to explore and 6 | new inventions to create. In Zero to One, 7 | legendary entrepreneur and investor Peter Thiel 8 | shows how we can find singular ways to create 9 | those new things. 10 | ----------------------------------------------------------------------------------------------------------------------------- 11 | September 16, 2014 Zero to One: Notes on Startups, or How to Build If you want to build a better future, you must 12 | the Future believe in secrets. 13 | The great secret of our time is that 14 | there are still uncharted frontiers to explore and 15 | new inventions to create. In Zero to One, 16 | legendary entrepreneur and investor Peter Thiel 17 | shows how we can find singular ways to create 18 | those new things. 19 | ----------------------------------------------------------------------------------------------------------------------------- 20 | -------------------------------------------------------------------------------- /tests/data/default-table.txt: -------------------------------------------------------------------------------- 1 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 2 | | Published Date | Title | Description | 3 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 4 | | September 16, 2014 | Zero to One: Notes on Startups, or How to Build | If you want to build a better future, you must | 5 | | | the Future | believe in secrets. | 6 | | | | The great secret of our time is that | 7 | | | | there are still uncharted frontiers to explore and | 8 | | | | new inventions to create. In Zero to One, | 9 | | | | legendary entrepreneur and investor Peter Thiel | 10 | | | | shows how we can find singular ways to create | 11 | | | | those new things. | 12 | | November 4, 2014 | Hooked: How to Build Habit-Forming Products | Why do some products capture widespread attention | 13 | | | | while others flop? What makes us engage with | 14 | | | | certain products out of sheer habit? Is there a | 15 | | | | pattern underlying how technologies hook us? Nir | 16 | | | | Eyal answers these questions (and many more) by | 17 | | | | explaining the Hook Model—a four-step process | 18 | | | | embedded into the products of many successful | 19 | | | | companies to subtly encourage customer behavior. | 20 | | | | Through consecutive “hook cycles,” these | 21 | | | | products reach their ultimate goal of bringing | 22 | | | | users back again and again without depending on | 23 | | | | costly advertising or aggressive messaging. | 24 | | | | | 25 | +--------------------+-------------------------------------------------+----------------------------------------------------+ 26 | -------------------------------------------------------------------------------- /tests/data/markdown-table.txt: -------------------------------------------------------------------------------- 1 | | Published Date | Title | Description | 2 | |--------------------|-------------------------------------------------|----------------------------------------------------| 3 | | September 16, 2014 | Zero to One: Notes on Startups, or How to Build | If you want to build a better future, you must | 4 | | | the Future | believe in secrets. | 5 | | | | The great secret of our time is that | 6 | | | | there are still uncharted frontiers to explore and | 7 | | | | new inventions to create. In Zero to One, | 8 | | | | legendary entrepreneur and investor Peter Thiel | 9 | | | | shows how we can find singular ways to create | 10 | | | | those new things. | 11 | | November 4, 2014 | Hooked: How to Build Habit-Forming Products | Why do some products capture widespread attention | 12 | | | | while others flop? What makes us engage with | 13 | | | | certain products out of sheer habit? Is there a | 14 | | | | pattern underlying how technologies hook us? Nir | 15 | | | | Eyal answers these questions (and many more) by | 16 | | | | explaining the Hook Model—a four-step process | 17 | | | | embedded into the products of many successful | 18 | | | | companies to subtly encourage customer behavior. | 19 | | | | Through consecutive “hook cycles,” these | 20 | | | | products reach their ultimate goal of bringing | 21 | | | | users back again and again without depending on | 22 | | | | costly advertising or aggressive messaging. | 23 | | | | | 24 | -------------------------------------------------------------------------------- /tests/data/sample.ini: -------------------------------------------------------------------------------- 1 | [core] 2 | verbose = true 3 | debug = yes 4 | pid_dir = /var/run 5 | 6 | [debug] 7 | throw = true 8 | -------------------------------------------------------------------------------- /tests/fixture/composer.json.phar-test: -------------------------------------------------------------------------------- 1 | { 2 | "name": "corneltek/cliframework", 3 | "homepage": "http://github.com/c9s/CLIFramework", 4 | "description": "Command-line framework for PHP", 5 | "keywords": [ "command", "command-line", "completion", "zsh", "framework", "getopt" ], 6 | "require": { 7 | "php": ">=5.3.0", 8 | "corneltek/getoptionkit": "~2", 9 | "corneltek/class-template": "*", 10 | "corneltek/codegen": "dev-master as 2.99", 11 | "universal/universal": "dev-master", 12 | "pimple/pimple": "*", 13 | "symfony/class-loader": "^2.7", 14 | "sebastian/version": "^1.0", 15 | "illuminate/support": "^5.1" 16 | }, 17 | "require-dev": { 18 | "corneltek/phpunit-testmore": "dev-master", 19 | "satooshi/php-coveralls": "dev-master", 20 | "symfony/finder": "^2.7" 21 | }, 22 | "license": "MIT", 23 | "authors": [ 24 | { 25 | "name": "Yo-An Lin", 26 | "email": "cornelius.howl@gmail.com", 27 | "homepage": "http://c9s.me" 28 | } 29 | ], 30 | "autoload": { 31 | "psr-4": { 32 | "CLIFramework\\": "src/CLIFramework/" 33 | } 34 | }, 35 | "version": "2.6.0" 36 | } 37 | -------------------------------------------------------------------------------- /tests/helpers.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | * 10 | */ 11 | 12 | if( !defined('DEBUG_BACKTRACE_PROVIDE_OBJECT') ) 13 | define('DEBUG_BACKTRACE_PROVIDE_OBJECT',1); 14 | 15 | function ok( $v , $msg = null ) 16 | { 17 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 18 | $testobj = $stacks[1]['object']; 19 | $testobj->assertTrue( $v ? true : false , $msg ); 20 | } 21 | 22 | function not_ok( $v , $msg = null ) 23 | { 24 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 25 | $testobj = $stacks[1]['object']; 26 | $testobj->assertFalse( $v ? true : false , $msg ); 27 | } 28 | 29 | function is( $expected , $v , $msg = null ) 30 | { 31 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 32 | $testobj = $stacks[1]['object']; 33 | $testobj->assertEquals( $expected , $v , $msg ); 34 | } 35 | 36 | function is_class( $expected , $v , $msg = null ) 37 | { 38 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 39 | $testobj = $stacks[1]['object']; 40 | $testobj->assertInstanceOf( $expected , $v , $msg ); 41 | } 42 | 43 | function count_ok( $expected,$v, $msg = null ) 44 | { 45 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 46 | $testobj = $stacks[1]['object']; 47 | $testobj->assertCount( $expected , $v , $msg ); 48 | } 49 | 50 | 51 | function like( $e, $v , $msg = null ) 52 | { 53 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 54 | $testobj = $stacks[1]['object']; 55 | $testobj->assertRegExp($e,$v,$msg); 56 | } 57 | 58 | function is_true($e,$v,$msg = null) 59 | { 60 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 61 | $testobj = $stacks[1]['object']; 62 | $testobj->assertTrue($e,$v,$msg); 63 | } 64 | 65 | function is_false($e,$v,$msg= null) 66 | { 67 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 68 | $testobj = $stacks[1]['object']; 69 | $testobj->assertFalse($e,$v,$msg); 70 | } 71 | 72 | function file_equals($e,$v,$msg = null) 73 | { 74 | $stacks = debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT ); # XXX: limit is only availabel in PHP5.4 75 | $testobj = $stacks[1]['object']; 76 | $testobj->assertFileEquals($e,$v,$msg); 77 | } 78 | 79 | function dump($e) 80 | { 81 | var_dump($e); 82 | ob_flush(); 83 | } 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/script/CLIFramework/IO/ReadlineConsoleReadLine.php: -------------------------------------------------------------------------------- 1 | readLine(''); 7 | -------------------------------------------------------------------------------- /tests/script/CLIFramework/IO/ReadlineConsoleReadPassword.php: -------------------------------------------------------------------------------- 1 | readPassword(''); 7 | -------------------------------------------------------------------------------- /tests/script/CLIFramework/IO/StandardConsoleReadLine.php: -------------------------------------------------------------------------------- 1 | readLine(''); 7 | -------------------------------------------------------------------------------- /tests/script/CLIFramework/IO/StandardConsoleReadPassword.php: -------------------------------------------------------------------------------- 1 | readPassword(''); 7 | echo $line; 8 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | todo 2 | ===== 3 | * command group 4 | * command alias 5 | * command completion 6 | * option completion 7 | --------------------------------------------------------------------------------