From 8a75b73cc2f9a0a799738ce87d766f60bdd42eb2 Mon Sep 17 00:00:00 2001 From: Guilherme Beltramini Date: Mon, 6 Jul 2026 20:19:47 -0300 Subject: [PATCH 1/4] Allow expansion of env vars in options --- scripts/doc_parser/docopt_ng/__init__.py | 24 ++++++++++++++-- tests/core/helpers/test_cli_parse_args.sh | 35 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/scripts/doc_parser/docopt_ng/__init__.py b/scripts/doc_parser/docopt_ng/__init__.py index 96209ef..f40da7d 100644 --- a/scripts/doc_parser/docopt_ng/__init__.py +++ b/scripts/doc_parser/docopt_ng/__init__.py @@ -308,7 +308,7 @@ def parse(cls, option_description: str) -> _Option: argcount = 1 if argcount: matched = re.findall(r"\[default: (.*)\]", description, flags=re.I) - value = matched[0] if matched else None + value = expand_env_vars(matched[0]) if matched else None return cls(short, longer, argcount, value) def single_match(self, left: list[_LeafPattern]) -> _SingleMatch: @@ -950,9 +950,29 @@ def show_log(message: str) -> None: print(f"{filename}:{line_number} - {message}", file=sys.stderr) +# cli customization: +def expand_env_vars(value: str) -> str: + """Expand `$VAR` and `${VAR}` in a docstring default against the environment. + + Defaults come from the (trusted) docstring, so expanding them is safe. This is a pure + string substitution — it never invokes a shell — so command substitution (`$(...)`, + backticks) and bash-only forms (`${VAR:-default}`) are left untouched, and the expanded + result still flows through `bash_quote()` before `eval`. An unset variable expands to the + empty string (like an unquoted `$VAR` in the shell). User-supplied values are parsed + elsewhere and are never expanded. + """ + import os + + return re.sub( + r"\$\{(\w+)\}|\$(\w+)", + lambda m: os.environ.get(m.group(1) or m.group(2), ""), + value, + ) + + # cli customization: def bash_quote(value: str) -> str: - """Return a single-quoted bash literal that is safe to `eval`. + r"""Return a single-quoted bash literal that is safe to `eval`. Single quotes disable all bash expansion (command substitution, `$` expansion, backticks, escapes), so user input cannot inject code. The only diff --git a/tests/core/helpers/test_cli_parse_args.sh b/tests/core/helpers/test_cli_parse_args.sh index 15a0214..3167eba 100755 --- a/tests/core/helpers/test_cli_parse_args.sh +++ b/tests/core/helpers/test_cli_parse_args.sh @@ -110,6 +110,41 @@ export some_flag='false' assertEquals "$expected" "$result" } +test_parse_args_expands_env_vars_in_defaults() { + local help_text result + + # `$VAR`/`${VAR}` in a `[default: ...]` are expanded against the environment (defaults come + # from the trusted docstring). An unset variable expands to the empty string; `$(...)` and + # backticks stay literal, and a user-supplied value is never expanded. + # shellcheck disable=SC2016 # single quotes intentional: `$VAR` must reach docopt literally + help_text='Usage: + anything cmd [--foo=] + +Options: + --foo= Some parameter [default: $MYCLI_TEST_DEFAULT]' + + export MYCLI_TEST_DEFAULT='from env' + result=$(parse_args "$help_text" 'cmd') + assertContains "env var in default is expanded" "$result" "export foo='from env'" + + # shellcheck disable=SC2016 # single quotes intentional: value must reach docopt literally + result=$(parse_args "$help_text" 'cmd' '--foo=$MYCLI_TEST_DEFAULT') + assertContains "user-supplied value is not expanded" "$result" "export foo='\$MYCLI_TEST_DEFAULT'" + + unset MYCLI_TEST_DEFAULT + result=$(parse_args "$help_text" 'cmd') + assertContains "unset var in default expands to empty" "$result" "export foo=''" + + # shellcheck disable=SC2016 # single quotes intentional: `$(id)` must reach docopt literally + help_text='Usage: + anything cmd [--foo=] + +Options: + --foo= Some parameter [default: $(id)]' + result=$(parse_args "$help_text" 'cmd') + assertContains "command substitution in default stays inert" "$result" "export foo='\$(id)'" +} + test__is_str_to_eval() { # shellcheck disable=SC2034 local -r multi_line_exports="# foo From c8b97ef7a581b69838ec265f7f82ddab9fa4d64e Mon Sep 17 00:00:00 2001 From: Guilherme Beltramini Date: Tue, 7 Jul 2026 12:16:46 -0300 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/doc_parser/docopt_ng/__init__.py | 2 +- tests/core/helpers/test_cli_parse_args.sh | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/doc_parser/docopt_ng/__init__.py b/scripts/doc_parser/docopt_ng/__init__.py index f40da7d..54a7983 100644 --- a/scripts/doc_parser/docopt_ng/__init__.py +++ b/scripts/doc_parser/docopt_ng/__init__.py @@ -972,7 +972,7 @@ def expand_env_vars(value: str) -> str: # cli customization: def bash_quote(value: str) -> str: - r"""Return a single-quoted bash literal that is safe to `eval`. + """Return a single-quoted bash literal that is safe to `eval`. Single quotes disable all bash expansion (command substitution, `$` expansion, backticks, escapes), so user input cannot inject code. The only diff --git a/tests/core/helpers/test_cli_parse_args.sh b/tests/core/helpers/test_cli_parse_args.sh index 3167eba..9752d00 100755 --- a/tests/core/helpers/test_cli_parse_args.sh +++ b/tests/core/helpers/test_cli_parse_args.sh @@ -123,16 +123,14 @@ test_parse_args_expands_env_vars_in_defaults() { Options: --foo= Some parameter [default: $MYCLI_TEST_DEFAULT]' - export MYCLI_TEST_DEFAULT='from env' - result=$(parse_args "$help_text" 'cmd') + result=$(MYCLI_TEST_DEFAULT='from env' parse_args "$help_text" 'cmd') assertContains "env var in default is expanded" "$result" "export foo='from env'" # shellcheck disable=SC2016 # single quotes intentional: value must reach docopt literally - result=$(parse_args "$help_text" 'cmd' '--foo=$MYCLI_TEST_DEFAULT') + result=$(MYCLI_TEST_DEFAULT='from env' parse_args "$help_text" 'cmd' '--foo=$MYCLI_TEST_DEFAULT') assertContains "user-supplied value is not expanded" "$result" "export foo='\$MYCLI_TEST_DEFAULT'" - unset MYCLI_TEST_DEFAULT - result=$(parse_args "$help_text" 'cmd') + result=$(unset MYCLI_TEST_DEFAULT; parse_args "$help_text" 'cmd') assertContains "unset var in default expands to empty" "$result" "export foo=''" # shellcheck disable=SC2016 # single quotes intentional: `$(id)` must reach docopt literally From 9deea8b48c3185de802b6ac204ea888324a1fe0e Mon Sep 17 00:00:00 2001 From: Guilherme Beltramini Date: Tue, 7 Jul 2026 12:17:26 -0300 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/doc_parser/docopt_ng/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/doc_parser/docopt_ng/__init__.py b/scripts/doc_parser/docopt_ng/__init__.py index 54a7983..7956283 100644 --- a/scripts/doc_parser/docopt_ng/__init__.py +++ b/scripts/doc_parser/docopt_ng/__init__.py @@ -964,9 +964,10 @@ def expand_env_vars(value: str) -> str: import os return re.sub( - r"\$\{(\w+)\}|\$(\w+)", + r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)", lambda m: os.environ.get(m.group(1) or m.group(2), ""), value, + flags=re.ASCII, ) From 9dc62e0a2b4f043e6c97f11f44b8621ecf927850 Mon Sep 17 00:00:00 2001 From: Guilherme Beltramini Date: Tue, 7 Jul 2026 12:22:03 -0300 Subject: [PATCH 4/4] Reorder tests --- tests/core/helpers/test_cli_parse_args.sh | 66 +++++++++++------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/core/helpers/test_cli_parse_args.sh b/tests/core/helpers/test_cli_parse_args.sh index 9752d00..c775c06 100755 --- a/tests/core/helpers/test_cli_parse_args.sh +++ b/tests/core/helpers/test_cli_parse_args.sh @@ -110,39 +110,6 @@ export some_flag='false' assertEquals "$expected" "$result" } -test_parse_args_expands_env_vars_in_defaults() { - local help_text result - - # `$VAR`/`${VAR}` in a `[default: ...]` are expanded against the environment (defaults come - # from the trusted docstring). An unset variable expands to the empty string; `$(...)` and - # backticks stay literal, and a user-supplied value is never expanded. - # shellcheck disable=SC2016 # single quotes intentional: `$VAR` must reach docopt literally - help_text='Usage: - anything cmd [--foo=] - -Options: - --foo= Some parameter [default: $MYCLI_TEST_DEFAULT]' - - result=$(MYCLI_TEST_DEFAULT='from env' parse_args "$help_text" 'cmd') - assertContains "env var in default is expanded" "$result" "export foo='from env'" - - # shellcheck disable=SC2016 # single quotes intentional: value must reach docopt literally - result=$(MYCLI_TEST_DEFAULT='from env' parse_args "$help_text" 'cmd' '--foo=$MYCLI_TEST_DEFAULT') - assertContains "user-supplied value is not expanded" "$result" "export foo='\$MYCLI_TEST_DEFAULT'" - - result=$(unset MYCLI_TEST_DEFAULT; parse_args "$help_text" 'cmd') - assertContains "unset var in default expands to empty" "$result" "export foo=''" - - # shellcheck disable=SC2016 # single quotes intentional: `$(id)` must reach docopt literally - help_text='Usage: - anything cmd [--foo=] - -Options: - --foo= Some parameter [default: $(id)]' - result=$(parse_args "$help_text" 'cmd') - assertContains "command substitution in default stays inert" "$result" "export foo='\$(id)'" -} - test__is_str_to_eval() { # shellcheck disable=SC2034 local -r multi_line_exports="# foo @@ -275,6 +242,39 @@ pos2='CDE'" assertEquals "$expected" "$result" } +test_parse_args_expands_env_vars_in_defaults() { + local help_text result + + # `$VAR`/`${VAR}` in a `[default: ...]` are expanded against the environment (defaults come + # from the trusted docstring). An unset variable expands to the empty string; `$(...)` and + # backticks stay literal, and a user-supplied value is never expanded. + # shellcheck disable=SC2016 # single quotes intentional: `$VAR` must reach docopt literally + help_text='Usage: + anything cmd [--foo=] + +Options: + --foo= Some parameter [default: $MYCLI_TEST_DEFAULT]' + + result=$(MYCLI_TEST_DEFAULT='from env' parse_args "$help_text" 'cmd') + assertContains "env var in default is expanded" "$result" "export foo='from env'" + + # shellcheck disable=SC2016 # single quotes intentional: value must reach docopt literally + result=$(MYCLI_TEST_DEFAULT='from env' parse_args "$help_text" 'cmd' '--foo=$MYCLI_TEST_DEFAULT') + assertContains "user-supplied value is not expanded" "$result" "export foo='\$MYCLI_TEST_DEFAULT'" + + result=$(unset MYCLI_TEST_DEFAULT; parse_args "$help_text" 'cmd') + assertContains "unset var in default expands to empty" "$result" "export foo=''" + + # shellcheck disable=SC2016 # single quotes intentional: `$(id)` must reach docopt literally + help_text='Usage: + anything cmd [--foo=] + +Options: + --foo= Some parameter [default: $(id)]' + result=$(parse_args "$help_text" 'cmd') + assertContains "command substitution in default stays inert" "$result" "export foo='\$(id)'" +} + test_parse_help_does_not_evaluate_injected_code() { local result