diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 8a03274b..804fcdf6 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -25,7 +25,9 @@ namespace CLI { +namespace detail { enum class Classifer {NONE, POSITIONAL_MARK, SHORT, LONG, SUBCOMMAND}; +} class App; @@ -42,8 +44,11 @@ protected: std::string name; std::string prog_description; std::vector options; - std::vector missing_options; - std::deque positionals; + + /// Pair of classifer, string for missing options. (extra detail is removed on returning from parse) + std::vector> missing; + bool no_extras {true}; + std::vector subcommands; bool parsed {false}; App* subcommand {nullptr}; @@ -115,6 +120,7 @@ public: App* add_subcommand(std::string name_, std::string description="", bool help=true) { subcommands.emplace_back(new App(description, help)); subcommands.back()->name = name_; + subcommands.back()->allow_extras(); return subcommands.back().get(); } @@ -337,19 +343,26 @@ public: /// Parses the command line - throws errors /// This must be called after the options are in but before the rest of the program. - void parse(int argc, char **argv) { + std::vector parse(int argc, char **argv) { progname = argv[0]; std::vector args; for(int i=argc-1; i>0; i--) args.push_back(argv[i]); - parse(args); + return parse(args); + } - /// The real work is done here. Expects a reversed vector - void parse(std::vector &args) { + /// The real work is done here. Expects a reversed vector. + /// Changes the vector to the remaining options. + std::vector& parse(std::vector &args) { return _parse(args); } + /// Remove the error when extras are left over on the command line. + void allow_extras (bool allow=true) { + no_extras = !allow; + } + /// Print a nice error message and return the exit code int exit(const Error& e) const { @@ -478,25 +491,25 @@ protected: } /// Selects a Classifer enum based on the type of the current argument - Classifer _recognize(std::string current) const { + detail::Classifer _recognize(std::string current) const { std::string dummy1, dummy2; if(current == "--") - return Classifer::POSITIONAL_MARK; + return detail::Classifer::POSITIONAL_MARK; for(const App_p &com : subcommands) { if(com->name == current) - return Classifer::SUBCOMMAND; + return detail::Classifer::SUBCOMMAND; } if(detail::split_long(current, dummy1, dummy2)) - return Classifer::LONG; + return detail::Classifer::LONG; if(detail::split_short(current, dummy1, dummy2)) - return Classifer::SHORT; - return Classifer::NONE; + return detail::Classifer::SHORT; + return detail::Classifer::NONE; } /// Internal parse function - void _parse(std::vector &args) { + std::vector& _parse(std::vector &args) { parsed = true; bool positional_only = false; @@ -504,23 +517,24 @@ protected: while(args.size()>0) { - Classifer classifer = positional_only ? Classifer::NONE : _recognize(args.back()); + detail::Classifer classifer = positional_only ? detail::Classifer::NONE : _recognize(args.back()); switch(classifer) { - case Classifer::POSITIONAL_MARK: + case detail::Classifer::POSITIONAL_MARK: + missing.emplace_back(classifer, args.back()); args.pop_back(); positional_only = true; break; - case Classifer::SUBCOMMAND: + case detail::Classifer::SUBCOMMAND: _parse_subcommand(args); break; - case Classifer::LONG: + case detail::Classifer::LONG: _parse_long(args); break; - case Classifer::SHORT: + case detail::Classifer::SHORT: _parse_short(args); break; - case Classifer::NONE: - positionals.push_back(args.back()); + case detail::Classifer::NONE: + missing.emplace_back(classifer, args.back()); args.pop_back(); } } @@ -531,11 +545,34 @@ protected: // Collect positionals - for(const Option_p& opt : options) { - while (opt->get_positional() && opt->count() < opt->get_expected() && positionals.size() > 0) { - opt->get_new(); - opt->add_result(0, positionals.front()); - positionals.pop_front(); + + // Loop over all positionals + for(int i=0; iget_positional() + && opt->count() < opt->get_expected() + && i < missing.size() + ) { + + // Skip options, only eat positionals + if(missing.at(i).first != detail::Classifer::NONE) { + i++; + continue; + } + + opt->get_new(); + opt->add_result(0, missing.at(i).second); + missing.erase(missing.begin() + i); // Remove option that was eaten + // Don't need to remove 1 from i since this while loop keeps reading i + } } } @@ -591,11 +628,22 @@ protected: if(required_subcommand && subcommand == nullptr) throw RequiredError("Subcommand required"); - if(positionals.size()>0) - throw PositionalError("[" + detail::join(positionals) + "]"); + // Convert missing (pairs) to extras (string only) + args.resize(missing.size()); + std::transform(std::begin(missing), std::end(missing), std::begin(args), + [](const std::pair& val){return val.second;}); + std::reverse(std::begin(args), std::end(args)); + + size_t num_left_over = std::count_if(std::begin(missing), std::end(missing), + [](std::pair& val){return val.first != detail::Classifer::POSITIONAL_MARK;}); + + if(num_left_over>0 && no_extras) + throw ExtrasError("[" + detail::join(args, " ") + "]"); pre_callback(); run_callback(); + + return args; } @@ -624,7 +672,7 @@ protected: auto op_ptr = std::find_if(std::begin(options), std::end(options), [name_](const Option_p &opt){return opt->check_sname(name_);}); if(op_ptr == std::end(options)) { - missing_options.push_back("-" + name_); + missing.emplace_back(detail::Classifer::SHORT, "-" + name_); return; } @@ -645,7 +693,7 @@ protected: if(num == -1) { - while(args.size()>0 && _recognize(args.back()) == Classifer::NONE) { + while(args.size()>0 && _recognize(args.back()) == detail::Classifer::NONE) { op->add_result(vnum, args.back()); args.pop_back(); } @@ -675,7 +723,7 @@ protected: auto op_ptr = std::find_if(std::begin(options), std::end(options), [name_](const Option_p &v){return v->check_lname(name_);}); if(op_ptr == std::end(options)) { - missing_options.push_back("--" + name_); + missing.emplace_back(detail::Classifer::LONG, "--" + name_); return; } @@ -699,7 +747,7 @@ protected: } if(num == -1) { - while(args.size() > 0 && _recognize(args.back()) == Classifer::NONE) { + while(args.size() > 0 && _recognize(args.back()) == detail::Classifer::NONE) { op->add_result(vnum, args.back()); args.pop_back(); } diff --git a/include/CLI/Error.hpp b/include/CLI/Error.hpp index 38239900..196a5bdb 100644 --- a/include/CLI/Error.hpp +++ b/include/CLI/Error.hpp @@ -95,9 +95,9 @@ struct ExcludesError : public ParseError { ExcludesError(std::string name, std::string subname) : ParseError("ExcludesError", name + " excludes " + subname, 14) {} }; -/// Thrown when too many positionals are found -struct PositionalError : public ParseError { - PositionalError(std::string name) : ParseError("PositionalError", name, 6) {} +/// Thrown when too many positionals or options are found +struct ExtrasError : public ParseError { + ExtrasError(std::string name) : ParseError("ExtrasError", name, 6) {} }; /// This is just a safety check to verify selection and parsing match diff --git a/tests/SimpleTest.cpp b/tests/SimpleTest.cpp index 01c5a85a..ea583e70 100644 --- a/tests/SimpleTest.cpp +++ b/tests/SimpleTest.cpp @@ -18,7 +18,7 @@ TEST(Basic, Empty) { { CLI::App app; input_t spare = {"spare"}; - EXPECT_THROW(app.parse(spare), CLI::PositionalError); + EXPECT_THROW(app.parse(spare), CLI::ExtrasError); } { CLI::App app; diff --git a/tests/SubcommandTest.cpp b/tests/SubcommandTest.cpp index db1aeb30..65e30a2e 100644 --- a/tests/SubcommandTest.cpp +++ b/tests/SubcommandTest.cpp @@ -89,13 +89,13 @@ TEST_F(SubcommandProgram, Working) { TEST_F(SubcommandProgram, Spare) { args = {"extra", "-d", "start", "-ffilename"}; - EXPECT_THROW(run(), CLI::PositionalError); + EXPECT_THROW(run(), CLI::ExtrasError); } TEST_F(SubcommandProgram, SpareSub) { args = {"-d", "start", "spare", "-ffilename"}; - EXPECT_THROW(run(), CLI::PositionalError); + EXPECT_THROW(run(), CLI::ExtrasError); }