1<?php
2
3/**
4 * Parser for command-line arguments for scripts. Like similar parsers, this
5 * class allows you to specify, validate, and render help for command-line
6 * arguments. For example:
7 *
8 *   name=create_dog.php
9 *   $args = new PhutilArgumentParser($argv);
10 *   $args->setTagline('make an new dog')
11 *   $args->setSynopsis(<<<EOHELP
12 *   **dog** [--big] [--name __name__]
13 *   Create a new dog. How does it work? Who knows.
14 *   EOHELP
15 *   );
16 *   $args->parse(
17 *     array(
18 *       array(
19 *         'name'     => 'name',
20 *         'param'    => 'dogname',
21 *         'default'  => 'Rover',
22 *         'help'     => 'Set the dog\'s name. By default, the dog will be '.
23 *                       'named "Rover".',
24 *       ),
25 *       array(
26 *         'name'     => 'big',
27 *         'short'    => 'b',
28 *         'help'     => 'If set, create a large dog.',
29 *       ),
30 *     ));
31 *
32 *   $dog_name = $args->getArg('name');
33 *   $dog_size = $args->getArg('big') ? 'big' : 'small';
34 *
35 *   // ... etc ...
36 *
37 * (For detailed documentation on supported keys in argument specifications,
38 * see @{class:PhutilArgumentSpecification}.)
39 *
40 * This will handle argument parsing, and generate appropriate usage help if
41 * the user provides an unsupported flag. @{class:PhutilArgumentParser} also
42 * supports some builtin "standard" arguments:
43 *
44 *   $args->parseStandardArguments();
45 *
46 * See @{method:parseStandardArguments} for details. Notably, this includes
47 * a "--help" flag, and an "--xprofile" flag for profiling command-line scripts.
48 *
49 * Normally, when the parser encounters an unknown flag, it will exit with
50 * an error. However, you can use @{method:parsePartial} to consume only a
51 * set of flags:
52 *
53 *   $args->parsePartial($spec_list);
54 *
55 * This allows you to parse some flags before making decisions about other
56 * parsing, or share some flags across scripts. The builtin standard arguments
57 * are implemented in this way.
58 *
59 * There is also builtin support for "workflows", which allow you to build a
60 * script that operates in several modes (e.g., by accepting commands like
61 * `install`, `upgrade`, etc), like `arc` does. For detailed documentation on
62 * workflows, see @{class:PhutilArgumentWorkflow}.
63 *
64 * @task parse    Parsing Arguments
65 * @task read     Reading Arguments
66 * @task help     Command Help
67 * @task internal Internals
68 */
69final class PhutilArgumentParser extends Phobject {
70
71  private $bin;
72  private $argv;
73  private $specs = array();
74  private $results = array();
75  private $parsed;
76
77  private $tagline;
78  private $synopsis;
79  private $workflows;
80  private $helpWorkflows;
81  private $showHelp;
82  private $requireArgumentTerminator = false;
83  private $sawTerminator = false;
84
85  const PARSE_ERROR_CODE = 77;
86
87  private static $traceModeEnabled = false;
88
89
90/* -(  Parsing Arguments  )-------------------------------------------------- */
91
92
93  /**
94   * Build a new parser. Generally, you start a script with:
95   *
96   *   $args = new PhutilArgumentParser($argv);
97   *
98   * @param list  Argument vector to parse, generally the $argv global.
99   * @task parse
100   */
101  public function __construct(array $argv) {
102    $this->bin = $argv[0];
103    $this->argv = array_slice($argv, 1);
104  }
105
106
107  /**
108   * Parse and consume a list of arguments, removing them from the argument
109   * vector but leaving unparsed arguments for later consumption. You can
110   * retrieve unconsumed arguments directly with
111   * @{method:getUnconsumedArgumentVector}. Doing a partial parse can make it
112   * easier to share common flags across scripts or workflows.
113   *
114   * @param   list  List of argument specs, see
115   *                @{class:PhutilArgumentSpecification}.
116   * @param bool Require flags appear before any non-flag arguments.
117   * @return  this
118   * @task parse
119   */
120  public function parsePartial(array $specs, $initial_only = false) {
121    return $this->parseInternal($specs, false, $initial_only);
122  }
123
124  /**
125   * @return  this
126   */
127  private function parseInternal(
128    array $specs,
129    $correct_spelling,
130    $initial_only) {
131
132    $specs = PhutilArgumentSpecification::newSpecsFromList($specs);
133    $this->mergeSpecs($specs);
134
135    // Wildcard arguments have a name like "argv", but we don't want to
136    // parse a corresponding flag like "--argv". Filter them out before
137    // building a list of available flags.
138    $non_wildcard = array();
139    foreach ($specs as $spec_key => $spec) {
140      if ($spec->getWildcard()) {
141        continue;
142      }
143
144      $non_wildcard[$spec_key] = $spec;
145    }
146
147    $specs_by_name  = mpull($non_wildcard, null, 'getName');
148    $specs_by_short = mpull($non_wildcard, null, 'getShortAlias');
149    unset($specs_by_short[null]);
150
151    $argv = $this->argv;
152    $len = count($argv);
153    $is_initial = true;
154    for ($ii = 0; $ii < $len; $ii++) {
155      $arg = $argv[$ii];
156      $map = null;
157      $options = null;
158      if (!is_string($arg)) {
159        // Non-string argument; pass it through as-is.
160      } else if ($arg == '--') {
161        // This indicates "end of flags".
162        $this->sawTerminator = true;
163        break;
164      } else if ($arg == '-') {
165        // This is a normal argument (e.g., stdin).
166        continue;
167      } else if (!strncmp('--', $arg, 2)) {
168        $pre = '--';
169        $arg = substr($arg, 2);
170        $map = $specs_by_name;
171        $options = array_keys($specs_by_name);
172      } else if (!strncmp('-', $arg, 1) && strlen($arg) > 1) {
173        $pre = '-';
174        $arg = substr($arg, 1);
175        $map = $specs_by_short;
176      } else {
177        $is_initial = false;
178      }
179
180      if ($map) {
181        $val = null;
182        $parts = explode('=', $arg, 2);
183        if (count($parts) == 2) {
184          list($arg, $val) = $parts;
185        }
186
187        // Try to correct flag spelling for full flags, to allow users to make
188        // minor mistakes.
189        if ($correct_spelling && $options && !isset($map[$arg])) {
190          $corrections = PhutilArgumentSpellingCorrector::newFlagCorrector()
191            ->correctSpelling($arg, $options);
192
193          $should_autocorrect = $this->shouldAutocorrect();
194          if (count($corrections) == 1 && $should_autocorrect) {
195            $corrected = head($corrections);
196
197            $this->logMessage(
198              tsprintf(
199                "%s\n",
200                pht(
201                  '(Assuming "%s" is the British spelling of "%s".)',
202                  $pre.$arg,
203                  $pre.$corrected)));
204
205            $arg = $corrected;
206          }
207        }
208
209        if (isset($map[$arg])) {
210          if ($initial_only && !$is_initial) {
211            throw new PhutilArgumentUsageException(
212              pht(
213                'Argument "%s" appears after the first non-flag argument. '.
214                'This special argument must appear before other arguments.',
215                "{$pre}{$arg}"));
216          }
217
218          $spec = $map[$arg];
219          unset($argv[$ii]);
220
221          $param_name = $spec->getParamName();
222          if ($val !== null) {
223            if ($param_name === null) {
224              throw new PhutilArgumentUsageException(
225                pht(
226                  'Argument "%s" does not take a parameter.',
227                  "{$pre}{$arg}"));
228            }
229          } else {
230            if ($param_name !== null) {
231              if ($ii + 1 < $len) {
232                $val = $argv[$ii + 1];
233                unset($argv[$ii + 1]);
234                $ii++;
235              } else {
236                throw new PhutilArgumentUsageException(
237                  pht(
238                    'Argument "%s" requires a parameter.',
239                    "{$pre}{$arg}"));
240              }
241            } else {
242              $val = true;
243            }
244          }
245
246          if (!$spec->getRepeatable()) {
247            if (array_key_exists($spec->getName(), $this->results)) {
248              throw new PhutilArgumentUsageException(
249                pht(
250                  'Argument "%s" was provided twice.',
251                  "{$pre}{$arg}"));
252            }
253          }
254
255          $conflicts = $spec->getConflicts();
256          foreach ($conflicts as $conflict => $reason) {
257            if (array_key_exists($conflict, $this->results)) {
258
259              if (!is_string($reason) || !strlen($reason)) {
260                $reason = '.';
261              } else {
262                $reason = ': '.$reason.'.';
263              }
264
265              throw new PhutilArgumentUsageException(
266                pht(
267                  'Argument "%s" conflicts with argument "%s"%s',
268                  "{$pre}{$arg}",
269                  "--{$conflict}",
270                  $reason));
271            }
272          }
273
274          if ($spec->getRepeatable()) {
275            if ($spec->getParamName() === null) {
276              if (empty($this->results[$spec->getName()])) {
277                $this->results[$spec->getName()] = 0;
278              }
279              $this->results[$spec->getName()]++;
280            } else {
281              $this->results[$spec->getName()][] = $val;
282            }
283          } else {
284            $this->results[$spec->getName()] = $val;
285          }
286        }
287      }
288    }
289
290    foreach ($specs as $spec) {
291      if ($spec->getWildcard()) {
292        $this->results[$spec->getName()] = $this->filterWildcardArgv($argv);
293        $argv = array();
294        break;
295      }
296    }
297
298    $this->argv = array_values($argv);
299
300    return $this;
301  }
302
303
304  /**
305   * Parse and consume a list of arguments, throwing an exception if there is
306   * anything left unconsumed. This is like @{method:parsePartial}, but raises
307   * a {class:PhutilArgumentUsageException} if there are leftovers.
308   *
309   * Normally, you would call @{method:parse} instead, which emits a
310   * user-friendly error. You can also use @{method:printUsageException} to
311   * render the exception in a user-friendly way.
312   *
313   * @param   list  List of argument specs, see
314   *                @{class:PhutilArgumentSpecification}.
315   * @return  this
316   * @task parse
317   */
318  public function parseFull(array $specs) {
319    $this->parseInternal($specs, true, false);
320
321    // If we have remaining unconsumed arguments other than a single "--",
322    // fail.
323    $argv = $this->filterWildcardArgv($this->argv);
324    if ($argv) {
325      throw new PhutilArgumentUsageException(
326        pht(
327          'Unrecognized argument "%s".',
328          head($argv)));
329    }
330
331    if ($this->getRequireArgumentTerminator()) {
332      if (!$this->sawTerminator) {
333        throw new ArcanistMissingArgumentTerminatorException();
334      }
335    }
336
337    if ($this->showHelp) {
338      $this->printHelpAndExit();
339    }
340
341    return $this;
342  }
343
344
345  /**
346   * Parse and consume a list of arguments, raising a user-friendly error if
347   * anything remains. See also @{method:parseFull} and @{method:parsePartial}.
348   *
349   * @param   list  List of argument specs, see
350   *                @{class:PhutilArgumentSpecification}.
351   * @return  this
352   * @task parse
353   */
354  public function parse(array $specs) {
355    try {
356      return $this->parseFull($specs);
357    } catch (PhutilArgumentUsageException $ex) {
358      $this->printUsageException($ex);
359      exit(self::PARSE_ERROR_CODE);
360    }
361  }
362
363
364  /**
365   * Parse and execute workflows, raising a user-friendly error if anything
366   * remains. See also @{method:parseWorkflowsFull}.
367   *
368   * See @{class:PhutilArgumentWorkflow} for details on using workflows.
369   *
370   * @param   list  List of argument specs, see
371   *                @{class:PhutilArgumentSpecification}.
372   * @return  this
373   * @task parse
374   */
375  public function parseWorkflows(array $workflows) {
376    try {
377      return $this->parseWorkflowsFull($workflows);
378    } catch (PhutilArgumentUsageException $ex) {
379      $this->printUsageException($ex);
380      exit(self::PARSE_ERROR_CODE);
381    }
382  }
383
384
385  /**
386   * Select a workflow. For commands that may operate in several modes, like
387   * `arc`, the modes can be split into "workflows". Each workflow specifies
388   * the arguments it accepts. This method takes a list of workflows, selects
389   * the chosen workflow, parses its arguments, and either executes it (if it
390   * is executable) or returns it for handling.
391   *
392   * See @{class:PhutilArgumentWorkflow} for details on using workflows.
393   *
394   * @param list List of @{class:PhutilArgumentWorkflow}s.
395   * @return PhutilArgumentWorkflow|no  Returns the chosen workflow if it is
396   *                                    not executable, or executes it and
397   *                                    exits with a return code if it is.
398   * @task parse
399   */
400  public function parseWorkflowsFull(array $workflows) {
401    assert_instances_of($workflows, 'PhutilArgumentWorkflow');
402
403    // Clear out existing workflows. We need to do this to permit the
404    // construction of sub-workflows.
405    $this->workflows = array();
406
407    foreach ($workflows as $workflow) {
408      $name = $workflow->getName();
409
410      if ($name === null) {
411        throw new PhutilArgumentSpecificationException(
412          pht('Workflow has no name!'));
413      }
414
415      if (isset($this->workflows[$name])) {
416        throw new PhutilArgumentSpecificationException(
417          pht("Two workflows with name '%s!", $name));
418      }
419
420      $this->workflows[$name] = $workflow;
421    }
422
423    $argv = $this->argv;
424    if (empty($argv)) {
425      // TODO: this is kind of hacky / magical.
426      if (isset($this->workflows['help'])) {
427        $argv = array('help');
428      } else {
429        throw new PhutilArgumentUsageException(pht('No workflow selected.'));
430      }
431    }
432
433    $flow = array_shift($argv);
434
435    if (empty($this->workflows[$flow])) {
436      $corrected = PhutilArgumentSpellingCorrector::newCommandCorrector()
437        ->correctSpelling($flow, array_keys($this->workflows));
438
439      $should_autocorrect = $this->shouldAutocorrect();
440      if (count($corrected) == 1 && $should_autocorrect) {
441        $corrected = head($corrected);
442
443        $this->logMessage(
444          tsprintf(
445            "%s\n",
446            pht(
447              '(Assuming "%s" is the British spelling of "%s".)',
448              $flow,
449              $corrected)));
450
451        $flow = $corrected;
452      } else {
453        if (!$this->showHelp) {
454          $this->raiseUnknownWorkflow($flow, $corrected);
455        }
456      }
457    }
458
459    $workflow = idx($this->workflows, $flow);
460
461    if ($this->showHelp) {
462      // Make "cmd flow --help" behave like "cmd help flow", not "cmd help".
463      $help_flow = idx($this->workflows, 'help');
464      if ($help_flow) {
465        if ($help_flow !== $workflow) {
466          $workflow = $help_flow;
467          $argv = array($flow);
468
469          // Prevent parse() from dumping us back out to standard help.
470          $this->showHelp = false;
471        }
472      } else {
473        $this->printHelpAndExit();
474      }
475    }
476
477    if (!$workflow) {
478      $this->raiseUnknownWorkflow($flow, $corrected);
479    }
480
481    $this->argv = array_values($argv);
482
483    if ($workflow->shouldParsePartial()) {
484      $this->parsePartial($workflow->getArguments());
485    } else {
486      $this->parse($workflow->getArguments());
487    }
488
489
490    if ($workflow->isExecutable()) {
491      $workflow->setArgv($this);
492      $err = $workflow->execute($this);
493      exit($err);
494    } else {
495      return $workflow;
496    }
497  }
498
499
500  /**
501   * Parse "standard" arguments and apply their effects:
502   *
503   *    --trace             Enable service call tracing.
504   *    --no-ansi           Disable ANSI color/style sequences.
505   *    --xprofile <file>   Write out an XHProf profile.
506   *    --help              Show help.
507   *
508   * @return this
509   *
510   * @phutil-external-symbol function xhprof_enable
511   */
512  public function parseStandardArguments() {
513    try {
514      $this->parsePartial(
515        array(
516          array(
517            'name'  => 'trace',
518            'help'  => pht('Trace command execution and show service calls.'),
519            'standard' => true,
520          ),
521          array(
522            'name'  => 'no-ansi',
523            'help'  => pht(
524              'Disable ANSI terminal codes, printing plain text with '.
525              'no color or style.'),
526            'conflicts' => array(
527              'ansi' => null,
528            ),
529            'standard' => true,
530          ),
531          array(
532            'name'  => 'ansi',
533            'help'  => pht(
534              "Use formatting even in environments which probably ".
535              "don't support it."),
536            'standard' => true,
537          ),
538          array(
539            'name'  => 'xprofile',
540            'param' => 'profile',
541            'help'  => pht(
542              'Profile script execution and write results to a file.'),
543            'standard' => true,
544          ),
545          array(
546            'name'  => 'help',
547            'short' => 'h',
548            'help'  => pht('Show this help.'),
549            'standard' => true,
550          ),
551          array(
552            'name'  => 'show-standard-options',
553            'help'  => pht(
554              'Show every option, including standard options like this one.'),
555            'standard' => true,
556          ),
557          array(
558            'name'  => 'recon',
559            'help'  => pht('Start in remote console mode.'),
560            'standard' => true,
561          ),
562        ));
563    } catch (PhutilArgumentUsageException $ex) {
564      $this->printUsageException($ex);
565      exit(self::PARSE_ERROR_CODE);
566    }
567
568    if ($this->getArg('trace')) {
569      PhutilServiceProfiler::installEchoListener();
570      self::$traceModeEnabled = true;
571    }
572
573    if ($this->getArg('no-ansi')) {
574      PhutilConsoleFormatter::disableANSI(true);
575    }
576
577    if ($this->getArg('ansi')) {
578      PhutilConsoleFormatter::disableANSI(false);
579    }
580
581    if ($this->getArg('help')) {
582      $this->showHelp = true;
583    }
584
585    $xprofile = $this->getArg('xprofile');
586    if ($xprofile) {
587      if (!function_exists('xhprof_enable')) {
588        throw new Exception(
589          pht('To use "--xprofile", you must install XHProf.'));
590      }
591
592      xhprof_enable(0);
593      register_shutdown_function(array($this, 'shutdownProfiler'));
594    }
595
596    $recon = $this->getArg('recon');
597    if ($recon) {
598      $remote_console = PhutilConsole::newRemoteConsole();
599      $remote_console->beginRedirectOut();
600      PhutilConsole::setConsole($remote_console);
601    } else if ($this->getArg('trace')) {
602      $server = new PhutilConsoleServer();
603      $server->setEnableLog(true);
604      $console = PhutilConsole::newConsoleForServer($server);
605      PhutilConsole::setConsole($console);
606    }
607
608    return $this;
609  }
610
611
612/* -(  Reading Arguments  )-------------------------------------------------- */
613
614
615  public function getArg($name) {
616    if (empty($this->specs[$name])) {
617      throw new PhutilArgumentSpecificationException(
618        pht('No specification exists for argument "%s"!', $name));
619    }
620
621    if (idx($this->results, $name) !== null) {
622      return $this->results[$name];
623    }
624
625    return $this->specs[$name]->getDefault();
626  }
627
628  public function getUnconsumedArgumentVector() {
629    return $this->argv;
630  }
631
632  public function setUnconsumedArgumentVector(array $argv) {
633    $this->argv = $argv;
634    return $this;
635  }
636
637  public function setWorkflows($workflows) {
638    $workflows = mpull($workflows, null, 'getName');
639    $this->workflows = $workflows;
640    return $this;
641  }
642
643  public function setHelpWorkflows(array $help_workflows) {
644    $help_workflows = mpull($help_workflows, null, 'getName');
645    $this->helpWorkflows = $help_workflows;
646    return $this;
647  }
648
649  public function getWorkflows() {
650    return $this->workflows;
651  }
652
653
654/* -(  Command Help  )------------------------------------------------------- */
655
656  public function setRequireArgumentTerminator($require) {
657    $this->requireArgumentTerminator = $require;
658    return $this;
659  }
660
661  public function getRequireArgumentTerminator() {
662    return $this->requireArgumentTerminator;
663  }
664
665  public function setSynopsis($synopsis) {
666    $this->synopsis = $synopsis;
667    return $this;
668  }
669
670  public function setTagline($tagline) {
671    $this->tagline = $tagline;
672    return $this;
673  }
674
675  public function printHelpAndExit() {
676    echo $this->renderHelp();
677    exit(self::PARSE_ERROR_CODE);
678  }
679
680  public function renderHelp() {
681    $out = array();
682    $more = array();
683
684    if ($this->bin) {
685      $out[] = $this->format('**%s**', pht('NAME'));
686      $name = $this->indent(6, '**%s**', basename($this->bin));
687      if ($this->tagline) {
688        $name .= $this->format(' - '.$this->tagline);
689      }
690      $out[] = $name;
691      $out[] = null;
692    }
693
694    if ($this->synopsis) {
695      $out[] = $this->format('**%s**', pht('SYNOPSIS'));
696      $out[] = $this->indent(6, $this->synopsis);
697      $out[] = null;
698    }
699
700    $workflows = $this->helpWorkflows;
701    if ($workflows === null) {
702      $workflows = $this->workflows;
703    }
704
705    if ($workflows) {
706      $has_help = false;
707      $out[] = $this->format('**%s**', pht('WORKFLOWS'));
708      $out[] = null;
709      $flows = $workflows;
710      ksort($flows);
711      foreach ($flows as $workflow) {
712        if ($workflow->getName() == 'help') {
713          $has_help = true;
714        }
715        $out[] = $this->renderWorkflowHelp(
716          $workflow->getName(),
717          $show_details = false);
718      }
719      if ($has_help) {
720        $more[] = pht(
721          'Use **%s** __command__ for a detailed command reference.', 'help');
722      }
723    }
724
725    $specs = $this->renderArgumentSpecs($this->specs);
726    if ($specs) {
727      $out[] = $this->format('**%s**', pht('OPTION REFERENCE'));
728      $out[] = null;
729      $out[] = $specs;
730    }
731
732    // If we have standard options but no --show-standard-options, print out
733    // a quick hint about it.
734    if (!empty($this->specs['show-standard-options']) &&
735        !$this->getArg('show-standard-options')) {
736      $more[] = pht(
737        'Use __%s__ to show additional options.', '--show-standard-options');
738    }
739
740    $out[] = null;
741
742    if ($more) {
743      foreach ($more as $hint) {
744        $out[] = $this->indent(0, $hint);
745      }
746      $out[] = null;
747    }
748
749    return implode("\n", $out);
750  }
751
752  public function renderWorkflowHelp(
753    $workflow_name,
754    $show_details = false) {
755
756    $out = array();
757
758    $indent = ($show_details ? 0 : 6);
759
760    $workflows = $this->helpWorkflows;
761    if ($workflows === null) {
762      $workflows = $this->workflows;
763    }
764
765    $workflow = idx($workflows, strtolower($workflow_name));
766    if (!$workflow) {
767      $out[] = $this->indent(
768        $indent,
769        pht('There is no **%s** workflow.', $workflow_name));
770    } else {
771      $out[] = $this->indent($indent, $workflow->getExamples());
772      $out[] = $this->indent($indent, $workflow->getSynopsis());
773      if ($show_details) {
774        $full_help = $workflow->getHelp();
775        if ($full_help) {
776          $out[] = null;
777          $out[] = $this->indent($indent, $full_help);
778        }
779        $specs = $this->renderArgumentSpecs($workflow->getArguments());
780        if ($specs) {
781          $out[] = null;
782          $out[] = $specs;
783        }
784      }
785    }
786
787    $out[] = null;
788
789    return implode("\n", $out);
790  }
791
792  public function printUsageException(PhutilArgumentUsageException $ex) {
793    $message = tsprintf(
794      "**%s** %B\n",
795      pht('Usage Exception:'),
796      $ex->getMessage());
797
798    $this->logMessage($message);
799  }
800
801
802  private function logMessage($message) {
803    fwrite(STDERR, $message);
804  }
805
806
807/* -(  Internals  )---------------------------------------------------------- */
808
809
810  private function filterWildcardArgv(array $argv) {
811    foreach ($argv as $key => $value) {
812      if ($value == '--') {
813        unset($argv[$key]);
814        break;
815      } else if (
816        is_string($value) &&
817        !strncmp($value, '-', 1) &&
818        strlen($value) > 1) {
819
820        throw new PhutilArgumentUsageException(
821          pht(
822            'Argument "%s" is unrecognized. Use "%s" to indicate '.
823            'the end of flags.',
824            $value,
825            '--'));
826      }
827    }
828    return array_values($argv);
829  }
830
831  private function mergeSpecs(array $specs) {
832
833    $short_map = mpull($this->specs, null, 'getShortAlias');
834    unset($short_map[null]);
835
836    $wildcard = null;
837    foreach ($this->specs as $spec) {
838      if ($spec->getWildcard()) {
839        $wildcard = $spec;
840        break;
841      }
842    }
843
844    foreach ($specs as $spec) {
845      $spec->validate();
846      $name = $spec->getName();
847
848      if (isset($this->specs[$name])) {
849        throw new PhutilArgumentSpecificationException(
850          pht(
851            'Two argument specifications have the same name ("%s").',
852            $name));
853      }
854
855      $short = $spec->getShortAlias();
856      if ($short) {
857        if (isset($short_map[$short])) {
858          throw new PhutilArgumentSpecificationException(
859            pht(
860              'Two argument specifications have the same short alias ("%s").',
861              $short));
862        }
863        $short_map[$short] = $spec;
864      }
865
866      if ($spec->getWildcard()) {
867        if ($wildcard) {
868          throw new PhutilArgumentSpecificationException(
869            pht(
870              'Two argument specifications are marked as wildcard arguments. '.
871              'You can have a maximum of one wildcard argument.'));
872        } else {
873          $wildcard = $spec;
874        }
875      }
876
877      $this->specs[$name] = $spec;
878    }
879
880    foreach ($this->specs as $name => $spec) {
881      foreach ($spec->getConflicts() as $conflict => $reason) {
882        if (empty($this->specs[$conflict])) {
883          throw new PhutilArgumentSpecificationException(
884            pht(
885              'Argument "%s" conflicts with unspecified argument "%s".',
886              $name,
887              $conflict));
888        }
889        if ($conflict == $name) {
890          throw new PhutilArgumentSpecificationException(
891            pht(
892              'Argument "%s" conflicts with itself!',
893              $name));
894        }
895      }
896    }
897
898  }
899
900  private function renderArgumentSpecs(array $specs) {
901    foreach ($specs as $key => $spec) {
902      if ($spec->getWildcard()) {
903        unset($specs[$key]);
904      }
905    }
906
907    $out = array();
908
909    $no_standard_options =
910      !empty($this->specs['show-standard-options']) &&
911      !$this->getArg('show-standard-options');
912
913    $specs = msort($specs, 'getName');
914    foreach ($specs as $spec) {
915      if ($spec->getStandard() && $no_standard_options) {
916        // If this is a standard argument and the user didn't pass
917        // --show-standard-options, skip it.
918        continue;
919      }
920      $name = $this->indent(6, '__--%s__', $spec->getName());
921      $short = null;
922      if ($spec->getShortAlias()) {
923        $short = $this->format(', __-%s__', $spec->getShortAlias());
924      }
925      if ($spec->getParamName()) {
926        $param = $this->format(' __%s__', $spec->getParamName());
927        $name .= $param;
928        if ($short) {
929          $short .= $param;
930        }
931      }
932      $out[] = $name.$short;
933      $out[] = $this->indent(10, $spec->getHelp());
934      $out[] = null;
935    }
936
937    return implode("\n", $out);
938  }
939
940  private function format($str /* , ... */) {
941    $args = func_get_args();
942    return call_user_func_array(
943      'phutil_console_format',
944      $args);
945  }
946
947  private function indent($level, $str /* , ... */) {
948    $args = func_get_args();
949    $args = array_slice($args, 1);
950    $text = call_user_func_array(array($this, 'format'), $args);
951    return phutil_console_wrap($text, $level);
952  }
953
954  /**
955   * @phutil-external-symbol function xhprof_disable
956   */
957  public function shutdownProfiler() {
958    $data = xhprof_disable();
959    $data = json_encode($data);
960    Filesystem::writeFile($this->getArg('xprofile'), $data);
961  }
962
963  public static function isTraceModeEnabled() {
964    return self::$traceModeEnabled;
965  }
966
967  private function raiseUnknownWorkflow($flow, array $maybe) {
968    if ($maybe) {
969      sort($maybe);
970
971      $maybe_list = id(new PhutilConsoleList())
972        ->setWrap(false)
973        ->setBullet(null)
974        ->addItems($maybe)
975        ->drawConsoleString();
976
977      $message = tsprintf(
978        "%B\n%B",
979        pht(
980          'Invalid command "%s". Did you mean:',
981          $flow),
982        $maybe_list);
983    } else {
984      $names = mpull($this->workflows, 'getName');
985      sort($names);
986
987      $message = tsprintf(
988        '%B',
989        pht(
990          'Invalid command "%s". Valid commands are: %s.',
991          $flow,
992          implode(', ', $names)));
993    }
994
995    if (isset($this->workflows['help'])) {
996      $binary = basename($this->bin);
997      $message = tsprintf(
998        "%B\n%s",
999        $message,
1000        pht(
1001          'For details on available commands, run "%s".',
1002          "{$binary} help"));
1003    }
1004
1005    throw new PhutilArgumentUsageException($message);
1006  }
1007
1008  private function shouldAutocorrect() {
1009    return !phutil_is_noninteractive();
1010  }
1011
1012}
1013