diff --git a/simgear/misc/CMakeLists.txt b/simgear/misc/CMakeLists.txt index 22adbf26..2931a3b3 100644 --- a/simgear/misc/CMakeLists.txt +++ b/simgear/misc/CMakeLists.txt @@ -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}) diff --git a/simgear/misc/argparse.cxx b/simgear/misc/argparse.cxx new file mode 100644 index 00000000..392f21b4 --- /dev/null +++ b/simgear/misc/argparse.cxx @@ -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 + +#include +#include +#include +#include // std::pair, std::move() +#include // std::size_t +#include + +#include +#include +#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 shortAliases, + std::vector longAliases, OptionArgType argumentType) + : _id(optionId), + _shortAliases(shortAliases), + _longAliases(longAliases), + _argumentType(argumentType) +{ } + +const std::string& OptionDesc::id() const +{ return _id; } + +const std::vector& OptionDesc::shortAliases() const +{ return _shortAliases; } + +const std::vector& OptionDesc::longAliases() const +{ return _longAliases; } + +OptionArgType OptionDesc::argumentType() const +{ return _argumentType; } + + +// *************************************************************************** +// * OptionValue class * +// *************************************************************************** +OptionValue::OptionValue(shared_ptr optionDesc, + const string& passedAs, const string& value, + bool hasValue) + : _optionDesc(std::move(optionDesc)), + _passedAs(passedAs), + _value(value), + _hasValue(hasValue) +{ } + +shared_ptr OptionValue::optionDesc() const +{ return _optionDesc; } // return a copy of the shared_ptr + +void OptionValue::setOptionDesc(shared_ptr 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 +ArgumentParser::removeHyphens(const std::vector& shortAliases, + std::vector& longAliases) +{ + std::vector 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 shortAliases, + std::vector longAliases) +{ + // Remove the leading dashes and do a sanity check for these arguments + std::vector shortAliasesCharVec = removeHyphens(shortAliases, + longAliases); + + const auto desc_p = std::make_shared( + 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 shortOptList; + std::vector 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, std::vector > +ArgumentParser::parseArgs(int argc, const char *const *argv) const +{ + std::pair< std::vector, std::vector > res; + std::vector& optsWithValues = res.first; + std::vector& 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. + // ). + // 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& 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& 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& optsWithValues) + const +{ + shared_ptr 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 diff --git a/simgear/misc/argparse.hxx b/simgear/misc/argparse.hxx new file mode 100644 index 00000000..aee2d901 --- /dev/null +++ b/simgear/misc/argparse.hxx @@ -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 +#include +#include +#include +#include // std::pair + +#include + +// 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 shortAliases, + std::vector longAliases, + OptionArgType argumentType); + + // Simple getters for the private members + const std::string& id() const; + const std::vector& shortAliases() const; + const std::vector& 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 _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 _longAliases; + OptionArgType _argumentType; +}; + +// All strings inside this class are encoded in UTF-8. +class OptionValue +{ +public: + explicit OptionValue(std::shared_ptr optionDesc, + const std::string& passedAs, + const std::string& value = std::string(), + bool hasValue = false); + + // Simple getters/accessors for the private members + std::shared_ptr optionDesc() const; + std::string passedAs() const; + std::string value() const; + bool hasValue() const; + + // The corresponding setters + void setOptionDesc(std::shared_ptr); + 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 _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 shortAliases, + std::vector 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, std::vector > + 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 + // from an std::vector. + static std::vector removeHyphens( + const std::vector& shortAliases, + std::vector& 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& 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& optsWithValues) const; + + // Keys are short option names without the leading dash + std::unordered_map< char, + std::shared_ptr > _shortOptionMap; + // Keys are long option names without the two leading dashes + std::unordered_map< std::string, + std::shared_ptr > _longOptionMap; +}; + +} // of namespace argparse + +} // of namespace simgear + +#endif // _SIMGEAR_ARGPARSE_HXX_ diff --git a/simgear/misc/argparse_test.cxx b/simgear/misc/argparse_test.cxx new file mode 100644 index 00000000..bd7b10df --- /dev/null +++ b/simgear/misc/argparse_test.cxx @@ -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 + +#include // std::cout +#include +#include // EXIT_SUCCESS + +#include +#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 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(1, 'O')); + SG_CHECK_EQUAL_NOSTREAM(opts[0].optionDesc()->longAliases(), vector()); + + 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(1, 'y')); + SG_CHECK_EQUAL_NOSTREAM(opts[1].optionDesc()->longAliases(), + vector(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(1, 'a')); + SG_CHECK_EQUAL_NOSTREAM(opts[2].optionDesc()->longAliases(), + vector(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(1, 'O')); + SG_CHECK_EQUAL_NOSTREAM(opts[3].optionDesc()->longAliases(), vector()); + + 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(1, 't')); + SG_CHECK_EQUAL_NOSTREAM(opts[4].optionDesc()->longAliases(), vector()); + + 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({"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 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({"non option 1", "non option 2", "--", "non option 3"})); + + // Test 2: some options but no non-options arguments + const vector 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()); + + // Test 3: same as test 2, but with useless end-of-options delimiter + const vector 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()); + + // Test 4: only non-option arguments + const vector 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({"non option 1", "non option 2", "--", "non option 3"})); + + // Test 5: only non-options arguments, but starting with -- + const vector 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({"non option 1", "non option 2", "--", "non option 3"})); + + // Test 6: use the '--' delimiter before what would otherwise be considered + // an option + const vector 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({"-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 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({"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 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({"-o", "-O", "-0"}), + vector({"--o-alias-1", "--o-alias-2"})); + parser.addOption("option -a", OptionArgType::MANDATORY_ARGUMENT, + vector({"-a", "-r"}), + vector({"--a-alias-1", "--a-alias-2", + "--a-alias-3"})); + parser.addOption("option -N", OptionArgType::NO_ARGUMENT, + vector({"-N", "-p"}), + vector({"--N-alias-1", "--N-alias-2"})); + + const vector 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({"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& 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 > 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; +}