Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash empty array expansion with `set -u`

Tags:

bash

According to the documentation,

An array variable is considered set if a subscript has been assigned a value. The null string is a valid value.

No subscript has been assigned a value, so the array isn't set.

But while the documentation suggests an error is appropriate here, this is no longer the case since 4.4.

$ bash --version | head -n 1
GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)

$ set -u

$ arr=()

$ echo "foo: '${arr[@]}'"
foo: ''

There is a conditional you can use inline to achieve what you want in older versions: Use ${arr[@]+"${arr[@]}"} instead of "${arr[@]}".

$ function args { perl -E'say 0+@ARGV; say "$_: $ARGV[$_]" for 0..$#ARGV' -- "$@" ; }

$ set -u

$ arr=()

$ args "${arr[@]}"
-bash: arr[@]: unbound variable

$ args ${arr[@]+"${arr[@]}"}
0

$ arr=("")

$ args ${arr[@]+"${arr[@]}"}
1
0: 

$ arr=(a b c)

$ args ${arr[@]+"${arr[@]}"}
3
0: a
1: b
2: c

Tested with bash 4.2.25 and 4.3.11.


The only safe idiom is ${arr[@]+"${arr[@]}"}

Unless you only care about Bash 4.4+, but you wouldn't be looking at this question if that were the case :)

This is already the recommendation in ikegami's answer, but there's a lot of misinformation and guesswork in this thread. Other patterns, such as ${arr[@]-} or ${arr[@]:0}, are not safe across all major versions of Bash.

As the table below shows, the only expansion that is reliable across all modern-ish Bash versions is ${arr[@]+"${arr[@]}"} (column +"). Of note, several other expansions fail in Bash 4.2, including (unfortunately) the shorter ${arr[@]:0} idiom, which doesn't just produce an incorrect result but actually fails. If you need to support versions prior to 4.4, and in particular 4.2, this is the only working idiom.

Screenshot of different idioms across versions

Unfortunately other + expansions that, at a glance, look the same do indeed emit different behavior. Using :+ instead of + (:+" in the table), for example, does not work because :-expansion treats an array with a single empty element (('')) as "null" and thus doesn't (consistently) expand to the same result.

Quoting the full expansion instead of the nested array ("${arr[@]+${arr[@]}}", "+ in the table), which I would have expected to be roughly equivalent, is similarly unsafe in 4.2.

You can see the code that generated this data along with results for several additional version of bash in this gist.


@ikegami's accepted answer is subtly wrong! The correct incantation is ${arr[@]+"${arr[@]}"}:

$ countArgs () { echo "$#"; }
$ arr=('')
$ countArgs "${arr[@]:+${arr[@]}}"
0   # WRONG
$ countArgs ${arr[@]+"${arr[@]}"}
1   # RIGHT
$ arr=()
$ countArgs ${arr[@]+"${arr[@]}"}
0   # Let's make sure it still works for the other case...

Turns out array handling has been changed in recently released (2016/09/16) bash 4.4 (available in Debian stretch, for example).

$ bash --version | head -n1
bash --version | head -n1
GNU bash, version 4.4.0(1)-release (x86_64-pc-linux-gnu)

Now empty arrays expansion does not emits warning

$ set -u
$ arr=()
$ echo "${arr[@]}"

$ # everything is fine

this may be another option for those who prefer not to duplicate arr[@] and are okay to have an empty string

echo "foo: '${arr[@]:-}'"

to test:

set -u
arr=()
echo a "${arr[@]:-}" b # note two spaces between a and b
for f in a "${arr[@]:-}" b; do echo $f; done # note blank line between a and b
arr=(1 2)
echo a "${arr[@]:-}" b
for f in a "${arr[@]:-}" b; do echo $f; done

@ikegami's answer is correct, but I consider the syntax ${arr[@]+"${arr[@]}"} dreadful. If you use long array variable names, it starts to looks spaghetti-ish quicker than usual.

Try this instead:

$ set -u

$ count() { echo $# ; } ; count x y z
3

$ count() { echo $# ; } ; arr=() ; count "${arr[@]}"
-bash: abc[@]: unbound variable

$ count() { echo $# ; } ; arr=() ; count "${arr[@]:0}"
0

$ count() { echo $# ; } ; arr=(x y z) ; count "${arr[@]:0}"
3

It looks like the Bash array slice operator is very forgiving.

So why did Bash make handling the edge case of arrays so difficult? Sigh. I cannot guarantee you version will allow such abuse of the array slice operator, but it works dandy for me.

Caveat: I am using GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu) Your mileage may vary.