move Horde_Argv to git
authorChuck Hagenbuch <chuck@horde.org>
Sat, 15 Nov 2008 14:53:18 +0000 (09:53 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Sat, 15 Nov 2008 14:53:18 +0000 (09:53 -0500)
78 files changed:
framework/Argv/CVS/Entries [new file with mode: 0644]
framework/Argv/CVS/Repository [new file with mode: 0644]
framework/Argv/CVS/Root [new file with mode: 0644]
framework/Argv/CVS/Template [new file with mode: 0644]
framework/Argv/lib/CVS/Entries [new file with mode: 0644]
framework/Argv/lib/CVS/Repository [new file with mode: 0644]
framework/Argv/lib/CVS/Root [new file with mode: 0644]
framework/Argv/lib/CVS/Template [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/AmbiguousOptionException.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/BadOptionException.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/CVS/Entries [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/CVS/Repository [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/CVS/Root [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/CVS/Template [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/Exception.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/HelpFormatter.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/IndentedHelpFormatter.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/Option.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/OptionConflictException.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/OptionContainer.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/OptionException.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/OptionGroup.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/OptionValueException.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/Parser.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/TitledHelpFormatter.php [new file with mode: 0644]
framework/Argv/lib/Horde/Argv/Values.php [new file with mode: 0644]
framework/Argv/lib/Horde/CVS/Entries [new file with mode: 0644]
framework/Argv/lib/Horde/CVS/Repository [new file with mode: 0644]
framework/Argv/lib/Horde/CVS/Root [new file with mode: 0644]
framework/Argv/lib/Horde/CVS/Template [new file with mode: 0644]
framework/Argv/package.xml [new file with mode: 0644]
framework/Argv/test/CVS/Entries [new file with mode: 0644]
framework/Argv/test/CVS/Repository [new file with mode: 0644]
framework/Argv/test/CVS/Root [new file with mode: 0644]
framework/Argv/test/CVS/Template [new file with mode: 0644]
framework/Argv/test/Horde/Argv/AllTests.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/BoolTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CVS/Entries [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CVS/Repository [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CVS/Root [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CVS/Template [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CallbackCheckAbbrevTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CallbackExtraArgsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CallbackManyArgsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CallbackMeddleArgsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CallbackTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CallbackVarArgsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ChoiceTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ConflictOverrideTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ConflictResolveTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ConflictTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ConflictTestBase.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ConflictingDefaultsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/CountTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/DefaultValuesTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ExpandDefaultsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ExtendAddActionsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ExtendAddTypesTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/HelpTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/InterceptedException.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/InterceptingParser.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/MatchAbbrevTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/MultipleArgsAppendTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/MultipleArgsTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/OptionChecksTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/OptionGroupTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/OptionValuesTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ParseNumTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ParserTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/ProgNameTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/StandardTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/TestBase.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/TypeAliasesTest.php [new file with mode: 0644]
framework/Argv/test/Horde/Argv/VersionTest.php [new file with mode: 0644]
framework/Argv/test/Horde/CVS/Entries [new file with mode: 0644]
framework/Argv/test/Horde/CVS/Repository [new file with mode: 0644]
framework/Argv/test/Horde/CVS/Root [new file with mode: 0644]
framework/Argv/test/Horde/CVS/Template [new file with mode: 0644]

diff --git a/framework/Argv/CVS/Entries b/framework/Argv/CVS/Entries
new file mode 100644 (file)
index 0000000..95639e3
--- /dev/null
@@ -0,0 +1,3 @@
+/package.xml/1.1/Fri Feb  1 01:41:30 2008//
+D/lib////
+D/test////
diff --git a/framework/Argv/CVS/Repository b/framework/Argv/CVS/Repository
new file mode 100644 (file)
index 0000000..77c3d32
--- /dev/null
@@ -0,0 +1 @@
+framework/Argv
diff --git a/framework/Argv/CVS/Root b/framework/Argv/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Argv/CVS/Template b/framework/Argv/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Argv/lib/CVS/Entries b/framework/Argv/lib/CVS/Entries
new file mode 100644 (file)
index 0000000..ae2779e
--- /dev/null
@@ -0,0 +1 @@
+D/Horde////
diff --git a/framework/Argv/lib/CVS/Repository b/framework/Argv/lib/CVS/Repository
new file mode 100644 (file)
index 0000000..bc9ed53
--- /dev/null
@@ -0,0 +1 @@
+framework/Argv/lib
diff --git a/framework/Argv/lib/CVS/Root b/framework/Argv/lib/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Argv/lib/CVS/Template b/framework/Argv/lib/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Argv/lib/Horde/Argv/AmbiguousOptionException.php b/framework/Argv/lib/Horde/Argv/AmbiguousOptionException.php
new file mode 100644 (file)
index 0000000..ccfbd64
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Raised if an ambiguous option is seen on the command line.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_AmbiguousOptionException extends Horde_Argv_BadOptionException
+{
+    public function __construct($opt_str, $possibilities)
+    {
+        // Have to skip the BadOptionException constructor or the string gets double-prefixed.
+        Horde_Argv_OptionException::__construct(sprintf(_("ambiguous option: %s (%s?)"), $opt_str, implode(', ', $possibilities)));
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/BadOptionException.php b/framework/Argv/lib/Horde/Argv/BadOptionException.php
new file mode 100644 (file)
index 0000000..56783f4
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Raised if an invalid option is seen on the command line.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_BadOptionException extends Horde_Argv_OptionException
+{
+    public function __construct($opt_str)
+    {
+        parent::__construct(sprintf(_("no such option: %s"), $opt_str));
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/CVS/Entries b/framework/Argv/lib/Horde/Argv/CVS/Entries
new file mode 100644 (file)
index 0000000..ef3afe4
--- /dev/null
@@ -0,0 +1,15 @@
+/AmbiguousOptionException.php/1.2/Wed Jun 18 19:00:45 2008//
+/BadOptionException.php/1.2/Wed Jun 18 19:00:45 2008//
+/Exception.php/1.2/Wed Jun 18 19:00:45 2008//
+/HelpFormatter.php/1.2/Wed Jun 18 19:00:45 2008//
+/IndentedHelpFormatter.php/1.3/Wed Jun 18 19:00:45 2008//
+/Option.php/1.3/Wed Jun 18 19:00:45 2008//
+/OptionConflictException.php/1.2/Wed Jun 18 19:00:45 2008//
+/OptionContainer.php/1.2/Wed Jun 18 19:00:45 2008//
+/OptionException.php/1.2/Wed Jun 18 19:00:45 2008//
+/OptionGroup.php/1.2/Wed Jun 18 19:00:45 2008//
+/OptionValueException.php/1.2/Wed Jun 18 19:00:45 2008//
+/Parser.php/1.3/Wed Jun 18 19:00:45 2008//
+/TitledHelpFormatter.php/1.2/Wed Jun 18 19:00:45 2008//
+/Values.php/1.2/Wed Jun 18 19:00:45 2008//
+D
diff --git a/framework/Argv/lib/Horde/Argv/CVS/Repository b/framework/Argv/lib/Horde/Argv/CVS/Repository
new file mode 100644 (file)
index 0000000..1867cc4
--- /dev/null
@@ -0,0 +1 @@
+framework/Argv/lib/Horde/Argv
diff --git a/framework/Argv/lib/Horde/Argv/CVS/Root b/framework/Argv/lib/Horde/Argv/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Argv/lib/Horde/Argv/CVS/Template b/framework/Argv/lib/Horde/Argv/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Argv/lib/Horde/Argv/Exception.php b/framework/Argv/lib/Horde/Argv/Exception.php
new file mode 100644 (file)
index 0000000..55222f3
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Horde command-line argument parser
+ *
+ * This package is ported from Python's Optik (http://optik.sourceforge.net/).
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_Exception extends Exception
+{}
diff --git a/framework/Argv/lib/Horde/Argv/HelpFormatter.php b/framework/Argv/lib/Horde/Argv/HelpFormatter.php
new file mode 100644 (file)
index 0000000..a01d74a
--- /dev/null
@@ -0,0 +1,263 @@
+<?php
+/**
+ * Provides HelpFormatter, used by Horde_Argv_Parser to generate formatted
+ * help text.
+ *
+ * This package is ported from Python's Optik (http://optik.sourceforge.net/).
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Abstract base class for formatting option help.  Horde_Argv_Parser
+ * instances should use one of the HelpFormatter subclasses for
+ * formatting help; by default IndentedHelpFormatter is used.
+ *
+    Instance attributes:
+      parser : Horde_Argv_Parser
+        the controlling Horde_Argv_Parser instance
+      indent_increment : int
+        the number of columns to indent per nesting level
+      max_help_position : int
+        the maximum starting column for option help text
+      help_position : int
+        the calculated starting column for option help text;
+        initially the same as the maximum
+      width : int
+        total number of columns for output (pass None to constructor for
+        this value to be taken from the $COLUMNS environment variable)
+      level : int
+        current indentation level
+      current_indent : int
+        current indentation level (in columns)
+      help_width : int
+        number of columns available for option help text (calculated)
+      default_tag : str
+        text to replace with each option's default value, "%default"
+        by default.  Set to false value to disable default value expansion.
+      option_strings : { Option : str }
+        maps Option instances to the snippet of help text explaining
+        the syntax of that option, e.g. "-h, --help" or
+        "-fFILE, --file=FILE"
+      _short_opt_fmt : str
+        format string controlling how short options with values are
+        printed in help text.  Must be either "%s%s" ("-fFILE") or
+        "%s %s" ("-f FILE"), because those are the two syntaxes that
+        Optik supports.
+      _long_opt_fmt : str
+        similar but for long options; must be either "%s %s" ("--file FILE")
+        or "%s=%s" ("--file=FILE").
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+abstract class Horde_Argv_HelpFormatter
+{
+    const NO_DEFAULT_VALUE = 'none';
+
+    public $parser = null;
+
+    public function __construct($indent_increment, $max_help_position, $width = null, $short_first = false)
+    {
+        $this->indent_increment = $indent_increment;
+        $this->help_position = $this->max_help_position = $max_help_position;
+        if (is_null($width)) {
+            if (!empty($_ENV['COLUMNS'])) {
+                $width = $_ENV['COLUMNS'];
+            } else {
+                $width = 80;
+            }
+            $width -= 2;
+        }
+        $this->width = $width;
+        $this->current_indent = 0;
+        $this->level = 0;
+        $this->help_width = null; // computed later
+        $this->short_first = $short_first;
+        $this->default_tag = "%default";
+        $this->option_strings = array();
+        $this->_short_opt_fmt = "%s %s";
+        $this->_long_opt_fmt = "%s=%s";
+    }
+
+    public function setParser($parser)
+    {
+        $this->parser = $parser;
+    }
+
+    public function setShortOptDelimiter($delim)
+    {
+        if (!in_array($delim, array('', ' '))) {
+            throw new InvalidArgumentException('invalid metavar delimiter for short options: ' . $delim);
+        }
+        $this->_short_opt_fmt = "%s$delim%s";
+    }
+
+    public function setLongOptDelimiter($delim)
+    {
+        if (!in_array($delim, array('=', ' '))) {
+            throw new InvalidArgumentException('invalid metavar delimiter for long options: ' . $delim);
+        }
+        $this->_long_opt_fmt = "%s$delim%s";
+    }
+
+    public function indent()
+    {
+        $this->current_indent += $this->indent_increment;
+        $this->level += 1;
+    }
+
+    public function dedent()
+    {
+        $this->current_indent -= $this->indent_increment;
+        assert($this->current_indent >= 0); // Indent decreased below 0
+        $this->level -= 1;
+    }
+
+    public abstract function formatUsage($usage);
+
+    public abstract function formatHeading($heading);
+
+    /**
+     * Format a paragraph of free-form text for inclusion in the
+     * help output at the current indentation level.
+     */
+    protected function _formatText($text)
+    {
+        $text_width = $this->width - $this->current_indent;
+        $indent = str_repeat(' ', $this->current_indent);
+        return wordwrap($indent . $text, $text_width, "\n" . $indent);
+    }
+
+    public function formatDescription($description)
+    {
+        if ($description) {
+            return $this->_formatText($description) . "\n";
+        } else {
+            return '';
+        }
+    }
+
+    public function formatEpilog($epilog)
+    {
+        if ($epilog) {
+            return "\n" . $this->_formatText($epilog) . "\n";
+        } else {
+            return '';
+        }
+    }
+
+    public function expandDefault($option)
+    {
+        if (is_null($this->parser) || !$this->default_tag) {
+            return $option->help;
+        }
+
+        $default_value = isset($this->parser->defaults[$option->dest]) ? $this->parser->defaults[$option->dest] : null;
+        if ($default_value == Horde_Argv_Option::$NO_DEFAULT || !$default_value) {
+            $default_value = self::NO_DEFAULT_VALUE;
+        }
+
+        return str_replace($this->default_tag, (string)$default_value, $option->help);
+    }
+
+    /**
+     * The help for each option consists of two parts:
+     *   * the opt strings and metavars
+     *     eg. ("-x", or "-fFILENAME, --file=FILENAME")
+     *   * the user-supplied help string
+     *     eg. ("turn on expert mode", "read data from FILENAME")
+     *
+     * If possible, we write both of these on the same line:
+     *   -x      turn on expert mode
+     *
+     * But if the opt string list is too long, we put the help
+     * string on a second line, indented to the same column it would
+     * start in if it fit on the first line.
+     *   -fFILENAME, --file=FILENAME
+     *           read data from FILENAME
+     */
+    public function formatOption($option)
+    {
+        $result = array();
+        $opts = isset($this->option_strings[(string)$option]) ? $this->option_strings[(string)$option] : null;
+        $opt_width = $this->help_position - $this->current_indent - 2;
+        if (strlen($opts) > $opt_width) {
+            $opts = sprintf('%' . $this->current_indent . "s%s\n", "", $opts);
+            $indent_first = $this->help_position;
+        } else {
+            // start help on same line as opts
+            $opts = sprintf("%" . $this->current_indent . "s%-" . $opt_width . "s  ", "", $opts);
+            $indent_first = 0;
+        }
+        $result[] = $opts;
+        if ($option->help) {
+            $help_text = $this->expandDefault($option);
+            $help_lines = explode("\n", wordwrap($help_text, $this->help_width));
+            $result[] = sprintf("%" . $indent_first . "s%s\n", '', $help_lines[0]);
+            for ($i = 1, $i_max = count($help_lines); $i < $i_max; $i++) {
+                $result[] = sprintf("%" . $this->help_position . "s%s\n", "", $help_lines[$i]);
+            }
+        } elseif (substr($opts, -1) != "\n") {
+            $result[] = "\n";
+        }
+        return implode('', $result);
+    }
+
+    public function storeOptionStrings($parser)
+    {
+        $this->indent();
+        $max_len = 0;
+        foreach ($parser->optionList as $opt) {
+            $strings = $this->formatOptionStrings($opt);
+            $this->option_strings[(string)$opt] = $strings;
+            $max_len = max($max_len, strlen($strings) + $this->current_indent);
+        }
+        $this->indent();
+        foreach ($parser->optionGroups as $group) {
+            foreach ($group->optionList as $opt) {
+                $strings = $this->formatOptionStrings($opt);
+                $this->option_strings[(string)$opt] = $strings;
+                $max_len = max($max_len, strlen($strings) + $this->current_indent);
+            }
+        }
+        $this->dedent();
+        $this->dedent();
+        $this->help_position = min($max_len + 2, $this->max_help_position);
+        $this->help_width = $this->width - $this->help_position;
+    }
+
+    /**
+     * Return a comma-separated list of option strings & metavariables.
+     */
+    public function formatOptionStrings($option)
+    {
+        if ($option->takesValue()) {
+            $metavar = $option->metavar ? $option->metavar : strtoupper($option->dest);
+            $short_opts = array();
+            foreach ($option->shortOpts as $sopt) {
+                $short_opts[] = sprintf($this->_short_opt_fmt, $sopt, $metavar);
+            }
+            $long_opts = array();
+            foreach ($option->longOpts as $lopt) {
+                $long_opts[] = sprintf($this->_long_opt_fmt, $lopt, $metavar);
+            }
+        } else {
+            $short_opts = $option->shortOpts;
+            $long_opts = $option->longOpts;
+        }
+
+        if ($this->short_first) {
+            $opts = array_merge($short_opts, $long_opts);
+        } else {
+            $opts = array_merge($long_opts, $short_opts);
+        }
+
+        return implode(', ', $opts);
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/IndentedHelpFormatter.php b/framework/Argv/lib/Horde/Argv/IndentedHelpFormatter.php
new file mode 100644 (file)
index 0000000..42f09b5
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Format help with indented section bodies.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_IndentedHelpFormatter extends Horde_Argv_HelpFormatter
+{
+    public function __construct(
+        $indent_increment = 2,
+        $max_help_position = 24,
+        $width = null,
+        $short_first = true)
+    {
+        parent::__construct($indent_increment, $max_help_position, $width, $short_first);
+    }
+
+    public function formatUsage($usage)
+    {
+        return sprintf(_("Usage:") . " %s\n", $usage);
+    }
+
+    public function formatHeading($heading)
+    {
+        return sprintf('%' . $this->current_indent . "s%s:\n", '', $heading);
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/Option.php b/framework/Argv/lib/Horde/Argv/Option.php
new file mode 100644 (file)
index 0000000..db8d5ec
--- /dev/null
@@ -0,0 +1,555 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Defines the Option class and some standard value-checking functions.
+ *
+ * Instance attributes:
+ *    shortOpts : [string]
+ *    longOpts : [string]
+ *
+ *    action : string
+ *    type : string
+ *    dest : string
+ *    default : any
+ *    nargs : int
+ *    const : any
+ *    choices : [string]
+ *    callback : function
+ *    callbackArgs : (any*)
+ *    help : string
+ *    metavar : string
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_Option
+{
+    const SUPPRESS_HELP = "SUPPRESS HELP";
+    const SUPPRESS_USAGE = "SUPPRESS USAGE";
+
+    /**
+     * Not supplying a default is different from a default of None,
+     * so we need an explicit "not supplied" value.
+     */
+    public static $NO_DEFAULT = array("NO", "DEFAULT");
+
+    public static function parseNumber($value)
+    {
+        if (!strlen($value)) {
+            return false;
+        }
+
+        // Values to check against or compute with.
+        $first = substr($value, 0, 1);
+        $prefix = substr($value, 0, 2);
+        $suffix = substr($value, 2);
+
+        // Hex
+        if ($prefix == '0x' || $prefix == '0X') {
+            if (strspn($suffix, '0123456789abcdefABCDEF') != strlen($suffix)) {
+                return false;
+            }
+            return hexdec($value);
+        }
+
+        // Binary
+        if ($prefix == '0b' || $prefix == '0B') {
+            if (strspn($suffix, '01') != strlen($suffix)) {
+                return false;
+            }
+            return bindec($value);
+        }
+
+        // Octal
+        if ($first == '0') {
+            $suffix = substr($value, 1);
+            if (strspn($suffix, '01234567') != strlen($suffix)) {
+                return false;
+            }
+            return octdec($suffix);
+        }
+
+        // Base 10
+        if (!is_numeric($value)) {
+            return false;
+        }
+        return intval($value);
+    }
+
+    public function checkBuiltin($opt, $value)
+    {
+        switch ($this->type) {
+        case 'int':
+        case 'long':
+            $number = self::parseNumber($value);
+            if ($number === false) {
+                $message = $this->type == 'int'
+                    ? _("option %s: invalid integer value: '%s'")
+                    : _("option %s: invalid long integer value: '%s'");
+                throw new Horde_Argv_OptionValueException(
+                    sprintf($message, $opt, $value));
+            }
+            return $number;
+
+        case 'float':
+            if (!is_numeric($value)) {
+                throw new Horde_Argv_OptionValueException(
+                    sprintf(_("option %s: invalid floating-point value: '%s'"),
+                            $opt, $value));
+            }
+            return floatval($value);
+        }
+    }
+
+    public function checkChoice($opt, $value)
+    {
+        if (in_array($value, $this->choices)) {
+            return $value;
+        } else {
+            $choices = array();
+            foreach ($this->choices as $choice) {
+                $choices[] = (string)$choice;
+            }
+            $choices = "'" . implode("', '", $choices) . "'";
+            throw new Horde_Argv_OptionValueException(sprintf(
+                _("option %s: invalid choice: '%s' (choose from %s)"),
+                $opt, $value, $choices));
+        }
+    }
+
+
+    # The list of instance attributes that may be set through
+    # keyword args to the constructor.
+    public $ATTRS = array('action',
+             'type',
+             'dest',
+             'default',
+             'nargs',
+             'const',
+             'choices',
+             'callback',
+             'callbackArgs',
+             'help',
+             'metavar',
+    );
+
+    # The set of actions allowed by option parsers.  Explicitly listed
+    # here so the constructor can validate its arguments.
+    public $ACTIONS = array("store",
+               "store_const",
+               "store_true",
+               "store_false",
+               "append",
+               "append_const",
+               "count",
+               "callback",
+               "help",
+               "version",
+    );
+
+    /**
+     * The set of actions that involve storing a value somewhere;
+     * also listed just for constructor argument validation.  (If
+     * the action is one of these, there must be a destination.)
+     */
+    public $STORE_ACTIONS = array("store",
+                     "store_const",
+                     "store_true",
+                     "store_false",
+                     "append",
+                     "append_const",
+                     "count",
+    );
+
+    # The set of actions for which it makes sense to supply a value
+    # type, ie. which may consume an argument from the command line.
+    public $TYPED_ACTIONS = array("store",
+                                  "append",
+                                  "callback",
+    );
+
+    /**
+     * The set of actions which *require* a value type, ie. that
+     * always consume an argument from the command line.
+     */
+    public $ALWAYS_TYPED_ACTIONS = array("store",
+                                         "append",
+    );
+
+    # The set of actions which take a 'const' attribute.
+    public $CONST_ACTIONS = array("store_const",
+                                  "append_const",
+    );
+
+    # The set of known types for option parsers.  Again, listed here for
+    # constructor argument validation.
+    public $TYPES = array("string", "int", "long", "float", "complex", "choice");
+
+    # Dictionary of argument checking functions, which convert and
+    # validate option arguments according to the option type.
+    #
+    # Signature of checking functions is:
+    #   check(option : Option, opt : string, value : string) -> any
+    # where
+    #   option is the Option instance calling the checker
+    #   opt is the actual option seen on the command-line
+    #     (eg. "-a", "--file")
+    #   value is the option argument seen on the command-line
+    #
+    # The return value should be in the appropriate Python type
+    # for option.type -- eg. an integer if option.type == "int".
+    #
+    # If no checker is defined for a type, arguments will be
+    # unchecked and remain strings.
+    public $TYPE_CHECKER = array("int"    => 'checkBuiltin',
+                                 "long"   => 'checkBuiltin',
+                                 "float"  => 'checkBuiltin',
+                                 "complex"=> 'checkBuiltin',
+                                 "choice" => 'checkChoice',
+    );
+
+    # CHECK_METHODS is a list of unbound method objects; they are called
+    # by the constructor, in order, after all attributes are
+    # initialized.  The list is created and filled in later, after all
+    # the methods are actually defined.  (I just put it here because I
+    # like to define and document all class attributes in the same
+    # place.)  Subclasses that add another _check_*() method should
+    # define their own CHECK_METHODS list that adds their check method
+    # to those from this class.
+    public $CHECK_METHODS = array('_checkAction',
+                                  '_checkType',
+                                  '_checkChoice',
+                                  '_checkDest',
+                                  '_checkConst',
+                                  '_checkNargs',
+                                  '_checkCallback',
+                                  );
+
+    // -- Constructor/initialization methods ----------------------------
+
+    public $shortOpts = array();
+    public $longOpts = array();
+    public $dest;
+    public $default;
+
+    public function __construct()
+    {
+        // The last argument to this function is an $attrs hash, if it
+        // is present and an array. All other arguments are $opts.
+        $opts = func_get_args();
+        $num = func_num_args();
+        if ($num == 0 || $num == 1 || !is_array($opts[$num - 1])) {
+            $attrs = array();
+        } else {
+            $attrs = array_pop($opts);
+        }
+
+        // Set shortOpts, longOpts attrs from 'opts' tuple.
+        // Have to be set now, in case no option strings are supplied.
+        $this->shortOpts = array();
+        $this->longOpts = array();
+        $opts = $this->_checkOptStrings($opts);
+        $this->_setOptStrings($opts);
+
+        // Set all other attrs (action, type, etc.) from 'attrs' dict
+        $this->_setAttrs($attrs);
+
+        // Check all the attributes we just set.  There are lots of
+        // complicated interdependencies, but luckily they can be farmed
+        // out to the _check*() methods listed in CHECK_METHODS -- which
+        // could be handy for subclasses!  The one thing these all share
+        // is that they raise OptionError if they discover a problem.
+        foreach ($this->CHECK_METHODS as $checker) {
+            call_user_func(array($this, $checker));
+        }
+    }
+
+    protected function _checkOptStrings($opts)
+    {
+        // Filter out None because early versions of Optik had exactly
+        // one short option and one long option, either of which
+        // could be None.
+        $opts = array_filter($opts);
+        if (!$opts) {
+            throw new InvalidArgumentException('at least one option string must be supplied');
+        }
+        return $opts;
+    }
+
+    protected function _setOptStrings($opts)
+    {
+        foreach ($opts as &$opt) {
+            $opt = (string)$opt;
+
+            if (strlen($opt) < 2) {
+                throw new Horde_Argv_OptionException(sprintf("invalid option string '%s': must be at least two characters long", $opt), $this);
+            } elseif (strlen($opt) == 2) {
+                if (!($opt[0] == "-" && $opt[1] != "-")) {
+                    throw new Horde_Argv_OptionException(sprintf(
+                        "invalid short option string '%s': " .
+                        "must be of the form -x, (x any non-dash char)", $opt), $this);
+                }
+                $this->shortOpts[] = $opt;
+            } else {
+                if (!(substr($opt, 0, 2) == '--' && $opt[2] != '-')) {
+                    throw new Horde_Argv_OptionException(sprintf(
+                        "invalid long option string '%s': " .
+                        "must start with --, followed by non-dash", $opt), $this);
+                }
+                $this->longOpts[] = $opt;
+            }
+        }
+    }
+
+    protected function _setAttrs($attrs)
+    {
+        foreach ($this->ATTRS as $attr) {
+            if (array_key_exists($attr, $attrs)) {
+                $this->$attr = $attrs[$attr];
+                unset($attrs[$attr]);
+            } else {
+                if ($attr == 'default') {
+                    $this->$attr = self::$NO_DEFAULT;
+                } else {
+                    $this->$attr = null;
+                }
+            }
+        }
+
+        if ($attrs) {
+            $attrs = array_keys($attrs);
+            sort($attrs);
+            throw new Horde_Argv_OptionException(sprintf(
+                "invalid keyword arguments: %s", implode(", ", $attrs)), $this);
+        }
+    }
+
+
+    // -- Constructor validation methods --------------------------------
+
+    public function _checkAction()
+    {
+        if (is_null($this->action)) {
+            $this->action = "store";
+        } elseif (!in_array($this->action, $this->ACTIONS)) {
+            throw new Horde_Argv_OptionException(sprintf("invalid action: '%s'", $this->action), $this);
+        }
+    }
+
+    public function _checkType()
+    {
+        if (is_null($this->type)) {
+            if (in_array($this->action, $this->ALWAYS_TYPED_ACTIONS)) {
+                if (!is_null($this->choices)) {
+                    // The "choices" attribute implies "choice" type.
+                    $this->type = "choice";
+                } else {
+                    // No type given?  "string" is the most sensible default.
+                    $this->type = "string";
+                }
+            }
+        } else {
+            if ($this->type == "str") {
+                $this->type = "string";
+            }
+
+            if (!in_array($this->type, $this->TYPES)) {
+                throw new Horde_Argv_OptionException(sprintf("invalid option type: '%s'", $this->type), $this);
+            }
+
+            if (!in_array($this->action, $this->TYPED_ACTIONS)) {
+                throw new Horde_Argv_OptionException(sprintf(
+                    "must not supply a type for action '%s'", $this->action), $this);
+            }
+        }
+    }
+
+    public function _checkChoice()
+    {
+        if ($this->type == "choice") {
+            if (is_null($this->choices)) {
+                throw new Horde_Argv_OptionException(
+                    "must supply a list of choices for type 'choice'", $this);
+            } elseif (!(is_array($this->choices) || $this->choices instanceof Iterator)) {
+                throw new Horde_Argv_OptionException(sprintf(
+                    "choices must be a list of strings ('%s' supplied)",
+                    gettype($this->choices)), $this);
+            }
+        } elseif (!is_null($this->choices)) {
+            throw new Horde_Argv_OptionException(sprintf(
+                "must not supply choices for type '%s'", $this->type), $this);
+        }
+    }
+
+    public function _checkDest()
+    {
+        // No destination given, and we need one for this action.  The
+        // $this->type check is for callbacks that take a value.
+        $takes_value = (in_array($this->action, $this->STORE_ACTIONS) ||
+                        !is_null($this->type));
+        if (is_null($this->dest) && $takes_value) {
+            // Glean a destination from the first long option string,
+            // or from the first short option string if no long options.
+            if ($this->longOpts) {
+                // eg. "--foo-bar" -> "foo_bar"
+                $this->dest = str_replace('-', '_', substr($this->longOpts[0], 2));
+            } else {
+                $this->dest = $this->shortOpts[0][1];
+            }
+        }
+    }
+
+    public function _checkConst()
+    {
+        if (!in_array($this->action, $this->CONST_ACTIONS) && !is_null($this->const)) {
+            throw new Horde_Argv_OptionException(sprintf(
+                "'const' must not be supplied for action '%s'", $this->action),
+                $this);
+        }
+    }
+
+    public function _checkNargs()
+    {
+        if (in_array($this->action, $this->TYPED_ACTIONS)) {
+            if (is_null($this->nargs)) {
+                $this->nargs = 1;
+            }
+        } elseif (!is_null($this->nargs)) {
+            throw new Horde_Argv_OptionException(sprintf(
+                "'nargs' must not be supplied for action '%s'", $this->action),
+                $this);
+        }
+    }
+
+    public function _checkCallback()
+    {
+        if ($this->action == "callback") {
+            if (!is_callable($this->callback)) {
+                $callback_name = is_array($this->callback) ?
+                    is_object($this->callback[0]) ? get_class($this->callback[0] . '#' . $this->callback[1]) : implode('#', $this->callback) :
+                    $this->callback;
+                throw new Horde_Argv_OptionException(sprintf(
+                    "callback not callable: '%s'", $callback_name), $this);
+            }
+            if (!is_null($this->callbackArgs) && !is_array($this->callbackArgs)) {
+                throw new Horde_Argv_OptionException(sprintf(
+                    "callbackArgs, if supplied, must be an array: not '%s'",
+                    $this->callbackArgs), $this);
+            }
+        } else {
+            if (!is_null($this->callback)) {
+                $callback_name = is_array($this->callback) ?
+                    is_object($this->callback[0]) ? get_class($this->callback[0] . '#' . $this->callback[1]) : implode('#', $this->callback) :
+                    $this->callback;
+                throw new Horde_Argv_OptionException(sprintf(
+                    "callback supplied ('%s') for non-callback option",
+                    $callback_name), $this);
+            }
+            if (!is_null($this->callbackArgs)) {
+                throw new Horde_Argv_OptionException(
+                    "callbackArgs supplied for non-callback option", $this);
+            }
+        }
+    }
+
+
+    // -- Miscellaneous methods -----------------------------------------
+
+    public function __toString()
+    {
+        return implode('/', array_merge($this->shortOpts, $this->longOpts));
+    }
+
+    public function takesValue()
+    {
+        return !is_null($this->type);
+    }
+
+    public function getOptString()
+    {
+        if ($this->longOpts)
+            return $this->longOpts[0];
+        else
+            return $this->shortOpts[0];
+    }
+
+
+    // -- Processing methods --------------------------------------------
+
+    public function checkValue($opt, $value)
+    {
+        if (!isset($this->TYPE_CHECKER[$this->type])) {
+            return $value;
+        }
+        $checker = $this->TYPE_CHECKER[$this->type];
+        return call_user_func(array($this, $checker), $opt, $value);
+    }
+
+    public function convertValue($opt, $value)
+    {
+        if (!is_null($value)) {
+            if ($this->nargs == 1) {
+                return $this->checkValue($opt, $value);
+            } else {
+                $return = array();
+                foreach ($value as $v) {
+                    $return[] = $this->checkValue($opt, $v);
+                }
+                return $return;
+            }
+        }
+    }
+
+    public function process($opt, $value, $values, $parser)
+    {
+        // First, convert the value(s) to the right type.  Howl if any
+        // value(s) are bogus.
+        $value = $this->convertValue($opt, $value);
+
+        // And then take whatever action is expected of us.
+        // This is a separate method to make life easier for
+        // subclasses to add new actions.
+        return $this->takeAction(
+            $this->action, $this->dest, $opt, $value, $values, $parser);
+    }
+
+    public function takeAction($action, $dest, $opt, $value, $values, $parser)
+    {
+        if ($action == 'store')
+            $values->$dest = $value;
+        elseif ($action == 'store_const')
+            $values->$dest = $this->const;
+        elseif ($action == 'store_true')
+            $values->$dest = true;
+        elseif ($action == 'store_false')
+            $values->$dest = false;
+        elseif ($action == 'append') {
+            $values->{$dest}[] = $value;
+        } elseif ($action == 'append_const') {
+            $values->{$dest}[] = $this->const;
+        } elseif ($action == 'count') {
+            $values->ensureValue($dest, 0);
+            $values->$dest++;
+        } elseif ($action == 'callback') {
+            call_user_func($this->callback, $this, $opt, $value, $parser, $this->callbackArgs);
+        } elseif ($action == 'help') {
+            $parser->printHelp();
+            $parser->parserExit();
+        } elseif ($action == 'version') {
+            $parser->printVersion();
+            $parser->parserExit();
+        } else {
+            throw new RuntimeException('unknown action ' . $this->action);
+        }
+
+        return 1;
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/OptionConflictException.php b/framework/Argv/lib/Horde/Argv/OptionConflictException.php
new file mode 100644 (file)
index 0000000..8238f31
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Raised if conflicting options are added to a Horde_Argv_Parser.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_OptionConflictException extends Horde_Argv_OptionException
+{}
diff --git a/framework/Argv/lib/Horde/Argv/OptionContainer.php b/framework/Argv/lib/Horde/Argv/OptionContainer.php
new file mode 100644 (file)
index 0000000..7873a21
--- /dev/null
@@ -0,0 +1,264 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ *   Abstract base class.
+ *
+ *  Class attributes:
+ *    standardOptionList : [Option]
+ *      list of standard options that will be accepted by all instances
+ *      of this parser class (intended to be overridden by subclasses).
+ *
+ *  Instance attributes:
+ *    optionList : [Option]
+ *      the list of Option objects contained by this OptionContainer
+ *    shortOpt : { string : Option }
+ *      dictionary mapping short option strings, eg. "-f" or "-X",
+ *      to the Option instances that implement them.  If an Option
+ *      has multiple short option strings, it will appears in this
+ *      dictionary multiple times. [1]
+ *    longOpt : { string : Option }
+ *      dictionary mapping long option strings, eg. "--file" or
+ *      "--exclude", to the Option instances that implement them.
+ *      Again, a given Option can occur multiple times in this
+ *      dictionary. [1]
+ *    defaults : { string : any }
+ *      dictionary mapping option destination names to default
+ *      values for each destination [1]
+ *
+ *  [1] These mappings are common to (shared by) all components of the
+ *      controlling Horde_Argv_Parser, where they are initially created.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_OptionContainer
+{
+    public $description = '';
+    public $optionList = array();
+    public $optionClass = 'Horde_Argv_Option';
+    public $defaults = array();
+    public $shortOpt = array();
+    public $longOpt = array();
+    public $conflictHandler;
+
+    /**
+     * Initialize the option list and related data structures.
+     * This method must be provided by subclasses, and it must
+     * initialize at least the following instance attributes:
+     * optionList, shortOpt, longOpt, defaults.
+    */
+    public function __construct($optionClass, $conflictHandler, $description)
+    {
+        $this->_createOptionList();
+
+        $this->optionClass = $optionClass;
+        $this->setConflictHandler($conflictHandler);
+        $this->setDescription($description);
+    }
+
+    /**
+     * For use by Horde_Argv_Parser constructor -- create the master
+     * option mappings used by this Horde_Argv_Parser and all
+     * OptionGroups that it owns.
+     */
+    protected function _createOptionMappings()
+    {
+        $this->shortOpt = array();       // single letter -> Option instance
+        $this->longOpt = array();        // long option -> Option instance
+        $this->defaults = array();       // maps option dest -> default value
+    }
+
+    /**
+     * For use by OptionGroup constructor -- use shared option
+     * mappings from the Horde_Argv_Parser that owns this OptionGroup.
+     */
+    protected function _shareOptionMappings($parser)
+    {
+        $this->shortOpt =& $parser->shortOpt;
+        $this->longOpt =& $parser->longOpt;
+        $this->defaults = $parser->defaults;
+    }
+
+    public function setConflictHandler($handler)
+    {
+        if (!in_array($handler, array('error', 'resolve'))) {
+            throw new InvalidArgumentException('invalid conflictHandler ' . var_export($handler, true));
+        }
+        $this->conflictHandler = $handler;
+    }
+
+    public function setDescription($description)
+    {
+        $this->description = $description;
+    }
+
+    public function getDescription()
+    {
+        return $this->description;
+    }
+
+    // -- Option-adding methods -----------------------------------------
+
+    protected function _checkConflict($option)
+    {
+        $conflictOpts = array();
+        foreach ($option->shortOpts as $opt) {
+            if (isset($this->shortOpt[$opt])) {
+                $conflictOpts[$opt] = $this->shortOpt[$opt];
+            }
+        }
+        foreach ($option->longOpts as $opt) {
+            if (isset($this->longOpt[$opt])) {
+                $conflictOpts[$opt] = $this->longOpt[$opt];
+            }
+        }
+
+        if ($conflictOpts) {
+            $handler = $this->conflictHandler;
+            if ($handler == 'error') {
+                throw new Horde_Argv_OptionConflictException(sprintf(
+                    'conflicting option string(s): %s',
+                    implode(', ', array_keys($conflictOpts))), $option);
+            } elseif ($handler == 'resolve') {
+                foreach ($conflictOpts as $opt => $c_option) {
+                    if (strncmp($opt, '--', 2) === 0) {
+                        $key = array_search($opt, $c_option->longOpts);
+                        if ($key !== false) {
+                            unset($c_option->longOpts[$key]);
+                        }
+                        unset($this->longOpt[$opt]);
+                    } else {
+                        $key = array_search($opt, $c_option->shortOpts);
+                        if ($key !== false) {
+                            unset($c_option->shortOpts[$key]);
+                        }
+                        unset($this->shortOpt[$opt]);
+                    }
+
+                    if (! ($c_option->shortOpts || $c_option->longOpts)) {
+                        $key = array_search($c_option, $c_option->container->optionList);
+                        unset($c_option->container->optionList[$key]);
+                    }
+                }
+            }
+        }
+    }
+
+    public function addOption()
+    {
+        $opts = func_get_args();
+
+        if (count($opts) && is_string($opts[0])) {
+            $optionFactory = new ReflectionClass($this->optionClass);
+            $option = $optionFactory->newInstanceArgs($opts);
+        } elseif (count($opts) == 1) {
+            $option = $opts[0];
+            if (!$option instanceof Horde_Argv_Option)
+                throw new InvalidArgumentException('not an Option instance: ' . var_export($option, true));
+        } else {
+            throw new InvalidArgumentException('invalid arguments');
+        }
+
+        $this->_checkConflict($option);
+
+        $this->optionList[] = $option;
+        $option->container = $this;
+        foreach ($option->shortOpts as $opt) {
+            $this->shortOpt[$opt] = $option;
+        }
+        foreach ($option->longOpts as $opt) {
+            $this->longOpt[$opt] = $option;
+        }
+
+        if (!is_null($option->dest)) {
+            // option has a dest, we need a default
+            if ($option->default !== Horde_Argv_Option::$NO_DEFAULT) {
+                $this->defaults[$option->dest] = $option->default;
+            } elseif (!isset($this->defaults[$option->dest])) {
+                $this->defaults[$option->dest] = null;
+            }
+        }
+
+        return $option;
+    }
+
+    public function addOptions($optionList)
+    {
+        foreach ($optionList as $option) {
+            $this->addOption($option);
+        }
+    }
+
+    // -- Option query/removal methods ----------------------------------
+
+    public function getOption($opt_str)
+    {
+        if (isset($this->shortOpt[$opt_str])) {
+            return $this->shortOpt[$opt_str];
+        } elseif (isset($this->longOpt[$opt_str])) {
+            return $this->longOpt[$opt_str];
+        } else {
+            return null;
+        }
+    }
+
+    public function hasOption($opt_str)
+    {
+        return isset($this->shortOpt[$opt_str])
+            || isset($this->longOpt[$opt_str]);
+    }
+
+    public function removeOption($opt_str)
+    {
+        $option = $this->getOption($opt_str);
+        if (is_null($option))
+            throw new InvalidArgumentException("no such option '$opt_str'");
+
+        foreach ($option->shortOpts as $opt) {
+            unset($this->shortOpt[$opt]);
+        }
+        foreach ($option->longOpts as $opt) {
+            unset($this->longOpt[$opt]);
+        }
+        $key = array_search($option, $option->container->optionList);
+        unset($option->container->optionList[$key]);
+    }
+
+
+    // -- Help-formatting methods ---------------------------------------
+
+    public function formatOptionHelp($formatter = null)
+    {
+        if (!$this->optionList)
+            return '';
+        $result = array();
+        foreach ($this->optionList as $option) {
+            if ($option->help != Horde_Argv_Option::SUPPRESS_HELP)
+                $result[] = $formatter->formatOption($option);
+        }
+        return implode('', $result);
+    }
+
+    public function formatDescription($formatter = null)
+    {
+        return $formatter->formatDescription($this->getDescription());
+    }
+
+    public function formatHelp($formatter = null)
+    {
+        $result = array();
+        if ($this->description)
+            $result[] = $this->formatDescription($formatter);
+        if ($this->optionList)
+            $result[] = $this->formatOptionHelp($formatter);
+        return implode("\n", $result);
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/OptionException.php b/framework/Argv/lib/Horde/Argv/OptionException.php
new file mode 100644 (file)
index 0000000..4d1516c
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Raised if an Option instance is created with invalid or
+ * inconsistent arguments.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_OptionException extends Horde_Argv_Exception
+{
+    public function __construct($msg, $option = null)
+    {
+        $this->optionId = (string)$option;
+        if ($this->optionId) {
+            parent::__construct(sprintf(_("option %s: %s"), $this->optionId, $msg));
+        } else {
+            parent::__construct($msg);
+        }
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/OptionGroup.php b/framework/Argv/lib/Horde/Argv/OptionGroup.php
new file mode 100644 (file)
index 0000000..c475506
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_OptionGroup extends Horde_Argv_OptionContainer
+{
+    protected $_title;
+
+    public function __construct($parser, $title, $description = null)
+    {
+        $this->parser = $parser;
+        parent::__construct($parser->optionClass, $parser->conflictHandler, $description);
+        $this->_title = $title;
+    }
+
+    protected function _createOptionList()
+    {
+        $this->optionList = array();
+        $this->_shareOptionMappings($this->parser);
+    }
+
+    public function setTitle($title)
+    {
+        $this->_title = $title;
+    }
+
+    public function __destruct()
+    {
+        unset($this->optionList);
+    }
+
+    // -- Help-formatting methods ---------------------------------------
+
+    public function formatHelp($formatter = null)
+    {
+        if (is_null($formatter))
+            return '';
+
+        $result = $formatter->formatHeading($this->_title);
+        $formatter->indent();
+        $result .= parent::formatHelp($formatter);
+        $formatter->dedent();
+        return $result;
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/OptionValueException.php b/framework/Argv/lib/Horde/Argv/OptionValueException.php
new file mode 100644 (file)
index 0000000..7880442
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Raised if an invalid option value is encountered on the command
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_OptionValueException extends Horde_Argv_OptionException
+{}
diff --git a/framework/Argv/lib/Horde/Argv/Parser.php b/framework/Argv/lib/Horde/Argv/Parser.php
new file mode 100644 (file)
index 0000000..ef07569
--- /dev/null
@@ -0,0 +1,674 @@
+<?php
+/**
+ * Horde command-line argument parsing package.
+ *
+ * This package is ported from Python's Optik (http://optik.sourceforge.net/).
+ *
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ *  Class attributes:
+ *    standardOptionList : [Option]
+ *      list of standard options that will be accepted by all instances
+ *      of this parser class (intended to be overridden by subclasses).
+ *
+ *  Instance attributes:
+ *    usage : string
+ *      a usage string for your program.  Before it is displayed
+ *      to the user, "%prog" will be expanded to the name of
+ *      your program ($this->prog or os.path.basename(sys.argv[0])).
+ *    prog : string
+ *      the name of the current program (to override
+ *      os.path.basename(sys.argv[0])).
+ *    epilog : string
+ *      paragraph of help text to print after option help
+ *
+ *    optionGroups : [OptionGroup]
+ *      list of option groups in this parser (option groups are
+ *      irrelevant for parsing the command-line, but very useful
+ *      for generating help)
+ *
+ *    allow_interspersed_args : bool = true
+ *      if true, positional arguments may be interspersed with options.
+ *      Assuming -a and -b each take a single argument, the command-line
+ *        -ablah foo bar -bboo baz
+ *      will be interpreted the same as
+ *        -ablah -bboo -- foo bar baz
+ *      If this flag were false, that command line would be interpreted as
+ *        -ablah -- foo bar -bboo baz
+ *      -- ie. we stop processing options as soon as we see the first
+ *      non-option argument.  (This is the tradition followed by
+ *      Python's getopt module, Perl's Getopt::Std, and other argument-
+ *      parsing libraries, but it is generally annoying to users.)
+ *
+ *    rargs : [string]
+ *      the argument list currently being parsed.  Only set when
+ *      parseArgs() is active, and continually trimmed down as
+ *      we consume arguments.  Mainly there for the benefit of
+ *      callback options.
+ *    largs : [string]
+ *      the list of leftover arguments that we have skipped while
+ *      parsing options.  If allow_interspersed_args is false, this
+ *      list is always empty.
+ *    values : Values
+ *      the set of option values currently being accumulated.  Only
+ *      set when parseArgs() is active.  Also mainly for callbacks.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_Parser extends Horde_Argv_OptionContainer
+{
+    public $standardOptionList = array();
+
+    protected $_usage;
+    public $optionGroups = array();
+
+    public function __construct($args = array())
+    {
+        $args = array_merge(array(
+            'usage' => null,
+            'optionList' => null,
+            'optionClass' => 'Horde_Argv_Option',
+            'version' => null,
+            'conflictHandler' => "error",
+            'description' => null,
+            'formatter' => null,
+            'addHelpOption' => true,
+            'prog' => null,
+            'epilog' => null),
+            $args);
+
+        parent::__construct($args['optionClass'], $args['conflictHandler'], $args['description']);
+        $this->setUsage($args['usage']);
+        $this->prog = $args['prog'];
+        $this->version = $args['version'];
+        $this->allow_interspersed_args = true;
+        if (is_null($args['formatter']))
+            $args['formatter'] = new Horde_Argv_IndentedHelpFormatter();
+        $this->formatter = $args['formatter'];
+        $this->formatter->setParser($this);
+        $this->epilog = $args['epilog'];
+
+        // Populate the option list; initial sources are the
+        // standardOptionList class attribute, the 'optionList'
+        // argument, and (if applicable) the _addVersionOption() and
+        // _addHelpOption() methods.
+        $this->_populateOptionList($args['optionList'],
+                                   $args['addHelpOption']);
+
+        $this->_initParsingState();
+    }
+
+    /**
+     *  Declare that you are done with this Horde_Argv_Parser.  This cleans up
+     *  reference cycles so the Horde_Argv_Parser (and all objects referenced by
+     *  it) can be garbage-collected promptly.  After calling destroy(), the
+     *  Horde_Argv_Parser is unusable.
+     */
+    public function __destruct()
+    {
+        foreach ($this->optionGroups as &$group) {
+            unset($group);
+        }
+
+        unset($this->optionList);
+        unset($this->optionGroups);
+        unset($this->formatter);
+    }
+
+    // -- Private methods -----------------------------------------------
+    // (used by our or OptionContainer's constructor)
+
+    protected function _createOptionList()
+    {
+        $this->optionList = array();
+        $this->optionGroups = array();
+        $this->_createOptionMappings();
+    }
+
+    protected function _addHelpOption()
+    {
+        $this->addOption('-h', '--help', array('action' => 'help',
+                                               'help' => _("show this help message and exit")));
+    }
+
+    protected function _addVersionOption()
+    {
+        $this->addOption('--version', array('action' => 'version',
+                                            'help' => _("show program's version number and exit")));
+    }
+
+    protected function _populateOptionList($optionList, $add_help = true)
+    {
+        if ($this->standardOptionList)
+            $this->addOptions($this->standardOptionList);
+        if ($optionList)
+            $this->addOptions($optionList);
+        if ($this->version)
+            $this->_addVersionOption();
+        if ($add_help)
+            $this->_addHelpOption();
+    }
+
+    protected function _initParsingState()
+    {
+        // These are set in parseArgs() for the convenience of callbacks.
+        $this->rargs = null;
+        $this->largs = null;
+        $this->values = null;
+    }
+
+    // -- Simple modifier methods ---------------------------------------
+
+    public function setUsage($usage)
+    {
+        if (is_null($usage))
+            $this->_usage = '%prog ' . _("[options]");
+        elseif ($usage == Horde_Argv_Option::SUPPRESS_USAGE)
+            $this->_usage = null;
+        else
+            $this->_usage = $usage;
+    }
+
+    public function enableInterspersedArgs()
+    {
+        $this->allow_interspersed_args = true;
+    }
+
+    public function disableInterspersedArgs()
+    {
+        $this->allow_interspersed_args = false;
+    }
+
+    public function setDefault($dest, $value)
+    {
+        $this->defaults[$dest] = $value;
+    }
+
+    public function setDefaults($defaults)
+    {
+        $this->defaults = array_merge($this->defaults, $defaults);
+    }
+
+    protected function _getAllOptions()
+    {
+        $options = $this->optionList;
+        foreach ($this->optionGroups as $group) {
+            $options = array_merge($options, $group->optionList);
+        }
+        return $options;
+    }
+
+    public function getDefaultValues()
+    {
+        $defaults = $this->defaults;
+        foreach ($this->_getAllOptions() as $option) {
+            $default = isset($defaults[$option->dest]) ? $defaults[$option->dest] : null;
+            if (is_string($default)) {
+                $opt_str = $option->getOptString();
+                $defaults[$option->dest] = $option->checkValue($opt_str, $default);
+            }
+        }
+
+        return new Horde_Argv_Values($defaults);
+    }
+
+
+    // -- OptionGroup methods -------------------------------------------
+
+    public function addOptionGroup()
+    {
+        // XXX lots of overlap with OptionContainer::addOption()
+        $args = func_get_args();
+
+        if (count($args) && is_string($args[0])) {
+            $groupFactory = new ReflectionClass('Horde_Argv_OptionGroup');
+            array_unshift($args, $this);
+            $group = $groupFactory->newInstanceArgs($args);
+        } elseif (count($args) == 1) {
+            $group = $args[0];
+            if (!$group instanceof Horde_Argv_OptionGroup)
+                throw new InvalidArgumentException("not an OptionGroup instance: " . var_export($group, true));
+            if ($group->parser !== $this)
+                throw new InvalidArgumentException("invalid OptionGroup (wrong parser)");
+        } else {
+            throw new InvalidArgumentException('invalid arguments');
+        }
+
+        $this->optionGroups[] = $group;
+        return $group;
+    }
+
+    public function getOptionGroup($opt_str)
+    {
+        if (isset($this->shortOpt[$opt_str])) {
+            $option = $this->shortOpt[$opt_str];
+        } elseif (isset($this->longOpt[$opt_str])) {
+            $option = $this->longOpt[$opt_str];
+        } else {
+            return null;
+        }
+
+        if ($option->container !== $this) {
+            return $option->container;
+        }
+
+        return null;
+    }
+
+    // -- Option-parsing methods ----------------------------------------
+
+    protected function _getArgs($args = null)
+    {
+        if (is_null($args)) {
+            $args = $_SERVER['argv'];
+            array_shift($args);
+            return $args;
+        } else {
+            return $args;
+        }
+    }
+
+    /**
+     *  Parse the command-line options found in 'args' (default:
+     *  sys.argv[1:]).  Any errors result in a call to 'parserError()', which
+     *  by default prints the usage message to stderr and calls
+     *  exit() with an error message.  On success returns a pair
+     *  (values, args) where 'values' is an Values instance (with all
+     *  your option values) and 'args' is the list of arguments left
+     *  over after parsing options.
+     */
+    public function parseArgs($args = null, $values = null)
+    {
+        $rargs = $this->_getArgs($args);
+        $largs = array();
+        if (is_null($values))
+            $values = $this->getDefaultValues();
+
+        // Store the halves of the argument list as attributes for the
+        // convenience of callbacks:
+        //   rargs
+        //     the rest of the command-line (the "r" stands for
+        //     "remaining" or "right-hand")
+        //   largs
+        //     the leftover arguments -- ie. what's left after removing
+        //     options and their arguments (the "l" stands for "leftover"
+        //     or "left-hand")
+        $this->rargs =& $rargs;
+        $this->largs =& $largs;
+        $this->values = $values;
+
+        try {
+            $this->_processArgs($largs, $rargs, $values);
+        } catch (Horde_Argv_BadOptionException $e) {
+            $this->parserError($e->getMessage());
+        } catch (Horde_Argv_OptionValueException $e) {
+            $this->parserError($e->getMessage());
+        }
+
+        $args = array_merge($largs, $rargs);
+        return $this->checkValues($values, $args);
+    }
+
+    /**
+     *  Check that the supplied option values and leftover arguments are
+     *  valid.  Returns the option values and leftover arguments
+     *  (possibly adjusted, possibly completely new -- whatever you
+     *  like).  Default implementation just returns the passed-in
+     *  values; subclasses may override as desired.
+     */
+    public function checkValues($values, $args)
+    {
+        return array($values, $args);
+    }
+
+    /**
+     *  _process_args(largs : [string],
+     *                rargs : [string],
+     *                values : Values)
+     *
+     *  Process command-line arguments and populate 'values', consuming
+     *  options and arguments from 'rargs'.  If 'allow_interspersed_args' is
+     *  false, stop at the first non-option argument.  If true, accumulate any
+     *  interspersed non-option arguments in 'largs'.
+     */
+    protected function _processArgs(&$largs, &$rargs, &$values)
+    {
+        while ($rargs) {
+            $arg = $rargs[0];
+            // We handle bare "--" explicitly, and bare "-" is handled by the
+            // standard arg handler since the short arg case ensures that the
+            // len of the opt string is greater than 1.
+            if ($arg == '--') {
+                array_shift($rargs);
+                return;
+            } elseif (substr($arg, 0, 2) == '--') {
+                // process a single long option (possibly with value(s))
+                $this->_processLongOpt($rargs, $values);
+            } elseif (substr($arg, 0, 1) == '-' && strlen($arg) > 1) {
+                // process a cluster of short options (possibly with
+                // value(s) for the last one only)
+                $this->_processShortOpts($rargs, $values);
+            } elseif ($this->allow_interspersed_args) {
+                $largs[] = $arg;
+                array_shift($rargs);
+            } else {
+                // stop now, leave this arg in rargs
+                return;
+            }
+        }
+
+        // Say this is the original argument list:
+        // [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
+        //                            ^
+        // (we are about to process arg(i)).
+        //
+        // Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
+        // [arg0, ..., arg(i-1)] (any options and their arguments will have
+        // been removed from largs).
+        //
+        // The while loop will usually consume 1 or more arguments per pass.
+        // If it consumes 1 (eg. arg is an option that takes no arguments),
+        // then after _process_arg() is done the situation is:
+        //
+        //   largs = subset of [arg0, ..., arg(i)]
+        //   rargs = [arg(i+1), ..., arg(N-1)]
+        //
+        // If allow_interspersed_args is false, largs will always be
+        // *empty* -- still a subset of [arg0, ..., arg(i-1)], but
+        // not a very interesting subset!
+    }
+
+    /**
+     *  opt : string) -> string
+     *
+     *    Determine which long option string 'opt' matches, ie. which one
+     *    it is an unambiguous abbrevation for.  Raises BadOptionError if
+     *    'opt' doesn't unambiguously match any long option string.
+     */
+    protected function _matchLongOpt($opt)
+    {
+        return self::matchAbbrev($opt, $this->longOpt);
+    }
+
+    /**
+     *  (s : string, wordmap : {string : Option}) -> string
+     *
+     *  Return the string key in 'wordmap' for which 's' is an unambiguous
+     *  abbreviation.  If 's' is found to be ambiguous or doesn't match any of
+     *  'words', raise BadOptionError.
+     */
+    public static function matchAbbrev($s, $wordmap)
+    {
+        // Is there an exact match?
+        if (array_key_exists($s, $wordmap)) {
+            return $s;
+        }
+
+        // Isolate all words with s as a prefix.
+        $possibilities = array();
+        foreach (array_keys($wordmap) as $word) {
+            if (strncmp($word, $s, strlen($s)) === 0) {
+                $possibilities[] = $word;
+            }
+        }
+
+        // No exact match, so there had better be just one possibility.
+        if (count($possibilities) == 1) {
+            return $possibilities[0];
+        } elseif (!$possibilities) {
+            throw new Horde_Argv_BadOptionException($s);
+        } else {
+            // More than one possible completion: ambiguous prefix.
+            sort($possibilities);
+            throw new Horde_Argv_AmbiguousOptionException($s, $possibilities);
+        }
+    }
+
+    protected function _processLongOpt(&$rargs, &$values)
+    {
+        $arg = array_shift($rargs);
+
+        // Value explicitly attached to arg?  Pretend it's the next
+        // argument.
+        if (strpos($arg, '=') !== false) {
+            list($opt, $next_arg) = explode('=', $arg, 2);
+            array_unshift($rargs, $next_arg);
+            $had_explicit_value = true;
+        } else {
+            $opt = $arg;
+            $had_explicit_value = false;
+        }
+
+        $opt = $this->_matchLongOpt($opt);
+        $option = $this->longOpt[$opt];
+        if ($option->takesValue()) {
+            $nargs = $option->nargs;
+            if (count($rargs) < $nargs) {
+                if ($nargs == 1)
+                    $this->parserError(sprintf(_("%s option requires an argument"), $opt));
+                else
+                    $this->parserError(sprintf(_("%s option requires %d arguments"),
+                                               $opt, $nargs));
+            } elseif ($nargs == 1) {
+                $value = array_shift($rargs);
+            } else {
+                $value = array_splice($rargs, 0, $nargs);
+            }
+
+        } elseif ($had_explicit_value) {
+            $this->parserError(sprintf(_("%s option does not take a value"), $opt));
+
+        } else {
+            $value = null;
+        }
+
+        $option->process($opt, $value, $values, $this);
+    }
+
+    protected function _processShortOpts(&$rargs, &$values)
+    {
+        $arg = array_shift($rargs);
+        $stop = false;
+        $i = 1;
+        for ($c = 1, $c_max = strlen($arg); $c < $c_max; $c++) {
+            $ch = $arg[$c];
+            $opt = '-' . $ch;
+            $option = isset($this->shortOpt[$opt]) ? $this->shortOpt[$opt] : null;
+            $i++; // we have consumed a character
+
+            if (!$option)
+                throw new Horde_Argv_BadOptionException($opt);
+
+            if ($option->takesValue()) {
+                // Any characters left in arg?  Pretend they're the
+                // next arg, and stop consuming characters of arg.
+                if ($i < strlen($arg)) {
+                    array_unshift($rargs, substr($arg, $i));
+                    $stop = true;
+                }
+
+                $nargs = $option->nargs;
+                if (count($rargs) < $nargs) {
+                    if ($nargs == 1)
+                        $this->parserError(sprintf(_("%s option requires an argument"), $opt));
+                    else
+                        $this->parserError(sprintf(_("%s option requires %d arguments"), $opt, $nargs));
+
+                } elseif ($nargs == 1) {
+                    $value = array_shift($rargs);
+                } else {
+                    $value = array_splice($rargs, 0, $nargs);
+                }
+
+            } else {
+                // option doesn't take a value
+                $value = null;
+            }
+
+            $option->process($opt, $value, $values, $this);
+
+            if ($stop)
+                break;
+        }
+    }
+
+    // -- Feedback methods ----------------------------------------------
+
+    public function getProgName()
+    {
+        if (is_null($this->prog))
+            return basename($_SERVER['argv'][0]);
+        else
+            return $this->prog;
+    }
+
+    public function expandProgName($s)
+    {
+        return str_replace("%prog", $this->getProgName(), $s);
+    }
+
+    public function getDescription()
+    {
+        return $this->expandProgName($this->description);
+    }
+
+    public function parserExit($status = 0, $msg = null)
+    {
+        if ($msg)
+            fwrite(STDERR, $msg);
+        exit($status);
+    }
+
+    /**
+     * Print a usage message incorporating $msg to stderr and exit.
+     * If you override this in a subclass, it should not return -- it
+     * should either exit or raise an exception.
+     *
+     * @param string $msg
+     */
+    public function parserError($msg)
+    {
+        $this->printUsage(STDERR);
+        $this->parserExit(2, sprintf("%s: error: %s\n", $this->getProgName(), $msg));
+    }
+
+    public function getUsage($formatter = null)
+    {
+        if (is_null($formatter))
+            $formatter = $this->formatter;
+        if ($this->_usage)
+            return $formatter->formatUsage($this->expandProgName($this->_usage));
+        else
+            return '';
+    }
+
+    /**
+     *  (file : file = stdout)
+     *
+     *  Print the usage message for the current program ($this->_usage) to
+     *  'file' (default stdout).  Any occurence of the string "%prog" in
+     *  $this->_usage is replaced with the name of the current program
+     *  (basename of sys.argv[0]).  Does nothing if $this->_usage is empty
+     *  or not defined.
+     */
+    public function printUsage($file = null)
+    {
+        if (!$this->_usage)
+            return;
+
+        if (is_null($file))
+            echo $this->getUsage();
+        else
+            fwrite($file, $this->getUsage());
+    }
+
+    public function getVersion()
+    {
+        if ($this->version)
+            return $this->expandProgName($this->version);
+        else
+            return '';
+    }
+
+    /**
+     * file : file = stdout
+     *
+     *    Print the version message for this program ($this->version) to
+     *    'file' (default stdout).  As with printUsage(), any occurence
+     *    of "%prog" in $this->version is replaced by the current program's
+     *    name.  Does nothing if $this->version is empty or undefined.
+     */
+    public function printVersion($file = null)
+    {
+        if (!$this->version)
+            return;
+
+        if (is_null($file))
+            echo $this->getVersion() . "\n";
+        else
+            fwrite($file, $this->getVersion() . "\n");
+    }
+
+    public function formatOptionHelp($formatter = null)
+    {
+        if (is_null($formatter))
+            $formatter = $this->formatter;
+        $formatter->storeOptionStrings($this);
+        $result = array();
+        $result[] = $formatter->formatHeading(_("Options"));
+        $formatter->indent();
+        if ($this->optionList) {
+            $result[] = parent::formatOptionHelp($formatter);
+            $result[] = "\n";
+        }
+        foreach ($this->optionGroups as $group) {
+            $result[] = $group->formatHelp($formatter);
+            $result[] = "\n";
+        }
+        $formatter->dedent();
+        // Drop the last "\n", or the header if no options or option groups:
+        array_pop($result);
+        return implode('', $result);
+    }
+
+    public function formatEpilog($formatter)
+    {
+        return $formatter->formatEpilog($this->epilog);
+    }
+
+    public function formatHelp($formatter = null)
+    {
+        if (is_null($formatter))
+            $formatter = $this->formatter;
+        $result = array();
+        if ($this->_usage)
+            $result[] = $this->getUsage($formatter) . "\n";
+        if ($this->description)
+            $result[] = $this->formatDescription($formatter) . "\n";
+        $result[] = $this->formatOptionHelp($formatter);
+        $result[] = $this->formatEpilog($formatter);
+        return implode('', $result);
+    }
+
+    /**
+     *  file : file = stdout
+     *
+     *  Print an extended help message, listing all options and any
+     *  help text provided with them, to 'file' (default stdout).
+     */
+    public function printHelp($file = null)
+    {
+        if (is_null($file))
+            echo $this->formatHelp();
+        else
+            fwrite($file, $this->formatHelp());
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/TitledHelpFormatter.php b/framework/Argv/lib/Horde/Argv/TitledHelpFormatter.php
new file mode 100644 (file)
index 0000000..6fcf72a
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Format help with underlined section headers.
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_TitledHelpFormatter extends Horde_Argv_HelpFormatter
+{
+    public function __construct(
+        $indent_increment = 0,
+        $max_help_position = 24,
+        $width = null,
+        $short_first = false)
+    {
+        parent::__construct($indent_increment, $max_help_position, $width, $short_first);
+    }
+
+    public function formatUsage($usage)
+    {
+        return sprintf("%s  %s\n", $this->formatHeading(_("Usage")), $usage);
+    }
+
+    public function formatHeading($heading)
+    {
+        $prefix = array('=', '-');
+        return sprintf("%s\n%s\n", $heading, str_repeat($prefix[$this->level], strlen($heading)));
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/Argv/Values.php b/framework/Argv/lib/Horde/Argv/Values.php
new file mode 100644 (file)
index 0000000..2b944eb
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+ * @author   Chuck Hagenbuch <chuck@horde.org>
+ * @author   Mike Naberezny <mike@maintainable.com>
+ * @license  http://opensource.org/licenses/bsd-license.php BSD
+ * @category Horde
+ * @package  Horde_Argv
+ */
+
+/**
+ * Result hash for Horde_Argv_Parser
+ *
+ * @category Horde
+ * @package  Horde_Argv
+ */
+class Horde_Argv_Values implements IteratorAggregate, ArrayAccess, Countable
+{
+    public function __construct($defaults = array())
+    {
+        foreach ($defaults as $attr => $val) {
+            $this->$attr = $val;
+        }
+    }
+
+    public function __toString()
+    {
+        $str = array();
+        foreach ($this as $attr => $val) {
+            $str[] = $attr . ': ' . (string)$val;
+        }
+        return implode(', ', $str);
+    }
+
+    public function offsetExists($attr)
+    {
+        return !is_null($this->$attr);
+    }
+
+    public function offsetGet($attr)
+    {
+        return $this->$attr;
+    }
+
+    public function offsetSet($attr, $val)
+    {
+        $this->$attr = $val;
+    }
+
+    public function offsetUnset($attr)
+    {
+        unset($this->$attr);
+    }
+
+    public function getIterator()
+    {
+        return new ArrayIterator(get_object_vars($this));
+    }
+
+    public function count()
+    {
+        return count(get_object_vars($this));
+    }
+
+    public function ensureValue($attr, $value)
+    {
+        if (is_null($this->$attr)) {
+            $this->$attr = $value;
+        }
+        return $this->$attr;
+    }
+
+}
diff --git a/framework/Argv/lib/Horde/CVS/Entries b/framework/Argv/lib/Horde/CVS/Entries
new file mode 100644 (file)
index 0000000..7f04381
--- /dev/null
@@ -0,0 +1 @@
+D/Argv////
diff --git a/framework/Argv/lib/Horde/CVS/Repository b/framework/Argv/lib/Horde/CVS/Repository
new file mode 100644 (file)
index 0000000..42d49e7
--- /dev/null
@@ -0,0 +1 @@
+framework/Argv/lib/Horde
diff --git a/framework/Argv/lib/Horde/CVS/Root b/framework/Argv/lib/Horde/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Argv/lib/Horde/CVS/Template b/framework/Argv/lib/Horde/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Argv/package.xml b/framework/Argv/package.xml
new file mode 100644 (file)
index 0000000..3d5712c
--- /dev/null
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Argv</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde command-line argument parsing package</summary>
+ <description>This package provides classes for parsing command line
+ arguments with various actions, providing help, grouping options, and
+ more.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Mike Naberezny</name>
+  <user>mnaberez</user>
+  <email>mike@maintainable.com</email>
+  <active>yes</active>
+ </lead>
+ <date>2008-01-31</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>
+   * Initial release, ported from Optik (http://optik.sourceforge.net/)
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Argv">
+      <file name="AmbiguousOptionException.php" role="php" />
+      <file name="BadOptionException.php" role="php" />
+      <file name="Exception.php" role="php" />
+      <file name="HelpFormatter.php" role="php" />
+      <file name="IndentedHelpFormatter.php" role="php" />
+      <file name="Option.php" role="php" />
+      <file name="OptionConflictException.php" role="php" />
+      <file name="OptionContainer.php" role="php" />
+      <file name="OptionException.php" role="php" />
+      <file name="OptionGroup.php" role="php" />
+      <file name="OptionValueException.php" role="php" />
+      <file name="Parser.php" role="php" />
+      <file name="TitledHelpFormatter.php" role="php" />
+      <file name="Values.php" role="php" />
+     </dir> <!-- /lib/Horde/Argv -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.1.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Argv/AmbiguousOptionException.php" as="Horde/Argv/AmbiguousOptionException.php" />
+   <install name="lib/Horde/Argv/BadOptionException.php" as="Horde/Argv/BadOptionException.php" />
+   <install name="lib/Horde/Argv/Exception.php" as="Horde/Argv/Exception.php" />
+   <install name="lib/Horde/Argv/HelpFormatter.php" as="Horde/Argv/HelpFormatter.php" />
+   <install name="lib/Horde/Argv/IndentedHelpFormatter.php" as="Horde/Argv/IndentedHelpFormatter.php" />
+   <install name="lib/Horde/Argv/Option.php" as="Horde/Argv/Option.php" />
+   <install name="lib/Horde/Argv/OptionConflictException.php" as="Horde/Argv/OptionConflictException.php" />
+   <install name="lib/Horde/Argv/OptionContainer.php" as="Horde/Argv/OptionContainer.php" />
+   <install name="lib/Horde/Argv/OptionException.php" as="Horde/Argv/OptionException.php" />
+   <install name="lib/Horde/Argv/OptionGroup.php" as="Horde/Argv/OptionGroup.php" />
+   <install name="lib/Horde/Argv/OptionValueException.php" as="Horde/Argv/OptionValueException.php" />
+   <install name="lib/Horde/Argv/Parser.php" as="Horde/Argv/Parser.php" />
+   <install name="lib/Horde/Argv/TitledHelpFormatter.php" as="Horde/Argv/TitledHelpFormatter.php" />
+   <install name="lib/Horde/Argv/Values.php" as="Horde/Argv/Values.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Argv/test/CVS/Entries b/framework/Argv/test/CVS/Entries
new file mode 100644 (file)
index 0000000..ae2779e
--- /dev/null
@@ -0,0 +1 @@
+D/Horde////
diff --git a/framework/Argv/test/CVS/Repository b/framework/Argv/test/CVS/Repository
new file mode 100644 (file)
index 0000000..721c097
--- /dev/null
@@ -0,0 +1 @@
+framework/Argv/test
diff --git a/framework/Argv/test/CVS/Root b/framework/Argv/test/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Argv/test/CVS/Template b/framework/Argv/test/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Argv/test/Horde/Argv/AllTests.php b/framework/Argv/test/Horde/Argv/AllTests.php
new file mode 100644 (file)
index 0000000..1cf19c0
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+/**
+ * Horde_Argv test suite
+ *
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Argv_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+class Horde_Argv_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        // Catch strict standards
+        error_reporting(E_ALL | E_STRICT);
+
+        // Set up autoload
+        set_include_path(dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'lib' . PATH_SEPARATOR . get_include_path());
+        if (!spl_autoload_functions()) {
+            spl_autoload_register(create_function('$class', '$filename = str_replace(array(\'::\', \'_\'), \'/\', $class); include "$filename.php";'));
+        }
+
+        // Test base classes and helper objects
+        require_once dirname(__FILE__) . '/TestBase.php';
+        require_once dirname(__FILE__) . '/ConflictTestBase.php';
+        require_once dirname(__FILE__) . '/InterceptedException.php';
+        require_once dirname(__FILE__) . '/InterceptingParser.php';
+
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Argv');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Argv_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Argv_AllTests::main') {
+    Horde_Argv_AllTests::main();
+}
diff --git a/framework/Argv/test/Horde/Argv/BoolTest.php b/framework/Argv/test/Horde/Argv/BoolTest.php
new file mode 100644 (file)
index 0000000..43f5c60
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_BoolTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(
+            $this->makeOption('-v', '--verbose',
+                        array('action' => 'store_true', 'dest' => 'verbose', 'default' => '')),
+            $this->makeOption('-q', '--quiet',
+                        array('action' => 'store_false', 'dest' => 'verbose'))
+        );
+
+        $this->parser = new Horde_Argv_Parser(array('optionList' => $options));
+    }
+
+    public function testBoolDefault()
+    {
+        $this->assertParseOk(array(),
+                             array('verbose' => ''),
+                             array());
+    }
+
+    public function testBoolFalse()
+    {
+        list($options, $args) = $this->assertParseOk(array('-q'),
+                                                     array('verbose' => false),
+                                                     array());
+
+        $this->assertSame(false, $options->verbose);
+    }
+
+    public function testBoolTrue()
+    {
+        list($options, $args) = $this->assertParseOk(array('-v'),
+                                                     array('verbose' => true),
+                                                     array());
+        $this->assertSame(true, $options->verbose);
+    }
+
+    public function testBoolFlickerOnAndOff()
+    {
+        $this->assertParseOk(array('-qvq', '-q', '-v'),
+                             array('verbose' => true),
+                             array());
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/CVS/Entries b/framework/Argv/test/Horde/Argv/CVS/Entries
new file mode 100644 (file)
index 0000000..81f39ef
--- /dev/null
@@ -0,0 +1,36 @@
+/AllTests.php/1.3/Wed Jun 18 19:00:45 2008//
+/BoolTest.php/1.2/Wed Jun 18 19:00:45 2008//
+/CallbackCheckAbbrevTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/CallbackExtraArgsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/CallbackManyArgsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/CallbackMeddleArgsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/CallbackTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/CallbackVarArgsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ChoiceTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ConflictOverrideTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ConflictResolveTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ConflictTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ConflictTestBase.php/1.2/Wed Jun 18 19:00:46 2008//
+/ConflictingDefaultsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/CountTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/DefaultValuesTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ExpandDefaultsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ExtendAddActionsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ExtendAddTypesTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/HelpTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/InterceptedException.php/1.2/Wed Jun 18 19:00:46 2008//
+/InterceptingParser.php/1.2/Wed Jun 18 19:00:46 2008//
+/MatchAbbrevTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/MultipleArgsAppendTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/MultipleArgsTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/OptionChecksTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/OptionGroupTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/OptionValuesTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ParseNumTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ParserTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/ProgNameTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/StandardTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/TestBase.php/1.2/Wed Jun 18 19:00:46 2008//
+/TypeAliasesTest.php/1.2/Wed Jun 18 19:00:46 2008//
+/VersionTest.php/1.2/Wed Jun 18 19:00:46 2008//
+D
diff --git a/framework/Argv/test/Horde/Argv/CVS/Repository b/framework/Argv/test/Horde/Argv/CVS/Repository
new file mode 100644 (file)
index 0000000..1c7ec4a
--- /dev/null
@@ -0,0 +1 @@
+framework/Argv/test/Horde/Argv
diff --git a/framework/Argv/test/Horde/Argv/CVS/Root b/framework/Argv/test/Horde/Argv/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Argv/test/Horde/Argv/CVS/Template b/framework/Argv/test/Horde/Argv/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Argv/test/Horde/Argv/CallbackCheckAbbrevTest.php b/framework/Argv/test/Horde/Argv/CallbackCheckAbbrevTest.php
new file mode 100644 (file)
index 0000000..d8b75d0
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_CallbackCheckAbbrevTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_Parser();
+        $this->parser->addOption('--foo-bar', array('action' => 'callback',
+                                                    'callback' => array($this, 'checkAbbrev')));
+    }
+
+    public function checkAbbrev($option, $opt, $value, $parser)
+    {
+        $this->assertEquals($opt, '--foo-bar');
+    }
+
+    public function testAbbrevCallbackExpansion()
+    {
+        $this->assertParseOk(array('--foo'), array(), array());
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/CallbackExtraArgsTest.php b/framework/Argv/test/Horde/Argv/CallbackExtraArgsTest.php
new file mode 100644 (file)
index 0000000..264d397
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_CallbackExtraArgsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(
+            $this->makeOption('-p', '--point', array('action' => 'callback',
+                'callback' => array($this, 'processTuple'),
+                'callbackArgs' => array(3, 'int'), 'type' => 'string',
+                'dest' => 'points', 'default' => array())),
+        );
+        $this->parser = new Horde_Argv_Parser(array('optionList' => $options));
+    }
+
+    public function processTuple($option, $opt, $value, $parser, $args)
+    {
+        list($len, $type) = $args;
+
+        $this->assertEquals(3, $len);
+        $this->assertEquals('int', $type);
+
+        if ($opt == '-p') {
+            $this->assertEquals('1,2,3', $value);
+        } else if ($option == '--point') {
+            $this->assertEquals('4,5,6', $value);
+        }
+
+        $values = explode(',', $value);
+        foreach ($values as &$value) {
+            settype($value, $type);
+        }
+
+        $parser->values->{$option->dest}[] = $values;
+    }
+
+    public function testCallbackExtraArgs()
+    {
+        $this->assertParseOk(array("-p1,2,3", "--point", "4,5,6"),
+                             array('points' => array(array(1,2,3), array(4,5,6))),
+                             array());
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/CallbackManyArgsTest.php b/framework/Argv/test/Horde/Argv/CallbackManyArgsTest.php
new file mode 100644 (file)
index 0000000..f98fca9
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_CallbackManyArgsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(
+            $this->makeOption('-a', '--apple', array('action' => 'callback', 'nargs' => 2,
+                                                        'callback' => array($this, 'processMany'), 'type' => 'string')),
+            $this->makeOption('-b', '--bob', array('action' => 'callback', 'nargs' => 3,
+                                                       'callback' => array($this, 'processMany'), 'type' => 'int'))
+        );
+
+        $this->parser = new Horde_Argv_Parser(array('optionList' => $options));
+    }
+
+    public function processMany($option, $opt, $value, $parser_)
+    {
+        if ($opt == '-a') {
+            $this->assertEquals(array('foo', 'bar'), $value);
+        } else if ($opt == '--apple') {
+            $this->assertEquals(array('ding', 'dong'), $value);
+        } else if ($opt == '-b') {
+            $this->assertEquals(array(1, 2, 3), $value);
+        } else if ($option == '--bob') {
+            $this->assertEquals(array(-666, 42, 0), $value);
+        }
+    }
+
+    public function testManyArgs()
+    {
+        $this->assertParseOk(array("-a", "foo", "bar", "--apple", "ding", "dong",
+                             "-b", "1", "2", "3", "--bob", "-666", "42",
+                             "0"),
+                             array('apple' => null, 'bob' => null),
+                             array());
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/CallbackMeddleArgsTest.php b/framework/Argv/test/Horde/Argv/CallbackMeddleArgsTest.php
new file mode 100644 (file)
index 0000000..7980090
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_CallbackMeddleArgsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array();
+        for ($i = -1; $i > -6; $i--) {
+            $options[] = $this->makeOption((string)$i, array('action' => 'callback',
+                                                             'callback' => array($this, 'process_n'),
+                                                             'dest' => 'things'));
+        }
+        $this->parser = new Horde_Argv_Parser(array('optionList' => $options));
+    }
+
+    /**
+     * Callback that meddles in rargs, largs
+     */
+    public function process_n($option, $opt, $value, $parser)
+    {
+        // option is -3, -5, etc.
+        $nargs = (int)substr($opt, 1);
+        $rargs =& $parser->rargs;
+        if (count($rargs) < $nargs) {
+            $this->fail(sprintf("Expected %d arguments for %s option.", $nargs, $opt));
+        }
+
+        $parser->values->{$option->dest}[] = array_splice($rargs, 0, $nargs);
+        $parser->largs[] = $nargs;
+    }
+
+    public function testCallbackMeddleArgs()
+    {
+        $this->assertParseOK(array("-1", "foo", "-3", "bar", "baz", "qux"),
+                             array('things' => array(array('foo'), array('bar', 'baz', 'qux'))),
+                             array(1, 3));
+    }
+
+    public function testCallbackMeddleArgsSeparator()
+    {
+        $this->assertParseOK(array("-2", "foo", "--"),
+                             array('things' => array(array('foo', '--'))),
+                             array(2));
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/CallbackTest.php b/framework/Argv/test/Horde/Argv/CallbackTest.php
new file mode 100644 (file)
index 0000000..ab1dc8c
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_CallbackTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(
+            new Horde_Argv_Option('-x', null,
+                array('action' => 'callback', 'callback' => array($this, 'processOpt'))),
+            new Horde_Argv_Option('-f', '--file',
+                array('action' => 'callback',
+                      'callback' => array($this, 'processOpt'),
+                      'type' => 'string',
+                      'dest' => 'filename')),
+        );
+
+        $this->parser = new Horde_Argv_Parser(array('optionList' => $options));
+    }
+
+    public function processOpt($option, $opt, $value, $parser_)
+    {
+        if ($opt == '-x') {
+            $this->assertEquals(array('-x'), $option->shortOpts);
+            $this->assertEquals(array(), $option->longOpts);
+            $this->assertType(get_class($this->parser), $parser_);
+            $this->assertNull($value);
+            $this->assertEquals(array('filename' => null), iterator_to_array($parser_->values));
+
+            $parser_->values->x = 42;
+        } else if ($opt == '--file') {
+            $this->assertEquals(array('-f'), $option->shortOpts);
+            $this->assertEquals(array('--file'), $option->longOpts);
+            $this->assertType(gettype($this->parser), $parser_);
+            $this->assertEquals('foo', $value);
+            $this->assertEquals(array('filename' => null, 'x' => 42), iterator_to_array($parser_->values));
+
+            $parser_->values->{$option->dest} = $value;
+        } else {
+            $this->fail(sprintf('Unknown option %r in processOpt.', $opt));
+        }
+    }
+
+    public function testCallback()
+    {
+        $this->assertParseOk(array('-x', '--file=foo'),
+                             array('filename' => 'foo', 'x' => 42),
+                             array());
+    }
+
+    public function testCallbackHelp()
+    {
+        // This test was prompted by SF bug #960515 -- the point is
+        // not to inspect the help text, just to make sure that
+        // formatHelp() doesn't crash.
+        $parser = new Horde_Argv_Parser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+        $parser->removeOption('-h');
+        $parser->addOption('-t', '--test',
+                           array('action' => 'callback', 'callback' => array($this, 'returnNull'),
+                                 'type' => 'string', 'help' => 'foo'));
+
+        $expectedHelp = "Options:\n  -t TEST, --test=TEST  foo\n";
+        $this->assertHelp($parser, $expectedHelp);
+    }
+
+    public function returnNull()
+    {}
+}
diff --git a/framework/Argv/test/Horde/Argv/CallbackVarArgsTest.php b/framework/Argv/test/Horde/Argv/CallbackVarArgsTest.php
new file mode 100644 (file)
index 0000000..174ecef
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_CallbackVarArgsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(
+            $this->makeOption('-a', array('type' => 'int', 'nargs' => 2, 'dest' => 'a')),
+            $this->makeOption('-b', array('action' => 'store_true', 'dest' => 'b')),
+            $this->makeOption('-c', '--callback', array('action' => 'callback', 'callback' => array($this, 'variableArgs'), 'dest' => 'c')),
+        );
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE,
+                                                                'optionList' => $options));
+    }
+
+    public function variableArgs($option, $opt, $value, $parser)
+    {
+        $this->assertNull($value);
+        $done = 0;
+        $value = array();
+        $rargs =& $parser->rargs;
+        while ($rargs) {
+            $arg = $rargs[0];
+            if ((substr($arg, 0, 2) == '--' && strlen($arg) > 2) ||
+                (substr($arg, 0, 1) == '-' && strlen($arg) > 1 && substr($arg, 1, 1) != '-')) {
+                break;
+            } else {
+                $value[] = $arg;
+                array_shift($rargs);
+            }
+        }
+        $parser->values->{$option->dest} = $value;
+    }
+
+    public function testVariableArgs()
+    {
+        $this->assertParseOK(array('-a3', '-5', '--callback', 'foo', 'bar'),
+                             array('a' => array(3, -5), 'b' => null, 'c' => array('foo', 'bar')),
+                             array());
+    }
+
+    public function testConsumeSeparatorStopAtOption()
+    {
+        $this->assertParseOK(array('-c', '37', '--', 'xxx', '-b', 'hello'),
+                             array('a' => null, 'b' => true, 'c' => array('37', '--', 'xxx')),
+                             array('hello'));
+    }
+
+    public function testPositionalArgAndVariableArgs()
+    {
+        $this->assertParseOK(array('hello', '-c', 'foo', '-', 'bar'),
+                             array('a' => null, 'b' => null, 'c' => array('foo', '-', 'bar')),
+                             array('hello'));
+    }
+
+    public function testStopAtOption()
+    {
+        $this->assertParseOK(array('-c', 'foo', '-b'),
+                             array('a' => null, 'b' => true, 'c' => array('foo')),
+                             array());
+    }
+
+    public function testStopAtInvalidOption()
+    {
+        $this->assertParseFail(array('-c', '3', '-5', '-a'), 'no such option: -5');
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/ChoiceTest.php b/framework/Argv/test/Horde/Argv/ChoiceTest.php
new file mode 100644 (file)
index 0000000..925b3d8
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ChoiceTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+        $this->parser->addOption('-c', array('action' => 'store', 'type' => 'choice',
+                                 'dest' => 'choice', 'choices' => array('one', 'two', 'three')));
+    }
+
+    public function testValidChoice()
+    {
+        $this->assertParseOk(array('-c', 'one', 'xyz'),
+                             array('choice' => 'one'),
+                             array('xyz'));
+    }
+
+    public function testInvalidChoice()
+    {
+        $this->assertParseFail(array('-c', 'four', 'abc'),
+                               "option -c: invalid choice: 'four' " .
+                               "(choose from 'one', 'two', 'three')");
+    }
+
+    public function testAddChoiceOption()
+    {
+        $this->parser->addOption('-d', '--default', array('choices' => array('four', 'five', 'six')));
+        $opt = $this->parser->getOption('-d');
+        $this->assertEquals('choice', $opt->type);
+        $this->assertEquals('store', $opt->action);
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/ConflictOverrideTest.php b/framework/Argv/test/Horde/Argv/ConflictOverrideTest.php
new file mode 100644 (file)
index 0000000..51943bf
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ConflictOverrideTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+        $this->parser->setConflictHandler('resolve');
+        $this->parser->addOption('-n', '--dry-run',
+                                 array('action' => 'store_true', 'dest' => 'dry_run',
+                                       'help' => "don't do anything"));
+        $this->parser->addOption('--dry-run', '-n',
+                                 array('action' => 'store_const', 'const' => 42, 'dest' => 'dry_run',
+                                       'help' => 'dry run mode'));
+    }
+
+    public function testConflictOverrideOpts()
+    {
+        $opt = $this->parser->getOption('--dry-run');
+
+        $this->assertEquals(array('-n'), $opt->shortOpts);
+        $this->assertEquals(array('--dry-run'), $opt->longOpts);
+    }
+
+    public function testConflictOverrideHelp()
+    {
+        $output = "Options:\n"
+                . "  -h, --help     show this help message and exit\n"
+                . "  -n, --dry-run  dry run mode\n";
+        $this->assertOutput(array('-h'), $output);
+    }
+
+    public function testConflictOverrideArgs()
+    {
+        $this->assertParseOk(array('-n'),
+                             array('dry_run' => 42),
+                             array());
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/ConflictResolveTest.php b/framework/Argv/test/Horde/Argv/ConflictResolveTest.php
new file mode 100644 (file)
index 0000000..5eb281a
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ConflictResolveTest extends Horde_Argv_ConflictTestBase
+{
+    public function setUp()
+    {
+        parent::setUp();
+        $this->parser->setConflictHandler('resolve');
+        $this->parser->addOption('-v', '--version', array('action' => 'callback',
+                                                          'callback' => array($this, 'showVersion'),
+                                                          'help' => 'show version'));
+    }
+
+    public function testConflictResolve()
+    {
+        $vOpt = $this->parser->getOption('-v');
+        $verboseOpt = $this->parser->getOption('--verbose');
+        $versionOpt = $this->parser->getOption('--version');
+
+        $this->assertSame($vOpt, $versionOpt);
+        $this->assertNotSame($vOpt, $verboseOpt);
+
+        $this->assertEquals(array('--version'), $vOpt->longOpts);
+        $this->assertEquals(array('-v'), $versionOpt->shortOpts);
+        $this->assertEquals(array('--version'), $versionOpt->longOpts);
+        $this->assertEquals(array(), $verboseOpt->shortOpts);
+        $this->assertEquals(array('--verbose'), $verboseOpt->longOpts);
+    }
+
+    public function testConflictResolveHelp()
+    {
+        $output = "Options:\n"
+                . "  --verbose      increment verbosity\n"
+                . "  -h, --help     show this help message and exit\n"
+                . "  -v, --version  show version\n";
+
+        $this->assertOutput(array('-h'), $output);
+    }
+
+    public function testConflictResolveShortOpt()
+    {
+        $this->assertParseOk(array('-v'),
+                             array('verbose' => null, 'showVersion' => 1),
+                             array());
+    }
+
+    public function testConflictResolveLongOpt()
+    {
+        $this->assertParseOk(array('--verbose'),
+                             array('verbose' => 1),
+                             array());
+    }
+
+    public function testConflictResolveLongOpts()
+    {
+        $this->assertParseOk(array('--verbose', '--version'),
+                             array('verbose' => 1, 'showVersion' => 1),
+                             array());
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/ConflictTest.php b/framework/Argv/test/Horde/Argv/ConflictTest.php
new file mode 100644 (file)
index 0000000..6263839
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ConflictTest extends Horde_Argv_ConflictTestBase
+{
+    public function assertConflictError($func)
+    {
+        try {
+            call_user_func($func, '-v', '--version', array(
+                'action'   => 'callback',
+                'callback' => array($this, 'showVersion'),
+                'help'     => 'show version'));
+            $this->fail();
+        } catch (Horde_Argv_OptionConflictException $e) {
+            $this->assertEquals("option -v/--version: conflicting option string(s): -v",
+                                $e->getMessage());
+            $this->assertEquals('-v/--version', $e->optionId);
+        }
+    }
+
+    public function testConflictError()
+    {
+        $this->assertConflictError(array($this->parser, 'addOption'));
+    }
+
+    public function testConflictErrorGroup()
+    {
+        $group = new Horde_Argv_OptionGroup($this->parser, 'Group 1');
+        $this->assertConflictError(array($group, 'addOption'));
+    }
+
+    public function testNoSuchConflictHandler()
+    {
+        $this->assertRaises(array($this->parser, 'setConflictHandler'), array('foo'), 'InvalidArgumentException', "invalid conflictHandler 'foo'");
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/ConflictTestBase.php b/framework/Argv/test/Horde/Argv/ConflictTestBase.php
new file mode 100644 (file)
index 0000000..769e66b
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ConflictTestBase extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(new Horde_Argv_Option('-v', '--verbose', array(
+            'action' => 'count',
+            'dest' => 'verbose',
+            'help' => 'increment verbosity'))
+        );
+
+        $this->parser = new Horde_Argv_InterceptingParser(
+            array('usage' => Horde_Argv_Option::SUPPRESS_USAGE, 'optionList' => $options)
+        );
+    }
+
+    public function showVersion($option, $opt, $value, $parser)
+    {
+        $this->parser->values->showVersion = 1;
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/ConflictingDefaultsTest.php b/framework/Argv/test/Horde/Argv/ConflictingDefaultsTest.php
new file mode 100644 (file)
index 0000000..1236154
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+/**
+ * Conflicting default values: the last one should win.
+ */
+class Horde_Argv_ConflictingDefaultsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(
+            $this->makeOption('-v', array('action' => 'store_true', 'dest' => 'verbose', 'default' => 1))
+        );
+
+        $this->parser = new Horde_Argv_Parser(array('optionList' => $options));
+    }
+
+    public function testConflictDefault()
+    {
+        $this->parser->addOption('-q', array('action' => 'store_false', 'dest' => 'verbose',
+                                             'default' => 0));
+
+        $this->assertParseOk(array(), array('verbose' => 0), array());
+    }
+
+    public function testConflictDefaultNone()
+    {
+        $this->parser->addOption('-q', array('action' => 'store_false', 'dest' => 'verbose',
+                                             'default' => null));
+
+        $this->assertParseOk(array(), array('verbose' => null), array());
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/CountTest.php b/framework/Argv/test/Horde/Argv/CountTest.php
new file mode 100644 (file)
index 0000000..293adaa
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_CountTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+        $this->vOpt = $this->makeOption('-v', array('action' => 'count', 'dest' => 'verbose'));
+        $this->parser->addOption($this->vOpt);
+        $this->parser->addOption('--verbose', array('type' => 'int', 'dest' => 'verbose'));
+        $this->parser->addOption('-q', '--quiet',
+                                 array('action' => 'store_const', 'dest' => 'verbose', 'const' => 0));
+    }
+
+    public function testEmpty()
+    {
+        $this->assertParseOk(array(), array('verbose' => null), array());
+    }
+
+    public function testCountOne()
+    {
+        $this->assertParseOk(array('-v'), array('verbose' => 1), array());
+    }
+
+    public function testCountThree()
+    {
+        $this->assertParseOk(array('-vvv'), array('verbose' => 3), array());
+    }
+
+    public function testCountThreeApart()
+    {
+        $this->assertParseOk(array('-v', '-v', '-v'), array('verbose' => 3), array());
+    }
+
+    public function testCountOverrideAmount()
+    {
+        $this->assertParseOk(array('-vvv', '--verbose=2'), array('verbose' => 2), array());
+    }
+
+    public function testCountOverrideQuiet()
+    {
+        $this->assertParseOk(array('-vvv', '--verbose=2', '-q'), array('verbose' => 0), array());
+    }
+
+    public function testCountOverriding()
+    {
+        $this->assertParseOk(array('-vvv', '--verbose=2', '-q', '-v'),
+                             array('verbose' => 1), array());
+    }
+
+    public function testCountInterspersedArgs()
+    {
+        $this->assertParseOk(array('--quiet', '3', '-v'),
+                             array('verbose' => 1),
+                             array('3'));
+    }
+
+    public function testCountNoInterspersedArgs()
+    {
+        $this->parser->disableInterspersedArgs();
+        $this->assertParseOk(array('--quiet', '3', '-v'),
+                             array('verbose' => 0),
+                             array('3', '-v'));
+    }
+
+    public function testCountNoSuchOption()
+    {
+        $this->assertParseFail(array('-q3', '-v'), 'no such option: -3');
+    }
+
+    public function testCountOptionNoValue()
+    {
+        $this->assertParseFail(array('--quiet=3', 'v'),
+                               '--quiet option does not take a value');
+    }
+
+    public function testCountWithDefault()
+    {
+        $this->parser->setDefault('verbose', 0);
+        $this->assertParseOk(array(), array('verbose' => 0), array());
+    }
+
+    public function testCountOverridingDefault()
+    {
+        $this->parser->setDefault('verbose', 0);
+        $this->assertParseOk(array('-vvv', '--verbose=2', '-q', '-v'),
+                             array('verbose' => 1), array());
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/DefaultValuesTest.php b/framework/Argv/test/Horde/Argv/DefaultValuesTest.php
new file mode 100644 (file)
index 0000000..3ea1c4e
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_DefaultValuesTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_Parser();
+        $this->parser->addOption('-v', '--verbose', array('default' => true));
+        $this->parser->addOption('-q', '--quiet', array('dest' => 'verbose'));
+        $this->parser->addOption('-n', array('type' => 'int', 'default' => 37));
+        $this->parser->addOption('-m', array('type' => 'int'));
+        $this->parser->addOption('-s', array('default' => 'foo'));
+        $this->parser->addOption('-t');
+        $this->parser->addOption('-u', array('default' => null));
+
+        $this->expected = array('verbose' => true,
+                                'n' => 37,
+                                'm' => null,
+                                's' => 'foo',
+                                't' => null,
+                                'u' => null);
+    }
+
+    public function testBasicDefault()
+    {
+        $this->assertEquals($this->expected, iterator_to_array($this->parser->getDefaultValues()));
+    }
+
+    public function testMixedDefaultsPost()
+    {
+        $this->parser->setDefaults(array('n' => 42, 'm' => -100));
+        $this->expected = array_merge($this->expected, array('n' => 42, 'm' => -100));
+        $this->assertEquals($this->expected, iterator_to_array($this->parser->getDefaultValues()));
+    }
+
+    public function testMixedDefaultsPre()
+    {
+        $this->parser->setDefaults(array('x' => 'barf', 'y' => 'blah'));
+        $this->parser->addOption('-x', array('default' => 'frob'));
+        $this->parser->addOption('-y');
+
+        $this->expected = array_merge($this->expected, array('x' => 'frob', 'y' => 'blah'));
+        $this->assertEquals($this->expected, iterator_to_array($this->parser->getDefaultValues()));
+
+        $this->parser->removeOption('-y');
+        $this->parser->addOption('-y', array('default' => null));
+        $this->expected = array_merge($this->expected, array('y' => null));
+        $this->assertEquals($this->expected, iterator_to_array($this->parser->getDefaultValues()));
+    }
+
+    public function testProcessDefault()
+    {
+        $this->parser->optionClass = 'Horde_Argv_DurationOption';
+        $this->parser->addOption('-d', array('type' => 'duration', 'default' => 300));
+        $this->parser->addOption('-e', array('type' => 'duration', 'default' => '6m'));
+        $this->parser->setDefaults(array('n' => '42'));
+
+        $this->expected = array_merge($this->expected, array('d' => 300, 'e' => 360, 'n' => '42'));
+        $this->assertEquals($this->expected, iterator_to_array($this->parser->getDefaultValues()));
+    }
+}
+
+class Horde_Argv_DurationOption extends Horde_Argv_Option
+{
+    public $TYPES = array('string', 'int', 'long', 'float', 'complex', 'choice', 'duration');
+
+    public $TYPE_CHECKER = array('int'    => 'checkBuiltin',
+                                 'long'   => 'checkBuiltin',
+                                 'float'  => 'checkBuiltin',
+                                 'complex'=> 'checkBuiltin',
+                                 'choice' => 'checkChoice',
+                                 'duration' => 'checkDuration',
+    );
+
+    public function checkDuration($opt, $value)
+    {
+        // Custom type for testing processing of default values.
+        $time_units = array('s' => 1, 'm' => 60, 'h' => 60 * 60, 'd' => 60 * 60 * 24);
+
+        $last = substr($value, -1);
+        if (is_numeric($last)) {
+            return (int)$value;
+        } elseif (isset($time_units[$last])) {
+            return (int)substr($value, 0, -1) * $time_units[$last];
+        } else {
+            throw new Horde_Argv_OptionValueException(sprintf(
+                'option %s: invalid duration: %s', $opt, $value));
+        }
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/ExpandDefaultsTest.php b/framework/Argv/test/Horde/Argv/ExpandDefaultsTest.php
new file mode 100644 (file)
index 0000000..ba8e96e
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ExpandDefaultsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_Parser(array('prog' => 'test'));
+        $this->help_prefix = 'Usage: test [options]
+
+Options:
+  -h, --help            show this help message and exit';
+
+        $this->file_help = "read from FILE [default: %default]";
+        $this->expected_help_file = $this->help_prefix . "\n" .
+            "  -f FILE, --file=FILE  read from FILE [default: foo.txt]\n";
+        $this->expected_help_none = $this->help_prefix . "\n" .
+            "  -f FILE, --file=FILE  read from FILE [default: none]\n";
+    }
+
+    public function testOptionDefault()
+    {
+        $this->parser->addOption("-f", "--file", array('default' => 'foo.txt', 'help' => $this->file_help));
+        $this->assertHelp($this->parser, $this->expected_help_file);
+    }
+
+    public function testParserDefault1()
+    {
+        $this->parser->addOption("-f", "--file",
+                                 array('help' => $this->file_help));
+        $this->parser->setDefault('file', "foo.txt");
+        $this->assertHelp($this->parser, $this->expected_help_file);
+    }
+
+    public function testParserDefault2()
+    {
+        $this->parser->addOption("-f", "--file",
+                                 array('help' => $this->file_help));
+        $this->parser->setDefaults(array('file' => 'foo.txt'));
+        $this->assertHelp($this->parser, $this->expected_help_file);
+    }
+
+    public function testNoDefault()
+    {
+        $this->parser->addOption("-f", "--file",
+                                 array('help' => $this->file_help));
+        $this->assertHelp($this->parser, $this->expected_help_none);
+    }
+
+    public function testDefaultNone1()
+    {
+        $this->parser->addOption("-f", "--file",
+                                 array('default' => null,
+                                       'help' => $this->file_help));
+        $this->assertHelp($this->parser, $this->expected_help_none);
+    }
+
+    public function testDefaultNone2()
+    {
+        $this->parser->addOption("-f", "--file",
+                                 array('help' => $this->file_help));
+        $this->parser->setDefaults(array('file' => null));
+        $this->assertHelp($this->parser, $this->expected_help_none);
+    }
+
+    public function testFloatDefault()
+    {
+        $this->parser->addOption(
+            "-p", "--prob",
+            array('help' => "blow up with probability PROB [default: %default]"));
+        $this->parser->setDefaults(array('prob' => 0.43));
+        $expected_help = $this->help_prefix . "\n" .
+            "  -p PROB, --prob=PROB  blow up with probability PROB [default: 0.43]\n";
+        $this->assertHelp($this->parser, $expected_help);
+    }
+
+    public function testAltExpand()
+    {
+        $this->parser->addOption("-f", "--file",
+                                 array('default' => "foo.txt",
+                                       'help' => "read from FILE [default: *DEFAULT*]"));
+        $this->parser->formatter->default_tag = "*DEFAULT*";
+        $this->assertHelp($this->parser, $this->expected_help_file);
+    }
+
+    public function testNoExpand()
+    {
+        $this->parser->addOption("-f", "--file",
+                                 array('default' => "foo.txt",
+                                       'help' => "read from %default file"));
+        $this->parser->formatter->default_tag = null;
+        $expected_help = $this->help_prefix . "\n" .
+            "  -f FILE, --file=FILE  read from %default file\n";
+        $this->assertHelp($this->parser, $expected_help);
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/ExtendAddActionsTest.php b/framework/Argv/test/Horde/Argv/ExtendAddActionsTest.php
new file mode 100644 (file)
index 0000000..59623e1
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ExtendAddActionsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(new Horde_Argv_ExtendAddActionsTest_MyOption("-a", "--apple", array(
+            'action' => "extend", 'type' => "string", 'dest' => "apple")));
+        $this->parser = new Horde_Argv_Parser(array('optionList' => $options));
+    }
+
+    public function testExtendAddAction()
+    {
+        $this->assertParseOK(array("-afoo,bar", "--apple=blah"),
+                             array('apple' => array("foo", "bar", "blah")),
+                             array());
+    }
+
+    public function testExtendAddActionNormal()
+    {
+        $this->assertParseOK(array("-a", "foo", "-abar", "--apple=x,y"),
+                             array('apple' => array("foo", "bar", "x", "y")),
+                             array());
+    }
+
+}
+
+class Horde_Argv_ExtendAddActionsTest_MyOption extends Horde_Argv_Option
+{
+    public $ACTIONS = array("store",
+                            "store_const",
+                            "store_true",
+                            "store_false",
+                            "append",
+                            "append_const",
+                            "count",
+                            "callback",
+                            "help",
+                            "version",
+                            "extend",
+                            );
+
+    public $STORE_ACTIONS = array("store",
+                     "store_const",
+                     "store_true",
+                     "store_false",
+                     "append",
+                     "append_const",
+                     "count",
+                     "extend",
+                                  );
+
+    public $TYPED_ACTIONS = array("store",
+                                  "append",
+                                  "callback",
+                                  "extend",
+                                  );
+
+    public function takeAction($action, $dest, $opt, $value, $values, $parser)
+    {
+        if ($action == "extend") {
+            $lvalue = explode(',', $value);
+            $values->$dest = array_merge($values->ensureValue($dest, array()), $lvalue);
+        } else {
+            parent::takeAction($action, $dest, $opt, $parser, $value, $values);
+        }
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/ExtendAddTypesTest.php b/framework/Argv/test/Horde/Argv/ExtendAddTypesTest.php
new file mode 100644 (file)
index 0000000..ef3a71e
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ExtendAddTypesTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE,
+                                                                'optionClass' => 'Horde_Argv_ExtendAddTypesTest_MyOption'));
+        $this->parser->addOption("-a", null, array('type' => "string", 'dest' => "a"));
+        $this->parser->addOption("-f", "--file", array('type' => "file", 'dest' => "file"));
+
+        /* @todo make more system independent */
+        $this->testPath = tempnam('/tmp', 'horde_argv');
+    }
+
+    public function tearDown()
+    {
+        if (!is_link($this->testPath) && is_dir($this->testPath)) {
+            rmdir($this->testPath);
+        } elseif (is_file($this->testPath)) {
+            unlink($this->testPath);
+        }
+    }
+
+    public function testFiletypeOk()
+    {
+        touch($this->testPath);
+        $this->assertParseOK(array("--file", $this->testPath, "-afoo"),
+                             array('file' => $this->testPath, 'a' => 'foo'),
+                             array());
+    }
+
+    public function testFiletypeNoexist()
+    {
+        unlink($this->testPath);
+        $this->assertParseFail(array("--file", $this->testPath, "-afoo"),
+                               sprintf("%s: file does not exist", $this->testPath));
+    }
+
+    public function testFiletypeNotfile()
+    {
+        unlink($this->testPath);
+        mkdir($this->testPath);
+        $this->assertParseFail(array("--file", $this->testPath, "-afoo"),
+                               sprintf("%s: not a regular file", $this->testPath));
+    }
+
+}
+
+class Horde_Argv_ExtendAddTypesTest_MyOption extends Horde_Argv_Option
+{
+    public $TYPES = array('string', 'int', 'long', 'float', 'complex', 'choice', 'file');
+
+    public $TYPE_CHECKER = array("int"    => 'checkBuiltin',
+                     "long"   => 'checkBuiltin',
+                     "float"  => 'checkBuiltin',
+                     "complex"=> 'checkBuiltin',
+                     "choice" => 'checkChoice',
+                     'file' => 'checkFile',
+    );
+
+    public function checkFile($opt, $value)
+    {
+        if (!file_exists($value)) {
+            throw new Horde_Argv_OptionValueException(sprintf("%s: file does not exist", $value));
+        } elseif (!is_file($value)) {
+            throw new Horde_Argv_OptionValueException(sprintf("%s: not a regular file", $value));
+        }
+        return $value;
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/HelpTest.php b/framework/Argv/test/Horde/Argv/HelpTest.php
new file mode 100644 (file)
index 0000000..ef33782
--- /dev/null
@@ -0,0 +1,202 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_HelpTest extends Horde_Argv_TestBase
+{
+
+    public static $expected_help_basic = 'Usage: bar.php [options]
+
+Options:
+  -a APPLE           throw APPLEs at basket
+  -b NUM, --boo=NUM  shout "boo!" NUM times (in order to frighten away all the
+                     evil spirits that cause trouble and mayhem)
+  --foo=FOO          store FOO in the foo list for later fooing
+  -h, --help         show this help message and exit
+';
+
+    public static $expected_help_long_opts_first = 'Usage: bar.php [options]
+
+Options:
+  -a APPLE           throw APPLEs at basket
+  --boo=NUM, -b NUM  shout "boo!" NUM times (in order to frighten away all the
+                     evil spirits that cause trouble and mayhem)
+  --foo=FOO          store FOO in the foo list for later fooing
+  --help, -h         show this help message and exit
+';
+
+    public static $expected_help_title_formatter = 'Usage
+=====
+  bar.php [options]
+
+Options
+=======
+-a APPLE           throw APPLEs at basket
+--boo=NUM, -b NUM  shout "boo!" NUM times (in order to frighten away all the
+                   evil spirits that cause trouble and mayhem)
+--foo=FOO          store FOO in the foo list for later fooing
+--help, -h         show this help message and exit
+';
+
+    public static $expected_help_short_lines = 'Usage: bar.php [options]
+
+Options:
+  -a APPLE           throw APPLEs at basket
+  -b NUM, --boo=NUM  shout "boo!" NUM times (in order to
+                     frighten away all the evil spirits
+                     that cause trouble and mayhem)
+  --foo=FOO          store FOO in the foo list for later
+                     fooing
+  -h, --help         show this help message and exit
+';
+
+    public function setUp()
+    {
+        $this->parser = $this->makeParser(80);
+        $this->origColumns = isset($_ENV['COLUMNS']) ? $_ENV['COLUMNS'] : null;
+    }
+
+    public function tearDown()
+    {
+        if (is_null($this->origColumns)) {
+            unset($_ENV['COLUMNS']);
+        } else {
+            $_ENV['COLUMNS'] = $this->origColumns;
+        }
+    }
+
+    public function makeParser($columns)
+    {
+        $options = array(
+            $this->makeOption("-a", array('type' => "string", 'dest' => 'a',
+                                          'metavar' => "APPLE", 'help' => "throw APPLEs at basket")),
+
+            $this->makeOption("-b", "--boo", array('type'    => "int", 'dest' => 'boo',
+                                                   'metavar' => "NUM",
+                                                   'help'    => "shout \"boo!\" NUM times (in order to frighten away " .
+                                                                "all the evil spirits that cause trouble and mayhem)")),
+
+            $this->makeOption("--foo", array('action' => 'append', 'type' => 'string', 'dest' => 'foo',
+                                             'help' => "store FOO in the foo list for later fooing")),
+        );
+
+        $_ENV['COLUMNS'] = $columns;
+
+        return new Horde_Argv_InterceptingParser(array('optionList' => $options));
+    }
+
+    public function assertHelpEquals($expectedOutput)
+    {
+        // @todo
+        // if type(expected_output) is types.UnicodeType:
+        //     encoding = self.parser._get_encoding(sys.stdout)
+        //     expected_output = expected_output.encode(encoding, "replace")
+
+        $origArgv = $_SERVER['argv'];
+        $_SERVER['argv'][0] = 'foo/bar.php';
+        $this->assertOutput(array('-h'), $expectedOutput);
+
+        $_SERVER['argv'] = $origArgv;
+    }
+
+    public function testHelp()
+    {
+        $this->assertHelpEquals(self::$expected_help_basic);
+    }
+
+    public function tesHelpOldUsage()
+    {
+        $this->parser->setUsage("Usage: %prog [options]");
+        $this->assertHelpEquals(self::$expected_help_basic);
+    }
+
+    public function testHelpLongOptsFirst()
+    {
+        $this->parser->formatter->short_first = false;
+        $this->assertHelpEquals(self::$expected_help_long_opts_first);
+    }
+
+    public function testHelpTitleFormatter()
+    {
+        $this->parser->formatter = new Horde_Argv_TitledHelpFormatter();
+        $this->assertHelpEquals(self::$expected_help_title_formatter);
+    }
+
+    public function testWrapColumns()
+    {
+        // Ensure that wrapping respects $COLUMNS environment variable.
+        // Need to reconstruct the parser, since that's the only time
+        // we look at $COLUMNS.
+        $this->parser = $this->makeParser(60);
+        $this->assertHelpEquals(self::$expected_help_short_lines);
+    }
+
+    public function testHelpDescriptionGroups()
+    {
+        $this->parser->setDescription(
+            "This is the program description for %prog.  %prog has " .
+            "an option group as well as single options.");
+
+        $group = new Horde_Argv_OptionGroup(
+            $this->parser, "Dangerous Options",
+            "Caution: use of these options is at your own risk.  " .
+            "It is believed that some of them bite.");
+        $group->addOption("-g", array('action' => "store_true", 'help' => "Group option."));
+        $this->parser->addOptionGroup($group);
+
+        $expect = 'Usage: bar.php [options]
+
+This is the program description for bar.php.  bar.php has an option group as
+well as single options.
+
+Options:
+  -a APPLE           throw APPLEs at basket
+  -b NUM, --boo=NUM  shout "boo!" NUM times (in order to frighten away all the
+                     evil spirits that cause trouble and mayhem)
+  --foo=FOO          store FOO in the foo list for later fooing
+  -h, --help         show this help message and exit
+
+  Dangerous Options:
+    Caution: use of these options is at your own risk.  It is believed
+    that some of them bite.
+
+    -g               Group option.
+';
+
+        $this->assertHelpEquals($expect);
+
+        $this->parser->epilog = "Please report bugs to /dev/null.";
+        $this->assertHelpEquals($expect . "\nPlease report bugs to /dev/null.\n");
+    }
+
+    /* @todo
+    def test_help_unicode(self):
+        self.parser = Horde_Argv_InterceptingParser(usage=Horde_Argv_Option::SUPPRESS_USAGE)
+        self.parser.addOption("-a", action="store_true", help=u"ol\u00E9!")
+        expect = u"""\
+Options:
+  -h, --help  show this help message and exit
+  -a          ol\u00E9!
+"""
+        self.assertHelpEquals(expect)
+
+    def test_help_unicode_description(self):
+        self.parser = Horde_Argv_InterceptingParser(usage=Horde_Argv_Option::SUPPRESS_USAGE,
+                                                    description=u"ol\u00E9!")
+        expect = u"""\
+ol\u00E9!
+
+Options:
+  -h, --help  show this help message and exit
+"""
+        self.assertHelpEquals(expect)
+
+    */
+
+}
diff --git a/framework/Argv/test/Horde/Argv/InterceptedException.php b/framework/Argv/test/Horde/Argv/InterceptedException.php
new file mode 100644 (file)
index 0000000..4f44646
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_InterceptedException extends Exception
+{
+    public function __construct($error_message = null, $exit_status = null, $exit_message = null)
+    {
+        $this->error_message = $error_message;
+        $this->exit_status = $exit_status;
+        $this->exit_message = $exit_message;
+    }
+
+    public function __toString()
+    {
+        if ($this->error_message)
+            return $this->error_message;
+        if ($this->exit_message)
+            return $this->exit_message;
+        return "intercepted error";
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/InterceptingParser.php b/framework/Argv/test/Horde/Argv/InterceptingParser.php
new file mode 100644 (file)
index 0000000..2878d19
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_InterceptingParser extends Horde_Argv_Parser
+{
+    public function parserExit($status = 0, $msg = null)
+    {
+        throw new Horde_Argv_InterceptedException(null, $status, $msg);
+    }
+
+    public function parserError($msg)
+    {
+        throw new Horde_Argv_InterceptedException($msg);
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/MatchAbbrevTest.php b/framework/Argv/test/Horde/Argv/MatchAbbrevTest.php
new file mode 100644 (file)
index 0000000..e531236
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_MatchAbbrevTest extends Horde_Argv_TestBase
+{
+    public function testMatchAbbrev()
+    {
+        $this->assertEquals(Horde_Argv_Parser::matchAbbrev("--f",
+            array("--foz" => null,
+                  "--foo" => null,
+                  "--fie" => null,
+                  "--f"   => null)),
+            '--f');
+    }
+
+    public function testMatchAbbrevError()
+    {
+        $s = '--f';
+        $wordmap = array("--foz" => null, "--foo" => null, "--fie" => null);
+
+        try {
+            Horde_Argv_Parser::matchAbbrev($s, $wordmap);
+            $this->fail();
+        } catch (Horde_Argv_BadOptionException $e) {
+            $this->assertEquals("ambiguous option: --f (--fie, --foo, --foz?)",
+                                $e->getMessage());
+        }
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/MultipleArgsAppendTest.php b/framework/Argv/test/Horde/Argv/MultipleArgsAppendTest.php
new file mode 100644 (file)
index 0000000..2ac1a31
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_MultipleArgsAppendTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+        $this->parser->addOption("-p", "--point", array(
+            'action' => "store", 'nargs' => 3, 'type' => 'float', 'dest' => 'point'));
+        $this->parser->addOption("-f", "--foo", array(
+            'action' => "append", 'nargs' => 2, 'type' => "int", 'dest' => "foo"));
+        $this->parser->addOption("-z", "--zero", array(
+            'action' => "append_const", 'dest' => "foo", 'const' => array(0, 0)));
+    }
+
+    public function testNargsAppend()
+    {
+        $this->assertParseOK(array("-f", "4", "-3", "blah", "--foo", "1", "666"),
+                             array('point' => null, 'foo' => array(array(4, -3), array(1, 666))),
+                             array('blah'));
+    }
+
+    public function testNargsAppendRequiredValues()
+    {
+        $this->assertParseFail(array("-f4,3"),
+                               "-f option requires 2 arguments");
+    }
+
+    public function testNargsAppendSimple()
+    {
+        $this->assertParseOK(array("--foo=3", "4"),
+                             array('point' => null, 'foo' => array(array(3, 4))),
+                             array());
+    }
+
+    public function testNargsAppendConst()
+    {
+        $this->assertParseOK(array("--zero", "--foo", "3", "4", "-z"),
+                             array('point' => null, 'foo' => array(array(0, 0), array(3, 4), array(0, 0))),
+                             array());
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/MultipleArgsTest.php b/framework/Argv/test/Horde/Argv/MultipleArgsTest.php
new file mode 100644 (file)
index 0000000..1b7a083
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_MultipleArgsTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+        $this->parser->addOption("-p", "--point",
+                                 array('action' => "store", 'nargs' => 3, 'type' => "float", 'dest' => "point"));
+    }
+
+    public function testNargsWithPositionalArgs()
+    {
+        $this->assertParseOK(array("foo", "-p", "1", "2.5", "-4.3", "xyz"),
+                             array('point' => array(1.0, 2.5, -4.3)),
+                             array('foo', 'xyz'));
+    }
+
+    public function testNargsLongOpt()
+    {
+        $this->assertParseOK(array("--point", "-1", "2.5", "-0", "xyz"),
+                             array('point' => array(-1.0, 2.5, -0.0)),
+                             array("xyz"));
+    }
+
+    public function testNargsInvalidFloatValue()
+    {
+        $this->assertParseFail(array("-p", "1.0", "2x", "3.5"),
+                               "option -p: invalid floating-point value: '2x'");
+    }
+
+    public function testNargsRequiredValues()
+    {
+        $this->assertParseFail(array("--point", "1.0", "3.5"),
+                               "--point option requires 3 arguments");
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/OptionChecksTest.php b/framework/Argv/test/Horde/Argv/OptionChecksTest.php
new file mode 100644 (file)
index 0000000..f2e6b17
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_OptionChecksTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_Parser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+    }
+
+    public function assertOptionError($expected_message, $args)
+    {
+        $this->assertRaises(array($this, 'makeOption'), $args, 'Horde_Argv_OptionException', $expected_message);
+    }
+
+    public function testOptStringEmpty()
+    {
+        try {
+            new Horde_Argv_Option();
+        } catch (Exception $e) {
+            $this->assertType('InvalidArgumentException', $e);
+            $this->assertEquals("at least one option string must be supplied", $e->getMessage());
+            return true;
+        }
+
+        $this->fail("InvalidArgumentException for no option strings not thrown");
+    }
+
+    public function testOptStringTooShort()
+    {
+        $this->assertOptionError(
+            "invalid option string 'b': must be at least two characters long",
+            array("b"));
+    }
+
+    public function testOptStringShortInvalid()
+    {
+        $this->assertOptionError(
+            "invalid short option string '--': must be " .
+            "of the form -x, (x any non-dash char)",
+            array("--"));
+    }
+
+    public function testOptStringLongInvalid()
+    {
+        $this->assertOptionError(
+            "invalid long option string '---': " .
+            "must start with --, followed by non-dash",
+            array("---"));
+    }
+
+    public function testAttrInvalid()
+    {
+        $this->assertOptionError(
+            "option -b: invalid keyword arguments: bar, foo",
+            array("-b", array('foo' => null, 'bar' => null)));
+    }
+
+    public function testActionInvalid()
+    {
+        $this->assertOptionError(
+            "option -b: invalid action: 'foo'",
+            array("-b", array('action' => 'foo')));
+    }
+
+    public function testTypeInvalid()
+    {
+        $this->assertOptionError(
+            "option -b: invalid option type: 'foo'",
+            array("-b", array('type' => 'foo')));
+        $this->assertOptionError(
+            "option -b: invalid option type: 'Array'",
+            array("-b", array('type' => array())));
+    }
+
+    public function testNoTypeForAction()
+    {
+        $this->assertOptionError(
+            "option -b: must not supply a type for action 'count'",
+            array("-b", array('action' => 'count', 'type' => 'int')));
+    }
+
+    public function testNoChoicesList()
+    {
+        $this->assertOptionError(
+            "option -b/--bad: must supply a list of " .
+            "choices for type 'choice'",
+            array("-b", "--bad", array('type' => "choice")));
+    }
+
+    public function testBadChoicesList()
+    {
+        $typename = gettype('');
+        $this->assertOptionError(
+            sprintf("option -b/--bad: choices must be a list of " .
+                    "strings ('%s' supplied)", $typename),
+            array("-b", "--bad", array('type' => 'choice', 'choices' => 'bad choices')));
+    }
+
+    public function testNoChoicesForType()
+    {
+        $this->assertOptionError(
+            "option -b: must not supply choices for type 'int'",
+            array("-b", array('type' => 'int', 'choices' => "bad")));
+    }
+
+    public function testNoConstForAction()
+    {
+        $this->assertOptionError(
+            "option -b: 'const' must not be supplied for action 'store'",
+            array("-b", array('action' => 'store', 'const' => 1)));
+    }
+
+    public function testNoNargsForAction()
+    {
+        $this->assertOptionError(
+            "option -b: 'nargs' must not be supplied for action 'count'",
+            array("-b", array('action' => 'count', 'nargs' => 2)));
+    }
+
+    public function testCallbackNotCallable()
+    {
+        $this->assertOptionError(
+            "option -b: callback not callable: 'foo'",
+            array("-b", array('action' => 'callback', 'callback' => 'foo')));
+    }
+
+    public function dummy()
+    {
+    }
+
+    public function testCallbackArgsNoArray()
+    {
+        $this->assertOptionError(
+            "option -b: callbackArgs, if supplied, " .
+            "must be an array: not 'foo'",
+            array("-b", array('action' => 'callback',
+                              'callback' => array($this, 'dummy'),
+                              'callbackArgs' => 'foo')));
+    }
+
+    public function testNoCallbackForAction()
+    {
+        $this->assertOptionError(
+            "option -b: callback supplied ('foo') for non-callback option",
+            array("-b", array('action' => 'store',
+                              'callback' => 'foo')));
+    }
+
+    public function testNoCallbackArgsForAction()
+    {
+        $this->assertOptionError(
+            "option -b: callbackArgs supplied for non-callback option",
+            array("-b", array('action' => 'store',
+                              'callbackArgs' => 'foo')));
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/OptionGroupTest.php b/framework/Argv/test/Horde/Argv/OptionGroupTest.php
new file mode 100644 (file)
index 0000000..447321c
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_OptionGroupTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_Parser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+    }
+
+    public function testOptionGroupCreateInstance()
+    {
+        $group = new Horde_Argv_OptionGroup($this->parser, "Spam");
+        $this->parser->addOptionGroup($group);
+        $group->addOption("--spam", array('action' => "store_true",
+                                          'help' => "spam spam spam spam"));
+        $this->assertParseOK(array("--spam"), array('spam' => true), array());
+    }
+
+    public function testAddGroupNoGroup()
+    {
+        $this->assertTypeError(array($this->parser, 'addOptionGroup'),
+                               "not an OptionGroup instance: NULL", array(null));
+    }
+
+    public function testAddGroupInvalidArguments()
+    {
+        $this->assertTypeError(array($this->parser, 'addOptionGroup'),
+                               "invalid arguments", null);
+    }
+
+    public function testAddGroupWrongParser()
+    {
+        $group = new Horde_Argv_OptionGroup($this->parser, "Spam");
+        $group->parser = new Horde_Argv_Parser();
+        $this->assertRaises(array($this->parser, 'addOptionGroup'), array($group),
+                            'InvalidArgumentException', "invalid OptionGroup (wrong parser)");
+    }
+
+    public function testGroupManipulate()
+    {
+        $group = $this->parser->addOptionGroup("Group 2",
+                                               array('description' => "Some more options"));
+        $group->setTitle("Bacon");
+        $group->addOption("--bacon", array('type' => "int"));
+        $this->assertSame($group, $this->parser->getOptionGroup("--bacon"));
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/OptionValuesTest.php b/framework/Argv/test/Horde/Argv/OptionValuesTest.php
new file mode 100644 (file)
index 0000000..caa8446
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_OptionValuesTest extends Horde_Argv_TestBase
+{
+    public function testBasics()
+    {
+        $values = new Horde_Argv_Values();
+        $this->assertEquals(iterator_to_array($values), array());
+        $this->assertNotEquals($values, array('foo' => 'bar'));
+        $this->assertNotEquals($values, '');
+
+        $dict = array('foo' => 'bar', 'baz' => 42);
+        $values = new Horde_Argv_Values($dict);
+        $this->assertEquals($dict, iterator_to_array($values));
+        $this->assertNotEquals($values, array('foo' => 'bar'));
+        $this->assertNotEquals($values, array());
+        $this->assertNotEquals($values, "");
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/ParseNumTest.php b/framework/Argv/test/Horde/Argv/ParseNumTest.php
new file mode 100644 (file)
index 0000000..3a7c024
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ParseNumTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser();
+        $this->parser->addOption('-n', array('type' => 'int'));
+        $this->parser->addOption('-l', array('type' => 'long'));
+    }
+
+    public function testParseNumFail()
+    {
+        $this->assertFalse(Horde_Argv_Option::parseNumber(''));
+        $this->assertFalse(Horde_Argv_Option::parseNumber("0xOoops"));
+    }
+
+    public function testParseNumOk()
+    {
+        $this->assertSame(0,
+                          Horde_Argv_Option::parseNumber('0'));
+        $this->assertSame(16,
+                          Horde_Argv_Option::parseNumber('0x10'));
+        $this->assertSame(10,
+                          Horde_Argv_Option::parseNumber('0XA'));
+        $this->assertSame(8,
+                          Horde_Argv_Option::parseNumber('010'));
+        $this->assertSame(3,
+                          Horde_Argv_Option::parseNumber('0b11'));
+        $this->assertSame(0,
+                          Horde_Argv_Option::parseNumber('0b'));
+    }
+
+    public function testNumericOptions()
+    {
+        $this->assertParseOk(array("-n", "42", "-l", "0x20"),
+                             array("n" => 42, "l" => 0x20), array());
+
+        $this->assertParseOk(array("-n", "0b0101", "-l010"),
+                             array("n" => 5, "l" => 8), array());
+
+        $this->assertParseFail(array("-n008"),
+                               "option -n: invalid integer value: '008'");
+
+        $this->assertParseFail(array("-l0b0123"),
+                               "option -l: invalid long integer value: '0b0123'");
+
+        $this->assertParseFail(array("-l", "0x12x"),
+                               "option -l: invalid long integer value: '0x12x'");
+    }
+}
diff --git a/framework/Argv/test/Horde/Argv/ParserTest.php b/framework/Argv/test/Horde/Argv/ParserTest.php
new file mode 100644 (file)
index 0000000..a52e4ad
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ParserTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_Parser();
+        $this->parser->addOption('-v', '--verbose', '-n', '--noisy',
+                                  array('action' => 'store_true', 'dest' => 'verbose'));
+        $this->parser->addOption('-q', '--quiet', '--silent',
+                                  array('action' => 'store_false', 'dest' => 'verbose'));
+    }
+
+    public function testAddOptionNoOption()
+    {
+        $this->assertTypeError(array($this->parser, 'addOption'),
+                               "not an Option instance: NULL", array(null));
+    }
+
+    public function testAddOptionInvalidArguments()
+    {
+        $this->assertTypeError(array($this->parser, 'addOption'),
+                               "invalid arguments", null);
+    }
+
+    public function testGetOption()
+    {
+        $opt1 = $this->parser->getOption("-v");
+        $this->assertType('Horde_Argv_Option', $opt1);
+        $this->assertEquals($opt1->shortOpts, array("-v", "-n"));
+        $this->assertEquals($opt1->longOpts, array("--verbose", "--noisy"));
+        $this->assertEquals($opt1->action, "store_true");
+        $this->assertEquals($opt1->dest, "verbose");
+    }
+
+    public function testGetOptionEquals()
+    {
+        $opt1 = $this->parser->getOption("-v");
+        $opt2 = $this->parser->getOption("--verbose");
+        $opt3 = $this->parser->getOption("-n");
+        $opt4 = $this->parser->getOption("--noisy");
+        $this->assertEquals($opt1, $opt2);
+        $this->assertEquals($opt1, $opt3);
+        $this->assertEquals($opt1, $opt4);
+    }
+
+    public function testHasOption()
+    {
+        $this->assertTrue($this->parser->hasOption("-v"));
+        $this->assertTrue($this->parser->hasOption("--verbose"));
+    }
+
+    public function assertRemoved()
+    {
+        $this->assertNull($this->parser->getOption("-v"));
+        $this->assertNull($this->parser->getOption("--verbose"));
+        $this->assertNull($this->parser->getOption("-n"));
+        $this->assertNull($this->parser->getOption("--noisy"));
+
+        $this->assertFalse($this->parser->hasOption("-v"));
+        $this->assertFalse($this->parser->hasOption("--verbose"));
+        $this->assertFalse($this->parser->hasOption("-n"));
+        $this->assertFalse($this->parser->hasOption("--noisy"));
+
+        $this->assertTrue($this->parser->hasOption("-q"));
+        $this->assertTrue($this->parser->hasOption("--silent"));
+    }
+
+    public function testRemoveShortOpt()
+    {
+        $this->parser->removeOption('-n');
+        $this->assertRemoved();
+    }
+
+    public function testRemoveLongOpt()
+    {
+        $this->parser->removeOption('--verbose');
+        $this->assertRemoved();
+    }
+
+    public function testRemoveNonexistent()
+    {
+        $this->assertRaises(array($this->parser, 'removeOption'), array('foo'), 'InvalidArgumentException', "no such option 'foo'");
+    }
+
+    /**
+    def test_refleak(self):
+        # If a Horde_Argv_Parser is carrying around a reference to a large
+        # object, various cycles can prevent it from being GC'd in
+        # a timely fashion.  destroy() breaks the cycles to ensure stuff
+        # can be cleaned up.
+        big_thing = [42]
+        refcount = sys.getrefcount(big_thing)
+        parser = Horde_Argv_Parser()
+        parser.addOption("-a", "--aaarggh")
+        parser.big_thing = big_thing
+
+        parser.destroy()
+        del parser
+        $this->assertEquals(refcount, sys.getrefcount(big_thing))
+    */
+
+}
diff --git a/framework/Argv/test/Horde/Argv/ProgNameTest.php b/framework/Argv/test/Horde/Argv/ProgNameTest.php
new file mode 100644 (file)
index 0000000..7e7be37
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_ProgNameTest extends Horde_Argv_TestBase
+{
+    public function assertUsage($parser, $expectedUsage)
+    {
+        $this->assertEquals($parser->getUsage(), $expectedUsage);
+    }
+
+    public function assertVersion($parser, $expectedVersion)
+    {
+        $this->assertEquals($parser->getVersion(), $expectedVersion);
+    }
+
+    public function testDefaultProgName()
+    {
+        // Make sure that program name is taken from $_SERVER['argv'][0] by default.
+        $saveArgv = $_SERVER['argv'];
+        try {
+            $_SERVER['argv'][0] = 'foo/bar/baz.php';
+            $parser = new Horde_Argv_Parser(array('usage' => "%prog ...", 'version' => "%prog 1.2"));
+            $expectedUsage = "Usage: baz.php ...\n";
+        } catch (Exception $e) {
+            $_SERVER['argv'] = $saveArgv;
+            throw($e);
+        }
+
+        $this->assertUsage($parser, $expectedUsage);
+        $this->assertVersion($parser, "baz.php 1.2");
+        $this->assertHelp($parser,
+                          $expectedUsage . "\n" .
+                          "Options:\n" .
+                          "  --version   show program's version number and exit\n" .
+                          "  -h, --help  show this help message and exit\n");
+    }
+
+    public function testCustomProgName()
+    {
+        $parser = new Horde_Argv_Parser(array('prog' => 'thingy',
+                                              'version' => "%prog 0.1",
+                                              'usage' => "%prog arg arg"));
+        $parser->removeOption('-h');
+        $parser->removeOption('--version');
+        $expectedUsage = "Usage: thingy arg arg\n";
+        $this->assertUsage($parser, $expectedUsage);
+        $this->assertVersion($parser, "thingy 0.1");
+        $this->assertHelp($parser, $expectedUsage . "\n");
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/StandardTest.php b/framework/Argv/test/Horde/Argv/StandardTest.php
new file mode 100644 (file)
index 0000000..b9de2be
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_StandardTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $options = array(
+            $this->makeOption('-a', array('type' => 'string')),
+            $this->makeOption('-b', '--boo', array('type' => 'int', 'dest' => 'boo')),
+            $this->makeOption('--foo', array('action' => 'append'))
+        );
+
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE,
+                                                                'optionList' => $options));
+    }
+
+    public function testRequiredValue()
+    {
+        $this->assertParseFail(array('-a'),
+                               '-a option requires an argument');
+    }
+
+    public function testInvalidInteger()
+    {
+        $this->assertParseFail(array('-b', '5x'),
+                               "option -b: invalid integer value: '5x'");
+    }
+
+    public function testNoSuchOption()
+    {
+        $this->assertParseFail(array('--boo13'),
+                               "no such option: --boo13");
+    }
+
+    public function testLongInvalidInteger()
+    {
+        $this->assertParseFail(array("--boo=x5"),
+                               "option --boo: invalid integer value: 'x5'");
+    }
+
+    public function testEmpty()
+    {
+        $this->assertParseOk(array(),
+                             array('a' => null, 'boo' => null, 'foo' => null), array());
+    }
+
+    public function testShortOptEmptyLongOptAppend()
+    {
+        $this->assertParseOk(array("-a", "", "--foo=blah", "--foo="),
+                             array('a' => "", 'boo' => null, 'foo' => array("blah", "")),
+                             array());
+    }
+
+    public function testLongOptionAppend()
+    {
+        $this->assertParseOk(array("--foo", "bar", "--foo", "", "--foo=x"),
+                             array('a' => null,
+                                   'boo' => null,
+                                   'foo' => array('bar', '', 'x')),
+                             array());
+    }
+
+    public function testOptionArgumentJoined()
+    {
+        $this->assertParseOk(array("-abc"),
+                             array('a' => "bc", 'boo' => null, 'foo' => null),
+                             array());
+    }
+
+    public function testOptionArgumentSplit()
+    {
+        $this->assertParseOk(array("-a", "34"),
+                             array('a' => "34", 'boo' => null, 'foo' => null),
+                             array());
+    }
+
+    public function testOptionArgumentJoinedInteger()
+    {
+        $this->assertParseOk(array("-b34"),
+                             array('a' => null, 'boo' => 34, 'foo' => null),
+                             array());
+    }
+
+    public function testOptionArgumentSplitNegativeInteger()
+    {
+        $this->assertParseOk(array("-b", "-5"),
+                             array('a' => null, 'boo' => -5, 'foo' => null),
+                             array());
+    }
+
+    public function testLongOptionArgumentJoined()
+    {
+        $this->assertParseOk(array("--boo=13"),
+                             array('a' => null, 'boo' => 13, 'foo' => null),
+                             array());
+    }
+
+    public function testLongOptionArgumentSplit()
+    {
+        $this->assertParseOk(array("--boo", "111"),
+                             array('a' => null, 'boo' => 111, 'foo' => null),
+                             array());
+    }
+
+    public function testLongOptionShortOption()
+    {
+        $this->assertParseOk(array("--foo=bar", "-axyz"),
+                             array('a' => 'xyz', 'boo' => null, 'foo' => array("bar")),
+                             array());
+    }
+
+    public function testAbbrevLongOption()
+    {
+        $this->assertParseOk(array("--f=bar", "-axyz"),
+                             array('a' => 'xyz', 'boo' => null, 'foo' => array("bar")),
+                             array());
+    }
+
+    public function testDefaults()
+    {
+        list($options, $args) = $this->parser->parseArgs(array());
+        $defaults = $this->parser->getDefaultValues();
+
+        $this->assertEquals($defaults, $options);
+    }
+
+    public function testAmbiguousOption()
+    {
+        $this->parser->addOption("--foz", array('action' => 'store',
+                                                'type' => 'string', 'dest' => 'foo'));
+        $this->assertParseFail(array('--f=bar'),
+                               "ambiguous option: --f (--foo, --foz?)");
+    }
+
+    public function testShortAndLongOptionSplit()
+    {
+        $this->assertParseOk(array("-a", "xyz", "--foo", "bar"),
+                             array('a' => 'xyz', 'boo' => null, 'foo' => array("bar")),
+                             array());
+    }
+
+    public function testShortOptionSplitLongOptionAppend()
+    {
+        $this->assertParseOk(array("--foo=bar", "-b", "123", "--foo", "baz"),
+                             array('a' => null, 'boo' => 123, 'foo' => array("bar", "baz")),
+                             array());
+    }
+
+    public function testShortOptionSplitOnePositionalArg()
+    {
+        $this->assertParseOk(array("-a", "foo", "bar"),
+                             array('a' => "foo", 'boo' => null, 'foo' => null),
+                             array("bar"));
+    }
+
+    public function testShortOptionConsumesSeparator()
+    {
+        $this->assertParseOk(array("-a", "--", "foo", "bar"),
+                             array('a' => "--", 'boo' => null, 'foo' => null),
+                             array("foo", "bar"));
+
+        $this->assertParseOk(array("-a", "--", "--foo", "bar"),
+                             array('a' => "--", 'boo' => null, 'foo' => array("bar")),
+                             array());
+    }
+
+    public function testShortOptionJoinedAndSeparator()
+    {
+        $this->assertParseOk(array("-ab", "--", "--foo", "bar"),
+                             array('a' => "b", 'boo' => null, 'foo' => null),
+                             array("--foo", "bar"));
+    }
+
+    public function testHyphenBecomesPositionalArg()
+    {
+        $this->assertParseOk(array("-ab", "-", "--foo", "bar"),
+                             array('a' => "b", 'boo' => null, 'foo' => array("bar")),
+                             array("-"));
+    }
+
+    public function testNoAppendVersusAppend()
+    {
+        $this->assertParseOk(array("-b3", "-b", "5", "--foo=bar", "--foo", "baz"),
+                             array('a' => null, 'boo' => 5, 'foo' => array("bar", "baz")),
+                             array());
+    }
+
+    public function testOptionConsumesOptionLikeString()
+    {
+        $this->assertParseOk(array("-a", "-b3"),
+                             array('a' => "-b3", 'boo' => null, 'foo' => null),
+                             array());
+    }
+}
+
diff --git a/framework/Argv/test/Horde/Argv/TestBase.php b/framework/Argv/test/Horde/Argv/TestBase.php
new file mode 100644 (file)
index 0000000..cd021f8
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_TestBase extends PHPUnit_Framework_TestCase
+{
+    public function makeOption()
+    {
+        $args = func_get_args();
+        $reflector = new ReflectionClass('Horde_Argv_Option');
+        return $reflector->newInstanceArgs($args);
+    }
+
+    /**
+     * Assert the options are what we expected when parsing arguments.
+     *
+     * Otherwise, fail with a nicely formatted message.
+     *
+     * Keyword arguments:
+     * args -- A list of arguments to parse with Horde_Argv_Parser.
+     * expected_opts -- The options expected.
+     * expected_positional_args -- The positional arguments expected.
+     *
+     * Returns the options and positional args for further testing.
+     */
+    public function assertParseOK($args, $expected_opts, $expected_positional_args)
+    {
+        list($options, $positional_args) = $this->parser->parseArgs($args);
+        $optdict = iterator_to_array($options);
+
+        $this->assertEquals($expected_opts, $optdict,
+                            'Expected options don\'t match. Args were ' . print_r($args, true));
+
+        $this->assertEquals($positional_args, $expected_positional_args,
+                            'Positional arguments don\'t match. Args were ' . print_r($args, true));
+
+        return array($options, $positional_args);
+    }
+
+    /**
+     *  Assert that the expected exception is raised when calling a
+     *  function, and that the right error message is included with
+     *  that exception.
+     *
+     *  Arguments:
+     *    func -- the function to call
+     *    args -- positional arguments to `func`
+     *    expected_exception -- exception that should be raised
+     *    expected_message -- expected exception message (or pattern
+     *      if a compiled regex object)
+     *
+     *  Returns the exception raised for further testing.
+     */
+    public function assertRaises($func, $args = array(),
+                                 $expected_exception, $expected_message) {
+        $caught = false;
+        try {
+            if (is_array($args)) {
+                call_user_func_array($func, $args);
+            } else {
+                call_user_func($func);
+            }
+        } catch (Exception $e) {
+            if (get_class($e) == $expected_exception) {
+                $caught = true;
+                $this->assertEquals($expected_message, $e->getMessage(), 'Expected exception message not matched');
+            }
+        }
+
+        if (!$caught) {
+            $this->fail("Expected exception $expected_exception not thrown");
+        }
+    }
+
+    // -- Assertions used in more than one class --------------------
+
+    /**
+     *   Assert the parser fails with the expected message.  Caller
+     *   must ensure that $this->parser is an InterceptingParser.
+     */
+    public function assertParseFail($cmdline_args, $expected_output)
+    {
+        try {
+            $this->parser->parseArgs($cmdline_args);
+        } catch (Horde_Argv_InterceptedException $e) {
+            $this->assertEquals($expected_output, (string)$e);
+            return true;
+        } catch (Exception $e) {
+            $this->fail("unexpected Exception: " . $e->getMessage());
+        }
+
+        $this->fail("expected parse failure");
+    }
+
+    /**
+     * Assert the parser prints the expected output on stdout.
+     */
+    public function assertOutput(
+        $cmdline_args,
+        $expected_output,
+        $expected_status = 0,
+        $expected_error = null)
+    {
+        ob_start();
+        try {
+            $this->parser->parseArgs($cmdline_args);
+        } catch (Horde_Argv_InterceptedException $e) {
+            $output = ob_get_clean();
+
+            $this->assertEquals($expected_output, $output, 'Expected parser output to match');
+            $this->assertEquals($expected_status, $e->exit_status);
+            $this->assertEquals($expected_error, $e->exit_message);
+            return;
+        }
+
+        ob_end_clean();
+
+        $this->fail("expected parser->parserExit()");
+    }
+
+    /**
+     * Assert that TypeError is raised when executing func.
+     */
+    public function assertTypeError($func, $expected_message, $args)
+    {
+        $this->assertRaises($func, $args, 'InvalidArgumentException', $expected_message);
+    }
+
+    public function assertHelp($parser, $expected_help)
+    {
+        $actual_help = $parser->formatHelp();
+        $this->assertEquals($expected_help, $actual_help);
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/TypeAliasesTest.php b/framework/Argv/test/Horde/Argv/TypeAliasesTest.php
new file mode 100644 (file)
index 0000000..e51c16f
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_TypeAliasesTest extends Horde_Argv_TestBase
+{
+    public function setUp()
+    {
+        $this->parser = new Horde_Argv_Parser();
+    }
+
+    public function testStrAliasesString()
+    {
+        $this->parser->addOption("-s", array('type' => "str"));
+        $this->assertEquals($this->parser->getOption("-s")->type, "string");
+    }
+
+    public function testNewTypeObject()
+    {
+        $this->parser->addOption("-s", array('type' => 'str'));
+        $this->assertEquals($this->parser->getOption("-s")->type, "string");
+        $this->parser->addOption("-x", array('type' => 'int'));
+        $this->assertEquals($this->parser->getOption("-x")->type, "int");
+    }
+
+}
diff --git a/framework/Argv/test/Horde/Argv/VersionTest.php b/framework/Argv/test/Horde/Argv/VersionTest.php
new file mode 100644 (file)
index 0000000..2621c65
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+/**
+ * @author     Chuck Hagenbuch <chuck@horde.org>
+ * @author     Mike Naberezny <mike@maintainable.com>
+ * @license    http://opensource.org/licenses/bsd-license.php BSD
+ * @category   Horde
+ * @package    Horde_Argv
+ * @subpackage UnitTests
+ */
+
+class Horde_Argv_VersionTest extends Horde_Argv_TestBase
+{
+    public function testVersion()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array(
+            'usage'   => Horde_Argv_Option::SUPPRESS_USAGE,
+            'version' => "%prog 0.1"));
+        $saveArgv = $_SERVER['argv'];
+        try {
+            $_SERVER['argv'][0] = dirname(__FILE__) . '/foo/bar';
+            $this->assertOutput(array("--version"), "bar 0.1\n");
+        } catch (Exception $e) {
+            $_SERVER['argv'] = $saveArgv;
+            throw $e;
+        }
+
+        $_SERVER['argv'] = $saveArgv;
+    }
+
+    public function testNoVersion()
+    {
+        $this->parser = new Horde_Argv_InterceptingParser(array('usage' => Horde_Argv_Option::SUPPRESS_USAGE));
+        $this->assertParseFail(array("--version"), "no such option: --version");
+    }
+}
diff --git a/framework/Argv/test/Horde/CVS/Entries b/framework/Argv/test/Horde/CVS/Entries
new file mode 100644 (file)
index 0000000..7f04381
--- /dev/null
@@ -0,0 +1 @@
+D/Argv////
diff --git a/framework/Argv/test/Horde/CVS/Repository b/framework/Argv/test/Horde/CVS/Repository
new file mode 100644 (file)
index 0000000..37c77fc
--- /dev/null
@@ -0,0 +1 @@
+framework/Argv/test/Horde
diff --git a/framework/Argv/test/Horde/CVS/Root b/framework/Argv/test/Horde/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Argv/test/Horde/CVS/Template b/framework/Argv/test/Horde/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)