diff --git a/cmdopt2/args.hpp b/cmdopt2/args.hpp index 758dfe2c..b0680a24 100644 --- a/cmdopt2/args.hpp +++ b/cmdopt2/args.hpp @@ -22,7 +22,7 @@ namespace cruft::cmdopt2 { struct argument { std::string name; std::optional description; - bool required = false; + bool required_ = false; using acceptor1_t = std::function; std::optional acceptor1; @@ -75,6 +75,18 @@ namespace cruft::cmdopt2 { res.acceptor1 = _acceptor; return res; } + + bool required (void) const + { + return required_; + } + + BaseT required (bool val) const + { + BaseT res = get (); + res.required_ = val; + return res; + } }; diff --git a/cmdopt2/parser.cpp b/cmdopt2/parser.cpp index b2fffd2b..7de99e4f 100644 --- a/cmdopt2/parser.cpp +++ b/cmdopt2/parser.cpp @@ -25,7 +25,7 @@ parser::add (keyword const &arg)& /////////////////////////////////////////////////////////////////////////////// int -parser::parse_named (int argc, const char *const *argv) const +parser::parse_named (int argc, const char *const *argv, int *found) const { auto cursor = argv[0]; if (!cursor or !*cursor) @@ -40,9 +40,9 @@ parser::parse_named (int argc, const char *const *argv) const ++cursor; if (!*cursor) throw std::runtime_error ("missing long argument name"); - return parse_long (argc, argv); + return parse_long (argc, argv, found); } else { - return parse_short (argc, argv); + return parse_short (argc, argv, found); } unreachable (); @@ -51,7 +51,7 @@ parser::parse_named (int argc, const char *const *argv) const //----------------------------------------------------------------------------- int -parser::parse_long (int argc, const char *const *argv) const +parser::parse_long (int argc, const char *const *argv, int *found) const { CHECK (argc >= 1); CHECK (strlen (argv[0]) >= 2); @@ -71,6 +71,8 @@ parser::parse_long (int argc, const char *const *argv) const if (pos == m_keyword.end ()) throw std::runtime_error ("Unknown long argument"); + found[std::distance (m_keyword.begin (), pos)] = true; + if (eq) { (*pos->acceptor1) (eq + 1); return 1; @@ -98,7 +100,7 @@ parser::parse_long (int argc, const char *const *argv) const //----------------------------------------------------------------------------- int -parser::parse_short (int argc, const char *const *argv) const +parser::parse_short (int argc, const char *const *argv, int *found) const { (void)argc; CHECK (argc >= 1); @@ -123,17 +125,23 @@ parser::parse_short (int argc, const char *const *argv) const if (pos == m_keyword.end ()) throw std::runtime_error ("Unknown short argument"); + found[std::distance (m_keyword.begin (), pos)] = true; + if (pos->acceptor0) { CHECK (!pos->acceptor1); (*pos->acceptor0) (); return 1; } - CHECK (!pos->acceptor0); - if (argc < 2) - throw std::runtime_error ("Missing short argument value"); - (*pos->acceptor1) (argv[1]); - return 2; + if (pos->acceptor1) { + CHECK (!pos->acceptor0); + if (argc < 2) + throw std::runtime_error ("Missing short argument value"); + (*pos->acceptor1) (argv[1]); + return 2; + } + + return 1; } // Handle strings of short arguments, eg: "-vvv" @@ -147,11 +155,13 @@ parser::parse_short (int argc, const char *const *argv) const }); if (pos == m_keyword.end ()) throw std::runtime_error ("Unknown short argument"); + found[std::distance (m_keyword.begin (), pos)] = true; if (pos->acceptor1) throw std::runtime_error ("Missing short argument value"); - (*pos->acceptor0) (); + if (pos->acceptor0) + (*pos->acceptor0) (); } return 1; @@ -162,8 +172,8 @@ parser::parse_short (int argc, const char *const *argv) const int parser::parse (int const argc, const char *const *argv) { - if (argc <= 1) - return argc; + std::vector found_positional (m_positional.size (), 0); + std::vector found_keyword (m_keyword.size (), 0); using namespace std::string_view_literals; if (contains ("--help"sv, cruft::view (argv, argc))) { @@ -180,13 +190,28 @@ parser::parse (int const argc, const char *const *argv) break; if (*arg == '-') { - arg_cursor += parse_named (argc - arg_cursor, argv + arg_cursor); + arg_cursor += parse_named (argc - arg_cursor, argv + arg_cursor, found_keyword.data ()); continue; } - (*m_positional[pos_cursor].acceptor1) (argv[arg_cursor++]); + found_positional[pos_cursor] = true; + auto &pos_arg = m_positional[pos_cursor]; + if (pos_arg.acceptor1) + (*pos_arg.acceptor1) (argv[arg_cursor++]); } + for (int i = 0, last = std::ssize (m_positional); i < last; ++i) + if (m_positional[i].required () and !found_positional[i]) + throw std::runtime_error ( + fmt::format ("Missing required position argument {}", m_positional[i].name) + ); + + for (int i = 0, last = std::ssize (m_keyword); i < last; ++i) + if (m_keyword[i].required () and !found_keyword[i]) + throw std::runtime_error ( + fmt::format ("Missing required named argument {}", m_keyword[i].name) + ); + return arg_cursor; } @@ -209,7 +234,7 @@ usage ( static char constexpr REQUIRED[2] { '<', '>' }; for (auto const &arg: keyword) { - auto const &delimiters = arg.required ? REQUIRED : OPTIONAL; + auto const &delimiters = arg.required () ? REQUIRED : OPTIONAL; if (arg.short_) { fmt::print (output, FMT_STRING (" {}-{}"), delimiters[0], *arg.short_); @@ -227,7 +252,7 @@ usage ( } for (auto const &arg: positional) { - auto const &delimiters = arg.required ? REQUIRED : OPTIONAL; + auto const &delimiters = arg.required () ? REQUIRED : OPTIONAL; fmt::print (output, FMT_STRING (" {}{}{}"), delimiters[0], arg.name, delimiters[1]); } diff --git a/cmdopt2/parser.hpp b/cmdopt2/parser.hpp index 333b1d10..9bfbafce 100644 --- a/cmdopt2/parser.hpp +++ b/cmdopt2/parser.hpp @@ -17,7 +17,7 @@ namespace cruft::cmdopt2 { class parser { public: - int parse (int argc, char const* const* argv); + int parse [[nodiscard]] (int argc, char const* const* argv); positional& add (positional const&) &; keyword& add (keyword const&) &; @@ -26,9 +26,9 @@ namespace cruft::cmdopt2 { void usage (int argc, char const * const* argv, std::ostream&) const; private: - int parse_named (int argc, char const* const* argv) const; - int parse_short (int argc, char const* const* argv) const; - int parse_long (int argc, char const* const* argv) const; + int parse_named (int argc, char const* const* argv, int*) const; + int parse_short (int argc, char const* const* argv, int*) const; + int parse_long (int argc, char const* const* argv, int*) const; std::vector m_positional; std::vector m_keyword; diff --git a/test/cmdopt2.cpp b/test/cmdopt2.cpp index 87d3ca1f..d8df85dd 100644 --- a/test/cmdopt2.cpp +++ b/test/cmdopt2.cpp @@ -93,10 +93,13 @@ test_combinations (cruft::TAP::logger &tap) qux = decltype(qux) {}; verbose = false; - p.parse (int (t.args.size ()), t.args.data ()); + auto const consumed = p.parse (int (t.args.size ()), t.args.data ()); tap.expect ( - foo == t.foo and bar == t.bar and qux == t.qux, + consumed == int (t.args.size ()) + and foo == t.foo + and bar == t.bar + and qux == t.qux, "{}", fmt::join (t.args.begin (), t.args.end (), " ") ); } @@ -171,21 +174,61 @@ static void test_presence (cruft::TAP::logger &tap) count = 0; present = false; - p.parse (int (t.args.size ()), t.args.data ()); + auto const consumed = p.parse (int (t.args.size ()), t.args.data ()); tap.expect ( - count == t.count and present == t.present, + consumed == int (t.args.size ()) + and count == t.count + and present == t.present, "{}", fmt::join (t.args.begin (), t.args.end (), " ") ); } } +/////////////////////////////////////////////////////////////////////////////// +static void +test_required (cruft::TAP::logger &tap) +{ + static const struct { + std::vector args; + bool success; + } TESTS[] = { + { .args = { "cmd", }, .success = false }, + { .args = { "cmd", "-y" }, .success = true }, + { .args = { "cmd", "-n" }, .success = false }, + }; + + using namespace cruft::cmdopt2; + + parser p; + p.add (keyword::create ("y").flag ('y').required (true ).ignore ()); + p.add (keyword::create ("n").flag ('n').required (false).ignore ()); + + for (auto const &t: TESTS) { + bool success; + try { + auto const consumed = p.parse (int (t.args.size ()), t.args.data ()); + bool const completed = consumed == int (t.args.size ()); + success = completed == t.success; + } catch (...) { + success = not t.success; + } + tap.expect ( + success, + "{}", + fmt::join (t.args.begin (), t.args.end (), " ") + ); + } +} + + /////////////////////////////////////////////////////////////////////////////// int main () { cruft::TAP::logger tap; test_combinations (tap); test_presence (tap); + test_required (tap); return tap.status (); } \ No newline at end of file