Bash empty array extension with `set -u`

I am writing a bash script that has set -u and I have a problem with an empty array extension: bash seems to treat the empty array as an unset variable during extension:

 $ 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, replacing an empty string instead of an empty array ("undefined"). However, this is not a very good solution, since now you cannot distinguish between an array with a single empty string in it and an empty array. (@ -expansion is special in bash, it extends "${arr[@]}" to "${arr[0]}" "${arr[1]}" … making it an ideal tool for building a command line .)

 $ countArgs() { echo $#; } $ countArgs abc 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 to solve this problem besides checking the length of the array in if (see the code example below) or disabling -u for this short part?

 if [ "${#arr[@]}" = 0 ]; then veryLongCommandLine else veryLongCommandLine "${arr[@]}" fi 

Update: Removed bugs tag due to ikegami explanation.

+77
bash
Sep 28 '11 at 0:22
source share
10 answers

According to the documentation,

An array variable is considered set if the subscript was assigned to the value. A null string is a valid value.

The subscript does not matter, so the array is not specified.

But although the documentation suggests that a mistake is relevant here, this is not the case starting with 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 condition that you can use to achieve what you want in older versions, built-in: 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=(abc) $ args ${arr[@]+"${arr[@]}"} 3 0: a 1: b 2: c 

Tested with bash 4.2.25 and 4.3.11.

+64
Sep 28 '11 at 0:50
source share

@ikegami accepted the answer subtly wrong! The correct spell is ${arr[@]+"${arr[@]}"} :

 $ countArgs () { echo "$#"; } $ arr=('') $ countArgs "${arr[@]:+${arr[@]}}" 0 # WRONG $ countArgs ${arr[@]+"${arr[@]}"} 1 # RIGHT $ arr=() $ countArgs ${arr[@]+"${arr[@]}"} 0 # Let make sure it still works for the other case... 
+22
Dec 18 '15 at 18:25
source share

this may be another option for those who prefer not to duplicate arr [@] and have no empty line

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

for check:

 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 
+13
Apr 03 '13 at 6:08
source share

It turns out that array processing has been changed in the recently released (2016/09/16) bash 4.4 (available, for example, in Debian Stretch).

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

Now the expansion of empty arrays does not generate a warning

 $ set -u $ arr=() $ echo "${arr[@]}" $ # everything is fine 
+12
Sep 25 '16 at
source share

@ikegami's answer is correct, but I think the syntax "${arr[@]:+${arr[@]}}" is terrible. If you use long array variable names, it starts to look spaghetti faster than usual.

Try this instead:

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

The Bash array slice operator seems very simple.

So why did Bash make boundary array processing so complicated? Sigh. I can not guarantee that the version will allow such abuse of the slice operator of the array, but it works for me.

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

+6
Nov 09 '13 at 17:18
source share

"Interesting" inconsistency really.

Besides,

 $ set -u $ echo $# 0 $ echo "$1" bash: $1: unbound variable # makes sense (I didn't set any) $ echo "$@" | cat -e $ # blank line, no error 

Although I agree that the current behavior cannot be a mistake in the sense that @ikegami explains, IMO we could say that the error is indicated in the definition (of the "set" itself) and / or that it was inconsistently applied. The previous paragraph on the man page says:

... ${name[@]} expands each element of the name to a single word. When there are no array elements, ${name[@]} expands to zero.

which is fully consistent with what he says about the expansion of positional parameters in "$@" . Not that there were no other discrepancies in the behavior of arrays and positional parameters ... but for me there is no hint that this part should be incompatible between them.

Continuation

 $ arr=() $ echo "${arr[@]}" bash: arr[@]: unbound variable # as we've observed. BUT... $ echo "${#arr[@]}" 0 # no error $ echo "${!arr[@]}" | cat -e $ # no error 

So, arr[] not so disconnected that we cannot get the number of its elements (0) or the (empty) list of its keys? For me, these are reasonable and useful - the only exception is the extension ${arr[@]} (and ${arr[*]} ).

+6
Feb 12 '16 at 2:35
source share

Here are some ways to do something like this using sentinels and the other using conditional additions:

 #!/bin/bash set -o nounset -o errexit -o pipefail countArgs () { echo "$#"; } arrA=( sentinel ) arrB=( sentinel "{1..5}" "./*" "with spaces" ) arrC=( sentinel '$PWD' ) cmnd=( countArgs "${arrA[@]:1}" "${arrB[@]:1}" "${arrC[@]:1}" ) echo "${cmnd[@]}" "${cmnd[@]}" arrA=( ) arrB=( "{1..5}" "./*" "with spaces" ) arrC=( '$PWD' ) cmnd=( countArgs ) # Checks expansion of indices. [[ ! ${!arrA[@]} ]] || cmnd+=( "${arrA[@]}" ) [[ ! ${!arrB[@]} ]] || cmnd+=( "${arrB[@]}" ) [[ ! ${!arrC[@]} ]] || cmnd+=( "${arrC[@]}" ) echo "${cmnd[@]}" "${cmnd[@]}" 
+2
Sep 28 2018-11-11T00:
source share

I am complementary on @ Ikehah's (accepted) and @kevinarpe's (also good) answers.

You can do "${arr[@]:+${arr[@]}}" to get around the problem. The right-hand side (i.e. after :+ ) provides an expression that will be used if the left-hand side is undefined / empty.

The syntax is secret. Please note that the right side of the expression will undergo parameter expansion, so special attention should be paid to the presence of consecutive quotes.

 : example copy arr into arr_copy arr=( "1 2" "3" ) arr_copy=( "${arr[@]:+${arr[@]}}" ) # good. same quoting. # preserves spaces arr_copy=( ${arr[@]:+"${arr[@]}"} ) # bad. quoting only on RHS. # copy will have ["1","2","3"], # instead of ["1 2", "3"] 

As @kevinarpe mentions, a less cryptic syntax is to use the ${arr[@]:0} array slice notation (in Bash versions >= 4.4 ), which extends to all parameters starting at index 0. This also doesn't require so much repetitions. This extension works regardless of set -u , so you can always use it. The man page says (under the options extension ):

  • ${parameter:offset}

  • ${parameter:offset:length}

    ... If the parameter is an indexed array name signed with the @ or * character, the result is the length of the array element starting with ${parameter[offset]} . A negative offset is taken relative to one that exceeds the maximum index of the specified array. This is an extension error if the length is zero.

This is an example provided by @kevinarpe, with alternative formatting to put the output as evidence:

 set -u function count() { echo $# ; }; ( count xyz ) : prints "3" ( arr=() count "${arr[@]}" ) : prints "-bash: arr[@]: unbound variable" ( arr=() count "${arr[@]:0}" ) : prints "0" ( arr=(xyz) count "${arr[@]:0}" ) : prints "3" 

This behavior is dependent on the version of Bash. You may also have noticed that the operator of length ${#arr[@]} will always be set to 0 for empty arrays, regardless of set -u , without causing an β€œunbound variable error”.

+2
Mar 13 '18 at 20:15
source share

Interesting inconsistency; this allows you to define something that is "not considered installed" but is shown on the output of declare -p

 arr=() set -o nounset echo $arr[@] => -bash: arr[@]: unbound variable declare -p arr => declare -a arr='()' 
+1
Mar 12 '15 at 21:13
source share

The easiest and most compatible way:

 $ set -u $ arr=() $ echo "foo: '${arr[@]-}'" 
-2
May 22 '17 at 19:57
source share



All Articles