7

Since bash 4.2, the -v conditional expression has been added. It's used to check if a variable is set (has been assigned a value). It's a useful tool when writing scripts that run under set -o nounset, because attempting to use a non-initialized variable causes an error.

I have one issue with it, see the sample in a POSIX bash (set -o posix):

$ declare testvar=0
$ [ -v testvar ] && echo "It works!"
It works!
$ declare -A fizz=( [buzz]=jazz )
$ [ -v fizz[buzz] ] && echo "It works!"
It works!
$ [ -v fizz ] && echo "It doesn't work :("
$ 

As you can see, using -v on a regular variable works as expected. Using it to check the presence of a field within an associative array works as expected as well. However, when checking the presence of the associative array itself, it doesn't work.

Why is that, and is there a workaround?

T. Barusseau
  • 315
  • 1
  • 8
  • Can't you still use `[ -z ${var+x} ]` even for `[ -z ${fizz[buzz]+x} ]` like in this [question](https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash)? – KamilCuk Dec 12 '19 at 12:16
  • 2
    Weird. When you populate `fizz[0]`, `-v fizz` works. It somewhat makes sense though, since bare array name (fizz) is equivalent to fizz[0] during expansion – oguz ismail Dec 12 '19 at 12:20
  • 1
    @oguzismail: Uh, you're right. This is very strange but this *feature* might be okay as a work-around :) – T. Barusseau Dec 12 '19 at 12:22
  • After further testing it seems that it only works if a `0` key is present. Which makes think that maybe, under the hood, it's not intended to be used on associative arrays: it checks for the presence of the 0th element. – T. Barusseau Dec 12 '19 at 12:26
  • Probably. I'll wait for someone with more knowledge to post a comprehensive explanation here anyways – oguz ismail Dec 12 '19 at 12:28
  • Take a look at `declare -p fizz` as well, I guess it could be of use – oguz ismail Dec 12 '19 at 12:37
  • 1
    Test if `fizz` is declared as an associative array, regardless of existing entries: `[[ "$(typeset -p fizz 2>&1)" =~ ^declare\ -A ]] && echo 'ok'` – Léa Gris Dec 12 '19 at 15:38
  • @oguzismail: `declare -p` is a good suggestion, still feels a bit hack-ish but less that the `0`-key workaround :) If you post it as an answer I'll gladly accept it. – T. Barusseau Dec 12 '19 at 16:34
  • I think it's worth reporting as a bug, or at least a feature request. `$foo` expanding as `${foo[0]}` makes some sense with regard to POSIX compatibility, but with `-v` already being undefined by POSIX, there's no need for `foo` and `foo[0]` to be treated the same. – chepner Dec 12 '19 at 18:51
  • Note that `[ -v fizz[buzz] ]` wasn't supported until 4.4 as well, and in a very real sense `fizz` is not defined in the absence of a key. Arrays aren't distinct entities, just syntactic "magic" to support indexing. – chepner Dec 12 '19 at 18:56
  • @chepner Wrt *Arrays aren't distinct entities, just syntactic "magic" to support indexing.* that sounds correct based on the behavior, but the maual says *An array variable is considered set if a subscript has been assigned a value.*. I think this is a bit misleading, it bears in the mind that `-v` unary can be used on bare array names – oguz ismail Dec 12 '19 at 19:27
  • @oguzismail Not really (unless this is a bug): try `-v` with `arr` after `declare -a arr=([1]=3)`. `-v` really does seem to be testing for index 0. – chepner Dec 12 '19 at 19:43

2 Answers2

1

If you want to know if it has any entries, you can check ${#fizz[@]}, but that doesn't tell you if it has been created empty. You can use a case statement or other pattern matching in conjunction with error checking.

tst() { local tst;   
  (( $# )) || return                             # *minimal* error handling...
  if tst="$(declare -p $1 2>&1)"                 # does the subshell error?
  then case "$tst" in                            # if not, check the output
       *\)*) (( $( eval echo \${#$1[@]} ) )) &&  # test number of entries
                echo "Has Args"              ||  # some implementations show '()'
                echo "Empty but defined";;       # some don't; this is for the *do*
       *-A*) echo "Empty but defined";;          # this is for the don't
          *) echo ERROR;;                        # shouldn't get here
       esac
  else echo Undefined                            # if doesn't exist
  fi
}


$: unset fizz
$: tst fizz
Undefined
$: declare -A fizz
$: tst fizz
Empty but defined
$: fizz[buzz]=jazz
$: tst fizz
Has Args

Dunno how useful that will be in your context though. Note that I wrote this one explicitly for associative arrays with minimal error checking not directly related to the point. For any sort of real production use this should be expanded a lot.

I don't like the eval, but my bash version doesn't have namerefs. :o/

Paul Hodges
  • 13,382
  • 1
  • 17
  • 36
  • I was really hoping for a simple way to use `${!fizz*}` or `${fizz?blah}` but nothing seems to be working the way I'd've hoped for this specific case of the difference between an empty associative array and an unset one... – Paul Hodges Dec 12 '19 at 15:24
  • Using `declare -p ` is a nice idea. Thanks for your input. – T. Barusseau Dec 12 '19 at 16:05
  • [oguz ismail](https://stackoverflow.com/users/10248678/oguz-ismail) suggested it above. I just expanded it out, lol – Paul Hodges Dec 12 '19 at 16:31
0

Strictly check is varname exist as an associative array:

check4Associative() {
    local foo typ
    read foo typ foo < <(declare -p $1 2>&1)
    [ "$typ" ] && [ -z "${typ//-*A*}" ]
}

You could use as:

if check4Associative myVar; then ... ;fi

Sample tries:

unset fizz
check4Associative fizz && echo yes;echo $?
1

fizz=Blah
check4Associative fizz && echo yes;echo $?
1

declare -A fizz
check4Associative fizz && echo yes;echo $?
yes
0

fizz[foo]=bar
check4Associative fizz && echo yes;echo $?
yes
0

One step further: entry count:

check4AssociativeArgs() {
    local -n var=$1
    local keys=(${!var[@]}) foo typ
    read foo typ foo < <(declare -p $1 2>&1)
    [ "$typ" ] && [ -z "${typ//-*A*}" ] &&
        printf "Name: %s, type: %s, entry count: %d\n" $1 ${typ#-} ${#keys[@]}
}

Then

unset fizz
check4AssociativeArgs fizz ; echo $?
1

declare -A fizz
check4AssociativeArgs fizz
Name: fizz, type: A, entry count: 0

fizz[foo]=bar fizz[bar]=baz
check4AssociativeArgs fizz
Name: fizz, type: A, entry count: 2
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137