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