comparison vendor/pear/console_commandline/Console/CommandLine.php @ 0:1e000243b222

vanilla 1.3.3 distro, I hope
author Charlie Root
date Thu, 04 Jan 2018 15:50:29 -0500
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:1e000243b222
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * This file is part of the PEAR Console_CommandLine package.
7 *
8 * A full featured package for managing command-line options and arguments
9 * hightly inspired from python optparse module, it allows the developper to
10 * easily build complex command line interfaces.
11 *
12 * PHP version 5
13 *
14 * LICENSE: This source file is subject to the MIT license that is available
15 * through the world-wide-web at the following URI:
16 * http://opensource.org/licenses/mit-license.php
17 *
18 * @category Console
19 * @package Console_CommandLine
20 * @author David JEAN LOUIS <izimobil@gmail.com>
21 * @copyright 2007 David JEAN LOUIS
22 * @license http://opensource.org/licenses/mit-license.php MIT License
23 * @version CVS: $Id$
24 * @link http://pear.php.net/package/Console_CommandLine
25 * @since Class available since release 0.1.0
26 * @filesource
27 */
28
29 /**
30 * Required unconditionally
31 */
32 require_once 'Console/CommandLine/Exception.php';
33 require_once 'Console/CommandLine/Outputter/Default.php';
34 require_once 'Console/CommandLine/Renderer/Default.php';
35 require_once 'Console/CommandLine/MessageProvider/Default.php';
36
37 /**
38 * Main class for parsing command line options and arguments.
39 *
40 * There are three ways to create parsers with this class:
41 * <code>
42 * // direct usage
43 * $parser = new Console_CommandLine();
44 *
45 * // with an xml definition file
46 * $parser = Console_CommandLine::fromXmlFile('path/to/file.xml');
47 *
48 * // with an xml definition string
49 * $validXmlString = '..your xml string...';
50 * $parser = Console_CommandLine::fromXmlString($validXmlString);
51 * </code>
52 *
53 * @category Console
54 * @package Console_CommandLine
55 * @author David JEAN LOUIS <izimobil@gmail.com>
56 * @copyright 2007 David JEAN LOUIS
57 * @license http://opensource.org/licenses/mit-license.php MIT License
58 * @version Release: @package_version@
59 * @link http://pear.php.net/package/Console_CommandLine
60 * @since File available since release 0.1.0
61 * @example docs/examples/ex1.php
62 * @example docs/examples/ex2.php
63 */
64 class Console_CommandLine
65 {
66 // Public properties {{{
67
68 /**
69 * Error messages.
70 *
71 * @var array $errors Error messages
72 * @todo move this to Console_CommandLine_MessageProvider
73 */
74 public static $errors = array(
75 'option_bad_name' => 'option name must be a valid php variable name (got: {$name})',
76 'argument_bad_name' => 'argument name must be a valid php variable name (got: {$name})',
77 'argument_no_default' => 'only optional arguments can have a default value',
78 'option_long_and_short_name_missing' => 'you must provide at least an option short name or long name for option "{$name}"',
79 'option_bad_short_name' => 'option "{$name}" short name must be a dash followed by a letter (got: "{$short_name}")',
80 'option_bad_long_name' => 'option "{$name}" long name must be 2 dashes followed by a word (got: "{$long_name}")',
81 'option_unregistered_action' => 'unregistered action "{$action}" for option "{$name}".',
82 'option_bad_action' => 'invalid action for option "{$name}".',
83 'option_invalid_callback' => 'you must provide a valid callback for option "{$name}"',
84 'action_class_does_not_exists' => 'action "{$name}" class "{$class}" not found, make sure that your class is available before calling Console_CommandLine::registerAction()',
85 'invalid_xml_file' => 'XML definition file "{$file}" does not exists or is not readable',
86 'invalid_rng_file' => 'RNG file "{$file}" does not exists or is not readable'
87 );
88
89 /**
90 * The name of the program, if not given it defaults to argv[0].
91 *
92 * @var string $name Name of your program
93 */
94 public $name;
95
96 /**
97 * A description text that will be displayed in the help message.
98 *
99 * @var string $description Description of your program
100 */
101 public $description = '';
102
103 /**
104 * A string that represents the version of the program, if this property is
105 * not empty and property add_version_option is not set to false, the
106 * command line parser will add a --version option, that will display the
107 * property content.
108 *
109 * @var string $version
110 * @access public
111 */
112 public $version = '';
113
114 /**
115 * Boolean that determine if the command line parser should add the help
116 * (-h, --help) option automatically.
117 *
118 * @var bool $add_help_option Whether to add a help option or not
119 */
120 public $add_help_option = true;
121
122 /**
123 * Boolean that determine if the command line parser should add the version
124 * (-v, --version) option automatically.
125 * Note that the version option is also generated only if the version
126 * property is not empty, it's up to you to provide a version string of
127 * course.
128 *
129 * @var bool $add_version_option Whether to add a version option or not
130 */
131 public $add_version_option = true;
132
133 /**
134 * Boolean that determine if providing a subcommand is mandatory.
135 *
136 * @var bool $subcommand_required Whether a subcommand is required or not
137 */
138 public $subcommand_required = false;
139
140 /**
141 * The command line parser renderer instance.
142 *
143 * @var object that implements Console_CommandLine_Renderer interface
144 * @access protected
145 */
146 public $renderer = false;
147
148 /**
149 * The command line parser outputter instance.
150 *
151 * @var Console_CommandLine_Outputter An outputter
152 */
153 public $outputter = false;
154
155 /**
156 * The command line message provider instance.
157 *
158 * @var Console_CommandLine_MessageProvider A message provider instance
159 */
160 public $message_provider = false;
161
162 /**
163 * Boolean that tells the parser to be POSIX compliant, POSIX demands the
164 * following behavior: the first non-option stops option processing.
165 *
166 * @var bool $force_posix Whether to force posix compliance or not
167 */
168 public $force_posix = false;
169
170 /**
171 * Boolean that tells the parser to set relevant options default values,
172 * according to the option action.
173 *
174 * @see Console_CommandLine_Option::setDefaults()
175 * @var bool $force_options_defaults Whether to force option default values
176 */
177 public $force_options_defaults = false;
178
179
180 /**
181 * Boolean that tells the parser to treat a single - option as an argument
182 * instead of trying to read STDIN.
183 *
184 * @var bool $avoid_reading_stdin Whether to treat - as an argument
185 */
186 public $avoid_reading_stdin = false;
187
188 /**
189 * An array of Console_CommandLine_Option objects.
190 *
191 * @var array $options The options array
192 */
193 public $options = array();
194
195 /**
196 * An array of Console_CommandLine_Argument objects.
197 *
198 * @var array $args The arguments array
199 */
200 public $args = array();
201
202 /**
203 * An array of Console_CommandLine_Command objects (sub commands).
204 *
205 * @var array $commands The commands array
206 */
207 public $commands = array();
208
209 /**
210 * Parent, only relevant in Command objects but left here for interface
211 * convenience.
212 *
213 * @var Console_CommandLine The parent instance
214 * @todo move Console_CommandLine::parent to Console_CommandLine_Command
215 */
216 public $parent = false;
217
218 /**
219 * Array of valid actions for an option, this array will also store user
220 * registered actions.
221 *
222 * The array format is:
223 * <pre>
224 * array(
225 * <ActionName:string> => array(<ActionClass:string>, <builtin:bool>)
226 * )
227 * </pre>
228 *
229 * @var array $actions List of valid actions
230 */
231 public static $actions = array(
232 'StoreTrue' => array('Console_CommandLine_Action_StoreTrue', true),
233 'StoreFalse' => array('Console_CommandLine_Action_StoreFalse', true),
234 'StoreString' => array('Console_CommandLine_Action_StoreString', true),
235 'StoreInt' => array('Console_CommandLine_Action_StoreInt', true),
236 'StoreFloat' => array('Console_CommandLine_Action_StoreFloat', true),
237 'StoreArray' => array('Console_CommandLine_Action_StoreArray', true),
238 'Callback' => array('Console_CommandLine_Action_Callback', true),
239 'Counter' => array('Console_CommandLine_Action_Counter', true),
240 'Help' => array('Console_CommandLine_Action_Help', true),
241 'Version' => array('Console_CommandLine_Action_Version', true),
242 'Password' => array('Console_CommandLine_Action_Password', true),
243 'List' => array('Console_CommandLine_Action_List', true),
244 );
245
246 /**
247 * Custom errors messages for this command
248 *
249 * This array is of the form:
250 * <code>
251 * <?php
252 * array(
253 * $messageName => $messageText,
254 * $messageName => $messageText,
255 * ...
256 * );
257 * ?>
258 * </code>
259 *
260 * If specified, these messages override the messages provided by the
261 * default message provider. For example:
262 * <code>
263 * <?php
264 * $messages = array(
265 * 'ARGUMENT_REQUIRED' => 'The argument foo is required.',
266 * );
267 * ?>
268 * </code>
269 *
270 * @var array
271 * @see Console_CommandLine_MessageProvider_Default
272 */
273 public $messages = array();
274
275 // }}}
276 // {{{ Private properties
277
278 /**
279 * Array of options that must be dispatched at the end.
280 *
281 * @var array $_dispatchLater Options to be dispatched
282 */
283 private $_dispatchLater = array();
284
285 private $_lastopt = false;
286 private $_stopflag = false;
287
288 // }}}
289 // __construct() {{{
290
291 /**
292 * Constructor.
293 * Example:
294 *
295 * <code>
296 * $parser = new Console_CommandLine(array(
297 * 'name' => 'yourprogram', // defaults to argv[0]
298 * 'description' => 'Description of your program',
299 * 'version' => '0.0.1', // your program version
300 * 'add_help_option' => true, // or false to disable --help option
301 * 'add_version_option' => true, // or false to disable --version option
302 * 'force_posix' => false // or true to force posix compliance
303 * ));
304 * </code>
305 *
306 * @param array $params An optional array of parameters
307 *
308 * @return void
309 */
310 public function __construct(array $params = array())
311 {
312 if (isset($params['name'])) {
313 $this->name = $params['name'];
314 } else if (isset($argv) && count($argv) > 0) {
315 $this->name = $argv[0];
316 } else if (isset($_SERVER['argv']) && count($_SERVER['argv']) > 0) {
317 $this->name = $_SERVER['argv'][0];
318 } else if (isset($_SERVER['SCRIPT_NAME'])) {
319 $this->name = basename($_SERVER['SCRIPT_NAME']);
320 }
321 if (isset($params['description'])) {
322 $this->description = $params['description'];
323 }
324 if (isset($params['version'])) {
325 $this->version = $params['version'];
326 }
327 if (isset($params['add_version_option'])) {
328 $this->add_version_option = $params['add_version_option'];
329 }
330 if (isset($params['add_help_option'])) {
331 $this->add_help_option = $params['add_help_option'];
332 }
333 if (isset($params['subcommand_required'])) {
334 $this->subcommand_required = $params['subcommand_required'];
335 }
336 if (isset($params['force_posix'])) {
337 $this->force_posix = $params['force_posix'];
338 } else if (getenv('POSIXLY_CORRECT')) {
339 $this->force_posix = true;
340 }
341 if (isset($params['messages']) && is_array($params['messages'])) {
342 $this->messages = $params['messages'];
343 }
344 // set default instances
345 $this->renderer = new Console_CommandLine_Renderer_Default($this);
346 $this->outputter = new Console_CommandLine_Outputter_Default();
347 $this->message_provider = new Console_CommandLine_MessageProvider_Default();
348 }
349
350 // }}}
351 // accept() {{{
352
353 /**
354 * Method to allow Console_CommandLine to accept either:
355 * + a custom renderer,
356 * + a custom outputter,
357 * + or a custom message provider
358 *
359 * @param mixed $instance The custom instance
360 *
361 * @return void
362 * @throws Console_CommandLine_Exception if wrong argument passed
363 */
364 public function accept($instance)
365 {
366 if ($instance instanceof Console_CommandLine_Renderer) {
367 if (property_exists($instance, 'parser') && !$instance->parser) {
368 $instance->parser = $this;
369 }
370 $this->renderer = $instance;
371 } else if ($instance instanceof Console_CommandLine_Outputter) {
372 $this->outputter = $instance;
373 } else if ($instance instanceof Console_CommandLine_MessageProvider) {
374 $this->message_provider = $instance;
375 } else {
376 throw Console_CommandLine_Exception::factory(
377 'INVALID_CUSTOM_INSTANCE',
378 array(),
379 $this,
380 $this->messages
381 );
382 }
383 }
384
385 // }}}
386 // fromXmlFile() {{{
387
388 /**
389 * Returns a command line parser instance built from an xml file.
390 *
391 * Example:
392 * <code>
393 * require_once 'Console/CommandLine.php';
394 * $parser = Console_CommandLine::fromXmlFile('path/to/file.xml');
395 * $result = $parser->parse();
396 * </code>
397 *
398 * @param string $file Path to the xml file
399 *
400 * @return Console_CommandLine The parser instance
401 */
402 public static function fromXmlFile($file)
403 {
404 include_once 'Console/CommandLine/XmlParser.php';
405 return Console_CommandLine_XmlParser::parse($file);
406 }
407
408 // }}}
409 // fromXmlString() {{{
410
411 /**
412 * Returns a command line parser instance built from an xml string.
413 *
414 * Example:
415 * <code>
416 * require_once 'Console/CommandLine.php';
417 * $xmldata = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
418 * <command>
419 * <description>Compress files</description>
420 * <option name="quiet">
421 * <short_name>-q</short_name>
422 * <long_name>--quiet</long_name>
423 * <description>be quiet when run</description>
424 * <action>StoreTrue/action>
425 * </option>
426 * <argument name="files">
427 * <description>a list of files</description>
428 * <multiple>true</multiple>
429 * </argument>
430 * </command>';
431 * $parser = Console_CommandLine::fromXmlString($xmldata);
432 * $result = $parser->parse();
433 * </code>
434 *
435 * @param string $string The xml data
436 *
437 * @return Console_CommandLine The parser instance
438 */
439 public static function fromXmlString($string)
440 {
441 include_once 'Console/CommandLine/XmlParser.php';
442 return Console_CommandLine_XmlParser::parseString($string);
443 }
444
445 // }}}
446 // addArgument() {{{
447
448 /**
449 * Adds an argument to the command line parser and returns it.
450 *
451 * Adds an argument with the name $name and set its attributes with the
452 * array $params, then return the Console_CommandLine_Argument instance
453 * created.
454 * The method accepts another form: you can directly pass a
455 * Console_CommandLine_Argument object as the sole argument, this allows
456 * you to contruct the argument separately, in order to reuse it in
457 * different command line parsers or commands for example.
458 *
459 * Example:
460 * <code>
461 * $parser = new Console_CommandLine();
462 * // add an array argument
463 * $parser->addArgument('input_files', array('multiple'=>true));
464 * // add a simple argument
465 * $parser->addArgument('output_file');
466 * $result = $parser->parse();
467 * print_r($result->args['input_files']);
468 * print_r($result->args['output_file']);
469 * // will print:
470 * // array('file1', 'file2')
471 * // 'file3'
472 * // if the command line was:
473 * // myscript.php file1 file2 file3
474 * </code>
475 *
476 * In a terminal, the help will be displayed like this:
477 * <code>
478 * $ myscript.php install -h
479 * Usage: myscript.php <input_files...> <output_file>
480 * </code>
481 *
482 * @param mixed $name A string containing the argument name or an
483 * instance of Console_CommandLine_Argument
484 * @param array $params An array containing the argument attributes
485 *
486 * @return Console_CommandLine_Argument the added argument
487 * @see Console_CommandLine_Argument
488 */
489 public function addArgument($name, $params = array())
490 {
491 if ($name instanceof Console_CommandLine_Argument) {
492 $argument = $name;
493 } else {
494 include_once 'Console/CommandLine/Argument.php';
495 $argument = new Console_CommandLine_Argument($name, $params);
496 }
497 $argument->validate();
498 $this->args[$argument->name] = $argument;
499 return $argument;
500 }
501
502 // }}}
503 // addCommand() {{{
504
505 /**
506 * Adds a sub-command to the command line parser.
507 *
508 * Adds a command with the given $name to the parser and returns the
509 * Console_CommandLine_Command instance, you can then populate the command
510 * with options, configure it, etc... like you would do for the main parser
511 * because the class Console_CommandLine_Command inherits from
512 * Console_CommandLine.
513 *
514 * An example:
515 * <code>
516 * $parser = new Console_CommandLine();
517 * $install_cmd = $parser->addCommand('install');
518 * $install_cmd->addOption(
519 * 'verbose',
520 * array(
521 * 'short_name' => '-v',
522 * 'long_name' => '--verbose',
523 * 'description' => 'be noisy when installing stuff',
524 * 'action' => 'StoreTrue'
525 * )
526 * );
527 * $parser->parse();
528 * </code>
529 * Then in a terminal:
530 * <code>
531 * $ myscript.php install -h
532 * Usage: myscript.php install [options]
533 *
534 * Options:
535 * -h, --help display this help message and exit
536 * -v, --verbose be noisy when installing stuff
537 *
538 * $ myscript.php install --verbose
539 * Installing whatever...
540 * $
541 * </code>
542 *
543 * @param mixed $name A string containing the command name or an
544 * instance of Console_CommandLine_Command
545 * @param array $params An array containing the command attributes
546 *
547 * @return Console_CommandLine_Command the added subcommand
548 * @see Console_CommandLine_Command
549 */
550 public function addCommand($name, $params = array())
551 {
552 if ($name instanceof Console_CommandLine_Command) {
553 $command = $name;
554 } else {
555 include_once 'Console/CommandLine/Command.php';
556 $params['name'] = $name;
557 $command = new Console_CommandLine_Command($params);
558 // some properties must cascade to the child command if not
559 // passed explicitely. This is done only in this case, because if
560 // we have a Command object we have no way to determine if theses
561 // properties have already been set
562 $cascade = array(
563 'add_help_option',
564 'add_version_option',
565 'outputter',
566 'message_provider',
567 'force_posix',
568 'force_options_defaults'
569 );
570 foreach ($cascade as $property) {
571 if (!isset($params[$property])) {
572 $command->$property = $this->$property;
573 }
574 }
575 if (!isset($params['renderer'])) {
576 $renderer = clone $this->renderer;
577 $renderer->parser = $command;
578 $command->renderer = $renderer;
579 }
580 }
581 $command->parent = $this;
582 $this->commands[$command->name] = $command;
583 return $command;
584 }
585
586 // }}}
587 // addOption() {{{
588
589 /**
590 * Adds an option to the command line parser and returns it.
591 *
592 * Adds an option with the name $name and set its attributes with the
593 * array $params, then return the Console_CommandLine_Option instance
594 * created.
595 * The method accepts another form: you can directly pass a
596 * Console_CommandLine_Option object as the sole argument, this allows
597 * you to contruct the option separately, in order to reuse it in different
598 * command line parsers or commands for example.
599 *
600 * Example:
601 * <code>
602 * $parser = new Console_CommandLine();
603 * $parser->addOption('path', array(
604 * 'short_name' => '-p', // a short name
605 * 'long_name' => '--path', // a long name
606 * 'description' => 'path to the dir', // a description msg
607 * 'action' => 'StoreString',
608 * 'default' => '/tmp' // a default value
609 * ));
610 * $parser->parse();
611 * </code>
612 *
613 * In a terminal, the help will be displayed like this:
614 * <code>
615 * $ myscript.php --help
616 * Usage: myscript.php [options]
617 *
618 * Options:
619 * -h, --help display this help message and exit
620 * -p, --path path to the dir
621 *
622 * </code>
623 *
624 * Various methods to specify an option, these 3 commands are equivalent:
625 * <code>
626 * $ myscript.php --path=some/path
627 * $ myscript.php -p some/path
628 * $ myscript.php -psome/path
629 * </code>
630 *
631 * @param mixed $name A string containing the option name or an
632 * instance of Console_CommandLine_Option
633 * @param array $params An array containing the option attributes
634 *
635 * @return Console_CommandLine_Option The added option
636 * @see Console_CommandLine_Option
637 */
638 public function addOption($name, $params = array())
639 {
640 include_once 'Console/CommandLine/Option.php';
641 if ($name instanceof Console_CommandLine_Option) {
642 $opt = $name;
643 } else {
644 $opt = new Console_CommandLine_Option($name, $params);
645 }
646 $opt->validate();
647 if ($this->force_options_defaults) {
648 $opt->setDefaults();
649 }
650 $this->options[$opt->name] = $opt;
651 if (!empty($opt->choices) && $opt->add_list_option) {
652 $this->addOption('list_' . $opt->name, array(
653 'long_name' => '--list-' . $opt->name,
654 'description' => $this->message_provider->get(
655 'LIST_OPTION_MESSAGE',
656 array('name' => $opt->name)
657 ),
658 'action' => 'List',
659 'action_params' => array('list' => $opt->choices),
660 ));
661 }
662 return $opt;
663 }
664
665 // }}}
666 // displayError() {{{
667
668 /**
669 * Displays an error to the user via stderr and exit with $exitCode if its
670 * value is not equals to false.
671 *
672 * @param string $error The error message
673 * @param int $exitCode The exit code number (default: 1). If set to
674 * false, the exit() function will not be called
675 *
676 * @return void
677 */
678 public function displayError($error, $exitCode = 1)
679 {
680 $this->outputter->stderr($this->renderer->error($error));
681 if ($exitCode !== false) {
682 exit($exitCode);
683 }
684 }
685
686 // }}}
687 // displayUsage() {{{
688
689 /**
690 * Displays the usage help message to the user via stdout and exit with
691 * $exitCode if its value is not equals to false.
692 *
693 * @param int $exitCode The exit code number (default: 0). If set to
694 * false, the exit() function will not be called
695 *
696 * @return void
697 */
698 public function displayUsage($exitCode = 0)
699 {
700 $this->outputter->stdout($this->renderer->usage());
701 if ($exitCode !== false) {
702 exit($exitCode);
703 }
704 }
705
706 // }}}
707 // displayVersion() {{{
708
709 /**
710 * Displays the program version to the user via stdout and exit with
711 * $exitCode if its value is not equals to false.
712 *
713 *
714 * @param int $exitCode The exit code number (default: 0). If set to
715 * false, the exit() function will not be called
716 *
717 * @return void
718 */
719 public function displayVersion($exitCode = 0)
720 {
721 $this->outputter->stdout($this->renderer->version());
722 if ($exitCode !== false) {
723 exit($exitCode);
724 }
725 }
726
727 // }}}
728 // findOption() {{{
729
730 /**
731 * Finds the option that matches the given short_name (ex: -v), long_name
732 * (ex: --verbose) or name (ex: verbose).
733 *
734 * @param string $str The option identifier
735 *
736 * @return mixed A Console_CommandLine_Option instance or false
737 */
738 public function findOption($str)
739 {
740 $str = trim($str);
741 if ($str === '') {
742 return false;
743 }
744 $matches = array();
745 foreach ($this->options as $opt) {
746 if ($opt->short_name == $str || $opt->long_name == $str ||
747 $opt->name == $str) {
748 // exact match
749 return $opt;
750 }
751 if (substr($opt->long_name, 0, strlen($str)) === $str) {
752 // abbreviated long option
753 $matches[] = $opt;
754 }
755 }
756 if ($count = count($matches)) {
757 if ($count > 1) {
758 $matches_str = '';
759 $padding = '';
760 foreach ($matches as $opt) {
761 $matches_str .= $padding . $opt->long_name;
762 $padding = ', ';
763 }
764 throw Console_CommandLine_Exception::factory(
765 'OPTION_AMBIGUOUS',
766 array('name' => $str, 'matches' => $matches_str),
767 $this,
768 $this->messages
769 );
770 }
771 return $matches[0];
772 }
773 return false;
774 }
775 // }}}
776 // registerAction() {{{
777
778 /**
779 * Registers a custom action for the parser, an example:
780 *
781 * <code>
782 *
783 * // in this example we create a "range" action:
784 * // the user will be able to enter something like:
785 * // $ <program> -r 1,5
786 * // and in the result we will have:
787 * // $result->options['range']: array(1, 5)
788 *
789 * require_once 'Console/CommandLine.php';
790 * require_once 'Console/CommandLine/Action.php';
791 *
792 * class ActionRange extends Console_CommandLine_Action
793 * {
794 * public function execute($value=false, $params=array())
795 * {
796 * $range = explode(',', str_replace(' ', '', $value));
797 * if (count($range) != 2) {
798 * throw new Exception(sprintf(
799 * 'Option "%s" must be 2 integers separated by a comma',
800 * $this->option->name
801 * ));
802 * }
803 * $this->setResult($range);
804 * }
805 * }
806 * // then we can register our action
807 * Console_CommandLine::registerAction('Range', 'ActionRange');
808 * // and now our action is available !
809 * $parser = new Console_CommandLine();
810 * $parser->addOption('range', array(
811 * 'short_name' => '-r',
812 * 'long_name' => '--range',
813 * 'action' => 'Range', // note our custom action
814 * 'description' => 'A range of two integers separated by a comma'
815 * ));
816 * // etc...
817 *
818 * </code>
819 *
820 * @param string $name The name of the custom action
821 * @param string $class The class name of the custom action
822 *
823 * @return void
824 */
825 public static function registerAction($name, $class)
826 {
827 if (!isset(self::$actions[$name])) {
828 if (!class_exists($class)) {
829 self::triggerError('action_class_does_not_exists',
830 E_USER_ERROR,
831 array('{$name}' => $name, '{$class}' => $class));
832 }
833 self::$actions[$name] = array($class, false);
834 }
835 }
836
837 // }}}
838 // triggerError() {{{
839
840 /**
841 * A wrapper for programming errors triggering.
842 *
843 * @param string $msgId Identifier of the message
844 * @param int $level The php error level
845 * @param array $params An array of search=>replaces entries
846 *
847 * @return void
848 * @todo remove Console::triggerError() and use exceptions only
849 */
850 public static function triggerError($msgId, $level, $params=array())
851 {
852 if (isset(self::$errors[$msgId])) {
853 $msg = str_replace(array_keys($params),
854 array_values($params), self::$errors[$msgId]);
855 trigger_error($msg, $level);
856 } else {
857 trigger_error('unknown error', $level);
858 }
859 }
860
861 // }}}
862 // parse() {{{
863
864 /**
865 * Parses the command line arguments and returns a
866 * Console_CommandLine_Result instance.
867 *
868 * @param integer $userArgc Number of arguments (optional)
869 * @param array $userArgv Array containing arguments (optional)
870 *
871 * @return Console_CommandLine_Result The result instance
872 * @throws Exception on user errors
873 */
874 public function parse($userArgc=null, $userArgv=null)
875 {
876 $this->_lastopt = false;
877 $this->_stopflag = false;
878
879 $this->addBuiltinOptions();
880 if ($userArgc !== null && $userArgv !== null) {
881 $argc = $userArgc;
882 $argv = $userArgv;
883 } else {
884 list($argc, $argv) = $this->getArgcArgv();
885 }
886 // build an empty result
887 include_once 'Console/CommandLine/Result.php';
888 $result = new Console_CommandLine_Result();
889 if (!($this instanceof Console_CommandLine_Command)) {
890 // remove script name if we're not in a subcommand
891 array_shift($argv);
892 $argc--;
893 }
894 // will contain arguments
895 $args = array();
896 foreach ($this->options as $name=>$option) {
897 $result->options[$name] = $option->default;
898 }
899 // parse command line tokens
900 while ($argc--) {
901 $token = array_shift($argv);
902 try {
903 if ($cmd = $this->_getSubCommand($token)) {
904 $result->command_name = $cmd->name;
905 $result->command = $cmd->parse($argc, $argv);
906 break;
907 } else {
908 $this->parseToken($token, $result, $args, $argc);
909 }
910 } catch (Exception $exc) {
911 throw $exc;
912 }
913 }
914 // Parse a null token to allow any undespatched actions to be despatched.
915 $this->parseToken(null, $result, $args, 0);
916 // Check if an invalid subcommand was specified. If there are
917 // subcommands and no arguments, but an argument was provided, it is
918 // an invalid subcommand.
919 if ( count($this->commands) > 0
920 && count($this->args) === 0
921 && count($args) > 0
922 ) {
923 throw Console_CommandLine_Exception::factory(
924 'INVALID_SUBCOMMAND',
925 array('command' => $args[0]),
926 $this,
927 $this->messages
928 );
929 }
930 // if subcommand_required is set to true we must check that we have a
931 // subcommand.
932 if ( count($this->commands)
933 && $this->subcommand_required
934 && !$result->command_name
935 ) {
936 throw Console_CommandLine_Exception::factory(
937 'SUBCOMMAND_REQUIRED',
938 array('commands' => implode(array_keys($this->commands), ', ')),
939 $this,
940 $this->messages
941 );
942 }
943 // minimum argument number check
944 $argnum = 0;
945 foreach ($this->args as $name=>$arg) {
946 if (!$arg->optional) {
947 $argnum++;
948 }
949 }
950 if (count($args) < $argnum) {
951 throw Console_CommandLine_Exception::factory(
952 'ARGUMENT_REQUIRED',
953 array('argnum' => $argnum, 'plural' => $argnum>1 ? 's': ''),
954 $this,
955 $this->messages
956 );
957 }
958 // handle arguments
959 $c = count($this->args);
960 foreach ($this->args as $name=>$arg) {
961 $c--;
962 if ($arg->multiple) {
963 $result->args[$name] = $c ? array_splice($args, 0, -$c) : $args;
964 } else {
965 $result->args[$name] = array_shift($args);
966 }
967 if (!$result->args[$name] && $arg->optional && $arg->default) {
968 $result->args[$name] = $arg->default;
969 }
970 // check value is in argument choices
971 if (!empty($this->args[$name]->choices)) {
972 foreach ($result->args[$name] as $value) {
973 if (!in_array($value, $arg->choices)) {
974 throw Console_CommandLine_Exception::factory(
975 'ARGUMENT_VALUE_NOT_VALID',
976 array(
977 'name' => $name,
978 'choices' => implode('", "', $arg->choices),
979 'value' => implode(' ', $result->args[$name]),
980 ),
981 $this,
982 $arg->messages
983 );
984 }
985 }
986 }
987 }
988 // dispatch deferred options
989 foreach ($this->_dispatchLater as $optArray) {
990 $optArray[0]->dispatchAction($optArray[1], $optArray[2], $this);
991 }
992 return $result;
993 }
994
995 // }}}
996 // parseToken() {{{
997
998 /**
999 * Parses the command line token and modifies *by reference* the $options
1000 * and $args arrays.
1001 *
1002 * @param string $token The command line token to parse
1003 * @param object $result The Console_CommandLine_Result instance
1004 * @param array &$args The argv array
1005 * @param int $argc Number of lasting args
1006 *
1007 * @return void
1008 * @access protected
1009 * @throws Exception on user errors
1010 */
1011 protected function parseToken($token, $result, &$args, $argc)
1012 {
1013 $last = $argc === 0;
1014 if (!$this->_stopflag && $this->_lastopt) {
1015 if (strlen($token) > ($this->avoid_reading_stdin ? 1 : 0) &&
1016 substr($token, 0, 1) == '-') {
1017 if ($this->_lastopt->argument_optional) {
1018 $this->_dispatchAction($this->_lastopt, '', $result);
1019 if ($this->_lastopt->action != 'StoreArray') {
1020 $this->_lastopt = false;
1021 }
1022 } else if (isset($result->options[$this->_lastopt->name])) {
1023 // case of an option that expect a list of args
1024 $this->_lastopt = false;
1025 } else {
1026 throw Console_CommandLine_Exception::factory(
1027 'OPTION_VALUE_REQUIRED',
1028 array('name' => $this->_lastopt->name),
1029 $this,
1030 $this->messages
1031 );
1032 }
1033 } else {
1034 // when a StoreArray option is positioned last, the behavior
1035 // is to consider that if there's already an element in the
1036 // array, and the commandline expects one or more args, we
1037 // leave last tokens to arguments
1038 if ($this->_lastopt->action == 'StoreArray'
1039 && !empty($result->options[$this->_lastopt->name])
1040 && count($this->args) > ($argc + count($args))
1041 ) {
1042 if (!is_null($token)) {
1043 $args[] = $token;
1044 }
1045 return;
1046 }
1047 if (!is_null($token) || $this->_lastopt->action == 'Password') {
1048 $this->_dispatchAction($this->_lastopt, $token, $result);
1049 }
1050 if ($this->_lastopt->action != 'StoreArray') {
1051 $this->_lastopt = false;
1052 }
1053 return;
1054 }
1055 }
1056 if (!$this->_stopflag && substr($token, 0, 2) == '--') {
1057 // a long option
1058 $optkv = explode('=', $token, 2);
1059 if (trim($optkv[0]) == '--') {
1060 // the special argument "--" forces in all cases the end of
1061 // option scanning.
1062 $this->_stopflag = true;
1063 return;
1064 }
1065 $opt = $this->findOption($optkv[0]);
1066 if (!$opt) {
1067 throw Console_CommandLine_Exception::factory(
1068 'OPTION_UNKNOWN',
1069 array('name' => $optkv[0]),
1070 $this,
1071 $this->messages
1072 );
1073 }
1074 $value = isset($optkv[1]) ? $optkv[1] : false;
1075 if (!$opt->expectsArgument() && $value !== false) {
1076 throw Console_CommandLine_Exception::factory(
1077 'OPTION_VALUE_UNEXPECTED',
1078 array('name' => $opt->name, 'value' => $value),
1079 $this,
1080 $this->messages
1081 );
1082 }
1083 if ($opt->expectsArgument() && $value === false) {
1084 // maybe the long option argument is separated by a space, if
1085 // this is the case it will be the next arg
1086 if ($last && !$opt->argument_optional) {
1087 throw Console_CommandLine_Exception::factory(
1088 'OPTION_VALUE_REQUIRED',
1089 array('name' => $opt->name),
1090 $this,
1091 $this->messages
1092 );
1093 }
1094 // we will have a value next time
1095 $this->_lastopt = $opt;
1096 return;
1097 }
1098 if ($opt->action == 'StoreArray') {
1099 $this->_lastopt = $opt;
1100 }
1101 $this->_dispatchAction($opt, $value, $result);
1102 } else if (!$this->_stopflag &&
1103 strlen($token) > ($this->avoid_reading_stdin ? 1 : 0) &&
1104 substr($token, 0, 1) == '-') {
1105 // a short option
1106 $optname = substr($token, 0, 2);
1107 if ($optname == '-' && !$this->avoid_reading_stdin) {
1108 // special case of "-": try to read stdin
1109 $args[] = file_get_contents('php://stdin');
1110 return;
1111 }
1112 $opt = $this->findOption($optname);
1113 if (!$opt) {
1114 throw Console_CommandLine_Exception::factory(
1115 'OPTION_UNKNOWN',
1116 array('name' => $optname),
1117 $this,
1118 $this->messages
1119 );
1120 }
1121 // parse other options or set the value
1122 // in short: handle -f<value> and -f <value>
1123 $next = substr($token, 2, 1);
1124 // check if we must wait for a value
1125 if (!$next) {
1126 if ($opt->expectsArgument()) {
1127 if ($last && !$opt->argument_optional) {
1128 throw Console_CommandLine_Exception::factory(
1129 'OPTION_VALUE_REQUIRED',
1130 array('name' => $opt->name),
1131 $this,
1132 $this->messages
1133 );
1134 }
1135 // we will have a value next time
1136 $this->_lastopt = $opt;
1137 return;
1138 }
1139 $value = false;
1140 } else {
1141 if (!$opt->expectsArgument()) {
1142 if ($nextopt = $this->findOption('-' . $next)) {
1143 $this->_dispatchAction($opt, false, $result);
1144 $this->parseToken('-' . substr($token, 2), $result,
1145 $args, $last);
1146 return;
1147 } else {
1148 throw Console_CommandLine_Exception::factory(
1149 'OPTION_UNKNOWN',
1150 array('name' => $next),
1151 $this,
1152 $this->messages
1153 );
1154 }
1155 }
1156 if ($opt->action == 'StoreArray') {
1157 $this->_lastopt = $opt;
1158 }
1159 $value = substr($token, 2);
1160 }
1161 $this->_dispatchAction($opt, $value, $result);
1162 } else {
1163 // We have an argument.
1164 // if we are in POSIX compliant mode, we must set the stop flag to
1165 // true in order to stop option parsing.
1166 if (!$this->_stopflag && $this->force_posix) {
1167 $this->_stopflag = true;
1168 }
1169 if (!is_null($token)) {
1170 $args[] = $token;
1171 }
1172 }
1173 }
1174
1175 // }}}
1176 // addBuiltinOptions() {{{
1177
1178 /**
1179 * Adds the builtin "Help" and "Version" options if needed.
1180 *
1181 * @return void
1182 */
1183 public function addBuiltinOptions()
1184 {
1185 if ($this->add_help_option) {
1186 $helpOptionParams = array(
1187 'long_name' => '--help',
1188 'description' => 'show this help message and exit',
1189 'action' => 'Help'
1190 );
1191 if (!($option = $this->findOption('-h')) || $option->action == 'Help') {
1192 // short name is available, take it
1193 $helpOptionParams['short_name'] = '-h';
1194 }
1195 $this->addOption('help', $helpOptionParams);
1196 }
1197 if ($this->add_version_option && !empty($this->version)) {
1198 $versionOptionParams = array(
1199 'long_name' => '--version',
1200 'description' => 'show the program version and exit',
1201 'action' => 'Version'
1202 );
1203 if (!$this->findOption('-v')) {
1204 // short name is available, take it
1205 $versionOptionParams['short_name'] = '-v';
1206 }
1207 $this->addOption('version', $versionOptionParams);
1208 }
1209 }
1210
1211 // }}}
1212 // getArgcArgv() {{{
1213
1214 /**
1215 * Tries to return an array containing argc and argv, or trigger an error
1216 * if it fails to get them.
1217 *
1218 * @return array The argc/argv array
1219 * @throws Console_CommandLine_Exception
1220 */
1221 protected function getArgcArgv()
1222 {
1223 if (php_sapi_name() != 'cli') {
1224 // we have a web request
1225 $argv = array($this->name);
1226 if (isset($_REQUEST)) {
1227 foreach ($_REQUEST as $key => $value) {
1228 if (!is_array($value)) {
1229 $value = array($value);
1230 }
1231 $opt = $this->findOption($key);
1232 if ($opt instanceof Console_CommandLine_Option) {
1233 // match a configured option
1234 $argv[] = $opt->short_name ?
1235 $opt->short_name : $opt->long_name;
1236 foreach ($value as $v) {
1237 if ($opt->expectsArgument()) {
1238 $argv[] = isset($_REQUEST[$key]) ? urldecode($v) : $v;
1239 } else if ($v == '0' || $v == 'false') {
1240 array_pop($argv);
1241 }
1242 }
1243 } else if (isset($this->args[$key])) {
1244 // match a configured argument
1245 foreach ($value as $v) {
1246 $argv[] = isset($_REQUEST[$key]) ? urldecode($v) : $v;
1247 }
1248 }
1249 }
1250 }
1251 return array(count($argv), $argv);
1252 }
1253 if (isset($argc) && isset($argv)) {
1254 // case of register_argv_argc = 1
1255 return array($argc, $argv);
1256 }
1257 if (isset($_SERVER['argc']) && isset($_SERVER['argv'])) {
1258 return array($_SERVER['argc'], $_SERVER['argv']);
1259 }
1260 return array(0, array());
1261 }
1262
1263 // }}}
1264 // _dispatchAction() {{{
1265
1266 /**
1267 * Dispatches the given option or store the option to dispatch it later.
1268 *
1269 * @param Console_CommandLine_Option $option The option instance
1270 * @param string $token Command line token to parse
1271 * @param Console_CommandLine_Result $result The result instance
1272 *
1273 * @return void
1274 */
1275 private function _dispatchAction($option, $token, $result)
1276 {
1277 if ($option->action == 'Password') {
1278 $this->_dispatchLater[] = array($option, $token, $result);
1279 } else {
1280 $option->dispatchAction($token, $result, $this);
1281 }
1282 }
1283 // }}}
1284 // _getSubCommand() {{{
1285
1286 /**
1287 * Tries to return the subcommand that matches the given token or returns
1288 * false if no subcommand was found.
1289 *
1290 * @param string $token Current command line token
1291 *
1292 * @return mixed An instance of Console_CommandLine_Command or false
1293 */
1294 private function _getSubCommand($token)
1295 {
1296 foreach ($this->commands as $cmd) {
1297 if ($cmd->name == $token || in_array($token, $cmd->aliases)) {
1298 return $cmd;
1299 }
1300 }
1301 return false;
1302 }
1303
1304 // }}}
1305 }