Add support for positional arguments in GetOpt
authorMikko Rasa <tdb@tdb.fi>
Fri, 3 May 2013 09:42:27 +0000 (12:42 +0300)
committerMikko Rasa <tdb@tdb.fi>
Fri, 3 May 2013 09:42:27 +0000 (12:42 +0300)
This addresses the issue of any positional arguments recognized by an
application not showing up in the generated help text.  It also reduces
the amount of processing and validation required from the application
itself and provides consistent error messages.

source/core/getopt.cpp
source/core/getopt.h

index fdeb2f8827b2a48075068d153d1cea2597f6ccb1..2561c9a3e51f0acfc5bca50c099a2a8d1a3cce53 100644 (file)
@@ -15,6 +15,8 @@ GetOpt::~GetOpt()
 {
        for(OptionList::iterator i=opts.begin(); i!=opts.end(); ++i)
                delete *i;
+       for(ArgumentList::iterator i=args.begin(); i!=args.end(); ++i)
+               delete *i;
 }
 
 GetOpt::OptionImpl &GetOpt::add_option(char s, const string &l, const Store &t, ArgType a)
@@ -39,6 +41,30 @@ GetOpt::OptionImpl &GetOpt::add_option(char s, const string &l, const Store &t,
        return *opts.back();
 }
 
+GetOpt::ArgumentImpl &GetOpt::add_argument(const string &n, const Store &t, ArgType a)
+{
+       if(a==NO_ARG)
+               throw invalid_argument("GetOpt::add_argument");
+
+       bool have_list = false;
+       bool have_optional = false;
+       for(ArgumentList::const_iterator i=args.begin(); i!=args.end(); ++i)
+       {
+               if((*i)->is_list_store())
+                       have_list = true;
+               else if((*i)->get_type()==OPTIONAL_ARG)
+                       have_optional = true;
+       }
+
+       if(have_optional && (t.is_list() || a!=OPTIONAL_ARG))
+               throw invalid_argument("GetOpt::add_argument");
+       if(have_list && (t.is_list() || a==OPTIONAL_ARG))
+               throw invalid_argument("GetOpt::add_argument");
+
+       args.push_back(new ArgumentImpl(n, t, a));
+       return *args.back();
+}
+
 GetOpt::OptionImpl &GetOpt::get_option(char s)
 {
        for(OptionList::iterator i=opts.begin(); i!=opts.end(); ++i)
@@ -59,6 +85,8 @@ void GetOpt::operator()(unsigned argc, const char *const *argv)
 {
        try
        {
+               /* Arguments must first be collected into an array to handle the case
+               where a variable-length argument list is followed by fixed arguments. */
                unsigned i = 1;
                for(; i<argc;)
                {
@@ -75,11 +103,37 @@ void GetOpt::operator()(unsigned argc, const char *const *argv)
                                        i += process_short(argv+i);
                        }
                        else
-                               args.push_back(argv[i++]);
+                               args_raw.push_back(argv[i++]);
                }
                
                for(; i<argc; ++i)
-                       args.push_back(argv[i]);
+                       args_raw.push_back(argv[i]);
+
+               i = 0;
+               for(ArgumentList::const_iterator j=args.begin(); j!=args.end(); ++j)
+               {
+                       if((*j)->is_list_store())
+                       {
+                               unsigned end = args_raw.size();
+                               for(ArgumentList::const_iterator k=j; ++k!=args.end(); )
+                                       --end;
+                               if(i==end && (*j)->get_type()==REQUIRED_ARG)
+                                       throw usage_error((*j)->get_name()+" is required");
+                               for(; i<end; ++i)
+                                       (*j)->process(args_raw[i]);
+                       }
+                       else
+                       {
+                               if(i<args_raw.size())
+                                       (*j)->process(args_raw[i++]);
+                               else if((*j)->get_type()==REQUIRED_ARG)
+                                       throw usage_error((*j)->get_name()+" is required");
+                       }
+               }
+
+               // XXX Enable this when get_args() is completely removed
+               /*if(i<args_raw.size())
+                       throw usage_error("Extra positional arguments");*/
        }
        catch(const usage_error &e)
        {
@@ -179,6 +233,18 @@ string GetOpt::generate_usage(const string &argv0) const
                result += ']';
        }
 
+       for(ArgumentList::const_iterator i=args.begin(); i!=args.end(); ++i)
+       {
+               result += ' ';
+               if((*i)->get_type()==OPTIONAL_ARG)
+                       result += '[';
+               result += format("<%s>", (*i)->get_name());
+               if((*i)->is_list_store())
+                       result += " ...";
+               if((*i)->get_type()==OPTIONAL_ARG)
+                       result += ']';
+       }
+
        return result;
 }
 
@@ -218,10 +284,26 @@ string GetOpt::generate_help() const
                maxw = max(maxw, swtch.size());
        }
 
+       list<string> pargs;
+       for(ArgumentList::const_iterator i=args.begin(); i!=args.end(); ++i)
+       {
+               string parg = format("<%s>", (*i)->get_name());
+               pargs.push_back(parg);
+               maxw = max(maxw, parg.size());
+       }
+
        string result;
+       result += "Options:\n";
        list<string>::const_iterator j = switches.begin();
        for(OptionList::const_iterator i=opts.begin(); i!=opts.end(); ++i, ++j)
                result += format("  %s%s%s\n", *j, string(maxw+2-j->size(), ' '), (*i)->get_help());
+       if(!pargs.empty())
+       {
+               result += "\nArguments:\n";
+               j = pargs.begin();
+               for(ArgumentList::const_iterator i=args.begin(); i!=args.end(); ++i, ++j)
+                       result += format("  %s%s%s\n", *j, string(maxw+2-j->size(), ' '), (*i)->get_help());
+       }
        
        return result;
 }
@@ -299,4 +381,34 @@ void GetOpt::OptionImpl::process(const string &arg)
        }
 }
 
+
+GetOpt::ArgumentImpl::ArgumentImpl(const string &n, const Store &t, ArgType a):
+       name(n),
+       type(a),
+       store(t.clone())
+{ }
+
+GetOpt::ArgumentImpl::~ArgumentImpl()
+{
+       delete store;
+}
+
+GetOpt::ArgumentImpl &GetOpt::ArgumentImpl::set_help(const string &h)
+{
+       help = h;
+       return *this;
+}
+
+void GetOpt::ArgumentImpl::process(const string &arg)
+{
+       try
+       {
+               store->store(arg);
+       }
+       catch(const exception &e)
+       {
+               throw usage_error("Invalid "+name+" ("+e.what()+")");
+       }
+}
+
 } // namespace Msp
index 70841cbf6b8dc50da4800bfae42ebc78e6d7e122..f4e318e55d3eec1e8b14a160101e75c7be9e9cbb 100644 (file)
@@ -44,9 +44,19 @@ A single option may have both alternative forms, but must always have at least
 a long form.  This is to encourage self-documenting options; it's much easier
 to remember words than letters.
 
-A built-in --help option is provided and will output a list of options and
-their associated help texts.  An application may override this by providing
-its own option with the same name.
+Positional arguments are also supported.  They are identified by an arbitrary
+string, but the identifier is only used in help text and error messages.  Any
+number of the final arguments may be optional.
+
+To support applications that take an arbitrary amount of arguments, a single
+positional argument list can be specified.  Fixed positional arguments are
+allowed together with a list, but they can't be optional.  An application that
+wants to do complex processing on the argument list can declare a list of
+string arguments.
+
+A built-in --help option is provided and will output a list of options,
+arguments and their associated help texts.  An application may override this by
+providing its own option with the same name.
 */
 class GetOpt
 {
@@ -78,6 +88,16 @@ public:
                virtual unsigned get_seen_count() const = 0;
        };
 
+       class Argument
+       {
+       protected:
+               Argument() { }
+       public:
+               virtual ~Argument() { }
+
+               virtual Argument &set_help(const std::string &) = 0;
+       };
+
 private:
        class Store
        {
@@ -122,6 +142,26 @@ private:
                void process(const std::string &);
        };
 
+       class ArgumentImpl: public Argument
+       {
+       private:
+               std::string name;
+               ArgType type;
+               std::string help;
+               Store *store;
+
+       public:
+               ArgumentImpl(const std::string &, const Store &, ArgType);
+               virtual ~ArgumentImpl();
+
+               virtual ArgumentImpl &set_help(const std::string &);
+               const std::string &get_name() const { return name; }
+               ArgType get_type() const { return type; }
+               const std::string &get_help() const { return help; }
+               bool is_list_store() const { return store->is_list(); }
+               void process(const std::string &);
+       };
+
        template<typename T>
        class SimpleStore: public Store
        {
@@ -163,17 +203,20 @@ private:
        };
 
        typedef std::list<OptionImpl *> OptionList;
+       typedef std::list<ArgumentImpl *> ArgumentList;
 
        bool help;
        OptionList opts;
-       std::vector<std::string> args;
+       ArgumentList args;
+       std::vector<std::string> args_raw;
 
 public:
        GetOpt();
        ~GetOpt();
 
-       /// Returns any non-option arguments encountered during processing.
-       const std::vector<std::string> &get_args() const { return args; }
+       /** Returns any non-option arguments encountered during processing.
+       Deprecated. */
+       const std::vector<std::string> &get_args() const { return args_raw; }
 
        /** Adds an option with both short and long forms.  Processing depends on
        the type of the destination variable and whether an argument is taken or
@@ -196,8 +239,22 @@ public:
        Option &add_option(const std::string &l, T &d, ArgType a)
        { return add_option(0, l, d, a); }
 
+       /** Adds a positional argument.  The value will be lexical_cast to the
+       appropriate type and stored in the destination. */
+       template<typename T>
+       Argument &add_argument(const std::string &n, T &d, ArgType a = REQUIRED_ARG)
+       { return add_argument(n, SimpleStore<T>(d), a); }
+
+       /** Adds a positional argument list.  If the list is declared as required,
+       at least one element must be given; an optional list may be empty.  Only one
+       list may be added, and optional fixed arguments can't be used with it. */
+       template<typename T>
+       Argument &add_argument(const std::string &n, std::list<T> &d, ArgType a = REQUIRED_ARG)
+       { return add_argument(n, ListStore<std::list<T> >(d), a); }
+
 private:
        OptionImpl &add_option(char, const std::string &, const Store &, ArgType);
+       ArgumentImpl &add_argument(const std::string &, const Store &, ArgType);
 
        OptionImpl &get_option(char);
        OptionImpl &get_option(const std::string &);
@@ -215,11 +272,11 @@ private:
        unsigned process_short(const char *const *);
 
 public:
-       /** Generates a single line that describes known options. */
+       /** Generates a single line that describes known options and arguments. */
        std::string generate_usage(const std::string &) const;
 
-       /** Generates help for known options in tabular format, one option per
-       line.  The returned string will have a linefeed at the end. */
+       /** Generates help for known options and arguments in tabular format, one
+       item per line.  The returned string will have a linefeed at the end. */
        std::string generate_help() const;
 };