├── .github └── workflows │ └── php.yml ├── .gitignore ├── .gitmodules ├── .phan └── config.php ├── .travis.yml ├── LICENSE-MIT ├── README.rst ├── composer.json ├── composer.lock ├── dev-setup ├── examples ├── any-options.php └── arguments.php ├── src └── docopt.php ├── test.php └── test ├── extra.docopt └── lib ├── LanguageAgnosticTest.php ├── PythonPortedTest.php └── TestCase.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Test docopt.php 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | php-version: ["7.2", "7.3", "7.4", "8.0", "8.1", "8.2"] 14 | 15 | steps: 16 | - name: Setup PHP ${{ matrix.php-version }} 17 | uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e 18 | with: 19 | php-version: ${{ matrix.php-version }} 20 | 21 | - uses: actions/checkout@v3 22 | with: 23 | submodules: true 24 | 25 | - name: Validate composer.json and composer.lock 26 | run: composer validate --strict 27 | 28 | - name: Cache Composer packages 29 | id: composer-cache 30 | uses: actions/cache@v3 31 | with: 32 | path: vendor 33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-php- 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-progress 39 | 40 | - name: Run test suite 41 | run: php test.php 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "py"] 2 | path = py 3 | url = https://github.com/docopt/docopt.git 4 | -------------------------------------------------------------------------------- /.phan/config.php: -------------------------------------------------------------------------------- 1 | array( 4 | ".", 5 | ), 6 | "exclude_analysis_directory_list" => array( 7 | 'vendor/', 8 | ), 9 | ); 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | before_install: 4 | - git submodule update --init 5 | - composer install 6 | 7 | # using multiple php versions in the build matrix yields this error: 8 | # phpenv global ["hhvm-3.3", "hhvm-3.6", "hhvm-3.9", "hhvm-3.12", "hhvm-3.15", "hhvm-3.18", "hhvm-nightly", "nightly", "7.1", "7.0", "5.6", "5.5", "5.4"] 2>/dev/null 9 | # [hhvm-3.3, hhvm-3.6, hhvm-3.9, hhvm-3.12, hhvm-3.15, hhvm-3.18, hhvm-nightly, nightly, 7.1, 7.0, 5.6, 5.5, 5.4] is not pre-installed; installing 10 | # hhvm-3.6,: command not found 11 | matrix: 12 | include: 13 | - { dist: trusty , php: "hhvm-3.30" } 14 | - { dist: trusty , php: "nightly" } 15 | - { dist: trusty , php: "7.4snapshot" } 16 | - { dist: trusty , php: "7.3" } 17 | - { dist: trusty , php: "7.2" } 18 | - { dist: trusty , php: "7.1" } 19 | - { dist: trusty , php: "7.0" } 20 | - { dist: trusty , php: "5.6" } 21 | - { dist: trusty , php: "5.5" } 22 | - { dist: trusty , php: "5.4" } 23 | - { dist: precise, php: "5.3" } 24 | 25 | allow_failures: 26 | - php: "nightly" 27 | 28 | branches: 29 | only: 30 | - master 31 | - develop 32 | 33 | script: php test.php 34 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Vladimir Keleshev, 2 | Blake Williams, 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``docopt`` creates *beautiful* command-line interfaces 2 | ====================================================================== 3 | 4 | .. image:: https://travis-ci.org/docopt/docopt.php.svg?branch=master 5 | :target: https://travis-ci.org/docopt/docopt.php 6 | 7 | This is a straight PHP transliteration of Vladimir Keleshev's brilliant 8 | `docopt `_ Python library. There are a 9 | few artefacts in the code as a result that may seem inefficient and 10 | non-idiomatic to PHP, this has been done to make integrating changes more 11 | efficient. 12 | 13 | As a result, unless a bug is present only in the PHP version, pull requests 14 | are unlikely to be accepted unless they are themselves direct transliterations 15 | of bugfixes in the Python version. 16 | 17 | **This port has been marked version 1.0**. It is based on the Python version at 18 | commit `a093f938b7f26564434f3c15a1dcc39e017ad638 19 | `_ 20 | (labelled **0.6.2**). 21 | 22 | It has been quite stable for a long time and has barely been changed. The Python version 23 | receives only occasional bugfixes and keeping the version numbers pinned has been more 24 | trouble than it has been worth. 25 | 26 | There are also some major backward compatibility breaks. Rather than dwell in 0.x semver 27 | hell, the PHP port will liberally bump major numbers henceforth when BC breaks regardless 28 | of the reason. 29 | 30 | - The PHP API has changed slightly. ``Docopt\docopt()`` has been renamed to 31 | ``Docopt::handle()`` to fix autoloader support. See `issue #3 32 | `_. 33 | 34 | - Docopt.py also has a significant BC break. Existing users should read the information 35 | below about Usage and Option sections. See `issue 102 36 | `_ for more info. 37 | 38 | Please see the `Python version's README `_ 39 | for details of any new and breaking changes that are not specific to the PHP version. 40 | 41 | There is also at least one significant known issue with the upstream Python version. Due 42 | to the porting strategy used for the PHP version, it inherits the bug surface of the Python 43 | version (and if it doesn't, that's actually a bug!): 44 | 45 | - Issues with multi-word argument and option values (`PHP report `_, 46 | `Upstream report `_) 47 | 48 | ----- 49 | 50 | Isn't it awesome how ``optparse`` and ``argparse`` generate help 51 | messages based on your code?! 52 | 53 | *Hell no!* You know what's awesome? It's when the option parser *is* 54 | generated based on the beautiful help message that you write yourself! 55 | This way you don't need to write this stupid repeatable parser-code, 56 | and instead can write only the help message--*the way you want it*. 57 | 58 | **docopt** helps you create most beautiful command-line interfaces 59 | *easily*: 60 | 61 | .. code:: php 62 | 63 | ... 69 | naval_fate.php ship move [--speed=] 70 | naval_fate.php ship shoot 71 | naval_fate.php mine (set|remove) [--moored | --drifting] 72 | naval_fate.php (-h | --help) 73 | naval_fate.php --version 74 | 75 | Options: 76 | -h --help Show this screen. 77 | --version Show version. 78 | --speed= Speed in knots [default: 10]. 79 | --moored Moored (anchored) mine. 80 | --drifting Drifting mine. 81 | 82 | DOC; 83 | 84 | require('path/to/src/docopt.php'); 85 | $args = Docopt::handle($doc, array('version'=>'Naval Fate 2.0')); 86 | foreach ($args as $k=>$v) 87 | echo $k.': '.json_encode($v).PHP_EOL; 88 | 89 | 90 | Beat that! The option parser is generated based on the docstring above 91 | that is passed to ``docopt`` function. ``docopt`` parses the usage 92 | pattern (``"Usage: ..."``) and option descriptions (lines starting 93 | with dash "``-``") and ensures that the program invocation matches the 94 | usage pattern; it parses options, arguments and commands based on 95 | that. The basic idea is that *a good help message has all necessary 96 | information in it to make a parser*. 97 | 98 | 99 | Installation 100 | ====================================================================== 101 | 102 | Install ``docopt.php`` using `Composer `_:: 103 | 104 | composer require docopt/docopt 105 | 106 | Alternatively, you can just drop ``docopt.php`` file into your project--it is 107 | self-contained. `Get source on github `_. 108 | 109 | ``docopt.php`` is tested with PHP 7; it should still work with PHP 5.3+ but this support 110 | will become increasingly fragile and will at some point cease to be supported at all. You 111 | should update to 7 as soon as you can. 112 | 113 | 114 | Testing 115 | ====================================================================== 116 | 117 | Configure your repo for running tests:: 118 | 119 | ./dev-setup 120 | 121 | You can run unit tests with the following command:: 122 | 123 | php test.php 124 | 125 | This will run the Python language agnostic tests as well as the PHP 126 | docopt tests. 127 | 128 | 129 | API 130 | ====================================================================== 131 | 132 | .. code:: php 133 | 134 | handle($sdoc); 142 | 143 | // long form, simple API (equivalent to short) 144 | $params = array( 145 | 'argv'=>array_slice($_SERVER['argv'], 1), 146 | 'help'=>true, 147 | 'version'=>null, 148 | 'optionsFirst'=>false, 149 | ); 150 | $args = Docopt::handle($doc, $params); 151 | 152 | // long form, full API 153 | $handler = new \Docopt\Handler(array( 154 | 'help'=>true, 155 | 'optionsFirst'=>false, 156 | )); 157 | $handler->handle($doc, $argv); 158 | 159 | 160 | ``Docopt::handle()`` takes 1 required and 1 optional argument: 161 | 162 | - ``doc`` is a string that contains a **help message** that will be parsed to 163 | create the option parser. The simple rules of how to write such a 164 | help message are given in next sections. Here is a quick example of 165 | such a string: 166 | 167 | .. code:: php 168 | 169 | handle()`` takes one required argument: 218 | 219 | - ``doc`` is a string that contains a **help message** that will be parsed to 220 | create the option parser. The simple rules of how to write such a 221 | help message are given in next sections. Here is a quick example of 222 | such a string: 223 | 224 | .. code:: php 225 | 226 | false, 'mine'=>false, 253 | '--help'=>false, 'move'=>true, 254 | '--moored'=>false, 'new'=>false, 255 | '--speed'=>'15', 'remove'=>false, 256 | '--version'=>false, 'set'=>false, 257 | ''=>array('Guardian'), 'ship'=>true, 258 | ''=>'100', 'shoot'=>false, 259 | ''=>'150' 260 | ); 261 | 262 | 263 | Help message format 264 | ====================================================================== 265 | 266 | Help message consists of 2 sections: 267 | 268 | - Usage section, starting with ``Usage:`` e.g.:: 269 | 270 | Usage: my_program.php [-hso FILE] [--quiet | --verbose] [INPUT ...] 271 | 272 | - Option section, starting with ``Options:`` e.g.:: 273 | 274 | Options: 275 | -h --help show this 276 | -s --sorted sorted output 277 | -o FILE specify output file [default: ./test.txt] 278 | --quiet print less text 279 | --verbose print more text 280 | 281 | Sections consist of a header and a body. The section body can begin on 282 | the same line as the header, but if it spans multiple lines, it must be 283 | indented. A section is terminated by an empty line or a string with no 284 | indentation:: 285 | 286 | Section header: Section body 287 | 288 | Section header: 289 | Section body, which is indented at least 290 | one space or tab from the section header 291 | 292 | Section header: Section body, which is indented at least 293 | one space or tab from the section header 294 | 295 | 296 | Usage section format 297 | ---------------------------------------------------------------------- 298 | 299 | Minimum example:: 300 | 301 | Usage: my_program.php 302 | 303 | 304 | The first word after ``usage:`` is interpreted as your program's name. 305 | You can specify your program's name several times to signify several 306 | exclusive patterns:: 307 | 308 | Usage: my_program.php FILE 309 | my_program.php COUNT FILE 310 | 311 | Each pattern can consist of the following elements: 312 | 313 | - ****, **ARGUMENTS**. Arguments are specified as either 314 | upper-case words, e.g. ``my_program.php CONTENT-PATH`` or words 315 | surrounded by angular brackets: ``my_program.php ``. 316 | 317 | - **--options**. Options are words started with dash (``-``), e.g. 318 | ``--output``, ``-o``. You can "stack" several of one-letter 319 | options, e.g. ``-oiv`` which will be the same as ``-o -i -v``. The 320 | options can have arguments, e.g. ``--input=FILE`` or ``-i FILE`` or 321 | even ``-iFILE``. However it is important that you specify option 322 | descriptions if you want your option to have an argument, a default 323 | value, or specify synonymous short/long versions of option (see next 324 | section on option descriptions). 325 | 326 | - **commands** are words that do *not* follow the described above 327 | conventions of ``--options`` or ```` or ``ARGUMENTS``, 328 | plus two special commands: dash "``-``" and double dash "``--``" 329 | (see below). 330 | 331 | Use the following constructs to specify patterns: 332 | 333 | - **[ ]** (brackets) **optional** elements. e.g.: ``my_program.php 334 | [-hvqo FILE]`` 335 | 336 | - **( )** (parens) **required** elements. All elements that are *not* 337 | put in **[ ]** are also required, e.g.: ``my_program.php 338 | --path= ...`` is the same as ``my_program.php 339 | (--path= ...)``. (Note, "required options" might be not 340 | a good idea for your users). 341 | 342 | - **|** (pipe) **mutually exclusive** elements. Group them using **( 343 | )** if one of the mutually exclusive elements is required: 344 | ``my_program.php (--clockwise | --counter-clockwise) TIME``. Group 345 | them using **[ ]** if none of the mutually-exclusive elements are 346 | required: ``my_program.php [--left | --right]``. 347 | 348 | - **...** (ellipsis) **one or more** elements. To specify that 349 | arbitrary number of repeating elements could be accepted, use 350 | ellipsis (``...``), e.g. ``my_program.php FILE ...`` means one or 351 | more ``FILE``-s are accepted. If you want to accept zero or more 352 | elements, use brackets, e.g.: ``my_program.php [FILE ...]``. Ellipsis 353 | works as a unary operator on the expression to the left. 354 | 355 | - **[options]** (case sensitive) shortcut for any options. You can 356 | use it if you want to specify that the usage pattern could be 357 | provided with any options defined below in the option-descriptions 358 | and do not want to enumerate them all in usage-pattern. 359 | "``[--]``". Double dash "``--``" is used by convention to separate 360 | positional arguments that can be mistaken for options. In order to 361 | support this convention add "``[--]``" to you usage patterns. 362 | "``[-]``". Single dash "``-``" is used by convention to signify that 363 | ``stdin`` is used instead of a file. To support this add "``[-]``" 364 | to you usage patterns. "``-``" act as a normal command. 365 | 366 | If your pattern allows to match argument-less option (a flag) several 367 | times:: 368 | 369 | Usage: my_program.php [-v | -vv | -vvv] 370 | 371 | then number of occurrences of the option will be counted. I.e. 372 | ``args['-v']`` will be ``2`` if program was invoked as ``my_program 373 | -vv``. Same works for commands. 374 | 375 | If your usage patterns allows to match same-named option with argument 376 | or positional argument several times, the matched arguments will be 377 | collected into a list:: 378 | 379 | Usage: my_program.php --path=... 380 | 381 | I.e. invoked with ``my_program.php file1 file2 --path=./here 382 | --path=./there`` the returned dict will contain ``args[''] == 383 | ['file1', 'file2']`` and ``args['--path'] == ['./here', './there']``. 384 | 385 | 386 | Options section format 387 | ---------------------------------------------------------------------- 388 | 389 | The **Option section** is an optional section that contains a list of 390 | options that can document or supplement your usage pattern. 391 | 392 | It is necessary to list option descriptions in order to specify: 393 | 394 | - synonymous short and long options, 395 | - if an option has an argument, 396 | - if option's argument has a default value. 397 | 398 | The rules are as follows: 399 | 400 | - Every line in the options section body that starts with one or more 401 | horizontal whitespace characters, followed by ``-`` or ``--`` is treated 402 | as an option description, e.g.:: 403 | 404 | Options: 405 | --verbose # GOOD 406 | -o FILE # GOOD 407 | Other: --bad # BAD, line does not start with dash "-" 408 | 409 | - To specify that option has an argument, put a word describing that 410 | argument after space (or equals "``=``" sign) as shown below. Follow 411 | either or UPPER-CASE convention for options' 412 | arguments. You can use comma if you want to separate options. In 413 | the example below, both lines are valid, however you are recommended 414 | to stick to a single style.:: 415 | 416 | -o FILE --output=FILE # without comma, with "=" sign 417 | -i , --input # with comma, wihtout "=" sign 418 | 419 | - Use two spaces to separate options with their informal description:: 420 | 421 | --verbose More text. # BAD, will be treated as if verbose option had 422 | # an argument "More", so use 2 spaces instead 423 | -q Quit. # GOOD 424 | -o FILE Output file. # GOOD 425 | --stdout Use stdout. # GOOD, 2 spaces 426 | 427 | - If you want to set a default value for an option with an argument, 428 | put it into the option-description, in form ``[default: 429 | ]``:: 430 | 431 | --coefficient=K The K coefficient [default: 2.95] 432 | --output=FILE Output file [default: test.txt] 433 | --directory=DIR Some directory [default: ./] 434 | 435 | - If the option is not repeatable, the value inside ``[default: ...]`` 436 | will be interpreted as string. If it *is* repeatable, it will be 437 | splited into a list on whitespace:: 438 | 439 | Usage: my_program.php [--repeatable= --repeatable=] 440 | [--another-repeatable=]... 441 | [--not-repeatable=] 442 | 443 | # will be ['./here', './there'] 444 | --repeatable= [default: ./here ./there] 445 | 446 | # will be ['./here'] 447 | --another-repeatable= [default: ./here] 448 | 449 | # will be './here ./there', because it is not repeatable 450 | --not-repeatable= [default: ./here ./there] 451 | 452 | 453 | Examples 454 | ---------------------------------------------------------------------- 455 | 456 | We have an extensive list of `examples 457 | `_ which cover 458 | every aspect of functionality of **docopt**. Try them out, read the 459 | source if in doubt. 460 | 461 | 462 | Subparsers, multi-level help and *huge* applications (like git) 463 | ---------------------------------------------------------------------- 464 | 465 | If you want to split your usage-pattern into several, implement 466 | multi-level help (with separate help-screen for each subcommand), 467 | want to interface with existing scripts that don't use **docopt**, or 468 | you're building the next "git", you will need the new ``options_first`` 469 | parameter (described in API section above). To get you started quickly 470 | we implemented a subset of git command-line interface as an example: 471 | `examples/git 472 | `_ 473 | 474 | 475 | Data validation 476 | ---------------------------------------------------------------------- 477 | 478 | **docopt** does one thing and does it well: it implements your 479 | command-line interface. However it does not validate the input data. 480 | You should supplement docopt with a validation library when your 481 | validation requirements extend beyond whether input is optional or required. 482 | 483 | 484 | Development 485 | ====================================================================== 486 | 487 | See the `Python version's page `_ for more info 488 | on developing. 489 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docopt/docopt", 3 | "type": "library", 4 | "description": "Port of Python's docopt for PHP >=5.3", 5 | "keywords": ["cli","docs"], 6 | "homepage": "http://github.com/docopt/docopt.php", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Blake Williams", 11 | "email": "code@shabbyrobe.org", 12 | "homepage": "http://docopt.org/", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=5.3.0" 18 | }, 19 | "autoload": { 20 | "classmap": ["src/docopt.php"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "0e49a702fe1a845cf02354167d7a3b29", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": ">=5.3.0" 17 | }, 18 | "platform-dev": [], 19 | "plugin-api-version": "2.2.0" 20 | } 21 | -------------------------------------------------------------------------------- /dev-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git submodule update --init 3 | composer install --dev 4 | -------------------------------------------------------------------------------- /examples/any-options.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | Options: 11 | -h --help show this help message and exit 12 | --version show version and exit 13 | -n, --number N use N as a number 14 | -t, --timeout TIMEOUT set timeout TIMEOUT seconds 15 | --apply apply changes to database 16 | -q operate in quiet mode 17 | 18 | DOCOPT; 19 | 20 | $result = Docopt::handle($doc, array('version'=>'1.0.0rc2')); 21 | foreach ($result as $k=>$v) 22 | echo $k.': '.json_encode($v).PHP_EOL; 23 | -------------------------------------------------------------------------------- /examples/arguments.php: -------------------------------------------------------------------------------- 1 | '1.0.0rc2')); 26 | foreach ($result as $k=>$v) 27 | echo $k.': '.json_encode($v).PHP_EOL; 28 | -------------------------------------------------------------------------------- /src/docopt.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | namespace 12 | { 13 | class Docopt 14 | { 15 | /** 16 | * API compatibility with python docopt 17 | * 18 | * @param string $doc 19 | * @param array $params 20 | * @return Docopt\Response 21 | */ 22 | static function handle($doc, $params=array()) 23 | { 24 | $argv = null; 25 | if (isset($params['argv'])) { 26 | $argv = $params['argv']; 27 | unset($params['argv']); 28 | } 29 | elseif (is_string($params)) { 30 | $argv = $params; 31 | $params = array(); 32 | } 33 | 34 | $h = new \Docopt\Handler($params); 35 | return $h->handle($doc, $argv); 36 | } 37 | } 38 | } 39 | 40 | namespace Docopt 41 | { 42 | /** 43 | * Return true if all cased characters in the string are uppercase and there is 44 | * at least one cased character, false otherwise. 45 | * Python method with no knowrn equivalent in PHP. 46 | * 47 | * @param string $string 48 | * @return bool 49 | */ 50 | function is_upper($string) 51 | { 52 | return preg_match('/[A-Z]/', $string) && !preg_match('/[a-z]/', $string); 53 | } 54 | 55 | /** 56 | * Return True if any element of the iterable is true. If the iterable is 57 | * empty, return False. Python method with no known equivalent in PHP. 58 | * 59 | * @param array|\Iterator $iterable 60 | * @return bool 61 | */ 62 | function any($iterable) 63 | { 64 | foreach ($iterable as $element) { 65 | if ($element) { 66 | return true; 67 | } 68 | } 69 | return false; 70 | } 71 | 72 | /** 73 | * The PHP version of this doesn't support array iterators 74 | * @param array|\Iterator $input 75 | * @param callable $callback 76 | * @param bool $reKey 77 | * @return array 78 | */ 79 | function array_filter($input, $callback, $reKey=false) 80 | { 81 | if ($input instanceof \Iterator) { 82 | $input = iterator_to_array($input); 83 | } 84 | $filtered = \array_filter($input, $callback); 85 | if ($reKey) { 86 | $filtered = array_values($filtered); 87 | } 88 | return $filtered; 89 | } 90 | 91 | /** 92 | * The PHP version of this doesn't support array iterators 93 | * @param array $values,... 94 | * @return array 95 | */ 96 | function array_merge() 97 | { 98 | $values = func_get_args(); 99 | $resolved = array(); 100 | foreach ($values as $v) { 101 | if ($v instanceof \Iterator) { 102 | $resolved[] = iterator_to_array($v); 103 | } else { 104 | $resolved[] = $v; 105 | } 106 | } 107 | return call_user_func_array('array_merge', $resolved); 108 | } 109 | 110 | /** 111 | * @param string $str 112 | * @param string $test The suffix to check 113 | * @return bool 114 | */ 115 | function ends_with($str, $test) 116 | { 117 | $len = strlen($test); 118 | return substr_compare($str, $test, -$len, $len) === 0; 119 | } 120 | 121 | /** 122 | * @param mixed $obj 123 | * @return string 124 | */ 125 | function get_class_name($obj) 126 | { 127 | $cls = get_class($obj); 128 | return substr($cls, strpos($cls, '\\')+1); 129 | } 130 | 131 | function dumpw($val) 132 | { 133 | echo dump($val); 134 | echo PHP_EOL; 135 | } 136 | 137 | function dump($val) 138 | { 139 | $out = ""; 140 | if (is_array($val) || $val instanceof \Traversable) { 141 | $out = '['; 142 | $cur = array(); 143 | foreach ($val as $i) { 144 | if (is_object($i)) { 145 | $cur[] = $i->dump(); 146 | } elseif (is_array($i)) { 147 | $cur[] = dump($i); 148 | } else { 149 | $cur[] = dump_scalar($i); 150 | } 151 | } 152 | $out .= implode(', ', $cur); 153 | $out .= ']'; 154 | } 155 | elseif ($val instanceof Pattern) { 156 | $out .= $val->dump(); 157 | } else { 158 | throw new \InvalidArgumentException(); 159 | } 160 | return $out; 161 | } 162 | 163 | function dump_scalar($scalar) 164 | { 165 | if ($scalar === null) { 166 | return 'None'; 167 | } elseif ($scalar === false) { 168 | return 'False'; 169 | } elseif ($scalar === true) { 170 | return 'True'; 171 | } elseif (is_int($scalar) || is_float($scalar)) { 172 | return $scalar; 173 | } else { 174 | return "'$scalar'"; 175 | } 176 | } 177 | 178 | /** 179 | * Error in construction of usage-message by developer 180 | */ 181 | class LanguageError extends \Exception 182 | { 183 | } 184 | 185 | /** 186 | * Exit in case user invoked program with incorrect arguments. 187 | * DocoptExit equivalent. 188 | */ 189 | class ExitException extends \RuntimeException 190 | { 191 | /** @var string */ 192 | public static $usage; 193 | 194 | /** @var int */ 195 | public $status; 196 | 197 | /** 198 | * @param ?string $message 199 | * @param int $status 200 | */ 201 | public function __construct($message=null, $status=1) 202 | { 203 | parent::__construct(trim($message.PHP_EOL.static::$usage)); 204 | $this->status = $status; 205 | } 206 | } 207 | 208 | abstract class Pattern 209 | { 210 | /** @var Pattern[] */ 211 | public $children = array(); 212 | 213 | /** 214 | * @param string[]|string $types 215 | * @return Pattern[] 216 | */ 217 | abstract function flat($types=array()); 218 | 219 | /** 220 | * @param Pattern[] $left 221 | * @param Pattern[] $collected 222 | */ 223 | abstract function match($left, $collected=null); 224 | 225 | /** @return string */ 226 | function name() { return ''; } 227 | 228 | /** @return string */ 229 | function dump() { return ''; } 230 | 231 | /** @return string */ 232 | public function __toString() 233 | { 234 | return serialize($this); 235 | } 236 | 237 | /** @return string */ 238 | public function hash() 239 | { 240 | return (string) crc32((string)$this); 241 | } 242 | 243 | /** @return $this */ 244 | public function fix() 245 | { 246 | $this->fixIdentities(); 247 | $this->fixRepeatingArguments(); 248 | return $this; 249 | } 250 | 251 | /** 252 | * Make pattern-tree tips point to same object if they are equal. 253 | * 254 | * @param Pattern[]|null $uniq 255 | */ 256 | public function fixIdentities($uniq=null) 257 | { 258 | if (!isset($this->children) || !$this->children) { 259 | return $this; 260 | } 261 | if ($uniq === null) { 262 | $uniq = array_unique($this->flat()); 263 | } 264 | 265 | foreach ($this->children as $i=>$child) { 266 | if (!$child instanceof BranchPattern) { 267 | if (!in_array($child, $uniq)) { 268 | // Not sure if this is a true substitute for 'assert c in uniq' 269 | throw new \UnexpectedValueException(); 270 | } 271 | $this->children[$i] = $uniq[array_search($child, $uniq)]; 272 | } 273 | else { 274 | $child->fixIdentities($uniq); 275 | } 276 | } 277 | } 278 | 279 | /** 280 | * Fix elements that should accumulate/increment values. 281 | * @return $this 282 | */ 283 | public function fixRepeatingArguments() 284 | { 285 | $either = array(); 286 | foreach (transform($this)->children as $child) { 287 | $either[] = $child->children; 288 | } 289 | 290 | foreach ($either as $case) { 291 | $counts = array(); 292 | foreach ($case as $child) { 293 | $ser = serialize($child); 294 | if (!isset($counts[$ser])) { 295 | $counts[$ser] = array('cnt'=>0, 'items'=>array()); 296 | } 297 | 298 | $counts[$ser]['cnt']++; 299 | $counts[$ser]['items'][] = $child; 300 | } 301 | 302 | $repeatedCases = array(); 303 | foreach ($counts as $child) { 304 | if ($child['cnt'] > 1) { 305 | $repeatedCases = array_merge($repeatedCases, $child['items']); 306 | } 307 | } 308 | 309 | foreach ($repeatedCases as $e) { 310 | if ($e instanceof Argument || ($e instanceof Option && $e->argcount)) { 311 | if (!$e->value) { 312 | $e->value = array(); 313 | } elseif (!is_array($e->value) && !$e->value instanceof \Traversable) { 314 | $e->value = preg_split('/\s+/', $e->value); 315 | } 316 | } 317 | if ($e instanceof Command || ($e instanceof Option && $e->argcount == 0)) { 318 | $e->value = 0; 319 | } 320 | } 321 | } 322 | 323 | return $this; 324 | } 325 | 326 | public function __get($name) 327 | { 328 | if ($name == 'name') { 329 | return $this->name(); 330 | } else { 331 | throw new \BadMethodCallException("Unknown property $name"); 332 | } 333 | } 334 | } 335 | 336 | /** 337 | * Expand pattern into an (almost) equivalent one, but with single Either. 338 | * 339 | * Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d) 340 | * Quirks: [-a] => (-a), (-a...) => (-a -a) 341 | * 342 | * @param Pattern $pattern 343 | * @return Either 344 | */ 345 | function transform($pattern) 346 | { 347 | $result = array(); 348 | $groups = array(array($pattern)); 349 | $parents = array('Required', 'Optional', 'OptionsShortcut', 'Either', 'OneOrMore'); 350 | 351 | while ($groups) { 352 | $children = array_shift($groups); 353 | $types = array(); 354 | foreach ($children as $c) { 355 | if (is_object($c)) { 356 | $types[get_class_name($c)] = true; 357 | } 358 | } 359 | 360 | if (array_intersect(array_keys($types), $parents)) { 361 | $child = null; 362 | foreach ($children as $currentChild) { 363 | if (in_array(get_class_name($currentChild), $parents)) { 364 | $child = $currentChild; 365 | break; 366 | } 367 | } 368 | unset($children[array_search($child, $children)]); 369 | $childClass = get_class_name($child); 370 | if ($childClass == 'Either') { 371 | foreach ($child->children as $c) { 372 | $groups[] = array_merge(array($c), $children); 373 | } 374 | } 375 | elseif ($childClass == 'OneOrMore') { 376 | $groups[] = array_merge($child->children, $child->children, $children); 377 | } 378 | else { 379 | $groups[] = array_merge($child->children, $children); 380 | } 381 | } 382 | else { 383 | $result[] = $children; 384 | } 385 | } 386 | 387 | $rs = array(); 388 | foreach ($result as $e) { 389 | $rs[] = new Required($e); 390 | } 391 | return new Either($rs); 392 | } 393 | 394 | abstract class LeafPattern extends Pattern 395 | { 396 | /** 397 | * @param Pattern[] $left 398 | * @return SingleMatch 399 | */ 400 | abstract function singleMatch($left); 401 | 402 | /** 403 | * @param string[]|string $types 404 | * @return Pattern[] 405 | */ 406 | public function flat($types=array()) 407 | { 408 | $types = is_array($types) ? $types : array($types); 409 | 410 | if (!$types || in_array(get_class_name($this), $types)) { 411 | return array($this); 412 | } else { 413 | return array(); 414 | } 415 | } 416 | 417 | /** 418 | * @param Pattern[] $left 419 | * @param Pattern[] $collected 420 | */ 421 | public function match($left, $collected=null) 422 | { 423 | if (!$collected) { 424 | $collected = array(); 425 | } 426 | 427 | list ($pos, $match) = $this->singleMatch($left)->toArray(); 428 | if (!$match) { 429 | return array(false, $left, $collected); 430 | } 431 | 432 | $left_ = $left; 433 | unset($left_[$pos]); 434 | $left_ = array_values($left_); 435 | 436 | $name = $this->name; 437 | $sameName = array_filter($collected, function ($a) use ($name) { return $name == $a->name; }, true); 438 | 439 | if (is_int($this->value) || is_array($this->value) || $this->value instanceof \Traversable) { 440 | if (is_int($this->value)) { 441 | $increment = 1; 442 | } else { 443 | $increment = is_string($match->value) ? array($match->value) : $match->value; 444 | } 445 | 446 | if (!$sameName) { 447 | $match->value = $increment; 448 | return array(true, $left_, array_merge($collected, array($match))); 449 | } 450 | 451 | if (is_array($increment) || $increment instanceof \Traversable) { 452 | $sameName[0]->value = array_merge($sameName[0]->value, $increment); 453 | } else { 454 | $sameName[0]->value += $increment; 455 | } 456 | 457 | return array(true, $left_, $collected); 458 | } 459 | 460 | return array(true, $left_, array_merge($collected, array($match))); 461 | } 462 | } 463 | 464 | class BranchPattern extends Pattern 465 | { 466 | /** 467 | * @param Pattern[]|Pattern $children 468 | */ 469 | public function __construct($children=null) 470 | { 471 | if (!$children) { 472 | $children = array(); 473 | } elseif ($children instanceof Pattern) { 474 | $children = func_get_args(); 475 | } 476 | foreach ($children as $child) { 477 | $this->children[] = $child; 478 | } 479 | } 480 | 481 | /** 482 | * @param string[]|string $types 483 | * @return Pattern[] 484 | */ 485 | public function flat($types=array()) 486 | { 487 | $types = is_array($types) ? $types : array($types); 488 | 489 | if (in_array(get_class_name($this), $types)) { 490 | return array($this); 491 | } 492 | $flat = array(); 493 | foreach ($this->children as $c) { 494 | $flat = array_merge($flat, $c->flat($types)); 495 | } 496 | return $flat; 497 | } 498 | 499 | /** @return string */ 500 | public function dump() 501 | { 502 | $out = get_class_name($this).'('; 503 | $cd = array(); 504 | foreach ($this->children as $c) { 505 | $cd[] = $c->dump(); 506 | } 507 | $out .= implode(', ', $cd).')'; 508 | return $out; 509 | } 510 | 511 | /** 512 | * @param Pattern[] $left 513 | * @param Pattern[] $collected 514 | */ 515 | public function match($left, $collected=null) 516 | { 517 | throw new \RuntimeException("Unsupported"); 518 | } 519 | } 520 | 521 | class Argument extends LeafPattern 522 | { 523 | /* {{{ this stuff is against LeafPattern in the python version 524 | * but it interferes with name() */ 525 | 526 | /** @var ?string */ 527 | public $name; 528 | 529 | /** @var mixed */ 530 | public $value; 531 | 532 | /** 533 | * @param ?string $name 534 | * @param mixed $value 535 | */ 536 | public function __construct($name, $value=null) 537 | { 538 | $this->name = $name; 539 | $this->value = $value; 540 | } 541 | /* }}} */ 542 | 543 | /** 544 | * @param Pattern[] $left 545 | * @return SingleMatch 546 | */ 547 | public function singleMatch($left) 548 | { 549 | foreach ($left as $n=>$pattern) { 550 | if ($pattern instanceof Argument) { 551 | return new SingleMatch($n, new Argument($this->name, $pattern->value)); 552 | } 553 | } 554 | return new SingleMatch(null, null); 555 | } 556 | 557 | /** 558 | * @param string $source 559 | * @return Argument 560 | */ 561 | public static function parse($source) 562 | { 563 | $name = null; 564 | $value = null; 565 | 566 | if (preg_match_all('@(<\S*?'.'>)@', $source, $matches)) { 567 | $name = $matches[0][0]; 568 | } 569 | if (preg_match_all('@\[default: (.*)\]@i', $source, $matches)) { 570 | $value = $matches[0][1]; 571 | } 572 | return new static($name, $value); 573 | } 574 | 575 | /** @return string */ 576 | public function dump() 577 | { 578 | return get_class_name($this)."(".dump_scalar($this->name).", ".dump_scalar($this->value).")"; 579 | } 580 | } 581 | 582 | class Command extends Argument 583 | { 584 | /** @var string */ 585 | public $name; 586 | 587 | public $value; 588 | 589 | /** 590 | * @param string $name 591 | * @param bool $value 592 | */ 593 | public function __construct($name, $value=false) 594 | { 595 | $this->name = $name; 596 | $this->value = $value; 597 | } 598 | 599 | /** 600 | * @param Pattern[] $left 601 | * @return SingleMatch 602 | */ 603 | function singleMatch($left) 604 | { 605 | foreach ($left as $n=>$pattern) { 606 | if ($pattern instanceof Argument) { 607 | if ($pattern->value == $this->name) { 608 | return new SingleMatch($n, new Command($this->name, true)); 609 | } else { 610 | break; 611 | } 612 | } 613 | } 614 | return new SingleMatch(null, null); 615 | } 616 | } 617 | 618 | class Option extends LeafPattern 619 | { 620 | /** @var ?string */ 621 | public $short; 622 | 623 | /** @var ?string */ 624 | public $long; 625 | 626 | /** @var int */ 627 | public $argcount; 628 | 629 | /** @var bool|string|null */ 630 | public $value; 631 | 632 | /** 633 | * @param ?string $short 634 | * @param ?string $long 635 | * @param int $argcount 636 | * @param bool|string|null $value 637 | */ 638 | public function __construct($short=null, $long=null, $argcount=0, $value=false) 639 | { 640 | if ($argcount != 0 && $argcount != 1) { 641 | throw new \InvalidArgumentException(); 642 | } 643 | 644 | $this->short = $short; 645 | $this->long = $long; 646 | $this->argcount = $argcount; 647 | $this->value = $value; 648 | 649 | if ($value === false && $argcount) { 650 | $this->value = null; 651 | } 652 | } 653 | 654 | /** 655 | * @param string 656 | */ 657 | public static function parse($optionDescription) 658 | { 659 | $short = null; 660 | $long = null; 661 | $argcount = 0; 662 | $value = false; 663 | 664 | $exp = explode(' ', trim($optionDescription), 2); 665 | $options = $exp[0]; 666 | $description = isset($exp[1]) ? $exp[1] : ''; 667 | 668 | $options = str_replace(',', ' ', str_replace('=', ' ', $options)); 669 | foreach (preg_split('/\s+/', $options) as $s) { 670 | if (strpos($s, '--')===0) { 671 | $long = $s; 672 | } elseif ($s && $s[0] == '-') { 673 | $short = $s; 674 | } else { 675 | $argcount = 1; 676 | } 677 | } 678 | 679 | if ($argcount) { 680 | $value = null; 681 | if (preg_match('@\[default: (.*)\]@i', $description, $match)) { 682 | $value = $match[1]; 683 | } 684 | } 685 | 686 | return new static($short, $long, $argcount, $value); 687 | } 688 | 689 | /** 690 | * @param Pattern[] $left 691 | * @return SingleMatch 692 | */ 693 | public function singleMatch($left) 694 | { 695 | foreach ($left as $n=>$pattern) { 696 | if ($this->name == $pattern->name) { 697 | return new SingleMatch($n, $pattern); 698 | } 699 | } 700 | return new SingleMatch(null, null); 701 | } 702 | 703 | /** @return string */ 704 | public function name() 705 | { 706 | return $this->long ?: $this->short; 707 | } 708 | 709 | /** @return string */ 710 | public function dump() 711 | { 712 | return "Option(".dump_scalar($this->short).", ".dump_scalar($this->long).", ".dump_scalar($this->argcount).", ".dump_scalar($this->value).")"; 713 | } 714 | } 715 | 716 | class Required extends BranchPattern 717 | { 718 | /** 719 | * @param Pattern[] $left 720 | * @param Pattern[] $collected 721 | */ 722 | public function match($left, $collected=null) 723 | { 724 | if (!$collected) { 725 | $collected = array(); 726 | } 727 | 728 | $l = $left; 729 | $c = $collected; 730 | 731 | foreach ($this->children as $pattern) { 732 | list ($matched, $l, $c) = $pattern->match($l, $c); 733 | if (!$matched) { 734 | return array(false, $left, $collected); 735 | } 736 | } 737 | 738 | return array(true, $l, $c); 739 | } 740 | } 741 | 742 | class Optional extends BranchPattern 743 | { 744 | /** 745 | * @param Pattern[] $left 746 | * @param Pattern[] $collected 747 | */ 748 | public function match($left, $collected=null) 749 | { 750 | if (!$collected) { 751 | $collected = array(); 752 | } 753 | 754 | foreach ($this->children as $pattern) { 755 | list($m, $left, $collected) = $pattern->match($left, $collected); 756 | } 757 | 758 | return array(true, $left, $collected); 759 | } 760 | } 761 | 762 | /** 763 | * Marker/placeholder for [options] shortcut. 764 | */ 765 | class OptionsShortcut extends Optional 766 | { 767 | } 768 | 769 | class OneOrMore extends BranchPattern 770 | { 771 | /** 772 | * @param Pattern[] $left 773 | * @param Pattern[] $collected 774 | */ 775 | public function match($left, $collected=null) 776 | { 777 | if (count($this->children) != 1) { 778 | throw new \UnexpectedValueException(); 779 | } 780 | if (!$collected) { 781 | $collected = array(); 782 | } 783 | 784 | $l = $left; 785 | $c = $collected; 786 | 787 | $lnew = array(); 788 | $matched = true; 789 | $times = 0; 790 | 791 | while ($matched) { 792 | # could it be that something didn't match but changed l or c? 793 | list ($matched, $l, $c) = $this->children[0]->match($l, $c); 794 | if ($matched) $times += 1; 795 | if ($lnew == $l) { 796 | break; 797 | } 798 | $lnew = $l; 799 | } 800 | 801 | if ($times >= 1) { 802 | return array(true, $l, $c); 803 | } else { 804 | return array(false, $left, $collected); 805 | } 806 | } 807 | } 808 | 809 | class Either extends BranchPattern 810 | { 811 | /** 812 | * @param Pattern[] $left 813 | * @param Pattern[] $collected 814 | */ 815 | public function match($left, $collected=null) 816 | { 817 | if (!$collected) { 818 | $collected = array(); 819 | } 820 | 821 | $outcomes = array(); 822 | foreach ($this->children as $pattern) { 823 | list ($matched, $dump1, $dump2) = $outcome = $pattern->match($left, $collected); 824 | if ($matched) { 825 | $outcomes[] = $outcome; 826 | } 827 | } 828 | if ($outcomes) { 829 | // return min(outcomes, key=lambda outcome: len(outcome[1])) 830 | $min = null; 831 | $ret = null; 832 | foreach ($outcomes as $o) { 833 | $cnt = count($o[1]); 834 | if ($min === null || $cnt < $min) { 835 | $min = $cnt; 836 | $ret = $o; 837 | } 838 | } 839 | return $ret; 840 | } 841 | else { 842 | return array(false, $left, $collected); 843 | } 844 | } 845 | } 846 | 847 | class Tokens extends \ArrayIterator 848 | { 849 | /** @var string */ 850 | public $error; 851 | 852 | /** 853 | * @param array|string $source 854 | * @param string $error Class name of error exception 855 | */ 856 | public function __construct($source, $error='ExitException') 857 | { 858 | if (!is_array($source)) { 859 | $source = trim($source); 860 | if ($source) { 861 | $source = preg_split('/\s+/', $source); 862 | } else { 863 | $source = array(); 864 | } 865 | } 866 | 867 | parent::__construct($source); 868 | 869 | $this->error = $error; 870 | } 871 | 872 | /** 873 | * @param string $source 874 | * @return self 875 | */ 876 | public static function fromPattern($source) 877 | { 878 | $source = preg_replace('@([\[\]\(\)\|]|\.\.\.)@', ' $1 ', $source); 879 | $source = preg_split('@\s+|(\S*<.*?'.'>)@', $source, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); 880 | 881 | return new static($source, 'LanguageError'); 882 | } 883 | 884 | /** 885 | * @return string 886 | */ 887 | function move() 888 | { 889 | $item = $this->current(); 890 | $this->next(); 891 | return $item; 892 | } 893 | 894 | /** 895 | * @return string[] 896 | */ 897 | function left() 898 | { 899 | $left = array(); 900 | while (($token = $this->move()) !== null) { 901 | $left[] = $token; 902 | } 903 | return $left; 904 | } 905 | 906 | /** 907 | * @param string $message 908 | */ 909 | function raiseException($message) 910 | { 911 | $class = __NAMESPACE__.'\\'.$this->error; 912 | throw new $class($message); 913 | } 914 | } 915 | 916 | /** 917 | * long ::= '--' chars [ ( ' ' | '=' ) chars ] ; 918 | * 919 | * @return Option[] 920 | */ 921 | function parse_long(Tokens $tokens, \ArrayIterator $options) 922 | { 923 | $token = $tokens->move(); 924 | $exploded = explode('=', $token, 2); 925 | if (count($exploded) == 2) { 926 | $long = $exploded[0]; 927 | $eq = '='; 928 | $value = $exploded[1]; 929 | } 930 | else { 931 | $long = $token; 932 | $eq = null; 933 | $value = null; 934 | } 935 | 936 | if (strpos($long, '--') !== 0) { 937 | throw new \UnexpectedValueException("Expected long option, found '$long'"); 938 | } 939 | 940 | $value = (!$eq && !$value) ? null : $value; 941 | 942 | $filter = function($o) use ($long) { return $o->long && $o->long == $long; }; 943 | $similar = array_filter($options, $filter, true); 944 | if ('ExitException' == $tokens->error && !$similar) { 945 | $filter = function($o) use ($long) { return $o->long && strpos($o->long, $long)===0; }; 946 | $similar = array_filter($options, $filter, true); 947 | } 948 | 949 | if (count($similar) > 1) { 950 | // might be simply specified ambiguously 2+ times? 951 | $tokens->raiseException("$long is not a unique prefix: ". 952 | implode(', ', array_map(function($o) { return $o->long; }, $similar))); 953 | } 954 | elseif (count($similar) < 1) { 955 | $argcount = $eq == '=' ? 1 : 0; 956 | $o = new Option(null, $long, $argcount); 957 | $options[] = $o; 958 | if ($tokens->error == 'ExitException') { 959 | $o = new Option(null, $long, $argcount, $argcount ? $value : true); 960 | } 961 | } 962 | else { 963 | $o = new Option($similar[0]->short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value); 964 | if ($o->argcount == 0) { 965 | if ($value !== null) { 966 | $tokens->raiseException("{$o->long} must not have an argument"); 967 | } 968 | } 969 | else { 970 | if ($value === null) { 971 | if ($tokens->current() === null || $tokens->current() == "--") { 972 | $tokens->raiseException("{$o->long} requires argument"); 973 | } 974 | $value = $tokens->move(); 975 | } 976 | } 977 | if ($tokens->error == 'ExitException') { 978 | $o->value = $value !== null ? $value : true; 979 | } 980 | } 981 | return array($o); 982 | } 983 | 984 | /** 985 | * shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ; 986 | * 987 | * @return Option[] 988 | */ 989 | function parse_shorts(Tokens $tokens, \ArrayIterator $options) 990 | { 991 | $token = $tokens->move(); 992 | 993 | if (strpos($token, '-') !== 0 || strpos($token, '--') === 0) { 994 | throw new \UnexpectedValueException("short token '$token' does not start with '-' or '--'"); 995 | } 996 | 997 | $left = ltrim($token, '-'); 998 | $parsed = array(); 999 | while ($left != '') { 1000 | $short = '-'.$left[0]; 1001 | $left = substr($left, 1); 1002 | $similar = array(); 1003 | foreach ($options as $o) { 1004 | if ($o->short == $short) { 1005 | $similar[] = $o; 1006 | } 1007 | } 1008 | 1009 | $similarCnt = count($similar); 1010 | if ($similarCnt > 1) { 1011 | $tokens->raiseException("$short is specified ambiguously $similarCnt times"); 1012 | } 1013 | elseif ($similarCnt < 1) { 1014 | $o = new Option($short, null, 0); 1015 | $options[] = $o; 1016 | if ($tokens->error == 'ExitException') { 1017 | $o = new Option($short, null, 0, true); 1018 | } 1019 | } 1020 | else { 1021 | $o = new Option($short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value); 1022 | $value = null; 1023 | if ($o->argcount != 0) { 1024 | if ($left == '') { 1025 | if ($tokens->current() === null || $tokens->current() == '--') { 1026 | $tokens->raiseException("$short requires argument"); 1027 | } 1028 | $value = $tokens->move(); 1029 | } 1030 | else { 1031 | $value = $left; 1032 | $left = ''; 1033 | } 1034 | } 1035 | if ($tokens->error == 'ExitException') { 1036 | $o->value = $value !== null ? $value : true; 1037 | } 1038 | } 1039 | $parsed[] = $o; 1040 | } 1041 | 1042 | return $parsed; 1043 | } 1044 | 1045 | /** 1046 | * @param string $source 1047 | * @return Required 1048 | */ 1049 | function parse_pattern($source, \ArrayIterator $options) 1050 | { 1051 | $tokens = Tokens::fromPattern($source); 1052 | $result = parse_expr($tokens, $options); 1053 | if ($tokens->current() != null) { 1054 | $tokens->raiseException('unexpected ending: '.implode(' ', $tokens->left())); 1055 | } 1056 | return new Required($result); 1057 | } 1058 | 1059 | /** 1060 | * expr ::= seq ( '|' seq )* ; 1061 | * 1062 | * @return Either|Pattern[] 1063 | */ 1064 | function parse_expr(Tokens $tokens, \ArrayIterator $options) 1065 | { 1066 | $seq = parse_seq($tokens, $options); 1067 | if ($tokens->current() != '|') { 1068 | return $seq; 1069 | } 1070 | 1071 | $result = null; 1072 | if (count($seq) > 1) { 1073 | $result = array(new Required($seq)); 1074 | } else { 1075 | $result = $seq; 1076 | } 1077 | 1078 | while ($tokens->current() == '|') { 1079 | $tokens->move(); 1080 | $seq = parse_seq($tokens, $options); 1081 | if (count($seq) > 1) { 1082 | $result[] = new Required($seq); 1083 | } else { 1084 | $result = array_merge($result, $seq); 1085 | } 1086 | } 1087 | 1088 | if (count($result) > 1) { 1089 | return new Either($result); 1090 | } else { 1091 | return $result; 1092 | } 1093 | } 1094 | 1095 | /** 1096 | * seq ::= ( atom [ '...' ] )* ; 1097 | * 1098 | * @return Pattern[] 1099 | */ 1100 | function parse_seq(Tokens $tokens, \ArrayIterator $options) 1101 | { 1102 | $result = array(); 1103 | $not = array(null, '', ']', ')', '|'); 1104 | while (!in_array($tokens->current(), $not, true)) { 1105 | $atom = parse_atom($tokens, $options); 1106 | if ($tokens->current() == '...') { 1107 | $atom = array(new OneOrMore($atom)); 1108 | $tokens->move(); 1109 | } 1110 | if ($atom) { 1111 | $result = array_merge($result, $atom); 1112 | } 1113 | } 1114 | return $result; 1115 | } 1116 | 1117 | /** 1118 | * atom ::= '(' expr ')' | '[' expr ']' | 'options' 1119 | * | long | shorts | argument | command ; 1120 | * 1121 | * @return Pattern[] 1122 | */ 1123 | function parse_atom(Tokens $tokens, \ArrayIterator $options) 1124 | { 1125 | $token = $tokens->current(); 1126 | $result = array(); 1127 | 1128 | if ($token == '(' || $token == '[') { 1129 | $tokens->move(); 1130 | 1131 | static $index; 1132 | if (!$index) { 1133 | $index = array('('=>array(')', __NAMESPACE__.'\Required'), '['=>array(']', __NAMESPACE__.'\Optional')); 1134 | } 1135 | list ($matching, $pattern) = $index[$token]; 1136 | 1137 | $result = new $pattern(parse_expr($tokens, $options)); 1138 | if ($tokens->move() != $matching) { 1139 | $tokens->raiseException("Unmatched '$token'"); 1140 | } 1141 | 1142 | return array($result); 1143 | } 1144 | elseif ($token == 'options') { 1145 | $tokens->move(); 1146 | return array(new OptionsShortcut); 1147 | } 1148 | elseif (strpos($token, '--') === 0 && $token != '--') { 1149 | return parse_long($tokens, $options); 1150 | } 1151 | elseif (strpos($token, '-') === 0 && $token != '-' && $token != '--') { 1152 | return parse_shorts($tokens, $options); 1153 | } 1154 | elseif (strpos($token, '<') === 0 && ends_with($token, '>') || is_upper($token)) { 1155 | return array(new Argument($tokens->move())); 1156 | } 1157 | else { 1158 | return array(new Command($tokens->move())); 1159 | } 1160 | } 1161 | 1162 | /** 1163 | * Parse command-line argument vector. 1164 | * 1165 | * If options_first: 1166 | * argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; 1167 | * else: 1168 | * argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; 1169 | * 1170 | * @param bool $optionsFirst 1171 | * @return Pattern[] 1172 | */ 1173 | function parse_argv(Tokens $tokens, \ArrayIterator $options, $optionsFirst=false) 1174 | { 1175 | $parsed = array(); 1176 | 1177 | while ($tokens->current() !== null) { 1178 | if ($tokens->current() == '--') { 1179 | while ($tokens->current() !== null) { 1180 | $parsed[] = new Argument(null, $tokens->move()); 1181 | } 1182 | return $parsed; 1183 | } 1184 | elseif (strpos($tokens->current(), '--')===0) { 1185 | $parsed = array_merge($parsed, parse_long($tokens, $options)); 1186 | } 1187 | elseif (strpos($tokens->current(), '-')===0 && $tokens->current() != '-') { 1188 | $parsed = array_merge($parsed, parse_shorts($tokens, $options)); 1189 | } 1190 | elseif ($optionsFirst) { 1191 | return array_merge($parsed, array_map(function($v) { return new Argument(null, $v); }, $tokens->left())); 1192 | } 1193 | else { 1194 | $parsed[] = new Argument(null, $tokens->move()); 1195 | } 1196 | } 1197 | return $parsed; 1198 | } 1199 | 1200 | /** 1201 | * @param string $doc 1202 | * @return \ArrayIterator 1203 | */ 1204 | function parse_defaults($doc) 1205 | { 1206 | $defaults = array(); 1207 | foreach (parse_section('options:', $doc) as $s) { 1208 | # FIXME corner case "bla: options: --foo" 1209 | list (, $s) = explode(':', $s, 2); 1210 | $splitTmp = array_slice(preg_split("@\n[ \t]*(-\S+?)@", "\n".$s, 0, PREG_SPLIT_DELIM_CAPTURE), 1); 1211 | $split = array(); 1212 | for ($cnt = count($splitTmp), $i=0; $i < $cnt; $i+=2) { 1213 | $split[] = $splitTmp[$i] . (isset($splitTmp[$i+1]) ? $splitTmp[$i+1] : ''); 1214 | } 1215 | $options = array(); 1216 | foreach ($split as $s) { 1217 | if (strpos($s, '-') === 0) { 1218 | $options[] = Option::parse($s); 1219 | } 1220 | } 1221 | $defaults = array_merge($defaults, $options); 1222 | } 1223 | 1224 | return new \ArrayIterator($defaults); 1225 | } 1226 | 1227 | /** 1228 | * @param string $name 1229 | * @param string $source 1230 | * @return string[] 1231 | */ 1232 | function parse_section($name, $source) 1233 | { 1234 | $ret = array(); 1235 | if (preg_match_all('@^([^\n]*'.$name.'[^\n]*\n?(?:[ \t].*?(?:\n|$))*)@im', 1236 | $source, $matches, PREG_SET_ORDER)) { 1237 | foreach ($matches as $match) { 1238 | $ret[] = trim($match[0]); 1239 | } 1240 | } 1241 | return $ret; 1242 | } 1243 | 1244 | /** 1245 | * @param string $section 1246 | * @return string 1247 | */ 1248 | function formal_usage($section) 1249 | { 1250 | list (, $section) = explode(':', $section, 2); # drop "usage:" 1251 | $pu = preg_split('/\s+/', trim($section)); 1252 | 1253 | $ret = array(); 1254 | foreach (array_slice($pu, 1) as $s) { 1255 | if ($s == $pu[0]) { 1256 | $ret[] = ') | ('; 1257 | } else { 1258 | $ret[] = $s; 1259 | } 1260 | } 1261 | 1262 | return '( '.implode(' ', $ret).' )'; 1263 | } 1264 | 1265 | /** 1266 | * @param bool $help 1267 | * @param ?string $version 1268 | * @param Pattern[] $argv 1269 | * @param string $doc 1270 | */ 1271 | function extras($help, $version, $argv, $doc) 1272 | { 1273 | $ofound = false; 1274 | $vfound = false; 1275 | foreach ($argv as $o) { 1276 | if ($o->value && ($o->name == '-h' || $o->name == '--help')) { 1277 | $ofound = true; 1278 | } 1279 | if ($o->value && $o->name == '--version') { 1280 | $vfound = true; 1281 | } 1282 | } 1283 | if ($help && $ofound) { 1284 | ExitException::$usage = null; 1285 | throw new ExitException($doc, 0); 1286 | } 1287 | if ($version && $vfound) { 1288 | ExitException::$usage = null; 1289 | throw new ExitException($version, 0); 1290 | } 1291 | } 1292 | 1293 | class Handler 1294 | { 1295 | /** @var bool */ 1296 | public $exit = true; 1297 | 1298 | /** @var bool */ 1299 | public $exitFullUsage = false; 1300 | 1301 | /** @var bool */ 1302 | public $help = true; 1303 | 1304 | /** @var bool */ 1305 | public $optionsFirst = false; 1306 | 1307 | /** @var ?string */ 1308 | public $version; 1309 | 1310 | public function __construct($options=array()) 1311 | { 1312 | foreach ($options as $k=>$v) { 1313 | $this->$k = $v; 1314 | } 1315 | } 1316 | 1317 | /** 1318 | * @param string $doc 1319 | * @param array $argv 1320 | * @return Response 1321 | */ 1322 | function handle($doc, $argv=null) 1323 | { 1324 | try { 1325 | if ($argv === null && isset($_SERVER['argv'])) { 1326 | $argv = array_slice($_SERVER['argv'], 1); 1327 | } 1328 | 1329 | $usageSections = parse_section('usage:', $doc); 1330 | if (count($usageSections) == 0) { 1331 | throw new LanguageError('"usage:" (case-insensitive) not found.'); 1332 | } elseif (count($usageSections) > 1) { 1333 | throw new LanguageError('More than one "usage:" (case-insensitive).'); 1334 | } 1335 | $usage = $usageSections[0]; 1336 | 1337 | // temp fix until python port provides solution 1338 | ExitException::$usage = !$this->exitFullUsage ? $usage : $doc; 1339 | 1340 | $options = parse_defaults($doc); 1341 | 1342 | $formalUse = formal_usage($usage); 1343 | $pattern = parse_pattern($formalUse, $options); 1344 | 1345 | $argv = parse_argv(new Tokens($argv), $options, $this->optionsFirst); 1346 | 1347 | $patternOptions = $pattern->flat('Option'); 1348 | foreach ($pattern->flat('OptionsShortcut') as $optionsShortcut) { 1349 | $docOptions = parse_defaults($doc); 1350 | $optionsShortcut->children = array_diff((array)$docOptions, $patternOptions); 1351 | } 1352 | 1353 | extras($this->help, $this->version, $argv, $doc); 1354 | 1355 | list($matched, $left, $collected) = $pattern->fix()->match($argv); 1356 | if ($matched && !$left) { 1357 | $return = array(); 1358 | foreach (array_merge($pattern->flat(), $collected) as $a) { 1359 | $name = $a->name; 1360 | if ($name) { 1361 | $return[$name] = $a->value; 1362 | } 1363 | } 1364 | return new Response($return); 1365 | } 1366 | throw new ExitException(); 1367 | } 1368 | catch (ExitException $ex) { 1369 | $this->handleExit($ex); 1370 | return new Response(array(), $ex->status, $ex->getMessage()); 1371 | } 1372 | } 1373 | 1374 | function handleExit(ExitException $ex) 1375 | { 1376 | if ($this->exit) { 1377 | echo $ex->getMessage().PHP_EOL; 1378 | exit($ex->status); 1379 | } 1380 | } 1381 | } 1382 | 1383 | class Response implements \ArrayAccess, \IteratorAggregate 1384 | { 1385 | /** @var int */ 1386 | public $status; 1387 | 1388 | /** @var string */ 1389 | public $output; 1390 | 1391 | /** @var array */ 1392 | public $args; 1393 | 1394 | /** 1395 | * @param array $args 1396 | * @param int $status 1397 | * @param string $output 1398 | */ 1399 | public function __construct(array $args, $status=0, $output='') 1400 | { 1401 | $this->args = $args; 1402 | $this->status = $status; 1403 | $this->output = $output; 1404 | } 1405 | 1406 | public function __get($name) 1407 | { 1408 | if ($name == 'success') { 1409 | return $this->status === 0; 1410 | } else { 1411 | throw new \BadMethodCallException("Unknown property $name"); 1412 | } 1413 | } 1414 | 1415 | #[\ReturnTypeWillChange] 1416 | public function offsetExists($offset) 1417 | { 1418 | return isset($this->args[$offset]); 1419 | } 1420 | 1421 | #[\ReturnTypeWillChange] 1422 | public function offsetGet($offset) 1423 | { 1424 | return $this->args[$offset]; 1425 | } 1426 | 1427 | #[\ReturnTypeWillChange] 1428 | public function offsetSet($offset, $value) 1429 | { 1430 | $this->args[$offset] = $value; 1431 | } 1432 | 1433 | #[\ReturnTypeWillChange] 1434 | public function offsetUnset($offset) 1435 | { 1436 | unset($this->args[$offset]); 1437 | } 1438 | 1439 | #[\ReturnTypeWillChange] 1440 | public function getIterator() 1441 | { 1442 | return new \ArrayIterator($this->args); 1443 | } 1444 | } 1445 | 1446 | class SingleMatch 1447 | { 1448 | /** @var ?int */ 1449 | public $pos; 1450 | 1451 | /** @var Pattern */ 1452 | public $pattern; 1453 | 1454 | /** 1455 | * @param ?int $pos 1456 | * @param Pattern $pattern 1457 | */ 1458 | public function __construct($pos, $pattern=null) 1459 | { 1460 | if ($pattern !== null && !$pattern instanceof Pattern) { 1461 | throw new \InvalidArgumentException(); 1462 | } 1463 | 1464 | $this->pos = $pos; 1465 | $this->pattern = $pattern; 1466 | } 1467 | 1468 | public function toArray() { return array($this->pos, $this->pattern); } 1469 | } 1470 | } 1471 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | getMethods() as $method) { 44 | if (substr($method->getName(), 0, 4) !== "test") { 45 | continue; 46 | } 47 | 48 | $tests++; 49 | $name = substr($method->getName(), 4); 50 | if (!$name) $name = $test->name; 51 | 52 | try { 53 | $method->invoke($test); 54 | if ($verbose) { 55 | echo "PASS: {$rc->getName()} {$name}\n"; 56 | } 57 | $passed++; 58 | 59 | } catch (\Docopt\Test\ExpectationFailed $e) { 60 | echo "FAIL: {$rc->getName()} {$name} {$e->getMessage()}\n"; 61 | $details[] = [ 62 | "suite" => $rc->getName(), 63 | "name" => $name, 64 | "message" => $e->getMessage(), 65 | "vardump" => [ 66 | "want" => dump($e->expected), 67 | "got" => dump($e->value), 68 | ], 69 | "serialize" => [ 70 | "want" => serialize($e->expected), 71 | "got" => serialize($e->value), 72 | ], 73 | ]; 74 | $failed++; 75 | } 76 | } 77 | } 78 | 79 | if (count($details)) { 80 | echo "\n"; 81 | echo "Failure details:\n"; 82 | foreach ($details as $failure) { 83 | $dump = $failure[$dumpMode]; 84 | echo "{$failure['suite']} {$failure['name']}\n"; 85 | echo "message: {$failure['message']}\n"; 86 | echo "want: {$dump['want']}\n"; 87 | echo "got: {$dump['got']}\n"; 88 | echo "\n"; 89 | } 90 | } 91 | 92 | echo "{$tests} test(s), {$passed} passed, {$failed} failed\n"; 93 | 94 | if ($failed > 0) { 95 | exit(2); 96 | } 97 | -------------------------------------------------------------------------------- /test/extra.docopt: -------------------------------------------------------------------------------- 1 | # Default option 0 value: 2 | r"""Usage: prog [-a ] 3 | Options: -a An option [default: 0].""" 4 | $ prog 5 | {"-a": "0"} 6 | 7 | 8 | # Default option '' value: 9 | r"""Usage: prog [-a ] 10 | Options: -a An option [default: ].""" 11 | $ prog 12 | {"-a": ""} 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/lib/LanguageAgnosticTest.php: -------------------------------------------------------------------------------- 1 | doc = $doc; 73 | $this->name = $name; 74 | $this->prog = $prog; 75 | $this->argv = $argv; 76 | 77 | if ($expect == "user-error") { 78 | $expect = array('user-error'); 79 | } 80 | 81 | $this->expect = $expect; 82 | } 83 | 84 | public function test() 85 | { 86 | $opt = null; 87 | 88 | try { 89 | $opt = \Docopt::handle($this->doc, array('argv'=>$this->argv, 'exit'=>false)); 90 | } 91 | catch (\Exception $ex) { 92 | // gulp 93 | } 94 | 95 | $found = null; 96 | if ($opt) { 97 | if (!$opt->success) { 98 | $found = array('user-error'); 99 | } elseif (empty($opt->args)) { 100 | $found = array(); 101 | } else { 102 | $found = $opt->args; 103 | } 104 | } 105 | 106 | ksort($this->expect); 107 | array_walk_recursive($this->expect, function($item) { if (is_array($item)) ksort($item); }); 108 | 109 | ksort($found); 110 | array_walk_recursive($found, function($item) { if (is_array($item)) ksort($item); }); 111 | 112 | $this->assertEquals($this->expect, $found); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/lib/PythonPortedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($required->flat(), 21 | array(new Argument('N'), new Option('-a'), new Argument('M')) 22 | ); 23 | } 24 | 25 | function testOption() 26 | { 27 | $this->assertEquals(Option::parse('-h'), new Option('-h')); 28 | $this->assertEquals(Option::parse('--help'), new Option(null, '--help')); 29 | $this->assertEquals(Option::parse('-h --help'), new Option('-h', '--help')); 30 | $this->assertEquals(Option::parse('-h, --help'), new Option('-h', '--help')); 31 | 32 | $this->assertEquals(Option::parse('-h TOPIC'), new Option('-h', null, 1)); 33 | $this->assertEquals(Option::parse('--help TOPIC'), new Option(null, '--help', 1)); 34 | $this->assertEquals(Option::parse('-h TOPIC --help TOPIC'), new Option('-h', '--help', 1)); 35 | $this->assertEquals(Option::parse('-h TOPIC, --help TOPIC'), new Option('-h', '--help', 1)); 36 | $this->assertEquals(Option::parse('-h TOPIC, --help=TOPIC'), new Option('-h', '--help', 1)); 37 | 38 | $this->assertEquals(Option::parse('-h Description...'), new Option('-h', null)); 39 | $this->assertEquals(Option::parse('-h --help Description...'), new Option('-h', '--help')); 40 | $this->assertEquals(Option::parse('-h TOPIC Description...'), new Option('-h', null, 1)); 41 | 42 | $this->assertEquals(Option::parse(' -h'), new Option('-h', null)); 43 | 44 | $this->assertEquals(Option::parse('-h TOPIC Descripton... [default: 2]'), new Option('-h', null, 1, '2')); 45 | $this->assertEquals(Option::parse('-h TOPIC Descripton... [default: topic-1]'), new Option('-h', null, 1, 'topic-1')); 46 | $this->assertEquals(Option::parse('--help=TOPIC ... [default: 3.14]'), new Option(null, '--help', 1, '3.14')); 47 | $this->assertEquals(Option::parse('-h, --help=DIR ... [default: ./]'), new Option('-h', '--help', 1, "./")); 48 | $this->assertEquals(Option::parse('-h TOPIC Descripton... [dEfAuLt: 2]'), new Option('-h', null, 1, '2')); 49 | } 50 | 51 | public function testOptionName() 52 | { 53 | $option = new Option('-h', null); 54 | $this->assertEquals($option->name, '-h'); 55 | 56 | $option = new Option('-h', '--help'); 57 | $this->assertEquals($option->name, '--help'); 58 | 59 | $option = new Option(null, '--help'); 60 | $this->assertEquals($option->name, '--help'); 61 | } 62 | 63 | public function testCommands() 64 | { 65 | $this->assertEquals($this->docopt('Usage: prog add', 'add')->args, array('add' => true)); 66 | 67 | $this->assertEquals($this->docopt('Usage: prog [add]', '')->args, array('add' => false)); 68 | $this->assertEquals($this->docopt('Usage: prog [add]', 'add')->args, array('add' => true)); 69 | $this->assertEquals($this->docopt('Usage: prog (add|rm)', 'add')->args, array('add' => true, 'rm' => false)); 70 | $this->assertEquals($this->docopt('Usage: prog (add|rm)', 'rm')->args, array('add' => false, 'rm' => true)); 71 | $this->assertEquals($this->docopt('Usage: prog a b', 'a b')->args, array('a' => true, 'b' => true)); 72 | 73 | // invalid input exit test 74 | $this->assertEquals($this->docopt('Usage: prog a b', 'b a')->status, 1); 75 | } 76 | 77 | public function testFormalUsage() 78 | { 79 | $doc = 80 | "Usage: prog [-hv] ARG\n" 81 | ." prog N M\n" 82 | ."\n" 83 | ."prog is a program" 84 | ; 85 | 86 | list ($usage, ) = \Docopt\parse_section('usage:', $doc); 87 | 88 | $this->assertEquals($usage, "Usage: prog [-hv] ARG\n prog N M"); 89 | $this->assertEquals(\Docopt\formal_usage($usage), "( [-hv] ARG ) | ( N M )"); 90 | } 91 | 92 | public function testParseArgv() 93 | { 94 | $o = new \ArrayIterator(array(new Option('-h'), new Option('-v', '--verbose'), new Option('-f', '--file', 1))); 95 | $ts = function($s) { return new \Docopt\Tokens($s, 'ExitException'); }; 96 | 97 | $this->assertEquals(\Docopt\parse_argv($ts(''), $o), array()); 98 | $this->assertEquals(\Docopt\parse_argv($ts('-h'), $o), array(new Option('-h', null, 0, true))); 99 | $this->assertEquals(\Docopt\parse_argv($ts('-h --verbose'), $o), array(new Option('-h', null, 0, true), new Option('-v', '--verbose', 0, true))); 100 | $this->assertEquals( 101 | \Docopt\parse_argv($ts('-h --file f.txt'), $o), 102 | array(new Option('-h', null, 0, true), new Option('-f', '--file', 1, 'f.txt')) 103 | ); 104 | $this->assertEquals( 105 | \Docopt\parse_argv($ts('-h --file f.txt arg'), $o), 106 | array(new Option('-h', null, 0, true), 107 | new Option('-f', '--file', 1, 'f.txt'), 108 | new Argument(null, 'arg') 109 | ) 110 | ); 111 | $this->assertEquals( 112 | \Docopt\parse_argv($ts('-h --file f.txt arg arg2'), $o), 113 | array(new Option('-h', null, 0, true), 114 | new Option('-f', '--file', 1, 'f.txt'), 115 | new Argument(null, 'arg'), 116 | new Argument(null, 'arg2') 117 | ) 118 | ); 119 | $this->assertEquals( 120 | \Docopt\parse_argv($ts('-h arg -- -v'), $o), 121 | array( 122 | new Option('-h', null, 0, true), 123 | new Argument(null, 'arg'), 124 | new Argument(null, '--'), 125 | new Argument(null, '-v') 126 | ) 127 | ); 128 | } 129 | 130 | public function testParsePattern() 131 | { 132 | $o = new \ArrayIterator(array(new Option('-h'), new Option('-v', '--verbose'), new Option('-f', '--file', 1))); 133 | $this->assertEquals( 134 | \Docopt\parse_pattern('[ -h ]', $o), 135 | new Required(new Optional(new Option('-h'))) 136 | ); 137 | 138 | $this->assertEquals( 139 | \Docopt\parse_pattern('[ ARG ... ]', $o), 140 | new Required(new Optional(new OneOrMore(new Argument('ARG')))) 141 | ); 142 | $this->assertEquals( 143 | \Docopt\parse_pattern('[ -h | -v ]', $o), 144 | new Required(new Optional( 145 | new Either(new Option('-h'), new Option('-v', '--verbose')) 146 | )) 147 | ); 148 | $this->assertEquals( 149 | \Docopt\parse_pattern('( -h | -v [ --file ] )', $o), 150 | new Required(new Required(new Either(new Option('-h'), new Required(new Option('-v', '--verbose'), new Optional(new Option('-f', '--file', 1, null)))))) 151 | ); 152 | $this->assertEquals( 153 | \Docopt\parse_pattern('(-h|-v[--file=]N...)', $o), 154 | new Required(new Required(new Either(new Option('-h'), 155 | new Required(new Option('-v', '--verbose'), 156 | new Optional(new Option('-f', '--file', 1, null)), 157 | new OneOrMore(new Argument('N')))))) 158 | ); 159 | $this->assertEquals( 160 | \Docopt\parse_pattern('(N [M | (K | L)] | O P)', new \ArrayIterator(array())), 161 | new Required(new Required(new Either(new Required(new Argument('N'), 162 | new Optional(new Either(new Argument('M'), new Required( 163 | new Either(new Argument('K'), new Argument('L')))))), 164 | new Required(new Argument('O'), new Argument('P'))))) 165 | ); 166 | $this->assertEquals(\Docopt\parse_pattern('[ -h ] [N]', $o), 167 | new Required( 168 | new Optional(new Option('-h')), 169 | new Optional(new Argument('N'))) 170 | ); 171 | $this->assertEquals( 172 | \Docopt\parse_pattern('[options]', $o), 173 | new Required(new Optional(new OptionsShortcut())) 174 | ); 175 | $this->assertEquals(\Docopt\parse_pattern('[options] A', $o), 176 | new Required( 177 | new Optional(new OptionsShortcut()), 178 | new Argument('A')) 179 | ); 180 | $this->assertEquals(\Docopt\parse_pattern('-v [options]', $o), 181 | new Required(new Option('-v', '--verbose'), 182 | new Optional(new OptionsShortcut())) 183 | ); 184 | $this->assertEquals(\Docopt\parse_pattern('ADD', $o), new Required(new Argument('ADD'))); 185 | $this->assertEquals(\Docopt\parse_pattern('', $o), new Required(new Argument(''))); 186 | $this->assertEquals(\Docopt\parse_pattern('add', $o), new Required(new Command('add'))); 187 | } 188 | 189 | public function testOptionMatch() 190 | { 191 | $option = new Option('-a'); 192 | $this->assertEquals( 193 | $option->match(array(new Option('-a', null, 0, true))), 194 | array(true, array(), array(new Option('-a', null, 0, true))) 195 | ); 196 | 197 | $option = new Option('-a'); 198 | $this->assertEquals( 199 | $option->match(array(new Option('-x'))), 200 | array(false, array(new Option('-x')), array()) 201 | ); 202 | 203 | $option = new Option('-a'); 204 | $this->assertEquals( 205 | $option->match(array(new Argument('N'))), 206 | array(false, array(new Argument('N')), array()) 207 | ); 208 | 209 | $option = new Option('-a'); 210 | $this->assertEquals( 211 | $option->match(array(new Option('-x'), new Option('-a'), new Argument('N'))), 212 | array(true, array(new Option('-x'), new Argument('N')), array(new Option('-a'))) 213 | ); 214 | 215 | $option = new Option('-a'); 216 | $this->assertEquals( 217 | $option->match(array(new Option('-a', null, 0, true), new Option('-a'))), 218 | array(true, array(new Option('-a')), array(new Option('-a', null, 0, true))) 219 | ); 220 | } 221 | 222 | function testArgumentMatch() 223 | { 224 | $argument = new Argument('N'); 225 | $this->assertEquals($argument->match(array(new Argument(null, 9))), 226 | array(true, array(), array(new Argument('N', 9)))); 227 | 228 | $argument = new Argument('N'); 229 | $this->assertEquals($argument->match(array(new Option('-x'))), 230 | array(false, array(new Option('-x')), array())); 231 | 232 | $argument = new Argument('N'); 233 | $this->assertEquals($argument->match(array(new Option('-x'), 234 | new Option('-a'), 235 | new Argument(null, 5))), 236 | array(true, array(new Option('-x'), new Option('-a')), array(new Argument('N', 5)))); 237 | 238 | $argument = new Argument('N'); 239 | $this->assertEquals($argument->match(array(new Argument(null, 9), new Argument(null, 0))), 240 | array(true, array(new Argument(null, 0)), array(new Argument('N', 9)))); 241 | } 242 | 243 | function testCommandMatch() 244 | { 245 | $command = new Command('c'); 246 | $this->assertEquals( 247 | $command->match(array(new Argument(null, 'c'))), 248 | array(true, array(), array(new Command('c', true))) 249 | ); 250 | 251 | $command = new Command('c'); 252 | $this->assertEquals( 253 | $command->match(array(new Option('-x'))), 254 | array(false, array(new Option('-x')), array()) 255 | ); 256 | 257 | $command = new Command('c'); 258 | $this->assertEquals($command->match(array(new Option('-x'), 259 | new Option('-a'), 260 | new Argument(null, 'c'))), 261 | array(true, array(new Option('-x'), new Option('-a')), array(new Command('c', true))) 262 | ); 263 | 264 | $either = new Either(new Command('add', false), new Command('rm', false)); 265 | $this->assertEquals( 266 | $either->match(array(new Argument(null, 'rm'))), 267 | array(true, array(), array(new Command('rm', true))) 268 | ); 269 | } 270 | 271 | function testOptionalMatch() 272 | { 273 | $optional = new Optional(new Option('-a')); 274 | $this->assertEquals( 275 | $optional->match(array(new Option('-a'))), 276 | array(true, array(), array(new Option('-a'))) 277 | ); 278 | 279 | $optional = new Optional(new Option('-a')); 280 | $this->assertEquals( 281 | $optional->match(array()), 282 | array(true, array(), array()) 283 | ); 284 | 285 | $optional = new Optional(new Option('-a')); 286 | $this->assertEquals( 287 | $optional->match(array(new Option('-x'))), 288 | array(true, array(new Option('-x')), array()) 289 | ); 290 | 291 | $optional = new Optional(new Option('-a'), new Option('-b')); 292 | $this->assertEquals( 293 | $optional->match(array(new Option('-a'))), 294 | array(true, array(), array(new Option('-a'))) 295 | ); 296 | 297 | $optional = new Optional(new Option('-a'), new Option('-b')); 298 | $this->assertEquals( 299 | $optional->match(array(new Option('-b'))), 300 | array(true, array(), array(new Option('-b'))) 301 | ); 302 | 303 | $optional = new Optional(new Option('-a'), new Option('-b')); 304 | $this->assertEquals( 305 | $optional->match(array(new Option('-x'))), 306 | array(true, array(new Option('-x')), array()) 307 | ); 308 | 309 | $optional = new Optional(new Argument('N')); 310 | $this->assertEquals( 311 | $optional->match(array(new Argument(null, 9))), 312 | array(true, array(), array(new Argument('N', 9))) 313 | ); 314 | 315 | $optional = new Optional(new Option('-a'), new Option('-b')); 316 | $this->assertEquals( 317 | $optional->match(array(new Option('-b'), new Option('-x'), new Option('-a'))), 318 | array(true, array(new Option('-x')), array(new Option('-a'), new Option('-b'))) 319 | ); 320 | } 321 | 322 | function testRequiredMatch() 323 | { 324 | $required = new Required(new Option('-a')); 325 | $this->assertEquals($required->match(array(new Option('-a'))), 326 | array(true, array(), array(new Option('-a'))) 327 | ); 328 | 329 | $required = new Required(new Option('-a')); 330 | $this->assertEquals( 331 | $required->match(array()), 332 | array(false, array(), array()) 333 | ); 334 | 335 | $required = new Required(new Option('-a')); 336 | $this->assertEquals( 337 | $required->match(array(new Option('-x'))), 338 | array(false, array(new Option('-x')), array()) 339 | ); 340 | 341 | $required = new Required(new Option('-a'), new Option('-b')); 342 | $this->assertEquals( 343 | $required->match(array(new Option('-a'))), 344 | array(false, array(new Option('-a')), array()) 345 | ); 346 | } 347 | 348 | function testEitherMatch() 349 | { 350 | $either = new Either(new Option('-a'), new Option('-b')); 351 | $this->assertEquals( 352 | $either->match(array(new Option('-a'))), 353 | array(true, array(), array(new Option('-a'))) 354 | ); 355 | 356 | $either = new Either(new Option('-a'), new Option('-b')); 357 | $this->assertEquals( 358 | $either->match(array(new Option('-a'), new Option('-b'))), 359 | array(true, array(new Option('-b')), array(new Option('-a'))) 360 | ); 361 | 362 | $either = new Either(new Option('-a'), new Option('-b')); 363 | $this->assertEquals( 364 | $either->match(array(new Option('-x'))), 365 | array(false, array(new Option('-x')), array()) 366 | ); 367 | 368 | $either = new Either(new Option('-a'), new Option('-b'), new Option('-c')); 369 | $this->assertEquals( 370 | $either->match(array(new Option('-x'), new Option('-b'))), 371 | array(true, array(new Option('-x')), array(new Option('-b'))) 372 | ); 373 | 374 | $either = new Either(new Argument('M'), 375 | new Required(new Argument('N'), new Argument('M'))); 376 | $this->assertEquals( 377 | $either->match(array(new Argument(null, 1), new Argument(null, 2))), 378 | array(true, array(), array(new Argument('N', 1), new Argument('M', 2))) 379 | ); 380 | } 381 | 382 | function testOneOrMoreMatch() 383 | { 384 | $oneOrMore = new OneOrMore(new Argument('N')); 385 | $this->assertEquals($oneOrMore->match(array(new Argument(null, 9))), 386 | array(true, array(), array(new Argument('N', 9))) 387 | ); 388 | 389 | $oneOrMore = new OneOrMore(new Argument('N')); 390 | $this->assertEquals( 391 | $oneOrMore->match(array()), 392 | array(false, array(), array()) 393 | ); 394 | 395 | $oneOrMore = new OneOrMore(new Argument('N')); 396 | $this->assertEquals( 397 | $oneOrMore->match(array(new Option('-x'))), 398 | array(false, array(new Option('-x')), array()) 399 | ); 400 | 401 | $oneOrMore = new OneOrMore(new Argument('N')); 402 | $this->assertEquals( 403 | $oneOrMore->match(array(new Argument(null, 9), new Argument(null, 8))), 404 | array(true, array(), array(new Argument('N', 9), new Argument('N', 8))) 405 | ); 406 | 407 | $oneOrMore = new OneOrMore(new Argument('N')); 408 | $this->assertEquals( 409 | $oneOrMore->match(array(new Argument(null, 9), new Option('-x'), new Argument(null, 8))), 410 | array(true, array(new Option('-x')), array(new Argument('N', 9), new Argument('N', 8))) 411 | ); 412 | 413 | $oneOrMore = new OneOrMore(new Option('-a')); 414 | $this->assertEquals( 415 | $oneOrMore->match(array(new Option('-a'), new Argument(null, 8), new Option('-a'))), 416 | array(true, array(new Argument(null, 8)), array(new Option('-a'), new Option('-a'))) 417 | ); 418 | 419 | $oneOrMore = new OneOrMore(new Option('-a')); 420 | $this->assertEquals( 421 | $oneOrMore->match(array(new Argument(null, 8), new Option('-x'))), 422 | array(false, array(new Argument(null, 8), new Option('-x')), array()) 423 | ); 424 | 425 | $oneOrMore = new OneOrMore(new Required(new Option('-a'), new Argument('N'))); 426 | $this->assertEquals( 427 | $oneOrMore->match(array(new Option('-a'), new Argument(null, 1), new Option('-x'), 428 | new Option('-a'), new Argument(null, 2))), 429 | array(true, array(new Option('-x')), 430 | array(new Option('-a'), new Argument('N', 1), new Option('-a'), new Argument('N', 2))) 431 | ); 432 | 433 | $oneOrMore = new OneOrMore(new Optional(new Argument('N'))); 434 | $this->assertEquals( 435 | $oneOrMore->match(array(new Argument(null, 9))), 436 | array(true, array(), array(new Argument('N', 9))) 437 | ); 438 | } 439 | 440 | function testListArgumentMatch() 441 | { 442 | $input = new Required(new Argument('N'), new Argument('N')); 443 | $this->assertEquals( 444 | $input->fix()->match( 445 | array(new Argument(null, '1'), new Argument(null, '2'))), 446 | array(true, array(), array(new Argument('N', array('1', '2')))) 447 | ); 448 | 449 | $input = new OneOrMore(new Argument('N')); 450 | $this->assertEquals( 451 | $input->fix()->match( 452 | array(new Argument(null, '1'), new Argument(null, '2'), new Argument(null, '3'))), 453 | array(true, array(), array(new Argument('N', array('1', '2', '3')))) 454 | ); 455 | 456 | $input = new Required(new Argument('N'), new OneOrMore(new Argument('N'))); 457 | $this->assertEquals( 458 | $input->fix()->match( 459 | array(new Argument(null, '1'), new Argument(null, '2'), new Argument(null, '3'))), 460 | array(true, array(), array(new Argument('N', array('1', '2', '3')))) 461 | ); 462 | 463 | $input = new Required(new Argument('N'), new Required(new Argument('N'))); 464 | $this->assertEquals( 465 | $input->fix()->match( 466 | array(new Argument(null, '1'), new Argument(null, '2'))), 467 | array(true, array(), array(new Argument('N', array('1', '2')))) 468 | ); 469 | } 470 | 471 | function testBasicPatternMatching() 472 | { 473 | # ( -a N [ -x Z ] ) 474 | $pattern = new Required(new Option('-a'), new Argument('N'), 475 | new Optional(new Option('-x'), new Argument('Z'))) 476 | ; 477 | # -a N 478 | $this->assertEquals($pattern->match(array(new Option('-a'), new Argument(null, 9))), 479 | array(true, array(), array(new Option('-a'), new Argument('N', 9))) 480 | ); 481 | # -a -x N Z 482 | $this->assertEquals($pattern->match(array(new Option('-a'), new Option('-x'), 483 | new Argument(null, 9), new Argument(null, 5))), 484 | array(true, array(), array(new Option('-a'), new Argument('N', 9), 485 | new Option('-x'), new Argument('Z', 5))) 486 | ); 487 | # -x N Z # BZZ! 488 | $this->assertEquals($pattern->match(array(new Option('-x'), 489 | new Argument(null, 9), 490 | new Argument(null, 5))), 491 | array(false, array(new Option('-x'), new Argument(null, 9), new Argument(null, 5)), array()) 492 | ); 493 | } 494 | 495 | function testPatternEither() 496 | { 497 | $input = new Option('-a'); 498 | $this->assertEquals( 499 | \Docopt\transform($input), 500 | new Either(new Required(new Option('-a'))) 501 | ); 502 | 503 | $input = new Argument('A'); 504 | $this->assertEquals( 505 | \Docopt\transform($input), 506 | new Either(new Required(new Argument('A'))) 507 | ); 508 | 509 | // XXX 20230322: This test has not passed since at least 2017, using PHP 5, 7 or 8: 510 | // $input = new Required(new Either(new Option('-a'), new Option('-b')), 511 | // new Option('-c')); 512 | // $this->assertEquals( 513 | // \Docopt\transform($input), 514 | // new Either(new Required(new Option('-a'), new Option('-c')), 515 | // new Required(new Option('-b'), new Option('-c'))) 516 | // ); 517 | 518 | // XXX 20230322: This test has not passed since at least 2017, using PHP 5, 7 or 8: 519 | // $input = new Optional(new Option('-a'), 520 | // new Either(new Option('-b'), 521 | // new Option('-c'))); 522 | // $this->assertEquals( 523 | // \Docopt\transform($input), 524 | // new Either(new Required(new Option('-b'), new Option('-a')), 525 | // new Required(new Option('-c'), new Option('-a'))) 526 | // ); 527 | 528 | $input = new Either(new Option('-x'), new Either(new Option('-y'), new Option('-z'))); 529 | $this->assertEquals( 530 | \Docopt\transform($input), 531 | new Either(new Required(new Option('-x')), 532 | new Required(new Option('-y')), 533 | new Required(new Option('-z'))) 534 | ); 535 | 536 | // XXX 20230322: This test has not passed since at least 2017, using PHP 5, 7 or 8: 537 | // $input = new OneOrMore(new Argument('N'), new Argument('M')); 538 | // $this->assertEquals( 539 | // \Docopt\transform($input), 540 | // new Either(new Required(new Argument('N'), new Argument('M'), 541 | // new Argument('N'), new Argument('M'))) 542 | // ); 543 | } 544 | 545 | function testPatternFixRepeatingArguments() 546 | { 547 | $input = new Option('-a'); 548 | $this->assertEquals($input->fixRepeatingArguments(), new Option('-a')); 549 | 550 | $input = new Argument('N', null); 551 | $this->assertEquals($input->fixRepeatingArguments(), new Argument('N', null)); 552 | 553 | $input = new Required(new Argument('N'), new Argument('N')); 554 | $this->assertEquals( 555 | $input->fixRepeatingArguments(), 556 | new Required(new Argument('N', array()), new Argument('N', array())) 557 | ); 558 | 559 | // XXX 20230322: This test has not passed since at least 2017, using PHP 5, 7 or 8: 560 | // $input = new Either(new Argument('N'), new OneOrMore(new Argument('N'))); 561 | // $this->assertEquals( 562 | // $input->fix(), 563 | // new Either(new Argument('N', array()), new OneOrMore(new Argument('N', array()))) 564 | // ); 565 | } 566 | 567 | function testSet() 568 | { 569 | $this->assertEquals(new Argument('N'), new Argument('N')); 570 | $this->assertEquals( 571 | array_unique(array(new Argument('N'), new Argument('N'))), 572 | array(new Argument('N')) 573 | ); 574 | } 575 | 576 | function testPatternFixIdentities1() 577 | { 578 | $pattern = new Required(new Argument('N'), new Argument('N')); 579 | $this->assertEquals($pattern->children[0], $pattern->children[1]); 580 | $this->assertNotSame($pattern->children[0], $pattern->children[1]); 581 | $pattern->fixIdentities(); 582 | $this->assertSame($pattern->children[0], $pattern->children[1]); 583 | } 584 | 585 | function testPatternFixIdentities2() 586 | { 587 | $pattern = new Required(new Optional(new Argument('X'), new Argument('N')), new Argument('N')); 588 | $this->assertEquals($pattern->children[0]->children[1], $pattern->children[1]); 589 | $this->assertNotSame($pattern->children[0]->children[1], $pattern->children[1]); 590 | $pattern->fixIdentities(); 591 | $this->assertSame($pattern->children[0]->children[1], $pattern->children[1]); 592 | } 593 | 594 | function testLongOptionsErrorHandling() 595 | { 596 | # $this->setExpectedException('Docopt\LanguageError'); 597 | # $this->docopt('Usage: prog --non-existent', '--non-existent') 598 | # $this->setExpectedException('Docopt\LanguageError'); 599 | # $this->docopt('Usage: prog --non-existent') 600 | $result = $this->docopt('Usage: prog', '--non-existent'); 601 | $this->assertFalse($result->success); 602 | 603 | $result = $this->docopt("Usage: prog [--version --verbose]\n". 604 | "Options: --version\n --verbose", '--ver'); 605 | 606 | $this->assertFalse($result->success); 607 | } 608 | 609 | function testLongOptionsErrorHandlingPart2() 610 | { 611 | $this->expectException('Docopt\LanguageError', function() { 612 | $result = $this->docopt("Usage: prog --long\nOptions: --long ARG"); 613 | }); 614 | } 615 | 616 | function testLongOptionsErrorHandlingPart3() 617 | { 618 | $result = $this->docopt("Usage: prog --long ARG\nOptions: --long ARG", '--long'); 619 | $this->assertFalse($result->success); 620 | } 621 | 622 | function testLongOptionsErrorHandlingPart4() 623 | { 624 | $this->expectException('Docopt\LanguageError', function() { 625 | $result = $this->docopt("Usage: prog --long=ARG\nOptions: --long"); 626 | }); 627 | } 628 | 629 | function testLongOptionsErrorHandlingPart5() 630 | { 631 | $result = $this->docopt("Usage: prog --long\nOptions: --long", '--long=ARG'); 632 | $this->assertFalse($result->success); 633 | } 634 | 635 | 636 | function testShortOptionsErrorHandlingPart1() 637 | { 638 | $this->expectException('Docopt\LanguageError', function() { 639 | $this->docopt("Usage: prog -x\nOptions: -x this\n -x that"); 640 | }); 641 | } 642 | 643 | function testShortOptionsErrorHandlingPart2() 644 | { 645 | $result = $this->docopt('Usage: prog', '-x'); 646 | $this->assertFalse($result->success); 647 | } 648 | 649 | function testShortOptionsErrorHandlingPart3() 650 | { 651 | $this->expectException('Docopt\LanguageError', function() { 652 | $this->docopt("Usage: prog -o\nOptions: -o ARG"); 653 | }); 654 | } 655 | 656 | function testShortOptionsErrorHandlingPart4() 657 | { 658 | $result = $this->docopt("Usage: prog -o ARG\n\n-o ARG", '-o'); 659 | $this->assertFalse($result->success); 660 | } 661 | 662 | function testMatchingParenPart1() 663 | { 664 | $this->expectException('Docopt\LanguageError', function() { 665 | $this->docopt('Usage: prog [a [b]'); 666 | }); 667 | } 668 | 669 | function testMatchingParenPart2() 670 | { 671 | $this->expectException('Docopt\LanguageError', function() { 672 | $this->docopt('Usage: prog [a [b] ] c )'); 673 | }); 674 | } 675 | 676 | function testAllowDoubleDash() 677 | { 678 | $this->assertEquals($this->docopt("usage: prog [-o] [--] \nOptions: -o", 679 | '-- -o')->args, array('-o'=> false, '--'=>true, ''=>'-o') 680 | ); 681 | $this->assertEquals($this->docopt("usage: prog [-o] [--] \nOptions: -o", 682 | '-o 1')->args, array('-o'=>true, '--'=>false, ''=>'1') 683 | ); 684 | 685 | $result = $this->docopt("usage: prog [-o] \nOptions: -o", '-- -o'); # "--" is not allowed; FIXME? 686 | $this->assertFalse($result->success); 687 | } 688 | 689 | function testDocopt() 690 | { 691 | $doc = "Usage: prog [-v] A\n\n Options: -v Be verbose."; 692 | 693 | $this->assertEquals($this->docopt($doc, 'arg')->args, array('-v'=>false, 'A'=>'arg')); 694 | $this->assertEquals($this->docopt($doc, '-v arg')->args, array('-v'=>true, 'A'=>'arg')); 695 | 696 | $doc = "Usage: prog [-vqr] [FILE] 697 | prog INPUT OUTPUT 698 | prog --help 699 | 700 | Options: 701 | -v print status messages 702 | -q report only file names 703 | -r show all occurrences of the same error 704 | --help 705 | 706 | "; 707 | $a = $this->docopt($doc, '-v file.py'); 708 | $this->assertEquals($a->args, array( 709 | '-v'=>true, '-q'=>false, '-r'=>false, 710 | 'FILE'=>'file.py', 'INPUT'=>null, 'OUTPUT'=>null, '--help'=>false)); 711 | 712 | $a = $this->docopt($doc, '-v'); 713 | $this->assertEquals($a->args, array( 714 | '-v'=>true, '-q'=>false, '-r'=>false, 715 | 'FILE'=>null, 'INPUT'=>null, 'OUTPUT'=>null, '--help'=>false)); 716 | 717 | $result = $this->docopt($doc, '-v input.py output.py'); 718 | $this->assertFalse($result->success); 719 | 720 | $result = $this->docopt($doc, '--fake'); 721 | $this->assertFalse($result->success); 722 | 723 | $result = $this->docopt($doc, '--hel'); 724 | $this->assertTrue($result['--help']); 725 | } 726 | 727 | function testLanguageErrors() 728 | { 729 | $this->expectException('Docopt\LanguageError', function() { 730 | $this->docopt('no usage with colon here'); 731 | }); 732 | } 733 | 734 | function testLanguageErrorsPart2() 735 | { 736 | $this->expectException('Docopt\LanguageError', function() { 737 | $this->docopt("usage: here \n\n and again usage: here"); 738 | }); 739 | } 740 | 741 | function testIssue40() 742 | { 743 | $result = $this->docopt('usage: prog --help-commands | --help', '--help'); 744 | $this->assertTrue($result['--help']); 745 | 746 | $this->assertEquals($this->docopt('usage: prog --aabb | --aa', '--aa')->args, array('--aabb'=>false, 747 | '--aa'=>true)); 748 | } 749 | 750 | function testCountMultipleFlags() 751 | { 752 | $this->assertEquals($this->docopt('usage: prog [-v]', '-v')->args, array('-v'=>true)); 753 | $this->assertEquals($this->docopt('usage: prog [-vv]', '')->args, array('-v'=>0)); 754 | $this->assertEquals($this->docopt('usage: prog [-vv]', '-v')->args, array('-v'=>1)); 755 | $this->assertEquals($this->docopt('usage: prog [-vv]', '-vv')->args, array('-v'=>2)); 756 | $this->assertEquals($this->docopt('usage: prog [-vv]', '-v -v')->args, array('-v'=>2)); 757 | 758 | $this->assertFalse($this->docopt('usage: prog [-vv]', '-vvv')->success); 759 | 760 | $this->assertEquals($this->docopt('usage: prog [-v | -vv | -vvv]', '-vvv')->args, array('-v'=>3)); 761 | $this->assertEquals($this->docopt('usage: prog -v...', '-vvvvvv')->args, array('-v'=>6)); 762 | $this->assertEquals($this->docopt('usage: prog [--ver --ver]', '--ver --ver')->args, array('--ver'=>2)); 763 | } 764 | 765 | function testOptionsShortcutParameter() 766 | { 767 | $result = $this->docopt('usage: prog [options]', '-foo --bar --spam=eggs'); 768 | $this->assertFalse($result->success); 769 | 770 | # $this->assertEquals($this->docopt('usage: prog [options]', '-foo --bar --spam=eggs', 771 | # any_options=true), array('-f'=>true, '-o'=>2, 772 | # '--bar'=>true, '--spam'=>'eggs'} 773 | $result = $this->docopt('usage: prog [options]', '--foo --bar --bar'); 774 | $this->assertFalse($result->success); 775 | 776 | # $this->assertEquals($this->docopt('usage: prog [options]', '--foo --bar --bar', 777 | # any_options=true), array('--foo'=>true, '--bar'=>2} 778 | $result = $this->docopt('usage: prog [options]', '--bar --bar --bar -ffff'); 779 | $this->assertFalse($result->success); 780 | 781 | # $this->assertEquals($this->docopt('usage: prog [options]', '--bar --bar --bar -ffff', 782 | # any_options=true), array('--bar'=>3, '-f'=>4} 783 | $result = $this->docopt('usage: prog [options]', '--long=arg --long=another'); 784 | $this->assertFalse($result->success); 785 | 786 | # $this->assertEquals($this->docopt('usage: prog [options]', '--long=arg --long=another', 787 | # any_options=true), array('--long'=>['arg', 'another']} 788 | } 789 | 790 | 791 | #def test_options_shortcut_multiple_commands(): 792 | # # any_options is disabled 793 | # $this->assertEquals($this->docopt('usage: prog c1 [options] prog c2 [options]', 794 | # 'c2 -o', any_options=true), array('-o'=>true, 'c1'=>false, 'c2'=>true} 795 | # $this->assertEquals($this->docopt('usage: prog c1 [options] prog c2 [options]', 796 | # 'c1 -o', any_options=true), array('-o'=>true, 'c1'=>true, 'c2'=>false} 797 | 798 | 799 | // removed in the python version for some reason 800 | public function testOptionsShortcutDoesNotAddOptionsToPatternSecondTime() 801 | { 802 | $this->assertEquals($this->docopt("usage: prog [options] [-a]\nOptions: -a -b", '-a')->args, 803 | array('-b'=>false, '-a'=>true)); 804 | 805 | $result = $this->docopt("usage: prog [options] [-a]\nOptions: -a -b", '-aa'); 806 | $this->assertFalse($result->success); 807 | } 808 | 809 | function testDefaultValueForPositionalArguments() 810 | { 811 | $doc = "Usage: prog [--data=...]\n". 812 | "Options:\n\t-d --data= Input data [default: x]"; 813 | $a = $this->docopt($doc, '')->args; 814 | $this->assertEquals($a, array('--data'=>array('x'))); 815 | 816 | $doc = "Usage: prog [--data=...]\n". 817 | "Options:\n\t-d --data= Input data [default: x y]"; 818 | $a = $this->docopt($doc, '')->args; 819 | $this->assertEquals($a, array('--data'=>array('x', 'y'))); 820 | 821 | $doc = "Usage: prog [--data=...]\n". 822 | "Options:\n\t-d --data= Input data [default: x y]"; 823 | $a = $this->docopt($doc, '--data=this')->args; 824 | $this->assertEquals($a, array('--data'=>array('this'))); 825 | 826 | /* Doesn't work. 827 | $doc = "Usage: prog [--data=...]\n". 828 | "Options:\n\t-d --data= Input data [default: \"hello world\"]"; 829 | $a = $this->docopt($doc, '')->args; 830 | $this->assertEquals($a, ['--data'=>['hello world']]); 831 | */ 832 | } 833 | 834 | #def test_parse_defaults(): 835 | # $this->assertEquals(parse_defaults("""usage: prog 836 | # 837 | # -o, --option 838 | # --another description 839 | # [default: x] 840 | # 841 | # description [default: y]"""), 842 | # ([new Option('-o', '--option', 1, null), 843 | # new Option(null, '--another', 1, 'x')], 844 | # [new Argument('', null), 845 | # new Argument('', 'y')]) 846 | # 847 | # doc = ''' 848 | # -h, --help Print help message. 849 | # -o FILE Output file. 850 | # --verbose Verbose mode.''' 851 | # $this->assertEquals(parse_defaults(doc)[0], [new Option('-h', '--help'), 852 | # new Option('-o', null, 1), 853 | # new Option(null, '--verbose')] 854 | 855 | public function testIssue59() 856 | { 857 | $this->assertEquals($this->docopt("usage: prog --long=", '--long=')->args, array('--long'=>'')); 858 | $this->assertEquals($this->docopt("usage: prog -l \noptions: -l ", array('-l', ''))->args, array('-l'=>'')); 859 | } 860 | 861 | public function testOptionsFirst() 862 | { 863 | $this->assertEquals( 864 | $this->docopt('usage: prog [--opt] [...]', '--opt this that')->args, 865 | array('--opt'=>true, ''=>array('this', 'that')) 866 | ); 867 | 868 | $this->assertEquals( 869 | $this->docopt('usage: prog [--opt] [...]', 'this that --opt')->args, 870 | array('--opt'=>true, ''=>array('this', 'that')) 871 | ); 872 | 873 | $this->assertEquals( 874 | $this->docopt('usage: prog [--opt] [...]', 'this that --opt', array('optionsFirst'=>true))->args, 875 | array('--opt'=>false, ''=>array('this', 'that', '--opt')) 876 | ); 877 | 878 | // found issue with PHP version in this situation 879 | $this->assertEquals( 880 | $this->docopt('usage: prog [--opt=] [...]', ' --opt=foo this that --opt', array('optionsFirst'=>true))->args, 881 | array('--opt'=>'foo', ''=>array('this', 'that', '--opt')) 882 | ); 883 | } 884 | 885 | public function testIssue68OptionsShortcutDoesNotIncludeOptionsInUsagePattern() 886 | { 887 | $args = $this->docopt("usage: prog [-ab] [options]\noptions: -x\n -y", '-ax'); 888 | $this->assertTrue($args['-a']); 889 | $this->assertFalse($args['-b']); 890 | $this->assertTrue($args['-x']); 891 | $this->assertFalse($args['-y']); 892 | } 893 | 894 | public function testIssue71DoubleDashIsNotAValidOptionArgument() 895 | { 896 | $result = $this->docopt("usage: prog [--log=LEVEL] [--] ...", "--log -- 1 2"); 897 | $this->assertFalse($result->success); 898 | 899 | $result = $this->docopt("usage: prog [-l LEVEL] [--] ...\noptions: -l LEVEL", "-l -- 1 2"); 900 | $this->assertFalse($result->success); 901 | } 902 | 903 | public function testParseSection() 904 | { 905 | $this->assertEquals(\Docopt\parse_section('usage:', 'foo bar fizz buzz'), array()); 906 | $this->assertEquals(\Docopt\parse_section('usage:', 'usage: prog'), array('usage: prog')); 907 | $this->assertEquals(\Docopt\parse_section('usage:', "usage: -x\n -y"), array("usage: -x\n -y")); 908 | 909 | $usage = <<assertEquals(\Docopt\parse_section("usage:", $usage), array( 930 | "usage: this", 931 | "usage:hai", 932 | "usage: this that", 933 | "usage: foo\n bar", 934 | "PROGRAM USAGE:\n foo\n bar", 935 | "usage:\n\ttoo\n\ttar", 936 | "Usage: eggs spam", 937 | "usage: pit stop", 938 | )); 939 | } 940 | 941 | public function testIssue126DefaultsNotParsedCorrectlyWhenTabs() 942 | { 943 | $section = "Options:\n\t--foo= [default: bar]"; 944 | $this->assertEquals(\Docopt\parse_defaults($section)->getArrayCopy(), array(new Option(null, '--foo', 1, 'bar'))); 945 | } 946 | } 947 | -------------------------------------------------------------------------------- /test/lib/TestCase.php: -------------------------------------------------------------------------------- 1 | false, 'help'=>false), $extra); 13 | $handler = new \Docopt\Handler($extra); 14 | return call_user_func(array($handler, 'handle'), $usage, $args); 15 | } 16 | 17 | protected function assertEquals($expected, $found) { 18 | if (serialize($expected) !== serialize($found)) { 19 | throw new ExpectationFailed($expected, $found, "expected equal, found not equal"); 20 | } 21 | } 22 | 23 | protected function assertFalse($value) { 24 | if ($value !== false) { 25 | throw new ExpectationFailed(false, $value, "expected value to be false"); 26 | } 27 | } 28 | 29 | protected function assertTrue($value) { 30 | if ($value !== true) { 31 | throw new ExpectationFailed(false, $value, "expected value to be true"); 32 | } 33 | } 34 | 35 | protected function assertNotEquals($expected, $found) { 36 | if (serialize($expected) === serialize($found)) { 37 | throw new ExpectationFailed($expected, $found, "expected values to not be equal"); 38 | } 39 | } 40 | 41 | protected function assertSame($expected, $found) { 42 | if ($expected !== $found) { 43 | throw new ExpectationFailed($expected, $found, "expected values to be the same instance"); 44 | } 45 | } 46 | 47 | protected function assertNotSame($expected, $found) { 48 | if ($expected === $found) { 49 | throw new ExpectationFailed($expected, $found, "expected values not to be the same instance"); 50 | } 51 | } 52 | 53 | protected function expectException($name, $impl) { 54 | $found = null; 55 | try { 56 | call_user_func($impl); 57 | } catch (\Exception $e) { 58 | $found = $e; 59 | } 60 | if ($found === null) { 61 | throw new ExpectationFailed($name, $found, "expected exception, but none thrown"); 62 | } else if ((new \ReflectionClass($name))->getName() !== (new \ReflectionClass($found))->getName()) { 63 | throw new ExpectationFailed($name, $found, "exception did not match expected exception"); 64 | } 65 | } 66 | } 67 | 68 | class ExpectationFailed extends \RuntimeException { 69 | public function __construct($expected, $value, $message) { 70 | parent::__construct($message); 71 | $this->expected = $expected; 72 | $this->value = $value; 73 | } 74 | } 75 | --------------------------------------------------------------------------------