diff --git a/lib/bash/README.md b/lib/bash/README.md index 24d4c3b..bfd17b2 100644 --- a/lib/bash/README.md +++ b/lib/bash/README.md @@ -25,3 +25,10 @@ Reusable Bash libraries for command wrappers and other Bash tooling. The Base runtime shell files and Base version helpers remain in `basefoundry/base`. This repository carries only sourceable reusable library modules. + +## Naming Contract + +Public helpers that write through caller-supplied variable or array names +reserve the `__` prefix for library-internal state. Passing an output name that +begins with `__` fails before the helper changes caller state. Use a regular +Bash variable name for public output values and arrays. diff --git a/lib/bash/arg/tests/lib_arg.bats b/lib/bash/arg/tests/lib_arg.bats index fa15503..5a0db27 100644 --- a/lib/bash/arg/tests/lib_arg.bats +++ b/lib/bash/arg/tests/lib_arg.bats @@ -90,6 +90,25 @@ EOF [ "${positionals_name[0]}" = "item" ] } +@test "arg_parse rejects reserved internal output names" { + local -A __arg_options=([sentinel]="keep") + local -a __arg_positionals=(sentinel) + local -a specs_name=("verbose|flag|--verbose|-v") + local stderr_file="$TEST_TMPDIR/arg-reserved-output.err" + local rc + + if arg_parse __arg_options __arg_positionals specs_name -- -v 2>"$stderr_file"; then + rc=0 + else + rc=$? + fi + + [ "$rc" -eq 1 ] + [ "${__arg_options[sentinel]}" = "keep" ] + [ "${__arg_positionals[0]}" = "sentinel" ] + [[ "$(cat "$stderr_file")" == *"uses the reserved '__' internal namespace"* ]] +} + @test "arg_parse accepts long option equals values and repeated options" { local -a specs=( "verbose|flag|--verbose|-v" diff --git a/lib/bash/gh/lib_gh.sh b/lib/bash/gh/lib_gh.sh index ed0ad59..22c362a 100644 --- a/lib/bash/gh/lib_gh.sh +++ b/lib/bash/gh/lib_gh.sh @@ -96,7 +96,7 @@ gh_infer_repo_from_origin() { local __gh_infer_repo_dir="$1" local __gh_infer_result_name="${2:-}" local __gh_infer_optional=0 - local __gh_infer_repo __gh_infer_remote_url + local gh_infer_parsed_repo __gh_infer_remote_url if [[ -z "$__gh_infer_repo_dir" || -z "$__gh_infer_result_name" ]]; then log_error -l base_bash_libs.gh "Usage: gh_infer_repo_from_origin [--optional]" @@ -110,7 +110,8 @@ gh_infer_repo_from_origin() { fi __gh_infer_remote_url="$(git -C "$__gh_infer_repo_dir" remote get-url origin 2>/dev/null || true)" - if [[ -z "$__gh_infer_remote_url" ]] || ! gh_repo_from_remote_url "$__gh_infer_remote_url" __gh_infer_repo; then + if [[ -z "$__gh_infer_remote_url" ]] || + ! gh_repo_from_remote_url "$__gh_infer_remote_url" gh_infer_parsed_repo; then if ((__gh_infer_optional)); then printf -v "$__gh_infer_result_name" '%s' "" return 0 @@ -119,7 +120,7 @@ gh_infer_repo_from_origin() { return 1 fi - printf -v "$__gh_infer_result_name" '%s' "$__gh_infer_repo" + printf -v "$__gh_infer_result_name" '%s' "$gh_infer_parsed_repo" } gh_repo_default_branch() { diff --git a/lib/bash/std/lib_std.sh b/lib/bash/std/lib_std.sh index 8fd06e6..b962590 100644 --- a/lib/bash/std/lib_std.sh +++ b/lib/bash/std/lib_std.sh @@ -1833,6 +1833,11 @@ __is_valid_variable_name__() { __std_assert_writable_output__() { local function_name="${1-}" output_name="${2-}" declaration attributes + if [[ "$output_name" == __* ]]; then + log_error -l base_bash_libs.std "$function_name: result variable '$output_name' uses the reserved '__' internal namespace." + return 1 + fi + declaration="$(declare -p "$output_name" 2>/dev/null || true)" [[ -n "$declaration" ]] || return 0 attributes="${declaration#declare -}" diff --git a/lib/bash/str/tests/lib_str.bats b/lib/bash/str/tests/lib_str.bats index b0f7acf..539e433 100644 --- a/lib/bash/str/tests/lib_str.bats +++ b/lib/bash/str/tests/lib_str.bats @@ -216,6 +216,22 @@ EOF [ "$joined" = "left|right" ] } +@test "str_lower rejects reserved internal output names" { + local __str_var_name="Mixed Case" + local stderr_file="$TEST_TMPDIR/str-reserved-output.err" + local rc + + if str_lower __str_var_name 2>"$stderr_file"; then + rc=0 + else + rc=$? + fi + + [ "$rc" -eq 1 ] + [ "$__str_var_name" = "Mixed Case" ] + [[ "$(cat "$stderr_file")" == *"uses the reserved '__' internal namespace"* ]] +} + @test "str_join rejects invalid variable names" { local script="$TEST_TMPDIR/str-join-invalid-array.sh"