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 1/3] 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; From c13e7714b12454a229c0df03c607ba2a3488807a Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:28:50 -0600 Subject: [PATCH 2/3] test_exec.sh: add plain argument pass-through case and executable bit Borrowed from #539: a terraform 'version -json' case that checks arguments reach terraform independent of -chdir handling, and chmod +x to match the other test_*.sh files. Amp-Thread-ID: https://ampcode.com/threads/T-01a18edd-7c03-774f-890d-1da6f2ebc8c4 Co-authored-by: Amp --- test/test_exec.sh | 9 +++++++++ 1 file changed, 9 insertions(+) mode change 100644 => 100755 test/test_exec.sh diff --git a/test/test_exec.sh b/test/test_exec.sh old mode 100644 new mode 100755 index 6a16efd1..2e8f15a4 --- a/test/test_exec.sh +++ b/test/test_exec.sh @@ -31,6 +31,15 @@ for shell in bash /bin/bash; do || error_and_proceed "terraform with no args under ${shell} did not reach terraform usage"; done; +log 'info' '## terraform (with args): arguments are passed through to terraform'; +( + declare output; + output="$(terraform version -json)" || exit 1; + echo "${output}" | grep -q 'terraform_version' || exit 1; + echo "${output}" | grep -q '1.6.1' || exit 1; +) && log 'info' '## terraform (with args): passed' \ + || error_and_proceed 'terraform arguments were not passed through'; + log 'info' '## terraform -chdir with a space in the path: argument is not word-split'; cleanup || log 'error' 'Cleanup failed?!'; ( From b71fe7ee94882bc70b8082a6ce927a8b3c60b294 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:22:07 -0600 Subject: [PATCH 3/3] test_exec.sh: add vim modeline per bash.instructions.md Amp-Thread-ID: https://ampcode.com/threads/T-01a18edd-7c03-774f-890d-1da6f2ebc8c4 Co-authored-by: Amp --- test/test_exec.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_exec.sh b/test/test_exec.sh index 2e8f15a4..85759b7b 100755 --- a/test/test_exec.sh +++ b/test/test_exec.sh @@ -57,3 +57,4 @@ cleanup || log 'error' 'Cleanup failed?!'; finish_tests 'exec'; exit 0; +# vim: set syntax=bash tabstop=2 softtabstop=2 shiftwidth=2 expandtab smarttab :