From a11c701ab1217d53051b3beef36513387706e4c8 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:52:57 -0600 Subject: [PATCH] Fix #532: terraform shim fails with "@: unbound variable" on Bash 3.2 when run with no arguments 0bd45d1 changed the argument loop in tfenv-exec() to `for _arg in "${@}"` to stop word-splitting arguments with spaces (#453). Under `set -u`, Bash 3.2 (macOS /bin/bash) treats an empty "$@" as an unbound variable, so running the shim with no arguments now aborts before terraform runs. zsh's bashcompinit invokes `complete -C` commands with no arguments, so every tab completion after `terraform` printed only this error. Use `for _arg; do`, which iterates the positional parameters without expanding "$@", so it neither word-splits nor trips `set -u` on any Bash version. Add test/test_exec.sh covering the shim with no arguments (under both `bash` and /bin/bash) and a -chdir path containing a space, and correct the for-loop guidance in AGENTS.md and bash.instructions.md. Amp-Thread-ID: https://ampcode.com/threads/T-01a0eee4-d46a-776d-9873-341f0d5f7fd2 Co-authored-by: Amp --- .github/instructions/bash.instructions.md | 5 ++- AGENTS.md | 5 ++- lib/tfenv-exec.sh | 5 ++- test/test_exec.sh | 50 +++++++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 test/test_exec.sh diff --git a/.github/instructions/bash.instructions.md b/.github/instructions/bash.instructions.md index 5a43e6ba..4b173892 100644 --- a/.github/instructions/bash.instructions.md +++ b/.github/instructions/bash.instructions.md @@ -84,7 +84,10 @@ set -uo pipefail; word-splitting 3. **Double-quoted traps:** `trap "rm ${var}" EXIT` expands at definition time. Use functions or single quotes. -4. **`$@` in for-loops:** Always quote: `for arg in "$@"` +4. **`$@` in for-loops:** Use `for arg; do`, which iterates `"$@"` without + expanding it. Unquoted `$@` word-splits, and quoted `"$@"` is an unbound + variable under `set -u` on Bash 3.2 (macOS `/bin/bash`) when there are + no arguments. 5. **Regex anchoring:** `^1.1` matches `1.10.x` because `.` is a regex wildcard. Use `^1\.1\.` for exact prefix matching. diff --git a/AGENTS.md b/AGENTS.md index ca97b1a7..9857260a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -300,7 +300,10 @@ These are the most frequent sources of bugs. Check for them in every change: conditions, and assignments. Unquoted variables cause word-splitting. 3. **Double-quoted traps:** `trap "rm ${var}" EXIT` expands at definition time and is vulnerable to word-splitting. Use functions or single quotes. -4. **`$@` in for-loops:** Always quote: `for arg in "$@"`. +4. **`$@` in for-loops:** Use `for arg; do`, which iterates `"$@"` without + expanding it. Unquoted `$@` word-splits, and quoted `"$@"` is an unbound + variable under `set -u` on Bash 3.2 (macOS `/bin/bash`) when there are + no arguments. 5. **Regex anchoring:** `^1.1` matches `1.10.x` because `.` is a regex wildcard. Use `^1\.1\.` for exact prefix matching. 6. **Cross-platform differences:** macOS uses BSD `sed`/`grep`/`readlink`. diff --git a/lib/tfenv-exec.sh b/lib/tfenv-exec.sh index 2b08573f..f8f86ed7 100644 --- a/lib/tfenv-exec.sh +++ b/lib/tfenv-exec.sh @@ -29,7 +29,10 @@ function realpath-relative-to() { export -f realpath-relative-to; function tfenv-exec() { - for _arg in "${@}"; do + # `for _arg; do` iterates "$@" without expanding it. Bash 3.2 (macOS /bin/bash) + # treats "$@" as unbound under `set -u` when there are no arguments, and + # zsh's bashcompinit runs `complete -C` commands with no arguments (#532). + for _arg; do if [[ "${_arg}" == -chdir=* ]]; then chdir="${_arg#-chdir=}"; log 'debug' "Found -chdir arg: ${chdir}"; diff --git a/test/test_exec.sh b/test/test_exec.sh new file mode 100644 index 00000000..6a16efd1 --- /dev/null +++ b/test/test_exec.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Source common test setup +source "$(dirname "${0}")/test_common.sh"; + +##################### +# Begin Script Body # +##################### + +declare -a errors=(); + +log 'info' '### Test Suite: terraform shim (tfenv-exec)'; + +cleanup || log 'error' 'Cleanup failed?!'; +tfenv install 1.6.1 || early_death 'Failed to install 1.6.1 for shim tests'; +tfenv use 1.6.1 || early_death 'Failed to use 1.6.1 for shim tests'; + +# zsh's bashcompinit runs `complete -C` commands with no arguments, and Bash 3.2 +# (macOS /bin/bash) treats "$@" as unbound under `set -u` when it is empty (#532). +# Run the shim under each available bash so the system bash is exercised on macOS +# even when a newer bash is first on PATH. +for shell in bash /bin/bash; do + command -v "${shell}" >/dev/null 2>&1 || continue; + log 'info' "## terraform (no args) under ${shell}: passes through to terraform usage"; + ( + declare output; + output="$("${shell}" "${TFENV_ROOT}/bin/terraform" 2>&1 || true)"; + echo "${output}" | grep -q 'unbound variable' && exit 1; + echo "${output}" | grep -q 'Usage: terraform' || exit 1; + ) && log 'info' "## terraform (no args) under ${shell}: passed" \ + || error_and_proceed "terraform with no args under ${shell} did not reach terraform usage"; +done; + +log 'info' '## terraform -chdir with a space in the path: argument is not word-split'; +cleanup || log 'error' 'Cleanup failed?!'; +( + echo '1.6.1' > .terraform-version; + mkdir -p 'chdir-dir/with space'; + echo '1.6.0' > 'chdir-dir/with space/.terraform-version'; + declare output; + output="$(terraform '-chdir=chdir-dir/with space' version 2>&1)" || exit 1; + echo "${output}" | grep -q 'Terraform v1.6.0' || exit 1; +) && log 'info' '## terraform -chdir with a space in the path: passed' \ + || error_and_proceed 'terraform -chdir with a space in the path did not resolve the version from that directory'; + +cleanup || log 'error' 'Cleanup failed?!'; + +finish_tests 'exec'; + +exit 0;