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