From 3e6d06e663ba4931421065c68526b94f306fcd84 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Fri, 3 Jul 2026 13:02:10 -0700 Subject: [PATCH] Tighten parser option handling --- lib/bash/arg/README.md | 6 ++++-- lib/bash/arg/lib_arg.sh | 2 +- lib/bash/arg/tests/lib_arg.bats | 12 ++++++++++++ lib/bash/std/README.md | 8 ++++++++ lib/bash/std/lib_std.sh | 4 ++++ lib/bash/std/tests/lib_std.bats | 30 ++++++++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/lib/bash/arg/README.md b/lib/bash/arg/README.md index d821c23..595d1f6 100644 --- a/lib/bash/arg/README.md +++ b/lib/bash/arg/README.md @@ -39,8 +39,10 @@ Spec entries use `name|kind|token[|token...]`: - each `token` is an exact option token, such as `--verbose` or `-v`. The parser supports `--option value`, `--option=value`, repeated options where -the last value wins, and `--` to stop option parsing. A value option followed by -another registered option token is treated as missing its value; use +the last value wins, and `--` to stop option parsing. When a value option is +waiting for a value, a standalone `--` is treated as that value; use another +`--` if you also need to stop option parsing after it. A value option followed +by another registered option token is treated as missing its value; use `--option=value` when a value is intentionally option-like. Unknown options, malformed specs, and missing values return status `2`. diff --git a/lib/bash/arg/lib_arg.sh b/lib/bash/arg/lib_arg.sh index 5af9b44..d15847e 100644 --- a/lib/bash/arg/lib_arg.sh +++ b/lib/bash/arg/lib_arg.sh @@ -151,7 +151,7 @@ arg_parse() { continue fi - if (($# == 0)) || [[ "${1-}" == "--" ]]; then + if (($# == 0)); then log_error "arg_parse: option '$__arg_option_token' requires a value." return 2 fi diff --git a/lib/bash/arg/tests/lib_arg.bats b/lib/bash/arg/tests/lib_arg.bats index b3bf10b..5657bf1 100644 --- a/lib/bash/arg/tests/lib_arg.bats +++ b/lib/bash/arg/tests/lib_arg.bats @@ -71,6 +71,18 @@ create_script() { [ "${positionals[0]}" = "item" ] } +@test "arg_parse accepts -- as a value option value" { + local -a specs=("output|value|--output|-o") + local -A options=() + local -a positionals=() + + arg_parse options positionals specs -- --output -- item + + [ "${options[output]}" = "--" ] + [ "${#positionals[@]}" -eq 1 ] + [ "${positionals[0]}" = "item" ] +} + @test "arg_parse returns usage status for unknown options" { local -a specs=("verbose|flag|--verbose|-v") local -A options=() diff --git a/lib/bash/std/README.md b/lib/bash/std/README.md index 721e805..f8ba14f 100644 --- a/lib/bash/std/README.md +++ b/lib/bash/std/README.md @@ -265,6 +265,14 @@ Use `std_run` for commands plus arguments. Keep shell features such as pipelines, redirection, process substitution, and complex conditionals explicit in the calling script so the code remains clear. +Unknown `std_run` options beginning with `--` are rejected before command +execution. If the command itself begins with `--`, terminate `std_run` options +first: + +```bash +std_run -- --command-name arg +``` + `run` remains available as a compatibility wrapper for existing callers, but new code should use `std_run` to avoid collisions with test frameworks and other Bash libraries that define their own `run` helper. diff --git a/lib/bash/std/lib_std.sh b/lib/bash/std/lib_std.sh index 3d2a6ee..f560e0d 100644 --- a/lib/bash/std/lib_std.sh +++ b/lib/bash/std/lib_std.sh @@ -932,6 +932,10 @@ __std_run_impl__() { break ;; *) + if [[ "${1-}" == --* ]]; then + log_error "$helper_name: unknown option '$1'. Use -- before commands that begin with --." + return 1 + fi break ;; esac diff --git a/lib/bash/std/tests/lib_std.bats b/lib/bash/std/tests/lib_std.bats index 6cfd7af..b79ccf4 100644 --- a/lib/bash/std/tests/lib_std.bats +++ b/lib/bash/std/tests/lib_std.bats @@ -848,6 +848,36 @@ EOF [[ "$(cat "$stderr_file")" == *"std_run: No command provided."* ]] } +@test "std_run rejects unknown long options before command execution" { + local stderr_file="$TEST_TMPDIR/run-unknown-option.err" + local rc + + if std_run --typo echo "should not run" 2>"$stderr_file"; then + rc=0 + else + rc=$? + fi + + [ "$rc" -eq 1 ] + [[ "$(cat "$stderr_file")" == *"std_run: unknown option '--typo'."* ]] + [[ "$(cat "$stderr_file")" == *"Use -- before commands that begin with --."* ]] +} + +@test "std_run allows command names beginning with -- after option terminator" { + local fake_bin="$TEST_TMPDIR/bin" + local output_file="$TEST_TMPDIR/option-like-command.out" + + mkdir -p "$fake_bin" + create_script "$fake_bin/--record-command" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$1" > "$2" +EOF + + PATH="$fake_bin:$PATH" std_run -- --record-command "ran" "$output_file" + + [ "$(cat "$output_file")" = "ran" ] +} + @test "std_run honors dry-run mode without executing the command" { local target="$TEST_TMPDIR/dry-run.txt" DRY_RUN=true