1<?php
2/**
3 * File containing the ezcConsoleInput class.
4 *
5 * @package ConsoleTools
6 * @version 1.6.1
7 * @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
8 * @license http://ez.no/licenses/new_bsd New BSD License
9 * @filesource
10 */
11
12/**
13 * The ezcConsoleInput class handles the given options and arguments on the console.
14 *
15 * This class allows the complete handling of options and arguments submitted
16 * to a console based application.
17 *
18 * The next example demonstrate how to capture the console options:
19 *
20 * <code>
21 * $optionHandler = new ezcConsoleInput();
22 *
23 * // Register simple parameter -h/--help
24 * $optionHandler->registerOption( new ezcConsoleOption( 'h', 'help' ) );
25 *
26 * // Register complex parameter -f/--file
27 * $file = new ezcConsoleOption(
28 *  'f',
29 *  'file',
30 *  ezcConsoleInput::TYPE_STRING,
31 *  null,
32 *  false,
33 *  'Process a file.',
34 *  'Processes a single file.'
35 * );
36 * $optionHandler->registerOption( $file );
37 *
38 * // Manipulate parameter -f/--file after registration
39 * $file->multiple = true;
40 *
41 * // Register another complex parameter that depends on -f and excludes -h
42 * $dir = new ezcConsoleOption(
43 *  'd',
44 *  'dir',
45 *  ezcConsoleInput::TYPE_STRING,
46 *  null,
47 *  true,
48 *  'Process a directory.',
49 *  'Processes a complete directory.',
50 *  array( new ezcConsoleOptionRule( $optionHandler->getOption( 'f' ) ) ),
51 *  array( new ezcConsoleOptionRule( $optionHandler->getOption( 'h' ) ) )
52 * );
53 * $optionHandler->registerOption( $dir );
54 *
55 * // Register an alias for this parameter
56 * $optionHandler->registerAlias( 'e', 'extended-dir', $dir );
57 *
58 * // Process registered parameters and handle errors
59 * try
60 * {
61 *      $optionHandler->process( array( 'example_input.php', '-h' ) );
62 * }
63 * catch ( ezcConsoleOptionException $e )
64 * {
65 *      echo $e->getMessage();
66 *      exit( 1 );
67 * }
68 *
69 * // Process a single parameter
70 * $file = $optionHandler->getOption( 'f' );
71 * if ( $file->value === false )
72 * {
73 *      echo "Parameter -{$file->short}/--{$file->long} was not submitted.\n";
74 * }
75 * elseif ( $file->value === true )
76 * {
77 *      echo "Parameter -{$file->short}/--{$file->long} was submitted without value.\n";
78 * }
79 * else
80 * {
81 *      echo "Parameter -{$file->short}/--{$file->long} was submitted with value '".var_export($file->value, true)."'.\n";
82 * }
83 *
84 * // Process all parameters at once:
85 * foreach ( $optionHandler->getOptionValues() as $paramShort => $val )
86 * {
87 *      switch ( true )
88 *      {
89 *          case $val === false:
90 *              echo "Parameter $paramShort was not submitted.\n";
91 *              break;
92 *          case $val === true:
93 *              echo "Parameter $paramShort was submitted without a value.\n";
94 *              break;
95 *          case is_array( $val ):
96 *              echo "Parameter $paramShort was submitted multiple times with value: '".implode(', ', $val)."'.\n";
97 *              break;
98 *          default:
99 *              echo "Parameter $paramShort was submitted with value: '$val'.\n";
100 *              break;
101 *      }
102 * }
103 * </code>
104 *
105 * @package ConsoleTools
106 * @version 1.6.1
107 * @mainclass
108 *
109 * @property ezcConsoleArguments $argumentDefinition Optional argument definition.
110 */
111class ezcConsoleInput
112{
113    /**
114     * Option does not carry a value.
115     */
116    const TYPE_NONE     = 1;
117
118    /**
119     * Option takes an integer value.
120     */
121    const TYPE_INT      = 2;
122
123    /**
124     * Option takes a string value.
125     */
126    const TYPE_STRING   = 3;
127
128    /**
129     * Array of option definitions, indexed by number.
130     *
131     * This array stores the ezcConsoleOption objects representing
132     * the options.
133     *
134     * For lookup of an option after its short or long values the attributes
135     * {@link ezcConsoleInput::$optionShort}
136     * {@link ezcConsoleInput::$optionLong}
137     * are used.
138     *
139     * @var array(array)
140     */
141    private $options = array();
142
143    /**
144     * Short option names.
145     *
146     * Each references a key in {@link ezcConsoleInput::$options}.
147     *
148     * @var array(string=>int)
149     */
150    private $optionShort = array();
151
152    /**
153     * Long option names.
154     *
155     * Each references a key in {@link ezcConsoleInput::$options}.
156     *
157     * @var array(string=>int)
158     */
159    private $optionLong = array();
160
161    /**
162     * Arguments, if submitted, are stored here.
163     *
164     * @var array(string)
165     */
166    private $arguments = array();
167
168    /**
169     * Wether the process() method has already been called.
170     *
171     * @var bool
172     */
173    private $processed = false;
174
175    /**
176     * Indicates if an option was submitted, that has the isHelpOption flag set.
177     *
178     * @var bool
179     */
180    private $helpOptionSet = false;
181
182    /**
183     * Tool object for multi-byte encoding safe string operations.
184     *
185     * @var ezcConsoleStringTool
186     */
187    private $stringTool;
188
189    /**
190     * Input validator.
191     *
192     * @var ezcConsoleInputValidator
193     */
194    private $validator;
195
196    /**
197     * Help generator.
198     *
199     * @var ezcConsoleInputHelpGenerator
200     */
201    private $helpGenerator;
202
203    /**
204     * Collection of properties.
205     *
206     * @var array(string=>mixed)
207     */
208    protected $properties = array();
209
210    /**
211     * Creates an input handler.
212     */
213    public function __construct()
214    {
215        $this->argumentDefinition = null;
216        $this->stringTool         = new ezcConsoleStringTool();
217
218        // @TODO Verify interface and make plugable
219        $this->validator     = new ezcConsoleStandardInputValidator();
220        $this->helpGenerator = new ezcConsoleInputStandardHelpGenerator( $this );
221    }
222
223    /**
224     * Registers the new option $option.
225     *
226     * This method adds the new option $option to your option collection. If
227     * already an option with the assigned short or long value exists, an
228     * exception will be thrown.
229     *
230     * @see ezcConsoleInput::unregisterOption()
231     *
232     * @param ezcConsoleOption $option
233     *
234     * @return ezcConsoleOption The recently registered option.
235     */
236    public function registerOption( ezcConsoleOption $option )
237    {
238        foreach ( $this->optionShort as $short => $ref )
239        {
240            if ( $short === $option->short )
241            {
242                throw new ezcConsoleOptionAlreadyRegisteredException( $short );
243            }
244        }
245        foreach ( $this->optionLong as $long => $ref )
246        {
247            if ( $long === $option->long )
248            {
249                throw new ezcConsoleOptionAlreadyRegisteredException( $long );
250            }
251        }
252        $this->options[] = $option;
253        $this->optionLong[$option->long] = $option;
254        if ( $option->short !== "" )
255        {
256            $this->optionShort[$option->short] = $option;
257        }
258        return $option;
259    }
260
261    /**
262     * Registers an alias for an option.
263     *
264     * Registers a new alias for an existing option. Aliases can
265     * be used as if they were a normal option.
266     *
267     * The alias is registered with the short option name $short and the
268     * long option name $long. The alias references to the existing
269     * option $option.
270     *
271     * @see ezcConsoleInput::unregisterAlias()
272     *
273     * @param string $short
274     * @param string $long
275     * @param ezcConsoleOption $option
276     *
277     *
278     * @throws ezcConsoleOptionNotExistsException
279     *         If the referenced option is not registered.
280     * @throws ezcConsoleOptionAlreadyRegisteredException
281     *         If another option/alias has taken the provided short or long name.
282     * @return void
283     */
284    public function registerAlias( $short, $long, ezcConsoleOption $option )
285    {
286        if ( !isset( $this->optionShort[$option->short] ) || !isset( $this->optionLong[$option->long] ) )
287        {
288            throw new ezcConsoleOptionNotExistsException( $option->long );
289        }
290        if ( isset( $this->optionShort[$short] ) || isset( $this->optionLong[$long] ) )
291        {
292            throw new ezcConsoleOptionAlreadyRegisteredException( isset( $this->optionShort[$short] ) ? "-$short" : "--$long" );
293        }
294        $this->optionShort[$short] = $option;
295        $this->optionLong[$long]   = $option;
296    }
297
298    /**
299     * Registers options according to a string specification.
300     *
301     * Accepts a string to define parameters and registers all parameters as
302     * options accordingly. String definition, specified in $optionDef, looks
303     * like this:
304     *
305     * <code>
306     * [s:|size:][u:|user:][a:|all:]
307     * </code>
308     *
309     * This string registers 3 parameters:
310     * -s / --size
311     * -u / --user
312     * -a / --all
313     *
314     * @param string $optionDef
315     * @return void
316     *
317     * @throws ezcConsoleOptionStringNotWellformedException
318     *         If provided string does not have the correct format.
319     */
320    public function registerOptionString( $optionDef )
321    {
322        $regex = '\[([a-z0-9-]+)([:?*+])?([^|]*)\|([a-z0-9-]+)([:?*+])?\]';
323        // Check string for wellformedness
324        if ( preg_match( "/^($regex)+$/", $optionDef ) == 0 )
325        {
326            throw new ezcConsoleOptionStringNotWellformedException( "Option definition not wellformed: \"$optionDef\"" );
327        }
328        if ( preg_match_all( "/$regex/", $optionDef, $matches ) )
329        {
330            foreach ( $matches[1] as $id => $short )
331            {
332                $option = null;
333                $option = new ezcConsoleOption( $short, $matches[4][$id] );
334                if ( !empty( $matches[2][$id] ) || !empty( $matches[5][$id] ) )
335                {
336                    switch ( !empty( $matches[2][$id] ) ? $matches[2][$id] : $matches[5][$id] )
337                    {
338                        case '*':
339                            // Allows 0 or more occurances
340                            $option->multiple = true;
341                            break;
342                        case '+':
343                            // Allows 1 or more occurances
344                            $option->multiple = true;
345                            $option->type = self::TYPE_STRING;
346                            break;
347                        case '?':
348                            $option->type = self::TYPE_STRING;
349                            $option->default = '';
350                            break;
351                        default:
352                            break;
353                    }
354                }
355                if ( !empty( $matches[3][$id] ) )
356                {
357                    $option->default = $matches[3][$id];
358                }
359                $this->registerOption( $option );
360            }
361        }
362    }
363
364    /**
365     * Removes an option.
366     *
367     * This function removes an option. All dependencies to that
368     * specific option are removed completely from every other registered
369     * option.
370     *
371     * @see ezcConsoleInput::registerOption()
372     *
373     * @param ezcConsoleOption $option The option object to unregister.
374     *
375     * @throws ezcConsoleOptionNotExistsException
376     *         If requesting a not registered option.
377     * @return void
378     */
379    public function unregisterOption( ezcConsoleOption $option )
380    {
381        $found = false;
382        foreach ( $this->options as $id => $existParam )
383        {
384            if ( $existParam === $option )
385            {
386                $found = true;
387                unset( $this->options[$id] );
388                continue;
389            }
390            $existParam->removeAllExclusions( $option );
391            $existParam->removeAllDependencies( $option );
392        }
393        if ( $found === false )
394        {
395            throw new ezcConsoleOptionNotExistsException( $option->long );
396        }
397        foreach ( $this->optionLong as $name => $existParam )
398        {
399            if ( $existParam === $option )
400            {
401                unset( $this->optionLong[$name] );
402            }
403        }
404        foreach ( $this->optionShort as $name => $existParam )
405        {
406            if ( $existParam === $option )
407            {
408                unset( $this->optionShort[$name] );
409            }
410        }
411    }
412
413    /**
414     * Removes an alias to an option.
415     *
416     * This function removes an alias with the short name $short and long
417     * name $long.
418     *
419     * @see ezcConsoleInput::registerAlias()
420     *
421     * @throws ezcConsoleOptionNoAliasException
422     *      If the requested short/long name belongs to a real parameter instead.
423     *
424     * @param string $short
425     * @param string $long
426     * @return void
427     *
428     * @todo Check if $short and $long refer to the same option!
429     */
430    public function unregisterAlias( $short, $long )
431    {
432        foreach ( $this->options as $id => $option )
433        {
434            if ( $option->short === $short )
435            {
436                throw new ezcConsoleOptionNoAliasException( $short );
437            }
438            if ( $option->long === $long )
439            {
440                throw new ezcConsoleOptionNoAliasException( $long );
441            }
442        }
443        if ( isset( $this->optionShort[$short] ) )
444        {
445            unset( $this->optionShort[$short] );
446        }
447        if ( isset( $this->optionLong[$long] ) )
448        {
449            unset( $this->optionLong[$long] );
450        }
451    }
452
453    /**
454     * Returns the definition object for the option with the name $name.
455     *
456     * This method receives the long or short name of an option and
457     * returns the ezcConsoleOption object.
458     *
459     * @param string $name  Short or long name of the option (without - or --).
460     * @return ezcConsoleOption
461     *
462     * @throws ezcConsoleOptionNotExistsException
463     *         If requesting a not registered parameter.
464     */
465    public function getOption( $name )
466    {
467        $name = $name;
468        if ( isset( $this->optionShort[$name] ) )
469        {
470            return $this->optionShort[$name];
471        }
472        if ( isset( $this->optionLong[$name] ) )
473        {
474            return $this->optionLong[$name];
475        }
476        throw new ezcConsoleOptionNotExistsException( $name );
477    }
478
479    /**
480     * Process the input parameters.
481     *
482     * Actually process the input options and arguments according to the actual
483     * settings.
484     *
485     * Per default this method uses $argc and $argv for processing. You can
486     * override this setting with your own input, if necessary, using the
487     * parameters of this method. (Attention, first argument is always the pro
488     * gram name itself!)
489     *
490     * All exceptions thrown by this method contain an additional attribute "option"
491     * which specifies the parameter on which the error occurred.
492     *
493     * @param array(string) $args The arguments
494     * @return void
495     *
496     * @throws ezcConsoleOptionNotExistsException
497     *         If an option that was submitted does not exist.
498     * @throws ezcConsoleOptionDependencyViolationException
499     *         If a dependency rule was violated.
500     * @throws ezcConsoleOptionExclusionViolationException
501     *         If an exclusion rule was violated.
502     * @throws ezcConsoleOptionTypeViolationException
503     *         If the type of a submitted value violates the options type rule.
504     * @throws ezcConsoleOptionArgumentsViolationException
505     *         If arguments are passed although a parameter disallowed them.
506     *
507     * @see ezcConsoleOptionException
508     */
509    public function process( array $args = null )
510    {
511        if ( $this->processed )
512        {
513            $this->reset();
514        }
515        $this->processed = true;
516
517        if ( !isset( $args ) )
518        {
519            $args = isset( $argv ) ? $argv : isset( $_SERVER['argv'] ) ? $_SERVER['argv'] : array();
520        }
521
522        $nextIndex = $this->processOptions( $args );
523
524        if ( $this->helpOptionSet() )
525        {
526            // No need to parse arguments
527            return;
528        }
529
530        $this->processArguments( $args, $nextIndex );
531
532        $this->checkRules();
533
534        $this->setOptionDefaults();
535    }
536
537    /**
538     * Sets defaults for options that have not been submitted.
539     *
540     * Checks all options if they have been submited. If not and a default
541     * values is present, this is set as the options value.
542     */
543    private function setOptionDefaults()
544    {
545        foreach ( $this->options as $option )
546        {
547            if ( $option->value === false || $option->value === array() )
548            {
549                // Default value to set?
550                if ( $option->default !== null )
551                {
552                    $option->value = $option->default;
553                }
554            }
555        }
556    }
557
558    /**
559     * Reads the submitted options from $args array.
560     *
561     * Returns the next index to check for arguments.
562     *
563     * @param array(string) $args
564     * @returns int
565     *
566     * @throws ezcConsoleOptionNotExistsException
567     *         if a submitted option does not exist.
568     * @throws ezcConsoleOptionTooManyValuesException
569     *         if an option that expects only a single value was submitted
570     *         with multiple values.
571     * @throws ezcConsoleOptionTypeViolationException
572     *         if an option was submitted with a value of the wrong type.
573     * @throws ezcConsoleOptionMissingValueException
574     *         if an option thats expects a value was submitted without.
575     */
576    private function processOptions( array $args )
577    {
578        $numArgs = count( $args );
579        $i = 1;
580
581        while ( $i < $numArgs )
582        {
583            if ( $args[$i] === '--' )
584            {
585                break;
586            }
587
588            // Equalize parameter handling (long params with =)
589            if ( iconv_substr( $args[$i], 0, 2, 'UTF-8' ) == '--' )
590            {
591                $this->preprocessLongOption( $args, $i );
592                // Update number of args, changed by preprocessLongOption()
593                $numArgs = count( $args );
594            }
595
596            // Check for parameter
597            if ( iconv_substr( $args[$i], 0, 1, 'UTF-8' ) === '-' )
598            {
599                if ( !$this->hasOption( preg_replace( '/^-*/', '', $args[$i] ) ) )
600                {
601                    throw new ezcConsoleOptionNotExistsException( $args[$i] );
602                }
603                $this->processOption( $args, $i );
604            }
605            // Must be the arguments
606            else
607            {
608                break;
609            }
610        }
611
612        // Move pointer over argument sign
613        isset( $args[$i] ) && $args[$i] == '--' ? ++$i : $i;
614
615        return $i;
616    }
617
618    /**
619     * Resets all option and argument values.
620     *
621     * This method is called automatically by {@link process()}, if this method
622     * is called twice or more, and may also be used to manually reset the
623     * values of all registered {@ezcConsoleOption} and {@link
624     * ezcConsoleArgument} objects.
625     */
626    public function reset()
627    {
628        foreach ( $this->options as $option )
629        {
630            $option->value = false;
631        }
632        if ( $this->argumentDefinition !== null )
633        {
634            foreach ( $this->argumentDefinition as $argument )
635            {
636                $argument->value = null;
637            }
638        }
639        $this->arguments = array();
640    }
641
642    /**
643     * Returns true if an option with the given name exists, otherwise false.
644     *
645     * Checks if an option with the given name is registered.
646     *
647     * @param string $name Short or long name of the option.
648     * @return bool True if option exists, otherwise false.
649     */
650    public function hasOption( $name )
651    {
652        try
653        {
654            $param = $this->getOption( $name );
655        }
656        catch ( ezcConsoleOptionNotExistsException $e )
657        {
658            return false;
659        }
660        return true;
661    }
662
663    /**
664     * Returns an array of all registered options.
665     *
666     * Returns an array of all registered options in the following format:
667     * <code>
668     * array(
669     *      0 => ezcConsoleOption,
670     *      1 => ezcConsoleOption,
671     *      2 => ezcConsoleOption,
672     *      ...
673     * );
674     * </code>
675     *
676     * @return array(string=>ezcConsoleOption) Registered options.
677     */
678    public function getOptions()
679    {
680        return $this->options;
681    }
682
683    /**
684     * Returns the values of all submitted options.
685     *
686     * Returns an array of all values submitted to the options. The array is
687     * indexed by the parameters short name (excluding the '-' prefix). The array
688     * does not contain any parameter, which value is 'false' (meaning: the
689     * parameter was not submitted).
690     *
691     * @param bool $longnames Wheather to use longnames for indexing.
692     * @return array(string=>mixed)
693     */
694    public function getOptionValues( $longnames = false )
695    {
696        $res = array();
697        foreach ( $this->options as $param )
698        {
699            if ( $param->value !== false )
700            {
701                $res[( $longnames === true ) ? $param->long : $param->short] = $param->value;
702            }
703        }
704        return $res;
705    }
706
707    /**
708     * Returns arguments provided to the program.
709     *
710     * This method returns all arguments provided to a program in an
711     * int indexed array. Arguments are sorted in the way
712     * they are submitted to the program. You can disable arguments
713     * through the 'arguments' flag of a parameter, if you want
714     * to disallow arguments.
715     *
716     * Arguments are either the last part of the program call (if the
717     * last parameter is not a 'multiple' one) or divided via the '--'
718     * method which is commonly used on Unix (if the last parameter
719     * accepts multiple values this is required).
720     *
721     * @return array(string) Arguments.
722     */
723    public function getArguments()
724    {
725        return $this->arguments;
726    }
727
728    /**
729     * Get help information for your options.
730     *
731     * This method returns an array of help information for your options,
732     * indexed by int. Each help info has 2 fields:
733     *
734     * 0 => The options names ("<short> / <long>")
735     * 1 => The help text (depending on the $long parameter)
736     *
737     * The $long options determines if you want to get the short or long help
738     * texts. The array returned can be used by {@link ezcConsoleTable}.
739     *
740     * If using the second options, you can filter the options shown in the
741     * help output (e.g. to show short help for related options). Provide
742     * as simple number indexed array of short and/or long values to set a filter.
743     *
744     * The $paramGrouping option can be used to group options in the help
745     * output. The structure of this array parameter is as follows:
746     *
747     * <code>
748     *  array(
749     *      'First section' => array(
750     *          'input',
751     *          'output'
752     *          'overwrite',
753     *      ),
754     *      'Second section' => array(
755     *          'v',
756     *          'h',
757     *      ),
758     *  )
759     * </code>
760     *
761     * As can be seen, short option names are possible as well as long ones.
762     * The key of the first array level is the name of the section, which is
763     * assigned to an array of options to group under this section. The $params
764     * parameter still influences if an option is displayed at all.
765     *
766     * @param bool $long
767     * @param array(string) $params
768     * @param array(string=>array(string)) $paramGrouping
769     * @return array(array(string)) Table structure as explained.
770     *
771     * @apichange In future versions, the default values of $params will change
772     *            to null instead of an empty array. Giving an empty array for
773     *            these will then be taken literally.
774     */
775    public function getHelp( $long = false, array $params = array(), array $paramGrouping = null )
776    {
777        // New handling
778        $params = ( $params === array() || $params === null ? null : $params );
779
780        $help = array();
781        if ( $paramGrouping === null )
782        {
783            // Original handling
784            $help = $this->getOptionHelpWithoutGrouping( $long, $params );
785        }
786        else
787        {
788            $help = $this->getOptionHelpWithGrouping( $long, $params, $paramGrouping );
789        }
790
791        if ( $this->argumentDefinition !== null )
792        {
793            $help[] = array( "Arguments:", '' );
794
795            $argumentsHelp = $this->helpGenerator->generateArgumentHelp( $long );
796            if ( $argumentsHelp === array() )
797            {
798                $help[] = array( '', "No arguments available." );
799            }
800            else
801            {
802                $help = array_merge( $help, $argumentsHelp );
803            }
804        }
805
806        return $help;
807    }
808
809    /**
810     * Creates the option help array in the original, ungrouped way.
811     *
812     * Creates the original help array generated by {@link getHelp()}. The
813     * $long and $params options are the same as they are for this method.
814     *
815     * @param bool $long
816     * @param array $params
817     * @return array
818     */
819    private function getOptionHelpWithoutGrouping( $long, $params )
820    {
821        return $this->helpGenerator->generateUngroupedOptionHelp(
822            $long,
823            $params
824        );
825    }
826
827    /**
828     * Generates options helo array with ordering and grouping.
829     *
830     * @param mixed $long
831     * @param mixed $params
832     * @param mixed $paramGrouping
833     * @return array()
834     */
835    private function getOptionHelpWithGrouping( $long, $params, $paramGrouping )
836    {
837        $rawHelp = $this->helpGenerator->generateGroupedOptionHelp(
838            $paramGrouping,
839            $long,
840            $params
841        );
842
843        $help  = array();
844        $first = true;
845        foreach ( $rawHelp as $category => $optionsHelp )
846        {
847            if ( !$first )
848            {
849                $help[] = array( '', '' );
850            }
851            else
852            {
853                $first = false;
854            }
855
856            $help[] = array( $category, '' );
857            $help = array_merge( $help, $optionsHelp );
858        }
859        return $help;
860    }
861
862
863    /**
864     * Get help information for your options as a table.
865     *
866     * This method provides the information returned by
867     * {@link ezcConsoleInput::getHelp()} in a table.
868     *
869     * The $paramGrouping option can be used to group options in the help
870     * output. The structure of this array parameter is as follows:
871     *
872     * <code>
873     *  array(
874     *      'First section' => array(
875     *          'input',
876     *          'output'
877     *          'overwrite',
878     *      ),
879     *      'Second section' => array(
880     *          'v',
881     *          'h',
882     *      ),
883     *  )
884     * </code>
885     *
886     * As can be seen, short option names are possible as well as long ones.
887     * The key of the first array level is the name of the section, which is
888     * assigned to an array of options to group under this section. The $params
889     * parameter still influences if an option as displayed at all.
890     *
891     * @param ezcConsoleTable $table     The table object to fill.
892     * @param bool $long                 Set this to true for getting the
893     *                                   long help version.
894     * @param array(string) $params Set of option names to generate help
895     *                                   for, default is all.
896     * @param array(string=>array(string)) $paramGrouping
897     * @return ezcConsoleTable           The filled table.
898     */
899    public function getHelpTable( ezcConsoleTable $table, $long = false, array $params = array(), $paramGrouping = null )
900    {
901        $help = $this->getHelp( $long, $params, $paramGrouping );
902        $i = 0;
903        foreach ( $help as $row )
904        {
905            $table[$i][0]->content = $row[0];
906            $table[$i++][1]->content = $row[1];
907        }
908        return $table;
909    }
910
911    /**
912     * Returns a standard help output for your program.
913     *
914     * This method generates a help text as it's commonly known from Unix
915     * command line programs. The output will contain the synopsis, your
916     * provided program description and the selected parameter help
917     * as also provided by {@link ezcConsoleInput::getHelp()}. The returned
918     * string can directly be printed to the console.
919     *
920     * The $paramGrouping option can be used to group options in the help
921     * output. The structure of this array parameter is as follows:
922     *
923     * <code>
924     *  array(
925     *      'First section' => array(
926     *          'input',
927     *          'output'
928     *          'overwrite',
929     *      ),
930     *      'Second section' => array(
931     *          'v',
932     *          'h',
933     *      ),
934     *  )
935     * </code>
936     *
937     * As can be seen, short option names are possible as well as long ones.
938     * The key of the first array level is the name of the section, which is
939     * assigned to an array of options to group under this section. The $params
940     * parameter still influences if an option as displayed at all.
941     *
942     * @param string $programDesc        The description of your program.
943     * @param int $width                 The width to adjust the output text to.
944     * @param bool $long                 Set this to true for getting the long
945     *                                   help version.
946     * @param array(string) $params Set of option names to generate help
947     *                                   for, default is all.
948     * @param array(string=>array(string)) $paramGrouping
949     * @return string The generated help text.
950     */
951    public function getHelpText( $programDesc, $width = 80, $long = false, array $params = null, $paramGrouping = null )
952    {
953        $help = $this->getHelp( $long, ( $params == null ? array() : $params ), $paramGrouping );
954
955        // Determine max length of first column text.
956        $maxLength = 0;
957        foreach ( $help as $row )
958        {
959            $maxLength = max( $maxLength, iconv_strlen( $row[0], 'UTF-8' ) );
960        }
961
962        // Width of left column
963        $leftColWidth = $maxLength + 2;
964        // Width of righ column
965        $rightColWidth = $width - $leftColWidth;
966
967        $res = 'Usage: ' . $this->getSynopsis( $params ) . PHP_EOL;
968        $res .= $this->stringTool->wordwrap( $programDesc, $width, PHP_EOL );
969        $res .= PHP_EOL . PHP_EOL;
970        foreach ( $help as $row )
971        {
972            $rowParts = explode(
973                "\n",
974                $this->stringTool->wordwrap( $row[1], $rightColWidth )
975            );
976
977            $res .= $this->stringTool->strPad( $row[0], $leftColWidth, ' ' );
978            $res .= $rowParts[0] . PHP_EOL;
979            // @TODO: Fix function call in loop header
980            for ( $i = 1; $i < sizeof( $rowParts ); $i++ )
981            {
982                $res .= str_repeat( ' ', $leftColWidth ) . $rowParts[$i] . PHP_EOL;
983            }
984        }
985        return $res;
986    }
987
988    /**
989     * Returns the synopsis string for the program.
990     *
991     * This gives you a synopsis definition for the options and arguments
992     * defined with this instance of ezcConsoleInput. You can filter the
993     * options named in the synopsis by submitting their short names in an
994     * array as the parameter of this method. If the parameter $optionNames
995     * is set, only those options are listed in the synopsis.
996     *
997     * @param array(string) $optionNames
998     * @return string
999     */
1000    public function getSynopsis( array $optionNames = null )
1001    {
1002        return $this->helpGenerator->generateSynopsis( $optionNames );
1003    }
1004
1005    /**
1006     * Returns if a help option was set.
1007     * This method returns if an option was submitted, which was defined to be
1008     * a help option, using the isHelpOption flag.
1009     *
1010     * @return bool If a help option was set.
1011     */
1012    public function helpOptionSet()
1013    {
1014        return $this->helpOptionSet;
1015    }
1016
1017    /**
1018     * Property read access.
1019     *
1020     * @throws ezcBasePropertyNotFoundException
1021     *         If the the desired property is not found.
1022     *
1023     * @param string $propertyName Name of the property.
1024     * @return mixed Value of the property or null.
1025     * @ignore
1026     */
1027    public function __get( $propertyName )
1028    {
1029        if ( !isset( $this->$propertyName ) )
1030        {
1031                throw new ezcBasePropertyNotFoundException( $propertyName );
1032        }
1033        return $this->properties[$propertyName];
1034    }
1035
1036    /**
1037     * Property set access.
1038     *
1039     * @param string $propertyName
1040     * @param string $propertyValue
1041     * @ignore
1042     * @return void
1043     */
1044    public function __set( $propertyName, $propertyValue )
1045    {
1046        switch ( $propertyName )
1047        {
1048            case "argumentDefinition":
1049                if ( ( $propertyValue instanceof ezcConsoleArguments ) === false && $propertyValue !== null )
1050                {
1051                    throw new ezcBaseValueException( $propertyName, $propertyValue, "ezcConsoleArguments" );
1052                }
1053                break;
1054            default:
1055                throw new ezcBasePropertyNotFoundException( $propertyName );
1056        }
1057        $this->properties[$propertyName] = $propertyValue;
1058    }
1059
1060    /**
1061     * Property isset access.
1062     *
1063     * @param string $propertyName Name of the property.
1064     * @return bool True if the property is set, otherwise false.
1065     * @ignore
1066     */
1067    public function __isset( $propertyName )
1068    {
1069        return array_key_exists( $propertyName, $this->properties );
1070    }
1071
1072    /**
1073     * Returns the synopsis string for a single option and its dependencies.
1074     *
1075     * This method returns a part of the program synopsis, specifically for a
1076     * certain parameter. The method recursively adds depending parameters up
1077     * to the 2nd depth level to the synopsis. The second parameter is used
1078     * to store the short names of all options that have already been used in
1079     * the synopsis (to avoid adding an option twice). The 3rd parameter
1080     * determines the actual deps in the option dependency recursion to
1081     * terminate that after 2 recursions.
1082     *
1083     * @param ezcConsoleOption $option        The option to include.
1084     * @param array(string) $usedOptions Array of used option short names.
1085     * @param int $depth                      Current recursion depth.
1086     * @return string The synopsis for this parameter.
1087     *
1088     * @apichange This method is deprecates. Implement your own {@link
1089     *            ezcConsoleInputHelpGenerator} instead, as soon as the
1090     *            interface is made public.
1091     */
1092    protected function createOptionSynopsis( ezcConsoleOption $option, &$usedOptions, $depth = 0 )
1093    {
1094        $synopsis = '';
1095
1096        // Break after a nesting level of 2
1097        if ( $depth++ > 2 || ( in_array( $option->short, $usedOptions['short'] ) && in_array( $option->long, $usedOptions['long'] ) ) ) return $synopsis;
1098
1099        $usedOptions['short'][] = $option->short;
1100        $usedOptions['long'][]  = $option->long;
1101
1102        $synopsis .= $option->short !== "" ? "-{$option->short}" : "--{$option->long}";
1103
1104        if ( isset( $option->default ) )
1105        {
1106            $synopsis .= " " . ( $option->type === ezcConsoleInput::TYPE_STRING ? '"' : '' ) . $option->default . ( $option->type === ezcConsoleInput::TYPE_STRING ? '"' : '' );
1107        }
1108        else if ( $option->type !== ezcConsoleInput::TYPE_NONE )
1109        {
1110            $synopsis .= " ";
1111            switch ( $option->type )
1112            {
1113                case ezcConsoleInput::TYPE_STRING:
1114                    $synopsis .= "<string>";
1115                    break;
1116                case ezcConsoleInput::TYPE_INT:
1117                    $synopsis .= "<int>";
1118                    break;
1119            }
1120        }
1121
1122        foreach ( $option->getDependencies() as $rule )
1123        {
1124            $deeperSynopsis = $this->createOptionSynopsis( $rule->option, $usedOptions, $depth );
1125            $synopsis .= ( iconv_strlen( trim( $deeperSynopsis ), 'UTF-8' ) > 0
1126                ? ' ' . $deeperSynopsis
1127                : ''
1128            );
1129        }
1130
1131        if ( $option->arguments === false )
1132        {
1133            $allowsArgs = false;
1134        }
1135
1136        // Make the whole thing optional?
1137        if ( $option->mandatory === false )
1138        {
1139            $synopsis = "[$synopsis]";
1140        }
1141
1142        return $synopsis . ' ';
1143    }
1144
1145    /**
1146     * Process an option.
1147     *
1148     * This method does the processing of a single option.
1149     *
1150     * @param array(string) $args The arguments array.
1151     * @param int $i                   The current position in the arguments array.
1152     * @return void
1153     *
1154     * @throws ezcConsoleOptionTooManyValuesException
1155     *         If an option that expects only a single value was submitted
1156     *         with multiple values.
1157     * @throws ezcConsoleOptionTypeViolationException
1158     *         If an option was submitted with a value of the wrong type.
1159     * @throws ezcConsoleOptionMissingValueException
1160     *         If an option thats expects a value was submitted without.
1161     */
1162    private function processOption( array $args, &$i )
1163    {
1164        $option = $this->getOption( preg_replace( '/^-+/', '', $args[$i++] ) );
1165
1166        // Is the actual option a help option?
1167        if ( $option->isHelpOption === true )
1168        {
1169            $this->helpOptionSet = true;
1170        }
1171        // No value expected
1172        if ( $option->type === ezcConsoleInput::TYPE_NONE )
1173        {
1174            // No value expected
1175            if ( isset( $args[$i] ) && iconv_substr( $args[$i], 0, 1, 'UTF-8' ) !== '-' && sizeof( $args ) > ( $i + 1 ) )
1176            {
1177                // But one found
1178                throw new ezcConsoleOptionTypeViolationException( $option, $args[$i] );
1179            }
1180            // Multiple occurance possible
1181            if ( $option->multiple === true )
1182            {
1183                $option->value[] = true;
1184            }
1185            else
1186            {
1187                $option->value = true;
1188            }
1189            // Everything fine, nothing to do
1190            return $i;
1191        }
1192        // Value expected, check for it
1193        if ( isset( $args[$i] ) && iconv_substr( $args[$i], 0, 1, 'UTF-8' ) !== '-' )
1194        {
1195            // Type check
1196            if ( $this->isCorrectType( $option->type, $args[$i] ) === false )
1197            {
1198                throw new ezcConsoleOptionTypeViolationException( $option, $args[$i] );
1199            }
1200            // Multiple values possible
1201            if ( $option->multiple === true )
1202            {
1203                $option->value[] = $args[$i];
1204            }
1205            // Only single value expected, check for multiple
1206            elseif ( isset( $option->value ) && $option->value !== false )
1207            {
1208                throw new ezcConsoleOptionTooManyValuesException( $option );
1209            }
1210            else
1211            {
1212                $option->value = $args[$i];
1213            }
1214            $i++;
1215        }
1216        // Value found? If not, use default, if available
1217        if ( !isset( $option->value ) || $option->value === false || ( is_array( $option->value ) && count( $option->value ) === 0) )
1218        {
1219            throw new ezcConsoleOptionMissingValueException( $option );
1220        }
1221    }
1222
1223    /**
1224     * Process arguments given to the program.
1225     *
1226     * @param array(string) $args The arguments array.
1227     * @param int $i                   Current index in arguments array.
1228     * @return void
1229     */
1230    private function processArguments( array $args, &$i )
1231    {
1232        $numArgs = count( $args );
1233        if ( $this->argumentDefinition === null || $this->argumentsAllowed() === false )
1234        {
1235            // Old argument handling, also used of a set option sets disallowing arguments
1236            while ( $i < $numArgs )
1237            {
1238                $this->arguments[] = $args[$i++];
1239            }
1240        }
1241        else
1242        {
1243            $mandatory = true;
1244            foreach ( $this->argumentDefinition as $arg )
1245            {
1246                // Check if all followinga arguments are optional
1247                if ( $arg->mandatory === false )
1248                {
1249                    $mandatory = false;
1250                }
1251
1252                // Check if the current argument is present and mandatory
1253                if ( $mandatory === true )
1254                {
1255                    if ( !isset( $args[$i] ) )
1256                    {
1257                        throw new ezcConsoleArgumentMandatoryViolationException( $arg );
1258                    }
1259                }
1260                else
1261                {
1262                    // Arguments are optional, if no more left: return.
1263                    if ( !isset( $args[$i] ) )
1264                    {
1265                        // Optional and no more arguments left, assign default
1266                        $arg->value = $arg->default;
1267                        continue;
1268                    }
1269                }
1270
1271                if ( $arg->multiple === true )
1272                {
1273                    $arg->value = array();
1274                    for ( $i = $i; $i < $numArgs; ++$i )
1275                    {
1276                        if ( $this->isCorrectType( $arg->type, $args[$i] ) === false )
1277                        {
1278                            throw new ezcConsoleArgumentTypeViolationException( $arg, $args[$i] );
1279                        }
1280                        $arg->value = array_merge( $arg->value, array( $args[$i] ) );
1281                        // Keep old handling, too
1282                        $this->arguments[] = $args[$i];
1283                    }
1284                    return;
1285                }
1286                else
1287                {
1288                    if ( $this->isCorrectType( $arg->type, $args[$i] ) === false )
1289                    {
1290                        throw new ezcConsoleArgumentTypeViolationException( $arg, $args[$i] );
1291                    }
1292                    $arg->value = $args[$i];
1293                    // Keep old handling, too
1294                    $this->arguments[] = $args[$i];
1295                }
1296                ++$i;
1297            }
1298
1299            if ( $i < $numArgs )
1300            {
1301                throw new ezcConsoleTooManyArgumentsException( $args, $i );
1302            }
1303        }
1304    }
1305
1306    /**
1307     * Returns if arguments are allowed with the current option submition.
1308     *
1309     * @return bool If arguments allowed.
1310     */
1311    protected function argumentsAllowed()
1312    {
1313        foreach ( $this->options as $id => $option )
1314        {
1315            if ( $option->value !== false && $option->arguments === false )
1316            {
1317                return false;
1318            }
1319        }
1320        return true;
1321    }
1322
1323    /**
1324     * Check the rules that may be associated with an option.
1325     *
1326     * Options are allowed to have rules associated for dependencies to other
1327     * options and exclusion of other options or arguments. This method
1328     * processes the checks.
1329     *
1330     * @throws ezcConsoleException
1331     *         in case validation fails.
1332     */
1333    private function checkRules()
1334    {
1335        // If a help option is set, skip rule checking
1336        if ( $this->helpOptionSet === true )
1337        {
1338            return;
1339        }
1340        $this->validator->validateOptions(
1341            $this->options,
1342            ( $this->arguments !== array() )
1343        );
1344    }
1345
1346    /**
1347     * Checks if a value is of a given type. Converts the value to the
1348     * correct PHP type on success.
1349     *
1350     * @param int $type   The type to check for. One of self::TYPE_*.
1351     * @param string $val The value to check. Will possibly altered!
1352     * @return bool True on succesful check, otherwise false.
1353     */
1354    private function isCorrectType( $type, &$val )
1355    {
1356        $res = false;
1357        switch ( $type )
1358        {
1359            case ezcConsoleInput::TYPE_STRING:
1360                $res = true;
1361                $val = preg_replace( '/^(["\'])(.*)\1$/', '\2', $val );
1362                break;
1363            case ezcConsoleInput::TYPE_INT:
1364                $res = preg_match( '/^[0-9]+$/', $val ) ? true : false;
1365                if ( $res )
1366                {
1367                    $val = ( int ) $val;
1368                }
1369                break;
1370        }
1371        return $res;
1372    }
1373
1374    /**
1375     * Split parameter and value for long option names.
1376     *
1377     * This method checks for long options, if the value is passed using =. If
1378     * this is the case parameter and value get split and replaced in the
1379     * arguments array.
1380     *
1381     * @param array(string) $args The arguments array
1382     * @param int $i                   Current arguments array position
1383     * @return void
1384     */
1385    private function preprocessLongOption( array &$args, $i )
1386    {
1387        // Value given?
1388        if ( preg_match( '/^--\w+\=[^ ]/i', $args[$i] ) )
1389        {
1390            // Split param and value and replace current param
1391            $parts = explode( '=', $args[$i], 2 );
1392            array_splice( $args, $i, 1, $parts );
1393        }
1394    }
1395}
1396?>
1397