Bash empty array expansion with `set -u`
I'm writing a bash script which has set -u
, and I have a problem with empty array expansion: bash appears to treat an empty array as an unset variable during expansion:
$ set -u
$ arr=()
$ echo "foo: '${arr[@]}'"
bash: arr[@]: unbound variable
(declare -a arr
doesn't help either.)
A common solution to this is to use ${arr[@]-}
instead, thus substituting an empty string instead of the ("undefined") empty array. However this is not a good solution, since now you can't discern between an array with a single empty string in it and an empty array. (@-expansion is special in bash, it expands "${arr[@]}"
into "${arr[0]}" "${arr[1]}" …
, which makes it a perfect tool for building command lines.)
$ countArgs() { echo $#; }
$ countArgs a b c
3
$ countArgs
0
$ countArgs ""
1
$ brr=("")
$ countArgs "${brr[@]}"
1
$ countArgs "${arr[@]-}"
1
$ countArgs "${arr[@]}"
bash: arr[@]: unbound variable
$ set +u
$ countArgs "${arr[@]}"
0
So is there a way around that problem, other than checking the length of an array in an if
(see code sample below), or turning off -u
setting for that short piece?
if [ "${#arr[@]}" = 0 ]; then
veryLongCommandLine
else
veryLongCommandLine "${arr[@]}"
fi
Update: Removed bugs
tag due to explanation by ikegami.
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.
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...