Skip to content

Uninitialised tf_var_file_paths array crashes with "unbound variable" when no tfvars files exist #51

Description

@Zordrak

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:606cat "${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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions