1<?php 2/** 3 * Command-line interface parser that will make you smile. 4 * 5 * - http://docopt.org 6 * - Repository and issue-tracker: https://github.com/docopt/docopt.php 7 * - Licensed under terms of MIT license (see LICENSE-MIT) 8 * - Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com 9 * Blake Williams, <code@shabbyrobe.org> 10 */ 11 12namespace Docopt; 13 14/** 15 * Return true if all cased characters in the string are uppercase and there is 16 * at least one cased character, false otherwise. 17 * Python method with no known equivalent in PHP. 18 */ 19function is_upper($string) 20{ 21 return preg_match('/[A-Z]/', $string) && !preg_match('/[a-z]/', $string); 22} 23 24/** 25 * Return True if any element of the iterable is true. If the iterable is empty, return False. 26 * Python method with no known equivalent in PHP. 27 */ 28function any($iterable) 29{ 30 foreach ($iterable as $element) { 31 if ($element) 32 return true; 33 } 34 return false; 35} 36 37/** 38 * The PHP version of this function doesn't work properly if the values aren't scalar. 39 */ 40function array_count_values($array) 41{ 42 $counts = array(); 43 foreach ($array as $v) { 44 if ($v && is_scalar($v)) 45 $key = $v; 46 elseif (is_object($v)) 47 $key = spl_object_hash($v); 48 else 49 $key = serialize($v); 50 51 if (!isset($counts[$key])) 52 $counts[$key] = array($v, 1); 53 else 54 $counts[$key][1]++; 55 } 56 return $counts; 57} 58 59/** 60 * The PHP version of this doesn't support array iterators 61 */ 62function array_filter($input, $callback, $reKey=false) 63{ 64 if ($input instanceof \ArrayIterator) 65 $input = $input->getArrayCopy(); 66 67 $filtered = \array_filter($input, $callback); 68 if ($reKey) $filtered = array_values($filtered); 69 return $filtered; 70} 71 72/** 73 * The PHP version of this doesn't support array iterators 74 */ 75function array_merge() 76{ 77 $values = func_get_args(); 78 $resolved = array(); 79 foreach ($values as $v) { 80 if ($v instanceof \ArrayIterator) 81 $resolved[] = $v->getArrayCopy(); 82 else 83 $resolved[] = $v; 84 } 85 return call_user_func_array('array_merge', $resolved); 86} 87 88function ends_with($str, $test) 89{ 90 $len = strlen($test); 91 return substr_compare($str, $test, -$len, $len) === 0; 92} 93 94function get_class_name($obj) 95{ 96 $cls = get_class($obj); 97 return substr($cls, strpos($cls, '\\')+1); 98} 99 100function dump($val) 101{ 102 if (is_array($val) || $val instanceof \Traversable) { 103 echo '['; 104 $cur = array(); 105 foreach ($val as $i) 106 $cur[] = $i->dump(); 107 echo implode(', ', $cur); 108 echo ']'; 109 } 110 else 111 echo $val->dump(); 112} 113 114function dump_scalar($scalar) 115{ 116 if ($scalar === null) 117 return 'None'; 118 elseif ($scalar === false) 119 return 'False'; 120 elseif ($scalar === true) 121 return 'True'; 122 elseif (is_int($scalar) || is_float($scalar)) 123 return $scalar; 124 else 125 return "'$scalar'"; 126} 127 128/** 129 * Error in construction of usage-message by developer 130 */ 131class LanguageError extends \Exception 132{ 133} 134 135/** 136 * Exit in case user invoked program with incorrect arguments. 137 * DocoptExit equivalent. 138 */ 139class ExitException extends \RuntimeException 140{ 141 public static $usage; 142 143 public $status; 144 145 public function __construct($message=null, $status=1) 146 { 147 parent::__construct(trim($message.PHP_EOL.static::$usage)); 148 $this->status = $status; 149 } 150} 151 152class Pattern 153{ 154 public function __toString() 155 { 156 return serialize($this); 157 } 158 159 public function hash() 160 { 161 return crc32((string)$this); 162 } 163 164 public function fix() 165 { 166 $this->fixIdentities(); 167 $this->fixRepeatingArguments(); 168 return $this; 169 } 170 171 /** 172 * Make pattern-tree tips point to same object if they are equal. 173 */ 174 public function fixIdentities($uniq=null) 175 { 176 if (!isset($this->children) || !$this->children) 177 return $this; 178 179 if (!$uniq) { 180 $uniq = array_unique($this->flat()); 181 } 182 183 foreach ($this->children as $i=>$c) { 184 if (!$c instanceof ParentPattern) { 185 if (!in_array($c, $uniq)) { 186 // Not sure if this is a true substitute for 'assert c in uniq' 187 throw new \UnexpectedValueException(); 188 } 189 $this->children[$i] = $uniq[array_search($c, $uniq)]; 190 } 191 else { 192 $c->fixIdentities($uniq); 193 } 194 } 195 } 196 197 /** 198 * Fix elements that should accumulate/increment values. 199 */ 200 public function fixRepeatingArguments() 201 { 202 $either = array(); 203 foreach ($this->either()->children as $c) { 204 $either[] = $c->children; 205 } 206 207 foreach ($either as $case) { 208 $case = array_map( 209 function($value) { return $value[0]; }, 210 array_filter(array_count_values($case), function($value) { return $value[1] > 1; }) 211 ); 212 213 foreach ($case as $e) { 214 if ($e instanceof Argument || ($e instanceof Option && $e->argcount)) { 215 if (!$e->value) 216 $e->value = array(); 217 elseif (!is_array($e->value) && !$e->value instanceof \Traversable) 218 $e->value = preg_split('/\s+/', $e->value); 219 } 220 if ($e instanceof Command || ($e instanceof Option && $e->argcount == 0)) 221 $e->value = 0; 222 } 223 } 224 225 return $this; 226 } 227 228 /** 229 * Transform pattern into an equivalent, with only top-level Either. 230 */ 231 public function either() 232 { 233 // Currently the pattern will not be equivalent, but more "narrow", 234 // although good enough to reason about list arguments. 235 $ret = array(); 236 $groups = array(array($this)); 237 while ($groups) { 238 $children = array_pop($groups); 239 $types = array(); 240 foreach ($children as $c) { 241 if (is_object($c)) { 242 $cls = get_class($c); 243 $types[] = substr($cls, strrpos($cls, '\\')+1); 244 } 245 } 246 247 if (in_array('Either', $types)) { 248 $either = null; 249 foreach ($children as $c) { 250 if ($c instanceof Either) { 251 $either = $c; 252 break; 253 } 254 } 255 256 unset($children[array_search($either, $children)]); 257 foreach ($either->children as $c) { 258 $groups[] = array_merge(array($c), $children); 259 } 260 } 261 elseif (in_array('Required', $types)) { 262 $required = null; 263 foreach ($children as $c) { 264 if ($c instanceof Required) { 265 $required = $c; 266 break; 267 } 268 } 269 unset($children[array_search($required, $children)]); 270 $groups[] = array_merge($required->children, $children); 271 } 272 elseif (in_array('Optional', $types)) { 273 $optional = null; 274 foreach ($children as $c) { 275 if ($c instanceof Optional) { 276 $optional = $c; 277 break; 278 } 279 } 280 unset($children[array_search($optional, $children)]); 281 $groups[] = array_merge($optional->children, $children); 282 } 283 elseif (in_array('AnyOptions', $types)) { 284 $optional = null; 285 foreach ($children as $c) { 286 if ($c instanceof AnyOptions) { 287 $optional = $c; 288 break; 289 } 290 } 291 unset($children[array_search($optional, $children)]); 292 $groups[] = array_merge($optional->children, $children); 293 } 294 elseif (in_array('OneOrMore', $types)) { 295 $oneormore = null; 296 foreach ($children as $c) { 297 if ($c instanceof OneOrMore) { 298 $oneormore = $c; 299 break; 300 } 301 } 302 unset($children[array_search($oneormore, $children)]); 303 $groups[] = array_merge($oneormore->children, $oneormore->children, $children); 304 } 305 else { 306 $ret[] = $children; 307 } 308 } 309 310 $rs = array(); 311 foreach ($ret as $e) { 312 $rs[] = new Required($e); 313 } 314 return new Either($rs); 315 } 316 317 public function name() 318 {} 319 320 public function __get($name) 321 { 322 if ($name == 'name') 323 return $this->name(); 324 else 325 throw new \BadMethodCallException("Unknown property $name"); 326 } 327} 328 329class ChildPattern extends Pattern 330{ 331 public function flat($types=array()) 332 { 333 $types = is_array($types) ? $types : array($types); 334 335 if (!$types || in_array(get_class_name($this), $types)) 336 return array($this); 337 else 338 return array(); 339 } 340 341 public function match($left, $collected=null) 342 { 343 if (!$collected) $collected = array(); 344 345 list ($pos, $match) = $this->singleMatch($left); 346 if (!$match) 347 return array(false, $left, $collected); 348 349 $left_ = $left; 350 unset($left_[$pos]); 351 $left_ = array_values($left_); 352 353 $name = $this->name; 354 $sameName = array_filter($collected, function ($a) use ($name) { return $name == $a->name; }, true); 355 356 if (is_int($this->value) || is_array($this->value) || $this->value instanceof \Traversable) { 357 if (is_int($this->value)) 358 $increment = 1; 359 else 360 $increment = is_string($match->value) ? array($match->value) : $match->value; 361 362 if (!$sameName) { 363 $match->value = $increment; 364 return array(true, $left_, array_merge($collected, array($match))); 365 } 366 367 if (is_array($increment) || $increment instanceof \Traversable) 368 $sameName[0]->value = array_merge($sameName[0]->value, $increment); 369 else 370 $sameName[0]->value += $increment; 371 372 return array(true, $left_, $collected); 373 } 374 375 return array(true, $left_, array_merge($collected, array($match))); 376 } 377} 378 379class ParentPattern extends Pattern 380{ 381 public $children = array(); 382 383 public function __construct($children=null) 384 { 385 if (!$children) 386 $children = array(); 387 elseif ($children instanceof Pattern) 388 $children = array($children); 389 390 foreach ($children as $c) { 391 $this->children[] = $c; 392 } 393 } 394 395 public function flat($types=array()) 396 { 397 $types = is_array($types) ? $types : array($types); 398 if (in_array(get_class_name($this), $types)) 399 return array($this); 400 401 $flat = array(); 402 foreach ($this->children as $c) { 403 $flat = array_merge($flat, $c->flat($types)); 404 } 405 return $flat; 406 } 407 408 public function dump() 409 { 410 $out = get_class_name($this).'('; 411 $cd = array(); 412 foreach ($this->children as $c) { 413 $cd[] = $c->dump(); 414 } 415 $out .= implode(', ', $cd).')'; 416 return $out; 417 } 418} 419 420class Argument extends ChildPattern 421{ 422 public $name; 423 public $value; 424 425 public function __construct($name, $value=null) 426 { 427 $this->name = $name; 428 $this->value = $value; 429 } 430 431 public function singleMatch($left) 432 { 433 foreach ($left as $n=>$p) { 434 if ($p instanceof Argument) { 435 return array($n, new Argument($this->name, $p->value)); 436 } 437 } 438 439 return array(null, null); 440 } 441 442 public static function parse($source) 443 { 444 $name = null; 445 $value = null; 446 447 if (preg_match_all('@(<\S*?>)@', $source, $matches)) { 448 $name = $matches[0][0]; 449 } 450 if (preg_match_all('@\[default: (.*)\]@i', $source, $matches)) { 451 $value = $matches[0][1]; 452 } 453 454 return new static($name, $value); 455 } 456 457 public function dump() 458 { 459 return "Argument('".dump_scalar($this->name)."', ".dump_scalar($this->value)."')"; 460 } 461} 462 463class Command extends Argument 464{ 465 public $name; 466 public $value; 467 468 public function __construct($name, $value=false) 469 { 470 $this->name = $name; 471 $this->value = $value; 472 } 473 474 function singleMatch($left) 475 { 476 foreach ($left as $n=>$p) { 477 if ($p instanceof Argument) { 478 if ($p->value == $this->name) 479 return array($n, new Command($this->name, true)); 480 else 481 break; 482 } 483 } 484 return array(null, null); 485 } 486} 487 488class Option extends ChildPattern 489{ 490 public $short; 491 public $long; 492 493 public function __construct($short=null, $long=null, $argcount=0, $value=false) 494 { 495 if ($argcount != 0 && $argcount != 1) 496 throw new \InvalidArgumentException(); 497 498 $this->short = $short; 499 $this->long = $long; 500 $this->argcount = $argcount; 501 $this->value = $value; 502 503 // Python checks "value is False". maybe we should check "$value === false" 504 if (!$value && $argcount) 505 $this->value = null; 506 } 507 508 public static function parse($optionDescription) 509 { 510 $short = null; 511 $long = null; 512 $argcount = 0; 513 $value = false; 514 515 $exp = explode(' ', trim($optionDescription), 2); 516 $options = $exp[0]; 517 $description = isset($exp[1]) ? $exp[1] : ''; 518 519 $options = str_replace(',', ' ', str_replace('=', ' ', $options)); 520 foreach (preg_split('/\s+/', $options) as $s) { 521 if (strpos($s, '--')===0) 522 $long = $s; 523 elseif ($s && $s[0] == '-') 524 $short = $s; 525 else 526 $argcount = 1; 527 } 528 529 if ($argcount) { 530 $value = null; 531 if (preg_match('@\[default: (.*)\]@i', $description, $match)) { 532 $value = $match[1]; 533 } 534 } 535 536 return new static($short, $long, $argcount, $value); 537 } 538 539 public function singleMatch($left) 540 { 541 foreach ($left as $n=>$p) { 542 if ($this->name == $p->name) { 543 return array($n, $p); 544 } 545 } 546 return array(null, null); 547 } 548 549 public function name() 550 { 551 return $this->long ?: $this->short; 552 } 553 554 public function dump() 555 { 556 return "Option('{$this->short}', ".dump_scalar($this->long).", ".dump_scalar($this->argcount).", ".dump_scalar($this->value).")"; 557 } 558} 559 560class Required extends ParentPattern 561{ 562 public function match($left, $collected=null) 563 { 564 if (!$collected) 565 $collected = array(); 566 567 $l = $left; 568 $c = $collected; 569 570 foreach ($this->children as $p) { 571 list ($matched, $l, $c) = $p->match($l, $c); 572 if (!$matched) 573 return array(false, $left, $collected); 574 } 575 576 return array(true, $l, $c); 577 } 578} 579 580class Optional extends ParentPattern 581{ 582 public function match($left, $collected=null) 583 { 584 if (!$collected) 585 $collected = array(); 586 587 foreach ($this->children as $p) { 588 list($m, $left, $collected) = $p->match($left, $collected); 589 } 590 591 return array(true, $left, $collected); 592 } 593} 594 595/** 596 * Marker/placeholder for [options] shortcut. 597 */ 598class AnyOptions extends Optional 599{ 600} 601 602class OneOrMore extends ParentPattern 603{ 604 public function match($left, $collected=null) 605 { 606 if (count($this->children) != 1) 607 throw new \UnexpectedValueException(); 608 609 if (!$collected) 610 $collected = array(); 611 612 $l = $left; 613 $c = $collected; 614 615 $lnew = array(); 616 $matched = true; 617 $times = 0; 618 619 while ($matched) { 620 # could it be that something didn't match but changed l or c? 621 list ($matched, $l, $c) = $this->children[0]->match($l, $c); 622 if ($matched) $times += 1; 623 if ($lnew == $l) 624 break; 625 $lnew = $l; 626 } 627 628 if ($times >= 1) 629 return array(true, $l, $c); 630 else 631 return array(false, $left, $collected); 632 } 633} 634 635class Either extends ParentPattern 636{ 637 public function match($left, $collected=null) 638 { 639 if (!$collected) 640 $collected = array(); 641 642 $outcomes = array(); 643 foreach ($this->children as $p) { 644 list ($matched, $dump1, $dump2) = $outcome = $p->match($left, $collected); 645 if ($matched) 646 $outcomes[] = $outcome; 647 } 648 if ($outcomes) { 649 // return min(outcomes, key=lambda outcome: len(outcome[1])) 650 $min = null; 651 $ret = null; 652 foreach ($outcomes as $o) { 653 $cnt = count($o[1]); 654 if ($min === null || $cnt < $min) { 655 $min = $cnt; 656 $ret = $o; 657 } 658 } 659 return $ret; 660 } 661 else 662 return array(false, $left, $collected); 663 } 664} 665 666class TokenStream extends \ArrayIterator 667{ 668 public $error; 669 670 public function __construct($source, $error) 671 { 672 if (!is_array($source)) 673 $source = preg_split('/\s+/', trim($source)); 674 675 parent::__construct($source); 676 677 $this->error = $error; 678 } 679 680 function move() 681 { 682 $item = $this->current(); 683 $this->next(); 684 return $item; 685 } 686 687 function raiseException($message) 688 { 689 $class = __NAMESPACE__.'\\'.$this->error; 690 throw new $class($message); 691 } 692} 693 694/** 695 * long ::= '--' chars [ ( ' ' | '=' ) chars ] ; 696 */ 697function parse_long($tokens, \ArrayIterator $options) 698{ 699 $token = $tokens->move(); 700 $exploded = explode('=', $token, 2); 701 if (count($exploded) == 2) { 702 $long = $exploded[0]; 703 $eq = '='; 704 $value = $exploded[1]; 705 } 706 else { 707 $long = $token; 708 $eq = null; 709 $value = null; 710 } 711 712 if (strpos($long, '--') !== 0) 713 throw new \UnexpectedValueExeption(); 714 715 if (!$value) $value = null; 716 717 718 $similar = array_filter($options, function($o) use ($long) { return $o->long && $o->long == $long; }, true); 719 if ('ExitException' == $tokens->error && !$similar) 720 $similar = array_filter($options, function($o) use ($long) { return $o->long && strpos($o->long, $long)===0; }, true); 721 722 if (count($similar) > 1) { 723 // might be simply specified ambiguously 2+ times? 724 $tokens->raiseException("$long is not a unique prefix: ".implode(', ', array_map(function($o) { return $o->long; }, $similar))); 725 } 726 elseif (count($similar) < 1) { 727 $argcount = $eq == '=' ? 1 : 0; 728 $o = new Option(null, $long, $argcount); 729 $options[] = $o; 730 if ($tokens->error == 'ExitException') { 731 $o = new Option(null, $long, $argcount, $argcount ? $value : true); 732 } 733 } 734 else { 735 $o = new Option($similar[0]->short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value); 736 if ($o->argcount == 0) { 737 if ($value !== null) { 738 $tokens->raiseException("{$o->long} must not have an argument"); 739 } 740 } 741 else { 742 if ($value === null) { 743 if ($tokens->current() === null) { 744 $tokens->raiseException("{$o->long} requires argument"); 745 } 746 $value = $tokens->move(); 747 } 748 } 749 if ($tokens->error == 'ExitException') { 750 $o->value = $value !== null ? $value : true; 751 } 752 } 753 754 return array($o); 755} 756 757/** 758 * shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ; 759 */ 760function parse_shorts($tokens, \ArrayIterator $options) 761{ 762 $token = $tokens->move(); 763 764 if (strpos($token, '-') !== 0 || strpos($token, '--') === 0) 765 throw new \UnexpectedValueExeption(); 766 767 $left = ltrim($token, '-'); 768 $parsed = array(); 769 while ($left != '') { 770 $short = '-'.$left[0]; 771 $left = substr($left, 1); 772 $similar = array(); 773 foreach ($options as $o) { 774 if ($o->short == $short) 775 $similar[] = $o; 776 } 777 778 $similarCnt = count($similar); 779 if ($similarCnt > 1) { 780 $tokens->raiseException("$short is specified ambiguously $similarCnt times"); 781 } 782 elseif ($similarCnt < 1) { 783 $o = new Option($short, null, 0); 784 $options[] = $o; 785 if ($tokens->error == 'ExitException') 786 $o = new Option($short, null, 0, true); 787 } 788 else { 789 $o = new Option($short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value); 790 $value = null; 791 if ($o->argcount != 0) { 792 if ($left == '') { 793 if ($tokens->current() === null) 794 $tokens->raiseException("$short requires argument"); 795 $value = $tokens->move(); 796 } 797 else { 798 $value = $left; 799 $left = ''; 800 } 801 } 802 if ($tokens->error == 'ExitException') { 803 $o->value = $value !== null ? $value : true; 804 } 805 } 806 $parsed[] = $o; 807 } 808 809 return $parsed; 810} 811 812function parse_pattern($source, \ArrayIterator $options) 813{ 814 $tokens = new TokenStream(preg_replace('@([\[\]\(\)\|]|\.\.\.)@', ' $1 ', $source), 'LanguageError'); 815 816 $result = parse_expr($tokens, $options); 817 if ($tokens->current() != null) { 818 $tokens->raiseException('unexpected ending: '.implode(' ', $tokens)); 819 } 820 return new Required($result); 821} 822 823/** 824 * expr ::= seq ( '|' seq )* ; 825 */ 826function parse_expr($tokens, \ArrayIterator $options) 827{ 828 $seq = parse_seq($tokens, $options); 829 if ($tokens->current() != '|') 830 return $seq; 831 832 $result = null; 833 if (count($seq) > 1) 834 $result = array(new Required($seq)); 835 else 836 $result = $seq; 837 838 while ($tokens->current() == '|') { 839 $tokens->move(); 840 $seq = parse_seq($tokens, $options); 841 if (count($seq) > 1) 842 $result[] = new Required($seq); 843 else 844 $result = array_merge($result, $seq); 845 } 846 847 if (count($result) > 1) 848 return new Either($result); 849 else 850 return $result; 851} 852 853/** 854 * seq ::= ( atom [ '...' ] )* ; 855 */ 856function parse_seq($tokens, \ArrayIterator $options) 857{ 858 $result = array(); 859 $not = array(null, '', ']', ')', '|'); 860 while (!in_array($tokens->current(), $not, true)) { 861 $atom = parse_atom($tokens, $options); 862 if ($tokens->current() == '...') { 863 $atom = array(new OneOrMore($atom)); 864 $tokens->move(); 865 } 866 if ($atom instanceof \ArrayIterator) 867 $atom = $atom->getArrayCopy(); 868 if ($atom) { 869 $result = array_merge($result, $atom); 870 } 871 } 872 return $result; 873} 874 875/** 876 * atom ::= '(' expr ')' | '[' expr ']' | 'options' 877 * | long | shorts | argument | command ; 878 */ 879function parse_atom($tokens, \ArrayIterator $options) 880{ 881 $token = $tokens->current(); 882 $result = array(); 883 if ($token == '(' || $token == '[') { 884 $tokens->move(); 885 886 static $index; 887 if (!$index) $index = array('('=>array(')', __NAMESPACE__.'\Required'), '['=>array(']', __NAMESPACE__.'\Optional')); 888 list ($matching, $pattern) = $index[$token]; 889 890 $result = new $pattern(parse_expr($tokens, $options)); 891 if ($tokens->move() != $matching) 892 $tokens->raiseException("Unmatched '$token'"); 893 894 return array($result); 895 } 896 elseif ($token == 'options') { 897 $tokens->move(); 898 return array(new AnyOptions); 899 } 900 elseif (strpos($token, '--') === 0 && $token != '--') { 901 return parse_long($tokens, $options); 902 } 903 elseif (strpos($token, '-') === 0 && $token != '-' && $token != '--') { 904 return parse_shorts($tokens, $options); 905 } 906 elseif (strpos($token, '<') === 0 && ends_with($token, '>') || is_upper($token)) { 907 return array(new Argument($tokens->move())); 908 } 909 else { 910 return array(new Command($tokens->move())); 911 } 912} 913 914/** 915 * Parse command-line argument vector. 916 * 917 * If options_first: 918 * argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; 919 * else: 920 * argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; 921 */ 922function parse_argv($tokens, \ArrayIterator $options, $optionsFirst=false) 923{ 924 $parsed = array(); 925 926 while ($tokens->current() !== null) { 927 if ($tokens->current() == '--') { 928 foreach ($tokens as $v) { 929 $parsed[] = new Argument(null, $v); 930 } 931 return $parsed; 932 } 933 elseif (strpos($tokens->current(), '--')===0) { 934 $parsed = array_merge($parsed, parse_long($tokens, $options)); 935 } 936 elseif (strpos($tokens->current(), '-')===0 && $tokens->current() != '-') { 937 $parsed = array_merge($parsed, parse_shorts($tokens, $options)); 938 } 939 elseif ($optionsFirst) { 940 return array_merge($parsed, array_map(function($v) { return new Argument(null, $v); }, $tokens)); 941 } 942 else { 943 $parsed[] = new Argument(null, $tokens->move()); 944 } 945 } 946 return $parsed; 947} 948 949function parse_defaults($doc) 950{ 951 $splitTmp = array_slice(preg_split('@\n[ ]*(<\S+?>|-\S+?)@', $doc, null, PREG_SPLIT_DELIM_CAPTURE), 1); 952 $split = array(); 953 for ($cnt = count($splitTmp), $i=0; $i < $cnt; $i+=2) { 954 $split[] = $splitTmp[$i] . (isset($splitTmp[$i+1]) ? $splitTmp[$i+1] : ''); 955 } 956 $options = new \ArrayIterator(); 957 foreach ($split as $s) { 958 if (strpos($s, '-') === 0) 959 $options[] = Option::parse($s); 960 } 961 return $options; 962} 963 964function printable_usage($doc) 965{ 966 $usageSplit = preg_split("@([Uu][Ss][Aa][Gg][Ee]:)@", $doc, null, PREG_SPLIT_DELIM_CAPTURE); 967 968 if (count($usageSplit) < 3) 969 throw new LanguageError('"usage:" (case-insensitive) not found.'); 970 elseif (count($usageSplit) > 3) 971 throw new LanguageError('More than one "usage:" (case-insensitive).'); 972 973 $split = preg_split("@\n\s*\n@", implode('', array_slice($usageSplit, 1))); 974 975 return trim($split[0]); 976} 977 978function formal_usage($printableUsage) 979{ 980 $pu = array_slice(preg_split('/\s+/', $printableUsage), 1); 981 982 $ret = array(); 983 foreach (array_slice($pu, 1) as $s) { 984 if ($s == $pu[0]) 985 $ret[] = ') | ('; 986 else 987 $ret[] = $s; 988 } 989 990 return '( '.implode(' ', $ret).' )'; 991} 992 993function extras($help, $version, $options, $doc) 994{ 995 $ofound = false; 996 $vfound = false; 997 foreach ($options as $o) { 998 if ($o->value && ($o->name == '-h' || $o->name == '--help')) 999 $ofound = true; 1000 if ($o->value && $o->name == '--version') 1001 $vfound = true; 1002 } 1003 if ($help && $ofound) { 1004 ExitException::$usage = null; 1005 throw new ExitException($doc, 0); 1006 } 1007 if ($version && $vfound) { 1008 ExitException::$usage = null; 1009 throw new ExitException($version, 0); 1010 } 1011} 1012 1013/** 1014 * API compatibility with python docopt 1015 */ 1016function docopt($doc, $params=array()) 1017{ 1018 $argv = array(); 1019 if (isset($params['argv'])) { 1020 $argv = $params['argv']; 1021 unset($params['argv']); 1022 } 1023 $h = new Handler($params); 1024 return $h->handle($doc, $argv); 1025} 1026 1027/** 1028 * Use a class in PHP because we can't autoload functions yet. 1029 */ 1030class Handler 1031{ 1032 public $exit = true; 1033 public $help = true; 1034 public $optionsFirst = false; 1035 public $version; 1036 1037 public function __construct($options=array()) 1038 { 1039 foreach ($options as $k=>$v) 1040 $this->$k = $v; 1041 } 1042 1043 function handle($doc, $argv=null) 1044 { 1045 try { 1046 if (!$argv && isset($_SERVER['argv'])) 1047 $argv = array_slice($_SERVER['argv'], 1); 1048 1049 ExitException::$usage = printable_usage($doc); 1050 $options = parse_defaults($doc); 1051 1052 $formalUse = formal_usage(ExitException::$usage); 1053 $pattern = parse_pattern($formalUse, $options); 1054 $argv = parse_argv(new TokenStream($argv, 'ExitException'), $options, $this->optionsFirst); 1055 foreach ($pattern->flat('AnyOptions') as $ao) { 1056 $docOptions = parse_defaults($doc); 1057 $ao->children = array_diff((array)$docOptions, $pattern->flat('Option')); 1058 } 1059 1060 extras($this->help, $this->version, $argv, $doc); 1061 1062 list($matched, $left, $collected) = $pattern->fix()->match($argv); 1063 if ($matched && !$left) { 1064 $return = array(); 1065 foreach (array_merge($pattern->flat(), $collected) as $a) { 1066 $name = $a->name; 1067 if ($name) 1068 $return[$name] = $a->value; 1069 } 1070 return new Response($return); 1071 } 1072 throw new ExitException(); 1073 } 1074 catch (ExitException $ex) { 1075 $this->handleExit($ex); 1076 return new Response(null, $ex->status, $ex->getMessage()); 1077 } 1078 } 1079 1080 function handleExit(ExitException $ex) 1081 { 1082 if ($this->exit) { 1083 echo $ex->getMessage().PHP_EOL; 1084 exit($ex->status); 1085 } 1086 } 1087} 1088 1089class Response implements \ArrayAccess, \IteratorAggregate 1090{ 1091 public $status; 1092 public $output; 1093 public $args; 1094 1095 public function __construct($args, $status=0, $output='') 1096 { 1097 $this->args = $args ?: array(); 1098 $this->status = $status; 1099 $this->output = $output; 1100 } 1101 1102 public function __get($name) 1103 { 1104 if ($name == 'success') 1105 return $this->status === 0; 1106 else 1107 throw new \BadMethodCallException("Unknown property $name"); 1108 } 1109 1110 public function offsetExists($offset) 1111 { 1112 return isset($this->args[$offset]); 1113 } 1114 1115 public function offsetGet($offset) 1116 { 1117 return $this->args[$offset]; 1118 } 1119 1120 public function offsetSet($offset, $value) 1121 { 1122 $this->args[$offset] = $value; 1123 } 1124 1125 public function offsetUnset($offset) 1126 { 1127 unset($this->args[$offset]); 1128 } 1129 1130 public function getIterator () 1131 { 1132 return new \ArrayIterator($this->args); 1133 } 1134} 1135