8

In bash, the conditional expression with the unary test -v myvariable tests wether the variable myvariable has been set. Note that myvariable should not be expanded by prefixing it with a dollar, so not $myvariable. Now I find that for array elements the conditional expression -v myarray[index] works well too, without the full expansion syntax ${myarray[$index]}. Try this:

    myarray[2]=myvalue
    for i in 1 2 3
    do
        [ -v myarray\[i] ] && echo element $i is set
    done

(note the escape \[ to prevent globbing, as alternative to using quotes)

gives the desired output:

    element 2 is set

Question Is this behaviour safe to use aka is this documented behaviour?

Addendum After reading the answer https://unix.stackexchange.com/a/677920/376817 of Stéphane Chazelas, I expanded my example:

    myarray[1]=val myarray[2]=val myarray[3]=val myarray[4]=val myarray[5]=val myarray[6]="" myarray[2]=""
    unset myarray[3] myarray[4] myarray[5]
    touch myarray4 myarrayi
    myarray4=val myarrayi=val

then

    for i in {0..7}; do [ -v myarray\[i] ] && echo element $i is set; done

gives

    element 1 is set
    element 2 is set
    element 6 is set

Without quoting or escaping the index expression [i] :

    for i in {0..7}; do [ -v myarray[i] ] && echo element $i is set; done

gives

    element 0 is set
    element 1 is set
    element 2 is set
    element 3 is set
    element 4 is set
    element 5 is set
    element 6 is set
    element 7 is set

The same with the variable myarrayi unset :

    unset myarrayi
    for i in {0..7}; do [ -v myarray[i] ] && echo element $i is set; done

gives

    %nothing%

And finally with expansion of the index as $i (still without quoting or escaping the bracket) :

    for i in {0..7}; do [ -v myarray[$i] ] && echo element $i is set; done

it gives

    element 1 is set
    element 2 is set
    element 4 is set

because

    ls -l myarray*

shows

    -rw-rw-r-- 1 me us 0 nov 17 15:37 myarray4
    -rw-rw-r-- 1 me us 0 nov 17 15:37 myarrayi
0

2 Answers 2

15

In bash, you can do:

[ -v 'a[2]' ]

or

[[ -v a[2] ]]

To test whether the array element of index 2, or the associative array element of key "2" is set (even if that's to an empty string), but note:

  • When using the [ (aka test) command, you need to quote the [ and ] characters as they are globbing operators. Since [ is just an ordinary command, it's interpreted like any other ordinary command, so the a[2] in [ -v a[2] ] would be expanded the same way as it would in ls -d a[2] or unset a[2]. If there was a file called a2 in the current working directory, a[2] would expand to that. And if there wasn't but the nullglob or failglob was enabled a[2] would expand to nothing or give an error respectively. [[ ... ]] is a special construct with its own syntax so wouldn't have the problem.

  • For associative arrays (at least in version 5.1 which I tested this on), if the key to check is in a $i variable, you'd want [ -v 'a[$i]' ] or [[ -v 'a[$i]' ]] and the assoc_expand_once option introduced in newer versions of bash not to be enabled. Using [ -v "a[$i]" ] or [[ -v a[$i] ]] wouldn't work for some values of $i containing ] or backslashes. There, it's the $ that must be quoted. See also How to use associative arrays safely inside arithmetic expressions?.

  • Still for associative arrays, note that bash (contrary to ksh93 which bash attempted to copy or zsh) doesn't support the empty keys. If you use [[ -v 'a[$i]' ]] and $i is the empty string, you'd get an error. So to test for arbitrary key values, use [[ -n $i && -v 'a[$i]' ]] or [ -n "$i" ] && [ -v 'a[$i]' ].

  • For normal (sparse) arrays, in [ -v 'a[expr]' ] or [[ -v a[expr] ]], expr is evaluated as an arithmetic expression. That's why both i and $i work. Since arithmetic expression can have side effects of assigning variables or running arbitrary commands, it's important that the value of $i used in [ -v 'a[i]' ] be sanitised or otherwise you have an arbitrary command execution vulnerability. As seen at Security implications of forgetting to quote a variable in bash/POSIX shells, the fact the bash's [ -v lvalue ] works for array indices makes things like [ -f $file ] (where the author forgot to quote $file) an ACE vulnerability.

  • in any case, you can always use the Bourne/POSIX method ([ -n "${var+set}" ]) applied to the (associative) array element: [ -n "${a[$key]+set}" ] which doesn't need any work around and works with all versions of bash with array support (2.0 (1996) or above) or associative array support (4.0 (2009) or above) and is portable across shells with those.

  • note that [ -v var ] is actually the same as [ -v 'var[0]' ]. Like in ksh88, scalar variables can be seen as the element of index 0 of an array.

  • to check whether an array or associative array has any element (or that a scalar variable be set), you can also do [ "${#a[@]}" -gt 0 ] or [ -v 'a[@]' ] or [ -v 'a[*]' ]. 2023 edit, as noted by @johnraff, those last two no longer work for associative array since version 5.2 unless you enable 5.1 compatibility.

  • to loop over the indices of sparse arrays or keys of associative arrays, you can do:

    for key in "${!a[@]}"; do
      printf 'The element of key "%s" is set\n' "$key"
    done
    

    (that gives 0 for a scalar variable).

As to whether that's documented: if you run info bash test or info bash '[', you'll see that it defers to Bash conditional expressions (as used inside [[ ... ]]), where the documentation of -v has:

-v VARNAME
True if the shell variable VARNAME is set (has been assigned a value).

While help test has:

 -v VAR         True if the shell variable VAR is set.

What VARNAME may be (in particular if array members are allowed) or what it does if VARNAME refers to a variable that is not a scalar variable is not clearly specified. But given that a[x] is generally allowed wherever a variable name is expected and that it's been the case for decades, we can probably safely assume it will remain the case in the future.

If you look at other sections of that official documentation, you'll see that it's generally implied that wherever a variable name is expected (whether it's referred to as NAME, VAR, VARNAME or more generally PARAMETER), varname[index] is also accepted. For instance, in the documentation of unset itself (info bash unset), there's no mention of unset 'array[i]', but it is mentioned in the section about arrays.

The NEWS file in the source distribution (the release notes) tells us that test -v was added in bash-4.2 (2011), likely inspired from ksh93 which added it not long before in ksh93t+ (2009, with array element support mentioned in its own release notes)

f. test/ [ / [[ have a new -v variable unary operator, which returns success if `variable' has been set.

And in 4.3:

ll. The test / [ / [[ -v variable binary operator now understands array references.

In 5.1:

x. test -v N can now test whether or not positional parameter N is set.

You'll find that CWRU/changelog in the source distribution has some mentions of test -v array[@] or [[ -v array[$key] ]] for associative arrays, hinting again at the fact that the feature is intentional.

It's not impossible that some things could be done in the future to address some of the problems mentioned above which could invalidate the work arounds I mention such as needing to quote $.

8
  • 1
    Good remark on quoting the operand of -v. I reran my example with a file myarrayi, and a variable myarrayi set or unset, and the globbing occurs as you say. I edited the example. Also thanks for explaining why myarray[$i] gives the same result as myarray[i]. But do you answer my question? You say that my code works; I tested it, so I knew that. But is it documentated that the -v unary operator also works on the name[index] construct, and not only on a simple name? I do not like to use undocumented features. That is my question, that I feel you do not answer.
    – db-inf
    Nov 17, 2021 at 14:19
  • I see you edited your answer to try to address my question, while I was writing my previous comment. I still hope someone may know of relevant documentation elsewhere.
    – db-inf
    Nov 17, 2021 at 14:27
  • 1
    @db-inf. The info book is the official documentation (you can also find it in other formats derived from the same source like the HTML one I linked). Ultimately, you can look at the source to see how it's done and see if the behaviour is intentional or an accident of implementation that may go away in the future. In any case, Chet Ramey will decide whether to keep it in the future or not, whether it's documented or not. As it's not clearly documented, he could decide to drop it, but that would upset a number of users. So most likely, you'd still be able to use BASH_COMPAT=5.1 to use it then. Nov 17, 2021 at 14:35
  • thanks for clarifying that
    – db-inf
    Nov 17, 2021 at 19:48
  • I followed your advice, and dived into the source code at ftp.gnu.org/pub/gnu/bash. Most of the code for the -v unary test turns out to be for array checking. So yes, it is on purpose that ` -v 'myarray[i]'` gives the result as is. However, between my current version 4.4(.18) and the most recent 5.1.8 there has been a lot of change to the code: it is not stable yet, and the lack of detailed documentation may be a consequence thereof.
    – db-inf
    Nov 17, 2021 at 20:45
2

With bash 5.2 it is no longer possible to test whether an associative array contains any elements with test -v 'arr[@]', which now tests for a key specifically named '@'.

I guess test "${#arr[@]}" -gt 0 or (( ${#k[@]} > 0 )) will do instead.

See compat51: https://www.gnu.org/software/bash/manual/bash.html#Shell-Compatibility-Mode

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .