diff --git a/CMakeLists.txt b/CMakeLists.txt index d8b6ed5..d6fac42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,4 +51,7 @@ install(EXPORT argument_parserTargets ) add_executable(test src/main.cpp) -target_link_libraries(test PRIVATE argument_parser) \ No newline at end of file +target_link_libraries(test PRIVATE argument_parser) + +add_executable(positional_tests src/test.cpp) +target_link_libraries(positional_tests PRIVATE argument_parser) \ No newline at end of file diff --git a/src/headers/parser/argument_parser.hpp b/src/headers/parser/argument_parser.hpp index c80b4d4..0776e46 100644 --- a/src/headers/parser/argument_parser.hpp +++ b/src/headers/parser/argument_parser.hpp @@ -202,11 +202,15 @@ namespace argument_parser { [[nodiscard]] bool is_invoked() const; [[nodiscard]] bool expects_parameter() const; [[nodiscard]] std::string get_help_text() const; + [[nodiscard]] bool is_positional() const; + [[nodiscard]] std::optional get_position_index() const; private: void set_required(bool val); void set_invoked(bool val); void set_help_text(std::string const &text); + void set_positional(bool val); + void set_position_index(std::optional idx); friend class base_parser; @@ -216,6 +220,8 @@ namespace argument_parser { bool required; bool invoked; std::string help_text; + bool positional = false; + std::optional position_index = std::nullopt; }; namespace helpers { @@ -260,6 +266,19 @@ namespace argument_parser { base_add_argument(short_arg, long_arg, help_text, required); } + template + void add_positional_argument(std::string const &name, std::string const &help_text, + parametered_action const &action, bool required, + std::optional position = std::nullopt) { + base_add_positional_argument(name, help_text, action, required, position); + } + + template + void add_positional_argument(std::string const &name, std::string const &help_text, bool required, + std::optional position = std::nullopt) { + base_add_positional_argument(name, help_text, required, position); + } + void on_complete(std::function const &action); template std::optional get_optional(std::string const &arg) const { @@ -302,7 +321,7 @@ namespace argument_parser { bool test_conventions(std::initializer_list convention_types, std::unordered_map &values_for_arguments, std::vector> &found_arguments, - std::optional &found_help, std::vector::iterator it, + std::optional &found_help, std::vector::iterator &it, std::stringstream &error_stream); void extract_arguments(std::initializer_list convention_types, std::unordered_map &values_for_arguments, @@ -315,8 +334,11 @@ namespace argument_parser { void enforce_creation_thread(); void assert_argument_not_exist(std::string const &short_arg, std::string const &long_arg) const; + void assert_positional_not_exist(std::string const &name) const; static void set_argument_status(bool is_required, std::string const &help_text, argument &arg); void place_argument(int id, argument const &arg, std::string const &short_arg, std::string const &long_arg); + void place_positional_argument(int id, argument const &arg, std::string const &name, + std::optional position); template void base_add_argument(std::string const &short_arg, std::string const &long_arg, std::string const &help_text, @@ -348,6 +370,33 @@ namespace argument_parser { } } + template + void base_add_positional_argument(std::string const &name, std::string const &help_text, + ActionType const &action, bool required, + std::optional position = std::nullopt) { + assert_positional_not_exist(name); + int id = id_counter.fetch_add(1); + argument arg(id, name, action); + set_argument_status(required, help_text, arg); + arg.set_positional(true); + arg.set_position_index(position); + place_positional_argument(id, arg, name, position); + } + + template + void base_add_positional_argument(std::string const &name, std::string const &help_text, bool required, + std::optional position = std::nullopt) { + assert_positional_not_exist(name); + int id = id_counter.fetch_add(1); + auto action = helpers::make_parametered_action( + [id, this](StoreType const &value) { stored_arguments[id] = std::any{value}; }); + argument arg(id, name, action); + set_argument_status(required, help_text, arg); + arg.set_positional(true); + arg.set_position_index(position); + place_positional_argument(id, arg, name, position); + } + void check_for_required_arguments(std::initializer_list convention_types); void fire_on_complete_events() const; @@ -360,6 +409,10 @@ namespace argument_parser { std::unordered_map long_arguments; std::unordered_map reverse_long_arguments; + std::vector positional_arguments; + std::unordered_map positional_name_map; + std::unordered_map reverse_positional_names; + std::initializer_list _current_conventions; internal::atomic::copyable_atomic creation_thread_id = std::this_thread::get_id(); diff --git a/src/headers/parser/parser_v2.hpp b/src/headers/parser/parser_v2.hpp index 57a5a5b..4454e86 100644 --- a/src/headers/parser/parser_v2.hpp +++ b/src/headers/parser/parser_v2.hpp @@ -15,7 +15,7 @@ #include namespace argument_parser::v2 { - enum class add_argument_flags { ShortArgument, LongArgument, HelpText, Action, Required }; + enum class add_argument_flags { ShortArgument, LongArgument, Positional, Position, HelpText, Action, Required }; namespace flags { constexpr static inline add_argument_flags ShortArgument = add_argument_flags::ShortArgument; @@ -23,12 +23,14 @@ namespace argument_parser::v2 { constexpr static inline add_argument_flags HelpText = add_argument_flags::HelpText; constexpr static inline add_argument_flags Action = add_argument_flags::Action; constexpr static inline add_argument_flags Required = add_argument_flags::Required; + constexpr static inline add_argument_flags Positional = add_argument_flags::Positional; + constexpr static inline add_argument_flags Position = add_argument_flags::Position; } // namespace flags class base_parser : private argument_parser::base_parser { public: - template using typed_flag_value = std::variant, bool>; - using non_typed_flag_value = std::variant; + template using typed_flag_value = std::variant, bool, int>; + using non_typed_flag_value = std::variant; template using typed_argument_pair = std::pair>; using non_typed_argument_pair = std::pair; @@ -102,6 +104,11 @@ namespace argument_parser::v2 { private: template void add_argument_impl(ArgsMap const &argument_pairs) { + if (argument_pairs.find(add_argument_flags::Positional) != argument_pairs.end()) { + add_positional_argument_impl(argument_pairs); + return; + } + std::unordered_map found_params{ {extended_add_argument_flags::IsTyped, IsTyped}}; @@ -206,6 +213,58 @@ namespace argument_parser::v2 { } } + template + void add_positional_argument_impl(ArgsMap const &argument_pairs) { + std::string positional_name = + get_or_throw(argument_pairs.at(add_argument_flags::Positional), "positional"); + + std::string help_text; + std::unique_ptr action; + bool required = false; + std::optional position = std::nullopt; + + if (argument_pairs.find(add_argument_flags::Action) != argument_pairs.end()) { + action = get_or_throw(argument_pairs.at(add_argument_flags::Action), "action").clone(); + } + if (argument_pairs.find(add_argument_flags::HelpText) != argument_pairs.end()) { + help_text = get_or_throw(argument_pairs.at(add_argument_flags::HelpText), "help"); + } + if (argument_pairs.find(add_argument_flags::Required) != argument_pairs.end() && + get_or_throw(argument_pairs.at(add_argument_flags::Required), "required")) { + required = true; + } + if (argument_pairs.find(add_argument_flags::Position) != argument_pairs.end()) { + position = get_or_throw(argument_pairs.at(add_argument_flags::Position), "position"); + } + + if (help_text.empty()) { + if constexpr (IsTyped) { + if constexpr (internal::sfinae::has_format_hint>::value && + internal::sfinae::has_purpose_hint>::value) { + auto format_hint = parsing_traits::parser_trait::format_hint; + auto purpose_hint = parsing_traits::parser_trait::purpose_hint; + help_text = + "Accepts " + std::string(purpose_hint) + " in " + std::string(format_hint) + " format."; + } else { + help_text = "Accepts value."; + } + } else { + help_text = "Accepts value."; + } + } + + if constexpr (IsTyped) { + if (action) { + base::add_positional_argument(positional_name, help_text, + *static_cast(&(*action)), required, position); + } else { + base::template add_positional_argument(positional_name, help_text, required, position); + } + } else { + base::template add_positional_argument(positional_name, help_text, required, position); + } + } + using base = argument_parser::base_parser; enum class extended_add_argument_flags { ShortArgument, LongArgument, Action, IsTyped }; @@ -250,4 +309,4 @@ namespace argument_parser::v2 { throw std::invalid_argument(std::string("variant type mismatch for key: ") + std::string(key)); } }; -} // namespace argument_parser::v2 \ No newline at end of file +} // namespace argument_parser::v2 diff --git a/src/main.cpp b/src/main.cpp index 5901896..801609f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -169,8 +169,29 @@ int v2Examples() { parser.add_argument({{ShortArgument, "v"}, {LongArgument, "verbose"}}); + parser.add_argument({ + {Positional, "input"}, + {HelpText, "Input file to process"}, + {Required, true}, + }); + + parser.add_argument({ + {Positional, "output"}, + {HelpText, "Output file path"}, + }); + parser.on_complete(::run_grep); parser.on_complete(::run_store_point); + parser.on_complete([](argument_parser::base_parser const &p) { + auto input = p.get_optional("input"); + auto output = p.get_optional("output"); + if (input) { + std::cout << "Input: " << input.value() << std::endl; + } + if (output) { + std::cout << "Output: " << output.value() << std::endl; + } + }); parser.handle_arguments(conventions); return 0; diff --git a/src/source/parser/argument_parser.cpp b/src/source/parser/argument_parser.cpp index 8c5f4a9..31e9507 100644 --- a/src/source/parser/argument_parser.cpp +++ b/src/source/parser/argument_parser.cpp @@ -32,7 +32,8 @@ namespace argument_parser { argument::argument(const argument &other) : id(other.id), name(other.name), action(other.action->clone()), required(other.required), - invoked(other.invoked), help_text(other.help_text) {} + invoked(other.invoked), help_text(other.help_text), positional(other.positional), + position_index(other.position_index) {} argument &argument::operator=(const argument &other) { if (this != &other) { @@ -42,6 +43,8 @@ namespace argument_parser { required = other.required; invoked = other.invoked; help_text = other.help_text; + positional = other.positional; + position_index = other.position_index; } return *this; } @@ -78,6 +81,22 @@ namespace argument_parser { help_text = text; } + bool argument::is_positional() const { + return positional; + } + + std::optional argument::get_position_index() const { + return position_index; + } + + void argument::set_positional(bool val) { + positional = val; + } + + void argument::set_position_index(std::optional idx) { + position_index = idx; + } + void base_parser::on_complete(std::function const &handler) { on_complete_events.emplace_back(handler); } @@ -85,7 +104,22 @@ namespace argument_parser { std::string base_parser::build_help_text(std::initializer_list convention_types) const { std::stringstream ss; - ss << "Usage: " << program_name << " [OPTIONS]...\n"; + ss << "Usage: " << program_name << " [OPTIONS]..."; + + for (auto const &pos_id : positional_arguments) { + if (pos_id == -1) + continue; + auto name_it = reverse_positional_names.find(pos_id); + if (name_it == reverse_positional_names.end()) + continue; + auto const &arg = argument_map.at(pos_id); + if (arg.is_required()) { + ss << " <" << name_it->second << ">"; + } else { + ss << " [" << name_it->second << "]"; + } + } + ss << "\n"; size_t max_short_len = 0; size_t max_long_len = 0; @@ -97,6 +131,9 @@ namespace argument_parser { std::vector help_lines; for (auto const &[id, arg] : argument_map) { + if (arg.is_positional()) + continue; + auto short_arg = reverse_short_arguments.find(id) != reverse_short_arguments.end() ? reverse_short_arguments.at(id) : ""; auto long_arg = @@ -124,17 +161,46 @@ namespace argument_parser { help_lines.push_back({parts, arg.help_text}); } - for (auto const &line : help_lines) { - ss << "\t"; - for (size_t i = 0; i < line.convention_parts.size(); ++i) { - auto const &parts = line.convention_parts[i]; - if (i > 0) { - ss << " "; + if (!help_lines.empty()) { + for (auto const &line : help_lines) { + ss << "\t"; + for (size_t i = 0; i < line.convention_parts.size(); ++i) { + auto const &parts = line.convention_parts[i]; + if (i > 0) { + ss << " "; + } + ss << std::left << std::setw(static_cast(max_short_len)) << parts.first << " " + << std::setw(static_cast(max_long_len)) << parts.second; } - ss << std::left << std::setw(static_cast(max_short_len)) << parts.first << " " - << std::setw(static_cast(max_long_len)) << parts.second; + ss << "\t" << line.desc << "\n"; + } + } + + if (!positional_arguments.empty()) { + ss << "\nPositional arguments:\n"; + size_t max_pos_name_len = 0; + for (auto const &pos_id : positional_arguments) { + if (pos_id == -1) + continue; + auto name_it = reverse_positional_names.find(pos_id); + if (name_it != reverse_positional_names.end()) { + size_t display_len = name_it->second.length() + 2; // for < > + if (display_len > max_pos_name_len) + max_pos_name_len = display_len; + } + } + + for (auto const &pos_id : positional_arguments) { + if (pos_id == -1) + continue; + auto name_it = reverse_positional_names.find(pos_id); + if (name_it == reverse_positional_names.end()) + continue; + auto const &arg = argument_map.at(pos_id); + std::string display_name = "<" + name_it->second + ">"; + ss << "\t" << std::left << std::setw(static_cast(max_pos_name_len)) << display_name << "\t" + << arg.get_help_text() << "\n"; } - ss << "\t" << line.desc << "\n"; } return ss.str(); @@ -169,7 +235,7 @@ namespace argument_parser { bool base_parser::test_conventions(std::initializer_list convention_types, std::unordered_map &values_for_arguments, std::vector> &found_arguments, - std::optional &found_help, std::vector::iterator it, + std::optional &found_help, std::vector::iterator &it, std::stringstream &error_stream) { std::string current_argument = *it; @@ -214,13 +280,43 @@ namespace argument_parser { std::vector> &found_arguments, std::optional &found_help) { + size_t next_positional_index = 0; + bool force_positional = false; + for (auto it = parsed_arguments.begin(); it != parsed_arguments.end(); ++it) { + if (*it == "--") { + force_positional = true; + continue; + } + + if (force_positional) { + if (next_positional_index >= positional_arguments.size()) { + throw std::runtime_error("Unexpected positional argument: \"" + *it + "\""); + } + int arg_id = positional_arguments[next_positional_index]; + argument &pos_arg = argument_map.at(arg_id); + std::string const &pos_name = reverse_positional_names.at(arg_id); + found_arguments.emplace_back(pos_name, pos_arg); + values_for_arguments[pos_name] = *it; + next_positional_index++; + continue; + } + std::stringstream error_stream; if (!test_conventions(convention_types, values_for_arguments, found_arguments, found_help, it, error_stream)) { - throw std::runtime_error("All trials for argument: \n\t\"" + *it + "\"\n failed with: \n" + - error_stream.str()); + if (next_positional_index < positional_arguments.size()) { + int arg_id = positional_arguments[next_positional_index]; + argument &pos_arg = argument_map.at(arg_id); + std::string const &pos_name = reverse_positional_names.at(arg_id); + found_arguments.emplace_back(pos_name, pos_arg); + values_for_arguments[pos_name] = *it; + next_positional_index++; + } else { + throw std::runtime_error("All trials for argument: \n\t\"" + *it + "\"\n failed with: \n" + + error_stream.str()); + } } } } @@ -254,6 +350,7 @@ namespace argument_parser { value.action->invoke(); } value.set_invoked(true); + argument_map.at(value.id).set_invoked(true); } catch (const std::runtime_error &e) { std::string err{e.what()}; err = replace_var(err, "KEY", "for " + key); @@ -295,6 +392,11 @@ namespace argument_parser { return long_pos->second; if (short_post != short_arguments.end()) return short_post->second; + + auto pos_it = positional_name_map.find(arg); + if (pos_it != positional_name_map.end()) + return pos_it->second; + return std::nullopt; } @@ -322,6 +424,36 @@ namespace argument_parser { } } + void base_parser::assert_positional_not_exist(std::string const &name) const { + if (positional_name_map.find(name) != positional_name_map.end()) { + throw std::runtime_error("Positional argument with name '" + name + "' already exists!"); + } + } + + void base_parser::place_positional_argument(int id, argument const &arg, std::string const &name, + std::optional position) { + argument_map[id] = arg; + positional_name_map[name] = id; + reverse_positional_names[id] = name; + + if (position.has_value()) { + auto idx = static_cast(position.value()); + if (idx > positional_arguments.size()) { + positional_arguments.resize(idx + 1, -1); + } + if (idx < positional_arguments.size() && positional_arguments[idx] != -1) { + throw std::runtime_error("Position " + std::to_string(idx) + " is already occupied!"); + } + if (idx == positional_arguments.size()) { + positional_arguments.push_back(id); + } else { + positional_arguments[idx] = id; + } + } else { + positional_arguments.push_back(id); + } + } + std::string get_one_name(std::string const &short_name, std::string const &long_name) { std::string res{}; if (short_name != "-") { @@ -340,43 +472,52 @@ namespace argument_parser { void base_parser::check_for_required_arguments( std::initializer_list convention_types) { - std::vector> required_args; + std::vector> required_args; for (auto const &[key, arg] : argument_map) { if (arg.is_required() && !arg.is_invoked()) { - auto short_arg = reverse_short_arguments.find(key) != reverse_short_arguments.end() - ? reverse_short_arguments.at(key) - : "-"; - auto long_arg = reverse_long_arguments.find(key) != reverse_long_arguments.end() - ? reverse_long_arguments.at(key) - : "-"; - - required_args.emplace_back>( - {short_arg, long_arg, arg.expects_parameter()}); + if (arg.is_positional()) { + auto pos_name = reverse_positional_names.find(key) != reverse_positional_names.end() + ? reverse_positional_names.at(key) + : "unknown"; + required_args.emplace_back(pos_name, "", true, true); + } else { + auto short_arg = reverse_short_arguments.find(key) != reverse_short_arguments.end() + ? reverse_short_arguments.at(key) + : "-"; + auto long_arg = reverse_long_arguments.find(key) != reverse_long_arguments.end() + ? reverse_long_arguments.at(key) + : "-"; + required_args.emplace_back(short_arg, long_arg, arg.expects_parameter(), false); + } } } if (!required_args.empty()) { std::cerr << "These arguments were expected but not provided: \n"; - for (auto const &[s, l, p] : required_args) { - std::cerr << "\t" << get_one_name(s, l) << ": must be provided as one of ["; - for (auto it = convention_types.begin(); it != convention_types.end(); ++it) { - auto generatedParts = (*it)->make_help_text(s, l, p); - std::string help_str = generatedParts.first; - if (!generatedParts.first.empty() && !generatedParts.second.empty()) { - help_str += " "; - } - help_str += generatedParts.second; + for (auto const &[s, l, p, is_pos] : required_args) { + if (is_pos) { + std::cerr << "\t<" << s << ">: positional argument must be provided\n"; + } else { + std::cerr << "\t" << get_one_name(s, l) << ": must be provided as one of ["; + for (auto it = convention_types.begin(); it != convention_types.end(); ++it) { + auto generatedParts = (*it)->make_help_text(s, l, p); + std::string help_str = generatedParts.first; + if (!generatedParts.first.empty() && !generatedParts.second.empty()) { + help_str += " "; + } + help_str += generatedParts.second; - size_t last_not_space = help_str.find_last_not_of(" \t"); - if (last_not_space != std::string::npos) { - help_str.erase(last_not_space + 1); - } - std::cerr << help_str; - if (it + 1 != convention_types.end()) { - std::cerr << ", "; + size_t last_not_space = help_str.find_last_not_of(" \t"); + if (last_not_space != std::string::npos) { + help_str.erase(last_not_space + 1); + } + std::cerr << help_str; + if (it + 1 != convention_types.end()) { + std::cerr << ", "; + } } + std::cerr << "]\n"; } - std::cerr << "]\n"; } std::cerr << "\n"; display_help(convention_types); diff --git a/src/test.cpp b/src/test.cpp new file mode 100644 index 0000000..6a2abe6 --- /dev/null +++ b/src/test.cpp @@ -0,0 +1,420 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +const std::initializer_list conventions = { + &argument_parser::conventions::gnu_argument_convention, + &argument_parser::conventions::gnu_equal_argument_convention, +}; + +namespace v2_test { + class fake_parser : public argument_parser::v2::base_parser { + public: + fake_parser(std::string const &program_name, std::initializer_list const &arguments) { + set_program_name(program_name); + ref_parsed_args() = std::vector(arguments); + prepare_help_flag(); + } + }; +} // namespace v2_test + +int tests_run = 0; +int tests_passed = 0; + +void test_result(const char *name, bool passed) { + tests_run++; + if (passed) { + tests_passed++; + std::cout << " [PASS] " << name << std::endl; + } else { + std::cout << " [FAIL] " << name << std::endl; + } +} + +// ============================================================ +// V1 Tests (using argument_parser::fake_parser) +// ============================================================ + +void test_v1_single_positional_store() { + argument_parser::fake_parser parser("test", {"hello"}); + parser.add_positional_argument("greeting", "A greeting", false); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("greeting"); + test_result("v1: single positional store", val.has_value() && val.value() == "hello"); +} + +void test_v1_multiple_positionals_ordered() { + argument_parser::fake_parser parser("test", {"alpha", "beta", "gamma"}); + parser.add_positional_argument("first", "First arg", false); + parser.add_positional_argument("second", "Second arg", false); + parser.add_positional_argument("third", "Third arg", false); + parser.handle_arguments(conventions); + + auto first = parser.get_optional("first"); + auto second = parser.get_optional("second"); + auto third = parser.get_optional("third"); + + bool ok = first.has_value() && first.value() == "alpha" && second.has_value() && second.value() == "beta" && + third.has_value() && third.value() == "gamma"; + test_result("v1: multiple positionals preserve order", ok); +} + +void test_v1_positional_with_explicit_position() { + argument_parser::fake_parser parser("test", {"first_val", "second_val"}); + parser.add_positional_argument("second", "Second", false, 1); + parser.add_positional_argument("first", "First", false, 0); + parser.handle_arguments(conventions); + + auto first = parser.get_optional("first"); + auto second = parser.get_optional("second"); + + bool ok = first.has_value() && first.value() == "first_val" && second.has_value() && second.value() == "second_val"; + test_result("v1: explicit position index", ok); +} + +void test_v1_positional_typed_int() { + argument_parser::fake_parser parser("test", {"42"}); + parser.add_positional_argument("count", "A count", false); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("count"); + test_result("v1: positional with int type", val.has_value() && val.value() == 42); +} + +void test_v1_positional_with_action() { + std::string captured; + argument_parser::fake_parser parser("test", {"world"}); + + auto action = + argument_parser::helpers::make_parametered_action([&](std::string const &v) { captured = v; }); + parser.add_positional_argument("name", "A name", action, false); + parser.handle_arguments(conventions); + + test_result("v1: positional with action", captured == "world"); +} + +void test_v1_mixed_named_and_positional() { + argument_parser::fake_parser parser("test", {"--verbose", "true", "myfile.txt"}); + parser.add_argument("v", "verbose", "Verbose mode", false); + parser.add_positional_argument("file", "Input file", false); + parser.handle_arguments(conventions); + + auto verbose = parser.get_optional("verbose"); + auto file = parser.get_optional("file"); + + bool ok = verbose.has_value() && verbose.value() == true && file.has_value() && file.value() == "myfile.txt"; + test_result("v1: mixed named and positional args", ok); +} + +void test_v1_positional_after_named() { + argument_parser::fake_parser parser("test", {"-n", "5", "output.txt"}); + parser.add_argument("n", "number", "A number", false); + parser.add_positional_argument("output", "Output file", false); + parser.handle_arguments(conventions); + + auto number = parser.get_optional("number"); + auto output = parser.get_optional("output"); + + bool ok = number.has_value() && number.value() == 5 && output.has_value() && output.value() == "output.txt"; + test_result("v1: positional after named args", ok); +} + +void test_v1_positional_between_named() { + argument_parser::fake_parser parser("test", {"-a", "1", "positional_val", "--beta", "2"}); + parser.add_argument("a", "alpha", "Alpha", false); + parser.add_argument("b", "beta", "Beta", false); + parser.add_positional_argument("middle", "Middle arg", false); + parser.handle_arguments(conventions); + + auto alpha = parser.get_optional("alpha"); + auto beta = parser.get_optional("beta"); + auto middle = parser.get_optional("middle"); + + bool ok = alpha.has_value() && alpha.value() == 1 && beta.has_value() && beta.value() == 2 && middle.has_value() && + middle.value() == "positional_val"; + test_result("v1: positional between named args", ok); +} + +void test_v1_double_dash_separator() { + argument_parser::fake_parser parser("test", {"--", "-not-a-flag"}); + parser.add_positional_argument("item", "An item", false); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("item"); + test_result("v1: -- separator treats next as positional", val.has_value() && val.value() == "-not-a-flag"); +} + +void test_v1_double_dash_multiple() { + argument_parser::fake_parser parser("test", {"--name", "hello", "--", "--weird", "-x"}); + parser.add_argument("n", "name", "A name", false); + parser.add_positional_argument("first", "First", false); + parser.add_positional_argument("second", "Second", false); + parser.handle_arguments(conventions); + + auto name = parser.get_optional("name"); + auto first = parser.get_optional("first"); + auto second = parser.get_optional("second"); + + bool ok = name.has_value() && name.value() == "hello" && first.has_value() && first.value() == "--weird" && + second.has_value() && second.value() == "-x"; + test_result("v1: -- separator with multiple positionals", ok); +} + +void test_v1_required_positional_missing() { + argument_parser::fake_parser parser("test", {}); + parser.add_positional_argument("file", "A file", true); + + bool threw = false; + try { + // check_for_required_arguments calls std::exit(1) so we can't easily test it + // instead, test that handle_arguments doesn't crash when positionals are provided + parser.handle_arguments(conventions); + } catch (...) { + threw = true; + } + // Note: required check calls std::exit(1), so if we get here the arg wasn't required-checked + // This test just verifies setup doesn't crash. The exit behavior is tested manually. + test_result("v1: required positional setup (no crash)", true); +} + +void test_v1_unexpected_positional_throws() { + argument_parser::fake_parser parser("test", {"unexpected"}); + // no positional args defined, but a bare token is provided + + bool threw = false; + try { + parser.handle_arguments(conventions); + } catch (const std::runtime_error &) { + threw = true; + } + test_result("v1: unexpected positional throws", threw); +} + +void test_v1_duplicate_positional_name_throws() { + argument_parser::fake_parser parser("test", {"a", "b"}); + parser.add_positional_argument("file", "A file", false); + + bool threw = false; + try { + parser.add_positional_argument("file", "Duplicate", false); + } catch (const std::runtime_error &) { + threw = true; + } + test_result("v1: duplicate positional name throws", threw); +} + +void test_v1_positional_on_complete() { + std::string captured_file; + argument_parser::fake_parser parser("test", {"data.csv"}); + parser.add_positional_argument("file", "Input file", false); + parser.on_complete([&](argument_parser::base_parser const &p) { + auto val = p.get_optional("file"); + if (val) + captured_file = val.value(); + }); + parser.handle_arguments(conventions); + + test_result("v1: positional accessible in on_complete", captured_file == "data.csv"); +} + +// ============================================================ +// V2 Tests (using v2_test::fake_parser) +// ============================================================ + +void test_v2_single_positional() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"hello"}); + + parser.add_argument({{Positional, "greeting"}, {HelpText, "A greeting"}}); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("greeting"); + test_result("v2: single positional store", val.has_value() && val.value() == "hello"); +} + +void test_v2_positional_required() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"value"}); + + parser.add_argument({{Positional, "arg"}, {Required, true}, {HelpText, "Required arg"}}); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("arg"); + test_result("v2: required positional", val.has_value() && val.value() == "value"); +} + +void test_v2_positional_with_position() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"first_val", "second_val"}); + + parser.add_argument({{Positional, "second"}, {Position, 1}, {HelpText, "Second"}}); + parser.add_argument({{Positional, "first"}, {Position, 0}, {HelpText, "First"}}); + parser.handle_arguments(conventions); + + auto first = parser.get_optional("first"); + auto second = parser.get_optional("second"); + + bool ok = first.has_value() && first.value() == "first_val" && second.has_value() && second.value() == "second_val"; + test_result("v2: positional with explicit Position", ok); +} + +void test_v2_positional_typed_int() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"99"}); + + parser.add_argument({{Positional, "count"}, {HelpText, "A count"}}); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("count"); + test_result("v2: positional with int type", val.has_value() && val.value() == 99); +} + +void test_v2_mixed_named_and_positional() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"--output", "out.txt", "input.txt"}); + + parser.add_argument({{ShortArgument, "o"}, {LongArgument, "output"}, {HelpText, "Output file"}}); + parser.add_argument({{Positional, "input"}, {HelpText, "Input file"}}); + parser.handle_arguments(conventions); + + auto output = parser.get_optional("output"); + auto input = parser.get_optional("input"); + + bool ok = output.has_value() && output.value() == "out.txt" && input.has_value() && input.value() == "input.txt"; + test_result("v2: mixed named and positional", ok); +} + +void test_v2_positional_with_action() { + using namespace argument_parser::v2::flags; + std::string captured; + v2_test::fake_parser parser("test", {"world"}); + + parser.add_argument({{Positional, "name"}, + {Action, argument_parser::helpers::make_parametered_action( + [&](std::string const &v) { captured = v; })}, + {HelpText, "A name"}}); + parser.handle_arguments(conventions); + + test_result("v2: positional with action", captured == "world"); +} + +void test_v2_double_dash_separator() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"--", "--not-a-flag"}); + + parser.add_argument({{Positional, "item"}, {HelpText, "An item"}}); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("item"); + test_result("v2: -- separator", val.has_value() && val.value() == "--not-a-flag"); +} + +void test_v2_positional_auto_help_text() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"42"}); + + // no HelpText provided — should auto-generate from traits + parser.add_argument({{Positional, "count"}}); + parser.handle_arguments(conventions); + + auto val = parser.get_optional("count"); + test_result("v2: positional auto help text (no crash)", val.has_value() && val.value() == 42); +} + +void test_v2_multiple_positionals_and_named() { + using namespace argument_parser::v2::flags; + v2_test::fake_parser parser("test", {"-v", "src.txt", "dst.txt"}); + + parser.add_argument({{ShortArgument, "v"}, {LongArgument, "verbose"}}); + parser.add_argument({{Positional, "source"}, {HelpText, "Source"}}); + parser.add_argument({{Positional, "destination"}, {HelpText, "Destination"}}); + parser.handle_arguments(conventions); + + auto verbose = parser.get_optional("verbose"); + auto source = parser.get_optional("source"); + auto dest = parser.get_optional("destination"); + + bool ok = verbose.has_value() && source.has_value() && source.value() == "src.txt" && dest.has_value() && + dest.value() == "dst.txt"; + test_result("v2: multiple positionals with named flag", ok); +} + +void test_v2_on_complete_with_positional() { + using namespace argument_parser::v2::flags; + std::string captured; + v2_test::fake_parser parser("test", {"payload"}); + + parser.add_argument({{Positional, "data"}, {HelpText, "Data"}}); + parser.on_complete([&](argument_parser::base_parser const &p) { + auto val = p.get_optional("data"); + if (val) + captured = val.value(); + }); + parser.handle_arguments(conventions); + + test_result("v2: positional accessible in on_complete", captured == "payload"); +} + +// ============================================================ +// Main +// ============================================================ + +int main() { + std::cout << "=== V1 Positional Argument Tests ===" << std::endl; + + std::array, 13> v1Tests { + test_v1_single_positional_store, + test_v1_multiple_positionals_ordered, + test_v1_positional_with_explicit_position, + test_v1_positional_typed_int, + test_v1_positional_with_action, + test_v1_mixed_named_and_positional, + test_v1_positional_after_named, + test_v1_positional_between_named, + test_v1_double_dash_separator, + test_v1_double_dash_multiple, + test_v1_unexpected_positional_throws, + test_v1_duplicate_positional_name_throws, + test_v1_positional_on_complete + }; + + for (auto const& test : v1Tests) { + try { + test(); + } catch(std::exception const& e) { + std::cout << "test failed: " << e.what() << std::endl; + } + } + + std::cout << "\n=== V2 Positional Argument Tests ===" << std::endl; + std::array, 10> v2Tests{ + test_v2_single_positional, + test_v2_positional_required, + test_v2_positional_with_position, + test_v2_positional_typed_int, + test_v2_mixed_named_and_positional, + test_v2_positional_with_action, + test_v2_double_dash_separator, + test_v2_positional_auto_help_text, + test_v2_multiple_positionals_and_named, + test_v2_on_complete_with_positional + }; + + + for (auto const& test : v2Tests) { + try { + test(); + } catch(std::exception const& e) { + std::cout << "test failed: " << e.what() << std::endl; + } + } + + std::cout << "\n=== Results: " << tests_passed << "/" << tests_run << " passed ===" << std::endl; + return (tests_passed == tests_run) ? 0 : 1; +}