This is an automated email from the ASF dual-hosted git repository.

dmeden pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new ba17b02fef ArgParser: apply default values after dependency validation 
(#12934)
ba17b02fef is described below

commit ba17b02fef0661088e58c5af1c26bb3a9ea60679
Author: Damian Meden <[email protected]>
AuthorDate: Thu Mar 5 14:31:01 2026 +0100

    ArgParser: apply default values after dependency validation (#12934)
    
    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 a4bd5916fe..fdb6086eba 100644
--- a/include/tscore/ArgParser.h
+++ b/include/tscore/ArgParser.h
@@ -226,6 +226,8 @@ public:
     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 65298317a6..90c3c2f5e4 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 672a702a28..0a32502e96 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);
+}

Reply via email to