From 158bc56e1b57b544e876f1f8dbbacfd135278b7a Mon Sep 17 00:00:00 2001 From: Damian Meden Date: Mon, 2 Mar 2026 12:06:35 +0000 Subject: [PATCH] ArgParser: apply default values after dependency validation validate_dependencies() incorrectly triggers on options that have default values but were not explicitly passed by the user. The root cause is that append_option_data() populates default values into the Arguments map before validate_dependencies() runs. When validate_dependencies() calls ret.get(key) for an option with a default, the lookup finds the entry and sets _is_called = true, making the option appear "used" even though the user never specified it on the command line. Fix by extracting the default-value loop into apply_option_defaults() and calling it after validate_dependencies() in parse(). --- include/tscore/ArgParser.h | 2 + src/tscore/ArgParser.cc | 14 ++++- src/tscore/unit_tests/test_ArgParser.cc | 69 +++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/include/tscore/ArgParser.h b/include/tscore/ArgParser.h index a4bd5916feb..fdb6086eba0 100644 --- a/include/tscore/ArgParser.h +++ b/include/tscore/ArgParser.h @@ -226,6 +226,8 @@ class ArgParser void validate_mutex_groups(Arguments &ret) const; // Helper method to validate option dependencies void validate_dependencies(Arguments &ret) const; + // Helper method to apply default values for options not explicitly set + void apply_option_defaults(Arguments &ret) const; // The command name and help message std::string _name; std::string _description; diff --git a/src/tscore/ArgParser.cc b/src/tscore/ArgParser.cc index 65298317a61..90c3c2f5e43 100644 --- a/src/tscore/ArgParser.cc +++ b/src/tscore/ArgParser.cc @@ -725,7 +725,14 @@ ArgParser::Command::append_option_data(Arguments &ret, AP_StrVec &args, int inde help_message(std::to_string(_option_list.at(it.first).arg_num) + " arguments expected by " + it.first); } } - // put in the default value of options +} + +// Apply default values for options not explicitly set by the user. +// This must be called AFTER validate_dependencies() so that default values +// (e.g. --timeout "0") don't falsely trigger dependency checks. +void +ArgParser::Command::apply_option_defaults(Arguments &ret) const +{ for (const auto &it : _option_list) { if (!it.second.default_value.empty() && ret.get(it.second.key).empty()) { std::istringstream ss(it.second.default_value); @@ -771,6 +778,11 @@ ArgParser::Command::parse(Arguments &ret, AP_StrVec &args) // Validate option dependencies validate_dependencies(ret); + + // Apply default values after validation so that defaults don't + // trigger dependency checks (e.g. --timeout with default "0" + // should not require --monitor when not explicitly used). + apply_option_defaults(ret); } if (command_called) { diff --git a/src/tscore/unit_tests/test_ArgParser.cc b/src/tscore/unit_tests/test_ArgParser.cc index 672a702a288..0a32502e964 100644 --- a/src/tscore/unit_tests/test_ArgParser.cc +++ b/src/tscore/unit_tests/test_ArgParser.cc @@ -148,3 +148,72 @@ TEST_CASE("Invoke test", "[invoke]") parsed_data.invoke(); REQUIRE(global == 2); } + +TEST_CASE("Case sensitive short options", "[parse]") +{ + ts::ArgParser cs_parser; + cs_parser.add_global_usage("test_prog [--SWITCH]"); + + // Add a command with two options that differ only in case: -t and -T + ts::ArgParser::Command &cmd = cs_parser.add_command("process", "process data"); + cmd.add_option("--tag", "-t", "a label", "", 1, ""); + cmd.add_option("--threshold", "-T", "a numeric value", "", 1, "100"); + + ts::Arguments parsed; + + // Use lowercase -t: should set "tag" only + const char *argv1[] = {"test_prog", "process", "-t", "my_tag", nullptr}; + parsed = cs_parser.parse(argv1); + REQUIRE(parsed.get("tag") == true); + REQUIRE(parsed.get("tag").value() == "my_tag"); + // threshold should still have its default + REQUIRE(parsed.get("threshold").value() == "100"); + + // Use uppercase -T: should set "threshold" only + const char *argv2[] = {"test_prog", "process", "-T", "200", nullptr}; + parsed = cs_parser.parse(argv2); + REQUIRE(parsed.get("threshold") == true); + REQUIRE(parsed.get("threshold").value() == "200"); + // tag should be empty (no default) + REQUIRE(parsed.get("tag").value() == ""); + + // Use both -t and -T together + const char *argv3[] = {"test_prog", "process", "-t", "foo", "-T", "500", nullptr}; + parsed = cs_parser.parse(argv3); + REQUIRE(parsed.get("tag") == true); + REQUIRE(parsed.get("tag").value() == "foo"); + REQUIRE(parsed.get("threshold") == true); + REQUIRE(parsed.get("threshold").value() == "500"); +} + +TEST_CASE("with_required does not trigger on default values", "[parse]") +{ + ts::ArgParser parser; + parser.add_global_usage("test_prog [OPTIONS]"); + + ts::ArgParser::Command &cmd = parser.add_command("scan", "scan targets"); + cmd.add_option("--tag", "-t", "a label", "", 1, ""); + cmd.add_option("--verbose", "-v", "enable verbose output"); + cmd.add_option("--threshold", "-T", "a numeric value", "", 1, "100").with_required("--verbose"); + + // -t alone should NOT trigger --threshold's dependency on --verbose. + // The default value "100" for --threshold must not count as "explicitly used". + const char *argv1[] = {"test_prog", "scan", "-t", "my_tag", nullptr}; + ts::Arguments parsed = parser.parse(argv1); + REQUIRE(parsed.get("tag").value() == "my_tag"); + // threshold default should still be applied after validation + REQUIRE(parsed.get("threshold").value() == "100"); + + // -T with -v should work fine + const char *argv2[] = {"test_prog", "scan", "-T", "200", "-v", nullptr}; + parsed = parser.parse(argv2); + REQUIRE(parsed.get("threshold").value() == "200"); + REQUIRE(parsed.get("verbose") == true); + + // -t and -T together with -v should work + const char *argv3[] = {"test_prog", "scan", "-t", "foo", "-T", "300", "-v", nullptr}; + parsed = parser.parse(argv3); + REQUIRE(parsed.get("tag").value() == "foo"); + REQUIRE(parsed.get("threshold").value() == "300"); + REQUIRE(parsed.get("verbose") == true); +}