Summary
The tf_var_file_paths array is declared without an empty initialiser
(declare -a tf_var_file_paths;) and populated only conditionally. When no
tfvars files are found, the array remains unset, and the first [@]
expansion at bin/terraform.sh:606 aborts under set -u
with tf_var_file_paths: unbound variable — on all bash versions.
Root Cause
set -uo pipefail is enabled at bin/terraform.sh:8.
The array is declared without an =() initialiser:
bin/terraform.sh:560
declare -a tf_var_file_paths;
Under set -u, a declare -a name; with no assignment is treated as an
unset variable — expanding "${name[@]}" or "${#name[@]}" aborts the
script. This is distinct from declare -a name=(), which creates a defined
(empty) array that expands safely. The other two arrays in the script are
correctly initialised:
tf_var_file_paths is the sole exception.
It is then populated only through conditional appends:
bin/terraform.sh:566-567
[ -f "${global_vars_file_path}" ] && tf_var_file_paths+=("${global_vars_file_path}");
[ -f "${region_vars_file_path}" ] && tf_var_file_paths+=("${region_vars_file_path}");
…plus further conditional appends for groups, env file, versions file, and
remote tfvars. If none of those files exist, the array is never assigned,
so it stays unset until the first expansion:
bin/terraform.sh:606
duplicate_variables="$(cat "${tf_var_file_paths[@]}" | ... )";
This is also reached again at
bin/terraform.sh:621 and
bin/terraform.sh:634, but line 606 aborts first.
Why it is usually masked
The shipped etc/global.tfvars guarantees at least one element by line 566, so
the example scaffold never triggers this. The bug surfaces in a real project
that does not ship a global.tfvars (or any region/group/env/versions/remote
tfvars) — for example a minimal bootstrap where the etc/ files have been
removed or not yet created.
Evidence
Minimal reproduction of the mechanism (matches the declaration style at
line 560):
$ bash -c 'set -uo pipefail
declare -a arr
echo "count: ${#arr[@]}"'
bash: line 1: arr: unbound variable
# exit code 1
Contrast with the safe form used by the other arrays:
$ bash -c 'set -uo pipefail
declare -a arr=()
echo "count: ${#arr[@]}"'
count: 0
# exit code 0
Expected: with no tfvars files present, tfscaffold proceeds with zero
-var-file arguments.
Actual: aborts with tf_var_file_paths: unbound variable at line 606.
Reproduction Steps
In a project whose etc/ directory contains no matching tfvars files (no
global.tfvars, no <region>.tfvars, no group_*, no env_<region>_<env>,
no versions_<region>_<env>) and with no remote tfvars in S3:
bin/terraform.sh \
-p myproject \
-c mycomponent \
-e dev \
-r eu-west-2 \
-a plan
The run aborts at bin/terraform.sh:606 with
line 606: tf_var_file_paths: unbound variable.
(Bootstrap runs are affected identically, and even more likely to have an empty
etc/.)
Suggested Fix
Initialise the array with an empty initialiser so it is always a defined
(possibly empty) array, matching secrets and groups:
declare -a tf_var_file_paths=();
The fixer should also sanity-check the two downstream consumers that assume a
non-empty array:
- bin/terraform.sh:606 —
cat "${tf_var_file_paths[@]}"
with zero elements becomes a bare cat that would read stdin. It should be
guarded (e.g. skip the duplicate-detection block when
${#tf_var_file_paths[@]} -eq 0) so it neither hangs nor misbehaves when the
array is legitimately empty.
- The
for file_path in "${tf_var_file_paths[@]}" loops at
621 and 634 are safe over an
empty initialised array and need no change beyond the initialiser.
Affected Platforms
All platforms and all bash versions (verified on bash 5.1.16). Unlike the
empty-array-expansion gotcha fixed in bash 4.4, an uninitialised
declare -a is treated as unset on every bash version, so this is not
macOS-specific.
Related
Summary
The
tf_var_file_pathsarray is declared without an empty initialiser(
declare -a tf_var_file_paths;) and populated only conditionally. When notfvars files are found, the array remains unset, and the first
[@]expansion at bin/terraform.sh:606 aborts under
set -uwith
tf_var_file_paths: unbound variable— on all bash versions.Root Cause
set -uo pipefailis enabled at bin/terraform.sh:8.The array is declared without an
=()initialiser:bin/terraform.sh:560
Under
set -u, adeclare -a name;with no assignment is treated as anunset variable — expanding
"${name[@]}"or"${#name[@]}"aborts thescript. This is distinct from
declare -a name=(), which creates a defined(empty) array that expands safely. The other two arrays in the script are
correctly initialised:
declare -a secrets=();(safe)declare -a groups=();(safe)tf_var_file_pathsis the sole exception.It is then populated only through conditional appends:
bin/terraform.sh:566-567
…plus further conditional appends for groups, env file, versions file, and
remote tfvars. If none of those files exist, the array is never assigned,
so it stays unset until the first expansion:
bin/terraform.sh:606
This is also reached again at
bin/terraform.sh:621 and
bin/terraform.sh:634, but line 606 aborts first.
Why it is usually masked
The shipped
etc/global.tfvarsguarantees at least one element by line 566, sothe example scaffold never triggers this. The bug surfaces in a real project
that does not ship a
global.tfvars(or any region/group/env/versions/remotetfvars) — for example a minimal bootstrap where the
etc/files have beenremoved or not yet created.
Evidence
Minimal reproduction of the mechanism (matches the declaration style at
line 560):
Contrast with the safe form used by the other arrays:
Expected: with no tfvars files present, tfscaffold proceeds with zero
-var-filearguments.Actual: aborts with
tf_var_file_paths: unbound variableat line 606.Reproduction Steps
In a project whose
etc/directory contains no matching tfvars files (noglobal.tfvars, no<region>.tfvars, nogroup_*, noenv_<region>_<env>,no
versions_<region>_<env>) and with no remote tfvars in S3:The run aborts at bin/terraform.sh:606 with
line 606: tf_var_file_paths: unbound variable.(Bootstrap runs are affected identically, and even more likely to have an empty
etc/.)Suggested Fix
Initialise the array with an empty initialiser so it is always a defined
(possibly empty) array, matching
secretsandgroups:The fixer should also sanity-check the two downstream consumers that assume a
non-empty array:
cat "${tf_var_file_paths[@]}"with zero elements becomes a bare
catthat would read stdin. It should beguarded (e.g. skip the duplicate-detection block when
${#tf_var_file_paths[@]} -eq 0) so it neither hangs nor misbehaves when thearray is legitimately empty.
for file_path in "${tf_var_file_paths[@]}"loops at621 and 634 are safe over an
empty initialised array and need no change beyond the initialiser.
Affected Platforms
All platforms and all bash versions (verified on bash 5.1.16). Unlike the
empty-array-expansion gotcha fixed in bash 4.4, an uninitialised
declare -ais treated as unset on every bash version, so this is notmacOS-specific.
Related
set -uregressions exposed by enablingset -uinv2.3.0. Same cause class (pre-existing unguarded references), distinct root
cause and fix. Ideally all addressed together as a "complete the
set -uhardening" pass.