Skip to content

Argparser variable arg option parsing fix/improvement. - #13570

Open
brbzull0 wants to merge 14 commits into
apache:masterfrom
brbzull0:argparser-variable-arg-option-parsing
Open

Argparser variable arg option parsing fix/improvement.#13570
brbzull0 wants to merge 14 commits into
apache:masterfrom
brbzull0:argparser-variable-arg-option-parsing

Conversation

@brbzull0

@brbzull0 brbzull0 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

ArgParser options taking a variable number of values collected every remaining token, so
an option written after one was swallowed and never parsed: config reload -D ip_allow.id=foo -t mytok lost the token, while the reverse order worked. Collection now
stops at a token naming another declared option of the same command, and -- ends option
recognition so a dash-prefixed value stays expressible.

Also, --option=value took the name up to the first = but the value from the last, so
--directive=ip_allow.id=foo arrived as foo.

The second commit drops the -D-must-be-last workaround in traffic_ctl, makes an empty
-D/-d an error rather than a silent full reload, and corrects the docs.

The third commit adds the arity that was missing entirely, AT_MOST_ONE_ARG_N, the
equivalent of nargs='?' in the argparse this parser imitates. An option whose value is
optional had to be declared as taking zero or more values, so it also ate the positional
arguments of its own command: config get -c FILE RECORD failed with an error naming
get, and --cold only worked written last or as --cold=FILE. That has been the
behaviour since 10.0. --cold is now declared with the new arity, and the count check for
the --option=value form asks is_variable_arg_num() instead of comparing against the
sentinels.

Fixes: #13569

Damian Meden added 2 commits August 19, 2026 12:36
ArgParser options declared with MORE_THAN_ZERO_ARG_N or
MORE_THAN_ONE_ARG_N collected every remaining token, so an option
written after one of them was silently swallowed as a value and never
parsed. Collection now stops at a token naming another option of the
same command, "--" ends option recognition so a value can still start
with '-', and only the range actually consumed is erased.

Separately, the --option=value path took the name up to the first '='
but the value from the last one, truncating any value containing '='.
That made --directive=key.sub=val unusable, since directive values are
key=value pairs by definition.

Fixes: apache#13569
The guard rejecting directive values that start with '-' existed only
because variable-argument parsing swallowed any option written after
-D. That no longer happens, so the guard can only fire for a value the
caller passed deliberately, and its advice to place -D last is now
wrong. A malformed value is reported by the directive format check
instead.

Require values for both -D and -d. Supplying either with no values
built a request identical to a plain reload, silently widening a scoped
reload to every handler.

Also document that -D may appear anywhere among the options and can be
combined with -d, which the previous note said was impossible.
@brbzull0 brbzull0 self-assigned this Aug 19, 2026
@brbzull0 brbzull0 added Tools traffic_ctl traffic_ctl related work. labels Aug 19, 2026
@brbzull0

Copy link
Copy Markdown
Contributor Author

[approve ci debian]

@brbzull0

Copy link
Copy Markdown
Contributor Author

[approve ci]

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes two long-standing ts::ArgParser option-parsing defects that impacted traffic_ctl config reload (and other variable-argument options): variable-argument options no longer swallow subsequent options on the same command, -- now terminates option recognition so dash-prefixed values remain expressible, and --option=value parsing no longer truncates values containing embedded =.

Changes:

  • Update ArgParser variable-argument handling to stop consuming at the next registered option (and honor --), and fix --option=value value extraction to split on the first =.
  • Remove the -D “must be last” workaround behavior in traffic_ctl, and make empty -D / -d invocations explicit errors instead of silently degrading to full reloads.
  • Add unit + gold tests covering the corrected parsing behaviors and update traffic_ctl documentation accordingly.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/tscore/ArgParser.cc Adjust variable-arg option collection logic, add registered-option detection, and fix --opt=... value splitting.
include/tscore/ArgParser.h Declare new Command helpers used for the updated parsing behavior.
src/tscore/unit_tests/test_ArgParser.cc Add unit coverage for variable-arg stop-at-next-option, -- handling, and embedded-= values.
src/traffic_ctl/CtrlCommands.cc Remove old -D ordering guard; error out on empty -d / -D to prevent silent full reload behavior.
tests/gold_tests/jsonrpc/config_reload_directive_cli.test.py Add end-to-end CLI parsing coverage for traffic_ctl config reload -D / -d interactions and --.
doc/appendices/command-line/traffic_ctl.en.rst Update operator guidance for -D/-d, including -- usage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/tscore/ArgParser.cc Outdated
@brbzull0

Copy link
Copy Markdown
Contributor Author

[approve ci centos]

@brbzull0
brbzull0 marked this pull request as ready for review August 21, 2026 15:42
An option whose value is optional had to be declared as taking zero or
more values, the only variable arity available, so it also consumed the
positional arguments of its own command. That is why traffic_ctl
rejected "config get -c FILE RECORD" with an error naming get, and why
--cold only worked written last or as --cold=FILE.

Add AT_MOST_ONE_ARG_N, the equivalent of nargs='?' in Python argparse
which this parser imitates, and declare --cold with it. The count check
for the --option=value form now asks is_variable_arg_num() rather than
comparing against the sentinels, so a third sentinel is not mistaken
for a literal argument count.
Copilot AI review requested due to automatic review settings August 24, 2026 11:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/tscore/ArgParser.cc:716

  • append_option_data() indexes args[i][0] / args[i][1] when checking for --option=value. If an argument is an empty string or a single "-" (possible for positional args / values), this is out-of-bounds and undefined behavior. Consider switching to a prefix check that doesn’t index into the string.
    if (args[i][0] == '-' && args[i][1] == '-' && args[i].find('=') != std::string::npos) {

@bryancall bryancall added this to the 11.0.0 milestone Aug 24, 2026
@bryancall
bryancall requested a review from cmcfarlen August 24, 2026 22:24
@bryancall

Copy link
Copy Markdown
Contributor

@brbzull0 does this need to be back ported to 10.2.x. If so, please mark the project.

The comment claimed the command's positional arguments were left in
place, but collection only stops at a token naming another option, so
positional tokens are still taken as values. Say so, and point at
AT_MOST_ONE_ARG_N for an option whose value is optional.
Copilot AI review requested due to automatic review settings August 26, 2026 07:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

An option expecting a fixed number of values took whatever token
followed it, so "traffic_ctl server debug enable -t -a" set the debug
tags to the literal "-a" and wrote that to the running configuration.

Apply the rule the variable-arity path already follows: a token naming
another option of the same command is not a value, so the missing value
is reported, and "--" still passes a value that starts with '-'.
Copilot AI review requested due to automatic review settings August 26, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comment thread tests/gold_tests/jsonrpc/config_reload_directive_cli.test.py
Comment thread doc/appendices/command-line/traffic_ctl.en.rst Outdated
Comment thread doc/developer-guide/internal-libraries/ArgParser.en.rst
Damian Meden added 2 commits August 28, 2026 12:24
is_variable_arg_num() exempts AT_MOST_ONE_ARG_N from the count check
for the --option=value form, so --cold=a --cold=b silently kept the
first and dropped the second, where the fixed arity equivalent is a
usage error.
A bare -c before a record takes the record as the file name, which
leaves config get with no records and exits with a usage error rather
than reading a file named for the record.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread doc/appendices/command-line/traffic_ctl.en.rst
Damian Meden added 2 commits August 28, 2026 13:18
The repetition check only counted the --option=value form, so "-c a -c b"
silently kept the last file name, and mixing the two spellings put two
values in an option that permits one. Fixed arity options keep their
existing last-one-wins behaviour, which is a separate concern.
Option recognition stays off for the rest of a variable-length value
list, so every later token becomes a value and any option written
afterwards is swallowed. Neither the guide nor the traffic_ctl page said
so, which made "--" look safe to use before other options.
@brbzull0
brbzull0 force-pushed the argparser-variable-arg-option-parsing branch from c21e8c8 to 68e5708 Compare August 28, 2026 11:20
Damian Meden added 5 commits September 1, 2026 14:46
Each occurrence reset the entry rather than adding to it, so
"-D a -D b" kept only b and "-d f1 -d f2" reloaded only f2, silently
dropping inline content the documentation says is merged. The
--option=value form has always accumulated, so the two spellings of one
option disagreed.

A fixed arity option keeps its last-one-wins behaviour. The default
command retry starts from a clean Arguments, since a global option is
otherwise parsed twice and would collect its values twice.
An empty token was taken as the value, and traffic_ctl reads an empty
--cold file name as a request for the default file, so a set whose file
name came from an unset variable wrote to the live records.yaml and
exited zero. The --option=value spelling already refuses an empty value,
so the two spellings disagreed.

The error names the option as it was written, matching that spelling.
A bare -c before a record takes the record as the file name, and the
docs quoted only the error config get produces. config set is left short
of its own arguments and reports a different one, so an operator who hit
that did not find their message.
set_default() writes a file scope default_command that nothing clears,
so the test left every later parse in the same binary inserting "info"
into its arguments. The mutex group and option dependency tests share
that binary and failed on four platforms, while the file passed on its
own.

The retry path the test covered keeps its guard in parse(); it cannot be
exercised without changing global state for the rest of the run.
autest reads the first character of every argument it splits, so an
empty argument written straight into Command raises IndexError before
the process starts, which failed the whole file and skipped the runs
after it. Pass those three through "sh -c" so the empty argument is
produced by the shell instead.

ContainsExpression is a regular expression, so "argument(s)" asked for
"arguments" and never matched the message it was quoting.
@brbzull0
brbzull0 requested a lite review from Copilot September 1, 2026 14:52
@brbzull0
brbzull0 marked this pull request as ready for review September 1, 2026 14:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/traffic_ctl/CtrlCommands.cc:601

  • Similarly, -D "" yields a single empty directive token, which bypasses the size()==0 check and then gets dropped by if (dir.empty()) continue;, leaving the request effectively unscoped. If the intent is "-D must specify at least one directive", the check should treat an all-empty value list as an error too (consistent with --directive= being rejected).
  auto dir_args = get_parsed_arguments()->get("directive");
  if (dir_args && dir_args.size() == 0) {
    _printer->write_output("Error: --directive (-D) requires at least one config_key.directive_key=value");
    App_Exit_Status_Code = CTRL_EX_ERROR;
    return;

Comment on lines +558 to +564
// Without content the request would silently degrade to a full reload of every handler,
// which is the opposite of the scoped reload the operator asked for.
if (data_args && data_args.size() == 0) {
_printer->write_output("Error: --data (-d) requires content: @file, @- or a YAML string");
App_Exit_Status_Code = CTRL_EX_ERROR;
return;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Tools traffic_ctl traffic_ctl related work.

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

ArgParser: variable-argument options consume following options, and --option=value truncates values containing '='

3 participants