Add classes to ease writing command line parsers with GNU-style options handling

See simgear/misc/argparse.hxx for API and documentation
(simgear/misc/argparse_test.cxx also has examples, although argparse.hxx
features a very simple one at the top).

These classes were also presented in
<https://sourceforge.net/p/flightgear/mailman/message/35785019/>.
This commit is contained in:
Florent Rougon
2017-04-08 12:03:13 +02:00
parent 60d1c87cef
commit 36275f5cce
4 changed files with 1157 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ set(HEADERS
ResourceManager.hxx
SimpleMarkdown.hxx
SVGpreserveAspectRatio.hxx
argparse.hxx
interpolator.hxx
make_new.hxx
sg_dir.hxx
@@ -25,6 +26,7 @@ set(SOURCES
ResourceManager.cxx
SimpleMarkdown.cxx
SVGpreserveAspectRatio.cxx
argparse.cxx
interpolator.cxx
sg_dir.cxx
sg_path.cxx
@@ -46,6 +48,10 @@ simgear_component(misc misc "${SOURCES}" "${HEADERS}")
if(ENABLE_TESTS)
add_executable(test_argparse argparse_test.cxx)
target_link_libraries(test_argparse ${TEST_LIBS})
add_test(argparse ${EXECUTABLE_OUTPUT_PATH}/test_argparse)
add_executable(test_CSSBorder CSSBorder_test.cxx)
add_test(CSSBorder ${EXECUTABLE_OUTPUT_PATH}/test_CSSBorder)
target_link_libraries(test_CSSBorder ${TEST_LIBS})

384
simgear/misc/argparse.cxx Normal file
View File

@@ -0,0 +1,384 @@
// -*- coding: utf-8 -*-
//
// argparse.cxx --- Simple, generic parser for command-line arguments
// Copyright (C) 2017 Florent Rougon
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include <simgear_config.h>
#include <string>
#include <vector>
#include <memory>
#include <utility> // std::pair, std::move()
#include <cstddef> // std::size_t
#include <cassert>
#include <simgear/misc/strutils.hxx>
#include <simgear/structure/exception.hxx>
#include "argparse.hxx"
using std::string;
using std::shared_ptr;
namespace simgear
{
namespace argparse
{
// ***************************************************************************
// * Base class for custom exceptions *
// ***************************************************************************
Error::Error(const string& message, const std::string& origin)
: sg_exception("Argument parser error: " + message, origin)
{ }
Error::Error(const char* message, const char* origin)
: Error(string(message), string(origin))
{ }
// ***************************************************************************
// * OptionDesc class *
// ***************************************************************************
OptionDesc::OptionDesc(
const string& optionId, std::vector<char> shortAliases,
std::vector<string> longAliases, OptionArgType argumentType)
: _id(optionId),
_shortAliases(shortAliases),
_longAliases(longAliases),
_argumentType(argumentType)
{ }
const std::string& OptionDesc::id() const
{ return _id; }
const std::vector<char>& OptionDesc::shortAliases() const
{ return _shortAliases; }
const std::vector<std::string>& OptionDesc::longAliases() const
{ return _longAliases; }
OptionArgType OptionDesc::argumentType() const
{ return _argumentType; }
// ***************************************************************************
// * OptionValue class *
// ***************************************************************************
OptionValue::OptionValue(shared_ptr<const OptionDesc> optionDesc,
const string& passedAs, const string& value,
bool hasValue)
: _optionDesc(std::move(optionDesc)),
_passedAs(passedAs),
_value(value),
_hasValue(hasValue)
{ }
shared_ptr<const OptionDesc> OptionValue::optionDesc() const
{ return _optionDesc; } // return a copy of the shared_ptr
void OptionValue::setOptionDesc(shared_ptr<const OptionDesc> descPtr)
{ _optionDesc = std::move(descPtr); }
string OptionValue::passedAs() const
{ return _passedAs; }
void OptionValue::setPassedAs(const string& passedAs)
{ _passedAs = passedAs; }
string OptionValue::value() const
{ return _value; }
void OptionValue::setValue(const string& value)
{ _value = value; }
bool OptionValue::hasValue() const
{ return _hasValue; }
void OptionValue::setHasValue(bool hasValue)
{ _hasValue = hasValue; }
const string OptionValue::id() const
{
const auto desc = optionDesc();
return (desc) ? desc->id() : string();
}
// ***************************************************************************
// * ArgumentParser class *
// ***************************************************************************
// Static utility method.
std::vector<char>
ArgumentParser::removeHyphens(const std::vector<string>& shortAliases,
std::vector<string>& longAliases)
{
std::vector<char> shortAliasesCharVec;
shortAliasesCharVec.reserve(shortAliases.size());
for (const string& opt: shortAliases) {
if (opt.size() != 2 || opt[0] != '-' || opt[1] == '-' || opt[1] > 127) {
throw Error("unexpected form for a short option: '" + opt + "' (expecting "
"a string of size 2 whose first character is a hyphen and "
"second character an ASCII char that is not a hyphen)");
}
shortAliasesCharVec.emplace_back(opt[1]); // emplace the char after hyphen
}
for (string& longOpt: longAliases) {
if (longOpt.size() < 3 ||
!simgear::strutils::starts_with(longOpt, string("--"))) {
throw Error("unexpected form for a long option: '" + longOpt + "' "
"(expecting a string of size 3 or more that starts with "
"two hyphens)");
}
longOpt.erase(0, 2); // remove the two leading hyphens
}
return shortAliasesCharVec;
}
void
ArgumentParser::addOption(const string& optionId,
OptionArgType argType,
std::vector<string> shortAliases,
std::vector<string> longAliases)
{
// Remove the leading dashes and do a sanity check for these arguments
std::vector<char> shortAliasesCharVec = removeHyphens(shortAliases,
longAliases);
const auto desc_p = std::make_shared<const OptionDesc>(
optionId, std::move(shortAliasesCharVec), std::move(longAliases), argType);
for (const char c: desc_p->shortAliases()) {
if (!_shortOptionMap.emplace(c, desc_p).second) {
throw Error(
"trying to add option '-" + string(1, c) + "', however it is already "
"in the short option map");
}
}
for (const string& longOpt: desc_p->longAliases()) {
if (!_longOptionMap.emplace(longOpt, desc_p).second) {
throw Error(
"trying to add option '--" + longOpt + "', however it is already in "
"the long option map");
}
}
}
void
ArgumentParser::addOption(const string& optionId, OptionArgType argumentType,
string shortOpt, string longOpt)
{
std::vector<string> shortOptList;
std::vector<string> longOptList;
if (!shortOpt.empty()) {
shortOptList.push_back(std::move(shortOpt));
}
if (!longOpt.empty()) {
longOptList.push_back(std::move(longOpt));
}
addOption(optionId, argumentType, std::move(shortOptList),
std::move(longOptList));
}
std::pair< std::vector<OptionValue>, std::vector<string> >
ArgumentParser::parseArgs(int argc, const char *const *argv) const
{
std::pair< std::vector<OptionValue>, std::vector<string> > res;
std::vector<OptionValue>& optsWithValues = res.first;
std::vector<string>& nonOptionArgs = res.second;
bool inOptions = true;
for (int i = 1; i < argc; i++) {
// Decode from command line encoding
const string currentArg = cmdEncToUtf8(argv[i]);
if ((inOptions) && (currentArg == "--")) {
// We found the end-of-options delimiter
inOptions = false;
continue;
}
if (inOptions) {
if (currentArg[0] == '-') {
if (currentArg[1] == '-') {
i += readLongOption(argc, argv, currentArg, i+1, optsWithValues);
} else {
i += readShortOptions(argc, argv, currentArg, i+1, optsWithValues);
}
} else { // the argument doesn't start with a '-'
inOptions = false;
nonOptionArgs.push_back(currentArg);
}
} else {
nonOptionArgs.push_back(currentArg);
}
}
return res;
}
// Static method
string ArgumentParser::cmdEncToUtf8(const string& s)
{
#if defined(SG_WINDOWS)
// Untested code path. Comments and/or testing by Windows people welcome.
return simgear::strutils::convertWindowsLocal8BitToUtf8(s);
#else
// XXX This assumes UTF-8 encoding for command line arguments on non-Windows
// platforms. Unfortunately, the current (April 2017) standard C++ API for
// encoding conversions has big problems (cf.
// <http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0618r0.html>).
// Should be fixed when we have a good way to do such conversions.
return s;
#endif
}
// Return the number of arguments used by the option value, if any (i.e., how
// much the caller should shift to resume arguments processing).
int ArgumentParser::readLongOption(int argc, const char *const *argv,
const string& currentArg, int nextArgIdx,
std::vector<OptionValue>& optsWithValues)
const
{
const string s = currentArg.substr(2); // skip the two initial dashes
// UTF-8 guarantees that ASCII bytes (here, '=') cannot be part of the
// encoding of a non-ASCII character.
std::size_t optEnd = s.find('=');
string opt = s.substr(0, optEnd);
const auto mapElt = _longOptionMap.find(opt);
if (mapElt != _longOptionMap.end()) {
const shared_ptr<const OptionDesc>& optDesc = mapElt->second;
OptionValue optVal(optDesc, string("--") + opt);
switch (optDesc->argumentType()) {
case OptionArgType::NO_ARGUMENT:
optVal.setHasValue(false);
optsWithValues.push_back(std::move(optVal));
return 0;
case OptionArgType::OPTIONAL_ARGUMENT: // pass through
case OptionArgType::MANDATORY_ARGUMENT:
if (optEnd != string::npos) {
// The optional value is present as in the same command line
// argument as the option name (syntax '--option=value').
optVal.setHasValue(true);
optVal.setValue(s.substr(optEnd + 1));
optsWithValues.push_back(std::move(optVal));
return 0;
} else if (nextArgIdx < argc && argv[nextArgIdx][0] != '-') {
// The optional value is present as a separate command line argument
// (syntax '--option value').
optVal.setHasValue(true);
optVal.setValue(cmdEncToUtf8(argv[nextArgIdx]));
optsWithValues.push_back(std::move(optVal));
return 1;
} else if (optDesc->argumentType() ==
OptionArgType::OPTIONAL_ARGUMENT) {
// No argument (value) can be found for the option
optVal.setHasValue(false);
optsWithValues.push_back(std::move(optVal));
return 0;
} else {
assert(optDesc->argumentType() == OptionArgType::MANDATORY_ARGUMENT);
throw InvalidUserInput("option '" + optVal.passedAs() + "' requires an "
"argument, but none was provided");
}
default:
throw sg_error("This piece of code should be unreachable.");
}
} else {
throw InvalidUserInput("invalid option: '--" + opt + "'");
}
}
int ArgumentParser::readShortOptions(int argc, const char *const *argv,
const string& currentArg, int nextArgIdx,
std::vector<OptionValue>& optsWithValues)
const
{
shared_ptr<const OptionDesc> optDesc;
const string s = currentArg.substr(1); // skip the initial dash
std::size_t i = 0; // index inside s
// Read all options taking no argument in 'currentArg'; stop at the first
// taking an optional or mandatory argument.
for (/* empty */; i < s.size(); i++) {
const auto mapElt = _shortOptionMap.find(s[i]);
if (mapElt != _shortOptionMap.end()) {
optDesc = mapElt->second;
if (optDesc->argumentType() == OptionArgType::NO_ARGUMENT) {
optsWithValues.emplace_back(optDesc, string("-") + s[i], string(),
false /* no value */);
} else {
break;
}
} else {
throw InvalidUserInput(string("invalid option: '-") + s[i] + "'");
}
}
if (i == s.size()) {
// The command line argument in 'currentArg' was fully read and only
// contains options that take no argument.
return 0;
}
// We've already “eaten” all options taking no argument in 'currentArg'.
assert(optDesc->argumentType() == OptionArgType::OPTIONAL_ARGUMENT ||
optDesc->argumentType() == OptionArgType::MANDATORY_ARGUMENT);
if (i + 1 < s.size()) {
// The option has a value at the end of 'currentArg': s.substr(i+1)
optsWithValues.emplace_back(optDesc, string("-") + s[i], s.substr(i+1),
true /* hasValue */);
return 0;
} else if (nextArgIdx < argc && argv[nextArgIdx][0] != '-') {
assert(i + 1 == s.size());
// The option is at the end of 'currentArg' and has a value:
// argv[nextArgIdx].
optsWithValues.emplace_back(optDesc, string("-") + s[i],
cmdEncToUtf8(argv[nextArgIdx]),
true /* hasValue */);
return 1;
} else if (optDesc->argumentType() ==
OptionArgType::OPTIONAL_ARGUMENT) {
// No argument (value) can be found for the option
optsWithValues.emplace_back(optDesc, string("-") + s[i], string(),
false /* no value */);
return 0;
} else {
assert(optDesc->argumentType() == OptionArgType::MANDATORY_ARGUMENT);
throw InvalidUserInput(string("option '-") + s[i] + "' requires an "
"argument, but none was provided");
}
}
} // of namespace argparse
} // of namespace simgear

279
simgear/misc/argparse.hxx Normal file
View File

@@ -0,0 +1,279 @@
// -*- coding: utf-8 -*-
//
// argparse.hxx --- Simple, generic parser for command-line arguments
// Copyright (C) 2017 Florent Rougon
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#ifndef _SIMGEAR_ARGPARSE_HXX_
#define _SIMGEAR_ARGPARSE_HXX_
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#include <utility> // std::pair
#include <simgear/structure/exception.hxx>
// Usage example:
//
// using simgear::argparse::OptionArgType;
//
// simgear::argparse::ArgumentParser parser;
// parser.addOption("root option", OptionArgType::MANDATORY_ARGUMENT,
// "", "--root");
// parser.addOption("test option", OptionArgType::NO_ARGUMENT, "-t", "--test");
//
// const auto res = parser.parseArgs(argc, argv);
//
// for (const auto& opt: res.first) {
// std::cerr << "Got option '" << opt.id() << "' as '" << opt.passedAs() <<
// "'" << ((opt.hasValue()) ? " with value '" + opt.value() + "'" : "") <<
// "\n";
// }
//
// for (const auto& arg: res.second) {
// std::cerr << "Got non-option argument '" << arg << "'\n";
// }
namespace simgear
{
namespace argparse
{
// Custom exception classes
class Error : public sg_exception
{
public:
explicit Error(const std::string& message,
const std::string& origin = std::string());
explicit Error(const char* message, const char* origin = nullptr);
};
class InvalidUserInput : public Error
{
using Error::Error; // inherit all constructors
};
enum class OptionArgType {
NO_ARGUMENT = 0,
OPTIONAL_ARGUMENT,
MANDATORY_ARGUMENT
};
// All strings inside this class are encoded in UTF-8.
class OptionDesc
{
public:
explicit OptionDesc(const std::string& optionId,
std::vector<char> shortAliases,
std::vector<std::string> longAliases,
OptionArgType argumentType);
// Simple getters for the private members
const std::string& id() const;
const std::vector<char>& shortAliases() const;
const std::vector<std::string>& longAliases() const;
OptionArgType argumentType() const;
private:
// Option identifier, invisible to the end user. Used to easily refer to the
// option despite the various forms it may take (short and/or long aliases).
std::string _id;
// Each element of _shortAliases must be an ASCII character. For instance,
// 'o' for an option called '-o'.
std::vector<char> _shortAliases;
// Each element of _longAliases should be the name of a long option, with
// the two leading dashes removed. For instance, 'generate-foobar' for an
// option named '--generate-foobar'.
std::vector<std::string> _longAliases;
OptionArgType _argumentType;
};
// All strings inside this class are encoded in UTF-8.
class OptionValue
{
public:
explicit OptionValue(std::shared_ptr<const OptionDesc> optionDesc,
const std::string& passedAs,
const std::string& value = std::string(),
bool hasValue = false);
// Simple getters/accessors for the private members
std::shared_ptr<const OptionDesc> optionDesc() const;
std::string passedAs() const;
std::string value() const;
bool hasValue() const;
// The corresponding setters
void setOptionDesc(std::shared_ptr<const OptionDesc>);
void setPassedAs(const std::string&);
void setValue(const std::string&);
void setHasValue(bool);
// For convenience: get the option ID from the result of optionDesc()
const std::string id() const;
private:
// Pointer to the option descriptor.
std::shared_ptr<const OptionDesc> _optionDesc;
// Exact option passed (e.g., -f or --foobar).
std::string _passedAs;
// Value given for the option, if any (otherwise, the empty string).
std::string _value;
// Tells whether the option has been given a value. This is of course mainly
// useful for options taking an *optional* argument. The value in question
// can be the empty string, if given on a separate command line argument
// from the option.
bool _hasValue;
};
// Main class for command line processing. Every string coming out of it is
// encoded in UTF-8.
class ArgumentParser
{
public:
// Register an option, with zero or more short aliases (e.g., -a, -u, - F)
// and zero or more long aliases (e.g., --foobar, --barnum, --bleh). The
// option may take no argument, or one optional argument, or one mandatory
// argument. The 'optionId' is used to refer to the option in a clear and
// simple way, even in the presence of several short or long aliases. It is
// thus visible to the programmer using this API, but not to users of the
// command line interface being implemented.
//
// Note: this method and all its overloads take options in the form "-o" or
// "--foobar" (as std::string instances). While it would be possible
// to only require a char for each short option and to take long
// option declarations without the two leading dashes, the API chosen
// here should lead to more readable and searchable user code.
//
// shortAliases: each element should consist of two characters: an ASCII
// hyphen (-) followed by an ASCII character.
// longAliases: each element should be a string in UTF-8 encoding, starting
// with two ASCII/UTF-8 hyphens (U+002D).
//
// This API could be extended to automatically generate --help output from
// strings passed to addOption().
void addOption(const std::string& optionId,
OptionArgType argumentType,
std::vector<std::string> shortAliases,
std::vector<std::string> longAliases);
// Convenience overload that should be enough for most cases. To register
// only a short option or only a long option, simply pass the empty string
// for the corresponding parameter.
void addOption(const std::string& optionId,
OptionArgType argumentType,
std::string shortOpt = std::string(),
std::string longOpt = std::string());
// Parse arguments from an argc/argv pair of variables. 'argc' should be the
// number of elements in 'argv', the first of which is ignored for the sake
// of options and arguments extraction (since it normally holds the program
// name).
//
// Note: this “number of elements” doesn't count the usual---and completely
// unneeded here---final null pointer.
//
// Short options may be grouped in the usual way. For instance, if '-x',
// '-z' and '-f' are three short options, the first two taking no argument
// and '-f' taking one mandatory argument, then both '-xzf bar' and
// '-xzfbar' are equivalent to '-x -z -f bar' as well as to '-x -z -fbar'
// ('bar' being the value taken by option '-f').
//
// Long options are handled in the usual way too:
//
// '--foobar' for an option taking no argument
//
// '--foobar=value' for an option taking an optional or mandatory
// or '--foobar value' argument (two separate command line arguments in the
// second case)
//
// Long option names may contain spaces, though this is extremely uncommon
// and inconvenient for users. Any option argument (be it for a long or a
// short option) may contain spaces, as expected.
//
// As usual too, the special '--' argument consisting of two ASCII/UTF-8
// hyphens, can be used to cause all subsequent arguments to be treated as
// non-option arguments, regardless of whether they start with a hyphen or
// not. In the absence of this special argument, the first argument that is
// not the value of an option and does not start with a hyphen marks the end
// of options. All subsequent arguments are read as non-option arguments.
//
// Return a pair containing:
// - the list of supplied options (with their respective values, when
// applicable);
// - the list of non-option arguments that were given after the options.
//
// Both of these lists (vectors) may be empty and preserve the order used in
// 'argv'.
std::pair< std::vector<OptionValue>, std::vector<std::string> >
parseArgs(int argc, const char *const *argv) const;
private:
// Convert from the encoding used for argv (command line arguments) to
// UTF-8.
//
// This method is currently not very satisfactory (cf. comments in the
// implementation). The Windows code path is untested; the non-Windows code
// path assumes command line arguments are encoded in UTF-8 (in other words,
// it's a no-op).
static std::string cmdEncToUtf8(const std::string& stringInCmdLineEncoding);
// Remove leading dashes and do sanity checks. 'longAliases' is modified
// in-place. 'shortAliases' is not, because we build an std::vector<char>
// from an std::vector<std::string>.
static std::vector<char> removeHyphens(
const std::vector<std::string>& shortAliases,
std::vector<std::string>& longAliases);
// Read a long option and its value, if any (in total: one or two command
// line arguments).
//
// Return the number of arguments consumed by this process after
// 'currentArg' (i.e., 0 or 1 depending on whether the last option in
// 'currentArg' has been given a value).
//
// 'currentArg' comes from argv[nextArgIdx-1], after decoding by
// cmdEncToUtf8(). Thus, argv[nextArgIdx] is the command-line argument
// coming after 'currentArg'.
int readLongOption(
int argc, const char *const *argv, const std::string& currentArg,
int nextArgIdx, std::vector<OptionValue>& optsWithValues) const;
// Read all short options in a command line argument, plus the option value
// of the last one of these, if any (even if the option value is in the next
// command line argument).
//
// See readLongOption() for the return value and meaning of parameters.
int readShortOptions(
int argc, const char *const *argv, const std::string& currentArg,
int nextArgIdx, std::vector<OptionValue>& optsWithValues) const;
// Keys are short option names without the leading dash
std::unordered_map< char,
std::shared_ptr<const OptionDesc> > _shortOptionMap;
// Keys are long option names without the two leading dashes
std::unordered_map< std::string,
std::shared_ptr<const OptionDesc> > _longOptionMap;
};
} // of namespace argparse
} // of namespace simgear
#endif // _SIMGEAR_ARGPARSE_HXX_

View File

@@ -0,0 +1,488 @@
// -*- coding: utf-8 -*-
//
// argparse_test.cxx --- Automated tests for argparse.cxx / argparse.hxx
//
// Copyright (C) 2017 Florent Rougon
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#include <simgear_config.h>
#include <iostream> // std::cout
#include <vector>
#include <cstdlib> // EXIT_SUCCESS
#include <simgear/misc/test_macros.hxx>
#include "argparse.hxx"
using std::string;
using std::vector;
using std::cout;
using std::cerr;
using std::endl;
void test_mixOfShortAndLongOptions()
{
cout << "Testing a mix of short and long options, plus non-option arguments"
<< endl;
using namespace simgear::argparse;
ArgumentParser parser;
parser.addOption("test option", OptionArgType::NO_ARGUMENT, "-t");
parser.addOption("long opt w/o arg", OptionArgType::NO_ARGUMENT,
"", "--long-option-without-arg");
parser.addOption("other test option", OptionArgType::NO_ARGUMENT, "-O");
parser.addOption("yet another test option",
OptionArgType::OPTIONAL_ARGUMENT, "-y", "--yes-we-can");
parser.addOption("and again", OptionArgType::MANDATORY_ARGUMENT, "-a",
"--all-you-need-is-love");
parser.addOption("long option with opt arg",
OptionArgType::OPTIONAL_ARGUMENT, "", // no short alias
"--long-option-with-opt-arg");
// Using an std::vector to avoid the need to count the elements ourselves
const vector<const char*> v({
"FoobarProg", "-Oy", "-aarg for -a", "-OtOty", "arg for -y",
"-tyOther arg for -y", "--long-option-without-arg", "-a", "Arg for -a",
"--long-option-with-opt-arg", "--long-option-with-opt-arg", "value 1",
"--long-option-with-opt-arg=value 2", "-t",
"--all-you-need-is-love", "oh this is true", "-ypouet",
"--all-you-need-is-love=right, I'll shut up ;-)", "non option",
" other non option ", "--", "<-- came too late, treated as an arg"});
// v.size() corresponds to argc, &v[0] corresponds to argv.
const auto res = parser.parseArgs(v.size(), &v[0]);
const auto& opts = res.first;
const auto& otherArgs = res.second;
SG_CHECK_EQUAL(opts.size(), 19); // number of passed options
SG_CHECK_EQUAL(otherArgs.size(), 4); // number of non-option arguments
// Check all passed options and their values
SG_CHECK_EQUAL(opts[0].passedAs(), "-O");
SG_CHECK_EQUAL(opts[0].value(), "");
SG_CHECK_EQUAL(opts[0].hasValue(), false);
SG_CHECK_EQUAL(opts[0].id(), "other test option");
SG_CHECK_EQUAL_NOSTREAM(opts[0].optionDesc()->argumentType(),
OptionArgType::NO_ARGUMENT);
SG_CHECK_EQUAL_NOSTREAM(opts[0].optionDesc()->shortAliases(),
vector<char>(1, 'O'));
SG_CHECK_EQUAL_NOSTREAM(opts[0].optionDesc()->longAliases(), vector<string>());
SG_CHECK_EQUAL(opts[1].passedAs(), "-y");
SG_CHECK_EQUAL(opts[1].value(), "");
SG_CHECK_EQUAL(opts[1].hasValue(), false);
SG_CHECK_EQUAL(opts[1].id(), "yet another test option");
SG_CHECK_EQUAL_NOSTREAM(opts[1].optionDesc()->argumentType(),
OptionArgType::OPTIONAL_ARGUMENT);
SG_CHECK_EQUAL_NOSTREAM(opts[1].optionDesc()->shortAliases(),
vector<char>(1, 'y'));
SG_CHECK_EQUAL_NOSTREAM(opts[1].optionDesc()->longAliases(),
vector<string>(1, "yes-we-can"));
SG_CHECK_EQUAL(opts[2].passedAs(), "-a");
SG_CHECK_EQUAL(opts[2].value(), "arg for -a");
SG_CHECK_EQUAL(opts[2].hasValue(), true);
SG_CHECK_EQUAL(opts[2].id(), "and again");
SG_CHECK_EQUAL_NOSTREAM(opts[2].optionDesc()->argumentType(),
OptionArgType::MANDATORY_ARGUMENT);
SG_CHECK_EQUAL_NOSTREAM(opts[2].optionDesc()->shortAliases(),
vector<char>(1, 'a'));
SG_CHECK_EQUAL_NOSTREAM(opts[2].optionDesc()->longAliases(),
vector<string>(1, "all-you-need-is-love"));
SG_CHECK_EQUAL(opts[3].passedAs(), "-O");
SG_CHECK_EQUAL(opts[3].value(), "");
SG_CHECK_EQUAL(opts[3].hasValue(), false);
SG_CHECK_EQUAL(opts[3].id(), "other test option");
SG_CHECK_EQUAL_NOSTREAM(opts[3].optionDesc()->argumentType(),
OptionArgType::NO_ARGUMENT);
SG_CHECK_EQUAL_NOSTREAM(opts[3].optionDesc()->shortAliases(),
vector<char>(1, 'O'));
SG_CHECK_EQUAL_NOSTREAM(opts[3].optionDesc()->longAliases(), vector<string>());
SG_CHECK_EQUAL(opts[4].passedAs(), "-t");
SG_CHECK_EQUAL(opts[4].value(), "");
SG_CHECK_EQUAL(opts[4].hasValue(), false);
SG_CHECK_EQUAL(opts[4].id(), "test option");
SG_CHECK_EQUAL_NOSTREAM(opts[4].optionDesc()->argumentType(),
OptionArgType::NO_ARGUMENT);
SG_CHECK_EQUAL_NOSTREAM(opts[4].optionDesc()->shortAliases(),
vector<char>(1, 't'));
SG_CHECK_EQUAL_NOSTREAM(opts[4].optionDesc()->longAliases(), vector<string>());
SG_CHECK_EQUAL(opts[5].passedAs(), "-O");
SG_CHECK_EQUAL(opts[5].value(), "");
SG_CHECK_EQUAL(opts[5].hasValue(), false);
SG_CHECK_EQUAL(opts[5].id(), "other test option");
SG_CHECK_EQUAL(opts[6].passedAs(), "-t");
SG_CHECK_EQUAL(opts[6].value(), "");
SG_CHECK_EQUAL(opts[6].hasValue(), false);
SG_CHECK_EQUAL(opts[6].id(), "test option");
SG_CHECK_EQUAL(opts[7].passedAs(), "-y");
SG_CHECK_EQUAL(opts[7].value(), "arg for -y");
SG_CHECK_EQUAL(opts[7].hasValue(), true);
SG_CHECK_EQUAL(opts[7].id(), "yet another test option");
SG_CHECK_EQUAL(opts[8].passedAs(), "-t");
SG_CHECK_EQUAL(opts[8].value(), "");
SG_CHECK_EQUAL(opts[8].hasValue(), false);
SG_CHECK_EQUAL(opts[8].id(), "test option");
SG_CHECK_EQUAL(opts[9].passedAs(), "-y");
SG_CHECK_EQUAL(opts[9].value(), "Other arg for -y");
SG_CHECK_EQUAL(opts[9].hasValue(), true);
SG_CHECK_EQUAL(opts[9].id(), "yet another test option");
SG_CHECK_EQUAL(opts[10].passedAs(), "--long-option-without-arg");
SG_CHECK_EQUAL(opts[10].value(), "");
SG_CHECK_EQUAL(opts[10].hasValue(), false);
SG_CHECK_EQUAL(opts[10].id(), "long opt w/o arg");
SG_CHECK_EQUAL(opts[11].passedAs(), "-a");
SG_CHECK_EQUAL(opts[11].value(), "Arg for -a");
SG_CHECK_EQUAL(opts[11].hasValue(), true);
SG_CHECK_EQUAL(opts[11].id(), "and again");
SG_CHECK_EQUAL(opts[12].passedAs(), "--long-option-with-opt-arg");
SG_CHECK_EQUAL(opts[12].value(), "");
SG_CHECK_EQUAL(opts[12].hasValue(), false);
SG_CHECK_EQUAL(opts[12].id(), "long option with opt arg");
SG_CHECK_EQUAL(opts[13].passedAs(), "--long-option-with-opt-arg");
SG_CHECK_EQUAL(opts[13].value(), "value 1");
SG_CHECK_EQUAL(opts[13].hasValue(), true);
SG_CHECK_EQUAL(opts[13].id(), "long option with opt arg");
SG_CHECK_EQUAL(opts[14].passedAs(), "--long-option-with-opt-arg");
SG_CHECK_EQUAL(opts[14].value(), "value 2");
SG_CHECK_EQUAL(opts[14].hasValue(), true);
SG_CHECK_EQUAL(opts[14].id(), "long option with opt arg");
SG_CHECK_EQUAL(opts[15].passedAs(), "-t");
SG_CHECK_EQUAL(opts[15].value(), "");
SG_CHECK_EQUAL(opts[15].hasValue(), false);
SG_CHECK_EQUAL(opts[15].id(), "test option");
SG_CHECK_EQUAL(opts[16].passedAs(), "--all-you-need-is-love");
SG_CHECK_EQUAL(opts[16].value(), "oh this is true");
SG_CHECK_EQUAL(opts[16].hasValue(), true);
SG_CHECK_EQUAL(opts[16].id(), "and again");
SG_CHECK_EQUAL(opts[17].passedAs(), "-y");
SG_CHECK_EQUAL(opts[17].value(), "pouet");
SG_CHECK_EQUAL(opts[17].hasValue(), true);
SG_CHECK_EQUAL(opts[17].id(), "yet another test option");
SG_CHECK_EQUAL(opts[18].passedAs(), "--all-you-need-is-love");
SG_CHECK_EQUAL(opts[18].value(), "right, I'll shut up ;-)");
SG_CHECK_EQUAL(opts[18].hasValue(), true);
SG_CHECK_EQUAL(opts[18].id(), "and again");
// Check all non-option arguments that were passed to parser.parseArgs()
SG_CHECK_EQUAL_NOSTREAM(
otherArgs,
vector<string>({"non option", " other non option ", "--",
"<-- came too late, treated as an arg"}));
}
void test_frontierBetweenOptionsAndNonOptions()
{
cout << "Testing around the frontier between options and non-options" << endl;
using namespace simgear::argparse;
ArgumentParser parser;
parser.addOption("option -T", OptionArgType::NO_ARGUMENT, "-T");
parser.addOption("long opt w/o arg", OptionArgType::NO_ARGUMENT,
"", "--long-option-without-arg");
parser.addOption("option -a", OptionArgType::MANDATORY_ARGUMENT, "-a",
"--this-is-option-a");
// Test 1: both options and non-options; '--' used as a normal non-option
// argument (i.e., after other non-option arguments).
const vector<const char*> v1({
"FoobarProg", "--long-option-without-arg", "-aval", "non option 1",
"non option 2", "--", "non option 3"});
// v1.size() corresponds to argc, &v1[0] corresponds to argv.
const auto res1 = parser.parseArgs(v1.size(), &v1[0]);
const auto& opts1 = res1.first;
const auto& otherArgs1 = res1.second;
SG_CHECK_EQUAL(opts1.size(), 2); // number of passed options
SG_CHECK_EQUAL(otherArgs1.size(), 4); // number of non-option arguments
SG_CHECK_EQUAL_NOSTREAM(
otherArgs1,
vector<string>({"non option 1", "non option 2", "--", "non option 3"}));
// Test 2: some options but no non-options arguments
const vector<const char*> v2({
"FoobarProg", "--long-option-without-arg", "-aval"});
const auto res2 = parser.parseArgs(v2.size(), &v2[0]);
const auto& opts2 = res2.first;
const auto& otherArgs2 = res2.second;
SG_CHECK_EQUAL(opts2.size(), 2);
SG_VERIFY(otherArgs2.empty());
SG_CHECK_EQUAL_NOSTREAM(otherArgs2, vector<string>());
// Test 3: same as test 2, but with useless end-of-options delimiter
const vector<const char*> v3({
"FoobarProg", "--long-option-without-arg", "-aval", "--"});
const auto res3 = parser.parseArgs(v3.size(), &v3[0]);
const auto& opts3 = res3.first;
const auto& otherArgs3 = res3.second;
SG_CHECK_EQUAL(opts3.size(), 2);
SG_VERIFY(otherArgs3.empty());
SG_CHECK_EQUAL_NOSTREAM(otherArgs3, vector<string>());
// Test 4: only non-option arguments
const vector<const char*> v4({
"FoobarProg", "non option 1",
"non option 2", "--", "non option 3"});
const auto res4 = parser.parseArgs(v4.size(), &v4[0]);
const auto& opts4 = res4.first;
const auto& otherArgs4 = res4.second;
SG_VERIFY(opts4.empty());
SG_CHECK_EQUAL(otherArgs4.size(), 4);
SG_CHECK_EQUAL_NOSTREAM(
otherArgs4,
vector<string>({"non option 1", "non option 2", "--", "non option 3"}));
// Test 5: only non-options arguments, but starting with --
const vector<const char*> v5({
"FoobarProg", "--", "non option 1",
"non option 2", "--", "non option 3"});
const auto res5 = parser.parseArgs(v5.size(), &v5[0]);
const auto& opts5 = res5.first;
const auto& otherArgs5 = res5.second;
SG_VERIFY(opts5.empty());
SG_CHECK_EQUAL(otherArgs5.size(), 4);
SG_CHECK_EQUAL_NOSTREAM(
otherArgs5,
vector<string>({"non option 1", "non option 2", "--", "non option 3"}));
// Test 6: use the '--' delimiter before what would otherwise be considered
// an option
const vector<const char*> v6({
"FoobarProg", "--long-option-without-arg", "-aval", "--", "-T",
"non option 1", "non option 2", "--", "non option 3"});
const auto res6 = parser.parseArgs(v6.size(), &v6[0]);
const auto& opts6 = res6.first;
const auto& otherArgs6 = res6.second;
SG_CHECK_EQUAL(opts6.size(), 2);
SG_CHECK_EQUAL(otherArgs6.size(), 5);
SG_CHECK_EQUAL_NOSTREAM(
otherArgs6,
vector<string>({"-T", "non option 1", "non option 2", "--",
"non option 3"}));
// Test 7: use the '--' delimiter before an argument that doesn't look like
// an option
const vector<const char*> v7({
"FoobarProg", "--long-option-without-arg", "-aval", "--",
"doesn't look like an option", "non option 1", "non option 2", "--",
"non option 3"});
const auto res7 = parser.parseArgs(v7.size(), &v7[0]);
const auto& opts7 = res7.first;
const auto& otherArgs7 = res7.second;
SG_CHECK_EQUAL(opts7.size(), 2);
SG_CHECK_EQUAL(otherArgs7.size(), 5);
SG_CHECK_EQUAL_NOSTREAM(
otherArgs7,
vector<string>({"doesn't look like an option", "non option 1",
"non option 2", "--", "non option 3"}));
// Test 8: no other argument than the program name in argv
const vector<const char*> v8({"FoobarProg"});
const auto res8 = parser.parseArgs(v8.size(), &v8[0]);
const auto& opts8 = res8.first;
const auto& otherArgs8 = res8.second;
SG_VERIFY(opts8.empty());
SG_VERIFY(otherArgs8.empty());
}
void test_optionsWithMultipleAliases()
{
cout << "Testing options with multiple aliases" << endl;
using namespace simgear::argparse;
ArgumentParser parser;
parser.addOption("option -o", OptionArgType::OPTIONAL_ARGUMENT,
vector<string>({"-o", "-O", "-0"}),
vector<string>({"--o-alias-1", "--o-alias-2"}));
parser.addOption("option -a", OptionArgType::MANDATORY_ARGUMENT,
vector<string>({"-a", "-r"}),
vector<string>({"--a-alias-1", "--a-alias-2",
"--a-alias-3"}));
parser.addOption("option -N", OptionArgType::NO_ARGUMENT,
vector<string>({"-N", "-p"}),
vector<string>({"--N-alias-1", "--N-alias-2"}));
const vector<const char*> v({
"FoobarProg", "--o-alias-1", "-aarg for -a", "-pO", "arg for -O",
"--a-alias-2=value 1", "--o-alias-2", "value 2", "-Novalue 3",
"--N-alias-2", "--a-alias-3=value 4", "-0value 5", "--N-alias-1",
"non option 1", "non option 2", "non option 3"});
// v.size() corresponds to argc, &v[0] corresponds to argv.
const auto res = parser.parseArgs(v.size(), &v[0]);
const auto& opts = res.first;
const auto& otherArgs = res.second;
SG_CHECK_EQUAL(opts.size(), 12); // number of passed options
SG_CHECK_EQUAL(otherArgs.size(), 3); // number of non-option arguments
SG_CHECK_EQUAL(opts[0].passedAs(), "--o-alias-1");
SG_CHECK_EQUAL(opts[0].value(), "");
SG_CHECK_EQUAL(opts[0].hasValue(), false);
SG_CHECK_EQUAL(opts[0].id(), "option -o");
SG_CHECK_EQUAL(opts[1].passedAs(), "-a");
SG_CHECK_EQUAL(opts[1].value(), "arg for -a");
SG_CHECK_EQUAL(opts[1].hasValue(), true);
SG_CHECK_EQUAL(opts[1].id(), "option -a");
SG_CHECK_EQUAL(opts[2].passedAs(), "-p");
SG_CHECK_EQUAL(opts[2].value(), "");
SG_CHECK_EQUAL(opts[2].hasValue(), false);
SG_CHECK_EQUAL(opts[2].id(), "option -N");
SG_CHECK_EQUAL(opts[3].passedAs(), "-O");
SG_CHECK_EQUAL(opts[3].value(), "arg for -O");
SG_CHECK_EQUAL(opts[3].hasValue(), true);
SG_CHECK_EQUAL(opts[3].id(), "option -o");
SG_CHECK_EQUAL(opts[4].passedAs(), "--a-alias-2");
SG_CHECK_EQUAL(opts[4].value(), "value 1");
SG_CHECK_EQUAL(opts[4].hasValue(), true);
SG_CHECK_EQUAL(opts[4].id(), "option -a");
SG_CHECK_EQUAL(opts[5].passedAs(), "--o-alias-2");
SG_CHECK_EQUAL(opts[5].value(), "value 2");
SG_CHECK_EQUAL(opts[5].hasValue(), true);
SG_CHECK_EQUAL(opts[5].id(), "option -o");
SG_CHECK_EQUAL(opts[6].passedAs(), "-N");
SG_CHECK_EQUAL(opts[6].value(), "");
SG_CHECK_EQUAL(opts[6].hasValue(), false);
SG_CHECK_EQUAL(opts[6].id(), "option -N");
SG_CHECK_EQUAL(opts[7].passedAs(), "-o");
SG_CHECK_EQUAL(opts[7].value(), "value 3");
SG_CHECK_EQUAL(opts[7].hasValue(), true);
SG_CHECK_EQUAL(opts[7].id(), "option -o");
SG_CHECK_EQUAL(opts[8].passedAs(), "--N-alias-2");
SG_CHECK_EQUAL(opts[8].value(), "");
SG_CHECK_EQUAL(opts[8].hasValue(), false);
SG_CHECK_EQUAL(opts[8].id(), "option -N");
SG_CHECK_EQUAL(opts[9].passedAs(), "--a-alias-3");
SG_CHECK_EQUAL(opts[9].value(), "value 4");
SG_CHECK_EQUAL(opts[9].hasValue(), true);
SG_CHECK_EQUAL(opts[9].id(), "option -a");
SG_CHECK_EQUAL(opts[10].passedAs(), "-0");
SG_CHECK_EQUAL(opts[10].value(), "value 5");
SG_CHECK_EQUAL(opts[10].hasValue(), true);
SG_CHECK_EQUAL(opts[10].id(), "option -o");
SG_CHECK_EQUAL(opts[11].passedAs(), "--N-alias-1");
SG_CHECK_EQUAL(opts[11].value(), "");
SG_CHECK_EQUAL(opts[11].hasValue(), false);
SG_CHECK_EQUAL(opts[11].id(), "option -N");
SG_CHECK_EQUAL_NOSTREAM(
otherArgs,
vector<string>({"non option 1", "non option 2", "non option 3"}));
}
// Auxiliary function used by test_invalidOptionOrArgumentMissing()
void aux_invalidOptionOrMissingArgument_checkRaiseExcecption(
const simgear::argparse::ArgumentParser& parser,
const vector<const char*>& v)
{
bool gotException = false;
try {
parser.parseArgs(v.size(), &v[0]);
} catch (const simgear::argparse::Error&) {
gotException = true;
}
SG_VERIFY(gotException);
}
void test_invalidOptionOrMissingArgument()
{
cout << "Testing passing invalid options and other syntax errors" << endl;
using simgear::argparse::OptionArgType;
simgear::argparse::ArgumentParser parser;
parser.addOption("option -o", OptionArgType::OPTIONAL_ARGUMENT, "-o");
parser.addOption("option -m", OptionArgType::MANDATORY_ARGUMENT,
"-m", "--mandatory-arg");
parser.addOption("option -n", OptionArgType::NO_ARGUMENT, "-n", "--no-arg");
const vector<vector<const char*> > listOfArgvs({
{"FoobarProg", "-ovalue", "-n", "-X",
"non option 1", "non option 2", "non option 3"},
{"FoobarProg", "-ovalue", "-nXn",
"non option 1", "non option 2", "non option 3"},
{"FoobarProg", "-ovalue", "-n", "--non-existent-option",
"non option 1", "non option 2", "non option 3"},
{"FoobarProg", "-ovalue", "-n", "--non-existent-option=value",
"non option 1", "non option 2", "non option 3"},
{"FoobarProg", "-ovalue", "-n", "-m", "--",
"non option 1", "non option 2", "non option 3"},
{"FoobarProg", "-ovalue", "-n", "-X", "-m"},
{"FoobarProg", "-ovalue", "-n", "--mandatory-arg", "--",
"non option 1", "non option 2", "non option 3"},
{"FoobarProg", "-ovalue", "-n", "--mandatory-arg"}
});
for (const auto& argv: listOfArgvs) {
aux_invalidOptionOrMissingArgument_checkRaiseExcecption(parser, argv);
}
}
int main(int argc, const char *const *argv)
{
test_mixOfShortAndLongOptions();
test_frontierBetweenOptionsAndNonOptions();
test_optionsWithMultipleAliases();
test_invalidOptionOrMissingArgument();
return EXIT_SUCCESS;
}