From 71e19efdca9a5759ee54afc4bae0d2b0c3ff0cc9 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:39:34 -0700 Subject: [PATCH] Make stdlib initialization explicit (#222) --- CHANGELOG.md | 4 + README.md | 6 + bin/base-bash | 37 +-- examples/cookbook-args-lists-strings.sh | 3 + examples/cookbook-cleanup-temp.sh | 3 + examples/std-usage.sh | 3 + lib/bash/README.md | 11 +- lib/bash/arg/README.md | 2 + lib/bash/arg/tests/lib_arg.bats | 13 +- lib/bash/file/README.md | 2 + lib/bash/file/tests/lib_file.bats | 10 + lib/bash/gh/README.md | 2 + lib/bash/gh/tests/lib_gh.bats | 22 ++ lib/bash/git/README.md | 2 + lib/bash/git/tests/lib_git.bats | 10 + lib/bash/list/README.md | 2 + lib/bash/list/tests/lib_list.bats | 13 +- lib/bash/std/README.md | 57 +++- lib/bash/std/lib_std.sh | 401 ++++++++++++++++-------- lib/bash/std/tests/lib_std.bats | 109 ++++++- lib/bash/str/README.md | 2 + lib/bash/str/tests/lib_str.bats | 15 +- tests/bash-42-logging-smoke.sh | 5 + tests/bash-option-contract.sh | 3 + 24 files changed, 543 insertions(+), 194 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 381706a..b45321b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ and versions are tracked in the repo-root `VERSION` file. ### Changed +- Made `lib_std.sh` sourcing passive and introduced the explicit, idempotent + `base_bash_libs_init` lifecycle API. Wrapper flags now return through a + caller-owned array without hidden positional-parameter mutation; launchers, + examples, and companion-library tests initialize explicitly. - Made timed foreground-TTY invocations fail closed with a safe diagnostic; callers must provide a pipe or explicit non-terminal stdin for the v2 hard descendant guarantee. diff --git a/README.md b/README.md index 3636793..9052daf 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ Source the installed stdlib from the Homebrew prefix: ```bash base_bash_libs_prefix="$(brew --prefix basefoundry/base/base-bash-libs)" source "$base_bash_libs_prefix/libexec/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- "$@" printf 'base-bash-libs version: %s\n' "$BASE_BASH_LIBS_VERSION" ``` @@ -115,6 +117,8 @@ Source the stdlib from that checkout: ```bash base_bash_libs_dir="$PWD/vendor/base-bash-libs" source "$base_bash_libs_dir/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- "$@" printf 'base-bash-libs version: %s\n' "$BASE_BASH_LIBS_VERSION" ``` @@ -145,6 +149,8 @@ project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" base_bash_libs_dir="$project_root/vendor/base-bash-libs" source "$base_bash_libs_dir/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- "$@" import "$base_bash_libs_dir/lib/bash/file/lib_file.sh" import "$base_bash_libs_dir/lib/bash/git/lib_git.sh" import "$base_bash_libs_dir/lib/bash/gh/lib_gh.sh" diff --git a/bin/base-bash b/bin/base-bash index 5ca16fc..c89fcb3 100755 --- a/bin/base-bash +++ b/bin/base-bash @@ -6,7 +6,7 @@ # # Responsibilities: # - run a Bash script with lib/bash/std/lib_std.sh preloaded -# - preserve stdlib wrapper-flag behavior for the target script +# - explicitly initialize stdlib runtime state and filter wrapper flags # - provide package-relative imports without establishing Base runtime state # @@ -127,36 +127,9 @@ base_bash_resolve_lib_dir() { } base_bash_source_stdlib() { - local script_path="$1" local stdlib_path="$BASE_BASH_LIBS_DIR/std/lib_std.sh" - shift - - # Consumed by lib_std.sh while it is being sourced below. - # shellcheck disable=SC2034 - BASE_BASH_BOOTSTRAP_SOURCE="$script_path" # shellcheck source=/dev/null - source "$stdlib_path" "$@" || exit $? - unset BASE_BASH_BOOTSTRAP_SOURCE -} - -base_bash_filter_runtime_args() { - local parse_options=1 - - base_bash_runtime_args=() - - while (($#)); do - if ((parse_options)) && [[ "$1" == "--" ]]; then - base_bash_runtime_args+=("$1") - parse_options=0 - # Keep consuming the deprecated --verbose-wrapper compatibility flag - # during the 1.x window so it never leaks into command argv. - elif ((parse_options)) && [[ "$1" == --debug-wrapper || "$1" == --verbose-wrapper || "$1" == --utc-wrapper || "$1" == --color ]]; then - : - else - base_bash_runtime_args+=("$1") - fi - shift - done + source "$stdlib_path" || exit $? } import_base_bash_lib() { @@ -183,7 +156,6 @@ base_bash_run_script() { local script_path="$1" local package_root="$2" local -a runtime_args=() - local -a base_bash_runtime_args=() shift 2 [[ -f "$script_path" ]] || base_bash_die "Script '$script_path' was not found." @@ -191,9 +163,8 @@ base_bash_run_script() { BASE_BASH_LIBS_DIR="$(base_bash_resolve_lib_dir "$package_root")" || exit $? export BASE_BASH_LIBS_DIR - base_bash_filter_runtime_args "$@" - base_bash_source_stdlib "$script_path" "$@" - runtime_args=("${base_bash_runtime_args[@]}") + base_bash_source_stdlib + base_bash_libs_init runtime_args --source "$script_path" -- "$@" || exit $? unset -f main diff --git a/examples/cookbook-args-lists-strings.sh b/examples/cookbook-args-lists-strings.sh index 6f6fc8d..d4c1cef 100755 --- a/examples/cookbook-args-lists-strings.sh +++ b/examples/cookbook-args-lists-strings.sh @@ -5,6 +5,9 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" || exit 1 # shellcheck source=/dev/null source "$repo_root/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- "$@" + import "$repo_root/lib/bash/arg/lib_arg.sh" import "$repo_root/lib/bash/list/lib_list.sh" import "$repo_root/lib/bash/str/lib_str.sh" diff --git a/examples/cookbook-cleanup-temp.sh b/examples/cookbook-cleanup-temp.sh index 2675fac..2d1b372 100755 --- a/examples/cookbook-cleanup-temp.sh +++ b/examples/cookbook-cleanup-temp.sh @@ -5,6 +5,9 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" || exit 1 # shellcheck source=/dev/null source "$repo_root/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- "$@" + base_bash_libs_require_version 1.0.0 workspace_dir="" diff --git a/examples/std-usage.sh b/examples/std-usage.sh index 18a549c..f9f7643 100755 --- a/examples/std-usage.sh +++ b/examples/std-usage.sh @@ -5,6 +5,9 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" || exit 1 # shellcheck source=/dev/null source "$repo_root/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- "$@" + import "$repo_root/lib/bash/file/lib_file.sh" example_file="${TMPDIR:-/tmp}/base-bash-libs-example.$$" diff --git a/lib/bash/README.md b/lib/bash/README.md index ea3f072..4e1b92e 100644 --- a/lib/bash/README.md +++ b/lib/bash/README.md @@ -29,11 +29,12 @@ modules. ## Caller Runtime Contract All public modules support Bash 4.2 or newer with every combination of caller- -selected `errexit`, `nounset`, and `pipefail`. Sourcing a module does not change -those settings, any other `set` or `shopt` option, `IFS`, `OPTIND`, the working -directory, the umask, traps, or ordinary positional arguments. The stdlib's -documented wrapper flags are the exception: its initializer removes recognized -wrapper flags and publishes the filtered positional arguments. +selected `errexit`, `nounset`, and `pipefail`. Sourcing a module is passive: it +does not change those settings, any other `set` or `shopt` option, `IFS`, +`OPTIND`, the working directory, the umask, traps, exports, or ordinary +positional arguments. After sourcing `lib_std.sh`, callers explicitly invoke +`base_bash_libs_init` to initialize runtime state and receive wrapper-filtered +arguments in a caller-owned array. Public API calls preserve the same process state unless their documented purpose is to change it. Examples of intentional mutation include PATH helpers, diff --git a/lib/bash/arg/README.md b/lib/bash/arg/README.md index 6f4fc0f..97e53ba 100644 --- a/lib/bash/arg/README.md +++ b/lib/bash/arg/README.md @@ -23,6 +23,8 @@ Aliasing is rejected before any caller-owned output is changed. ```bash source "/absolute/path/to/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- source "/absolute/path/to/lib/bash/arg/lib_arg.sh" declare -A options=() diff --git a/lib/bash/arg/tests/lib_arg.bats b/lib/bash/arg/tests/lib_arg.bats index 0390cda..cfdb5f4 100644 --- a/lib/bash/arg/tests/lib_arg.bats +++ b/lib/bash/arg/tests/lib_arg.bats @@ -5,12 +5,21 @@ load ../../tests/test_helper.sh setup() { setup_test_tmpdir source "$BASE_BASH_DIR/std/lib_std.sh" + declare -a setup_args=() + base_bash_libs_init setup_args --source "$BASE_BASH_DIR/arg/tests/lib_arg.bats" -- source "$BASE_BASH_DIR/arg/lib_arg.sh" } create_script() { local script_path="$1" - cat > "$script_path" + local content source_line init_lines + content="$(cat)" + source_line="source \"$BASE_BASH_DIR/std/lib_std.sh\"" + if [[ "$content" == *"$source_line"* ]]; then + init_lines=$'declare -a base_bash_libs_test_args=()\nbase_bash_libs_init base_bash_libs_test_args -- "$@"\nset -- "${base_bash_libs_test_args[@]}"' + content="${content/"$source_line"/"$source_line"$'\n'"$init_lines"}" + fi + printf '%s\n' "$content" > "$script_path" chmod +x "$script_path" } @@ -47,6 +56,8 @@ create_script() { case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args -- source "$3" arg_parse exit $? diff --git a/lib/bash/file/README.md b/lib/bash/file/README.md index 365e51b..6bf3a8f 100644 --- a/lib/bash/file/README.md +++ b/lib/bash/file/README.md @@ -27,6 +27,8 @@ settings; they do not impose a strict-mode policy on the calling script. ```bash source "/absolute/path/to/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- source "/absolute/path/to/lib/bash/file/lib_file.sh" update_file_section ~/.bash_profile "# BEGIN APP" "# END APP" \ diff --git a/lib/bash/file/tests/lib_file.bats b/lib/bash/file/tests/lib_file.bats index 49a6e72..a143d00 100644 --- a/lib/bash/file/tests/lib_file.bats +++ b/lib/bash/file/tests/lib_file.bats @@ -5,6 +5,8 @@ load ../../tests/test_helper.sh setup() { setup_test_tmpdir source "$BASE_BASH_DIR/std/lib_std.sh" + declare -a setup_args=() + base_bash_libs_init setup_args --source "$BASE_BASH_DIR/file/tests/lib_file.bats" -- source "$BASE_BASH_DIR/file/lib_file.sh" } @@ -39,6 +41,8 @@ file_mode() { cat > "$script" < "\$1" update_file_section "\$1" "# BEGIN" "# END" "first" @@ -103,6 +107,8 @@ EOF #!/usr/bin/env bash set -euo pipefail source "$BASE_BASH_DIR/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "\${BASH_SOURCE[0]}" -- "\$@" source "$BASE_BASH_DIR/file/lib_file.sh" update_file_section printf 'after\n' @@ -127,6 +133,8 @@ EOF #!/usr/bin/env bash set -euo pipefail source "$BASE_BASH_DIR/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "\${BASH_SOURCE[0]}" -- "\$@" source "$BASE_BASH_DIR/file/lib_file.sh" update_file_section "\$1" "# BEGIN" "# END" printf 'strict=preserved\n' @@ -307,6 +315,8 @@ EOF cat > "$script" <> "$log_file"' EXIT before_trap="\$(trap -p EXIT)" diff --git a/lib/bash/gh/README.md b/lib/bash/gh/README.md index 2b547ff..c4f7107 100644 --- a/lib/bash/gh/README.md +++ b/lib/bash/gh/README.md @@ -8,6 +8,8 @@ Source the stdlib before this library: ```bash source "/path/to/base-bash-libs/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- import "/path/to/base-bash-libs/lib/bash/gh/lib_gh.sh" ``` diff --git a/lib/bash/gh/tests/lib_gh.bats b/lib/bash/gh/tests/lib_gh.bats index 10a2991..841ab50 100644 --- a/lib/bash/gh/tests/lib_gh.bats +++ b/lib/bash/gh/tests/lib_gh.bats @@ -8,6 +8,8 @@ setup() { mkdir -p "$TEST_TMPDIR/bin" PATH="$TEST_TMPDIR/bin:$BASE_TEST_ORIG_PATH" source "$BASE_BASH_DIR/std/lib_std.sh" + declare -a setup_args=() + base_bash_libs_init setup_args --source "$BASE_BASH_DIR/gh/tests/lib_gh.bats" -- source "$BASE_BASH_DIR/gh/lib_gh.sh" } @@ -163,6 +165,8 @@ gh_api_retry_observed() { case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args -- source "$3" "$4" rc=$? @@ -301,6 +305,8 @@ EOF case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args -- source "$3" PATH="$4:$PATH" gh_run issue list @@ -318,6 +324,8 @@ EOF case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args -- source "$3" PATH="$4:$PATH" gh_run --sensitive --safe-display "strict protected operation" -- issue list @@ -402,6 +410,8 @@ EOF bats_run "$BASH" -c ' source "$1" + declare -a app_args=() + base_bash_libs_init app_args -- source "$2" PATH="$3" gh_require_cli "$4" @@ -572,6 +582,8 @@ EOF #!/usr/bin/env bash set -euo pipefail source "$BASE_BASH_DIR/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "\${BASH_SOURCE[0]}" -- "\$@" source "$BASE_BASH_DIR/gh/lib_gh.sh" PATH="$TEST_TMPDIR/bin:$BASE_TEST_ORIG_PATH" gh_run issue create --title Example @@ -610,6 +622,8 @@ EOF bats_run "$BASH" -c ' source "$1" + declare -a app_args=() + base_bash_libs_init app_args -- source "$2" PATH="$3" gh_run issue list @@ -1711,6 +1725,8 @@ EOF cat > "$script" <<'EOF' #!/usr/bin/env bash source "$1" +declare -a app_args=() +base_bash_libs_init app_args -- source "$2" TMPDIR="$3" STATUS_FILE="$4" @@ -1753,6 +1769,8 @@ EOF #!/usr/bin/env bash set -u source "$1" +declare -a app_args=() +base_bash_libs_init app_args -- source "$2" TMPDIR="$3" TRAP_MARKER="$4" @@ -1904,6 +1922,8 @@ EOF #!/usr/bin/env bash set -e source "$BASE_BASH_DIR/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "\${BASH_SOURCE[0]}" -- "\$@" source "$BASE_BASH_DIR/gh/lib_gh.sh" PATH="$TEST_TMPDIR/bin:$PATH" gh_api_with_retry repos/owner/missing @@ -1931,6 +1951,8 @@ EOF bats_run "$BASH" -c ' source "$1" + declare -a app_args=() + base_bash_libs_init app_args -- source "$2" set -C value="$(gh_api_with_retry --max-attempts 1 -- repos/owner/repo)" || exit $? diff --git a/lib/bash/git/README.md b/lib/bash/git/README.md index 4fbcc70..12c8188 100644 --- a/lib/bash/git/README.md +++ b/lib/bash/git/README.md @@ -39,6 +39,8 @@ helpers manage branch selection, retries, and cleanup. ```bash source "/absolute/path/to/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- source "/absolute/path/to/lib/bash/git/lib_git.sh" branch="" diff --git a/lib/bash/git/tests/lib_git.bats b/lib/bash/git/tests/lib_git.bats index 860a8a9..1f7be6a 100644 --- a/lib/bash/git/tests/lib_git.bats +++ b/lib/bash/git/tests/lib_git.bats @@ -5,6 +5,8 @@ load ../../tests/test_helper.sh setup() { setup_test_tmpdir source "$BASE_BASH_DIR/std/lib_std.sh" + declare -a setup_args=() + base_bash_libs_init setup_args --source "$BASE_BASH_DIR/git/tests/lib_git.bats" -- source "$BASE_BASH_DIR/git/lib_git.sh" } @@ -49,6 +51,8 @@ setup() { case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args --source "$0" -- source "$3" "$4" rc=$? @@ -129,6 +133,8 @@ setup() { case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args -- source "$3" git_branch_merged_to_ref "$4" feature main rc=$? @@ -206,6 +212,8 @@ EOF bats_run "$BASH" -c ' set -u source "$1" + declare -a app_args=() + base_bash_libs_init app_args -- source "$2" git() { printf "%s\n" \ @@ -1301,6 +1309,8 @@ EOF case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args -- source "$3" check_script_up_to_date "$4" ' bash "$mode" "$BASE_BASH_DIR/std/lib_std.sh" "$BASE_BASH_DIR/git/lib_git.sh" "$script_path" diff --git a/lib/bash/list/README.md b/lib/bash/list/README.md index 3e9280a..bf554cf 100644 --- a/lib/bash/list/README.md +++ b/lib/bash/list/README.md @@ -26,6 +26,8 @@ helpers are available. ```bash source "/absolute/path/to/lib/bash/std/lib_std.sh" +declare -a app_args=() +base_bash_libs_init app_args --source "${BASH_SOURCE[0]}" -- source "/absolute/path/to/lib/bash/list/lib_list.sh" declare -a packages=("jq") diff --git a/lib/bash/list/tests/lib_list.bats b/lib/bash/list/tests/lib_list.bats index 5870ad1..2de8337 100644 --- a/lib/bash/list/tests/lib_list.bats +++ b/lib/bash/list/tests/lib_list.bats @@ -5,12 +5,21 @@ load ../../tests/test_helper.sh setup() { setup_test_tmpdir source "$BASE_BASH_DIR/std/lib_std.sh" + declare -a setup_args=() + base_bash_libs_init setup_args --source "$BASE_BASH_DIR/list/tests/lib_list.bats" -- source "$BASE_BASH_DIR/list/lib_list.sh" } create_script() { local script_path="$1" - cat > "$script_path" + local content source_line init_lines + content="$(cat)" + source_line="source \"$BASE_BASH_DIR/std/lib_std.sh\"" + if [[ "$content" == *"$source_line"* ]]; then + init_lines=$'declare -a base_bash_libs_test_args=()\nbase_bash_libs_init base_bash_libs_test_args -- "$@"\nset -- "${base_bash_libs_test_args[@]}"' + content="${content/"$source_line"/"$source_line"$'\n'"$init_lines"}" + fi + printf '%s\n' "$content" > "$script_path" chmod +x "$script_path" } @@ -54,6 +63,8 @@ create_script() { case "$mode" in *u*) set -u ;; esac case "$mode" in *p*) set -o pipefail ;; esac source "$2" + declare -a app_args=() + base_bash_libs_init app_args -- source "$3" "$4" exit $? diff --git a/lib/bash/std/README.md b/lib/bash/std/README.md index 1efef51..6807798 100644 --- a/lib/bash/std/README.md +++ b/lib/bash/std/README.md @@ -62,6 +62,9 @@ such output names are rejected before caller state is changed. ### Runtime and Imports +- `base_bash_libs_init [--source