14

In Bourne like shell which support array variable, we can use some parsing to check if variable is an array.

All commands below were run after running a=(1 2 3).

zsh:

$ declare -p a
typeset -a a
a=( 1 2 3 )

bash:

$ declare -p a
declare -a a='([0]="1" [1]="2" [2]="3")'

ksh93:

$ typeset -p a
typeset -a a=(1 2 3)

pdksh and its derivative:

$ typeset -p a
set -A a
typeset a[0]=1
typeset a[1]=2
typeset a[2]=3

yash:

$ typeset -p a
a=('1' '2' '3')
typeset a

An example in bash:

if declare -p var 2>/dev/null | grep -q 'declare -a'; then
  echo array variable
fi

This approach is too much work and need to spawn a subshell. Using other shell builtin like =~ in [[ ... ]] do not need a subshell, but is still too complicated.

Is there easier way to accomplish this task?

  • Under what circumstances would you need to check whether your variables are arrays or not? – Kusalananda Dec 20 '18 at 10:07
10

I don't think you can, and I don't think it actually makes any difference.

unset a
a=x
echo "${a[0]-not array}"

x

That does the same thing in either of ksh93 and bash. It looks like possibly all variables are arrays in those shells, or at least any regular variable which has not been assigned special attributes, but I didn't check much of that.

The bash manual talks about different behaviors for an array versus a string variable when using += assignments, but it afterwards hedges and states that the the array only behaves differently in a compound assignment context.

It also states that a variable is considered an array if any subscript has been assigned a value - and explicitly includes the possibility of a null-string. Above you can see that a regular assignment definitely results in a subscript being assigned - and so I guess everything is an array.

Practically, possibly you can use:

[ 1 = "${a[0]+${#a[@]}}" ] && echo not array

...to clearly pinpoint set variables that have only been assigned a single subscript of value 0.

  • So I guess checking if ${a[1]-not array} can do the task, can't it? – cuonglm Nov 28 '15 at 4:58
  • @cuonglm - Well, not according to the bash manual: An array variable is considered set if a subscript has been assigned a value. The null string is a valid value. If any subscript is assigned its an array per spec. In practice, also no, because you can do a[5]=x. I guess [ 1 -eq "${#a[@]}" ] && [ -n "${a[0]+1}" ] could work. – mikeserv Nov 28 '15 at 5:03
6

So you effectively want just the middle part of declare -p without the junk around it?

You could write a macro such as:

readonly VARTYPE='{ read __; 
       case "`declare -p "$__"`" in
            "declare -a"*) echo array;; 
            "declare -A"*) echo hash;; 
            "declare -- "*) echo scalar;; 
       esac; 
         } <<<'

so that you can do:

a=scalar
b=( array ) 
declare -A c; c[hashKey]=hashValue;
######################################
eval "$VARTYPE" a #scalar
eval "$VARTYPE" b #array
eval "$VARTYPE" c #hash

(A mere function won't do if you'll want to use this on function-local variables).


With aliases

shopt -s expand_aliases
alias vartype='eval "$VARTYPE"'

vartype a #scalar
vartype b #array
vartype c #hash
  • @mikeserv Good point. Aliases make it look prettier. +1 – PSkocik Nov 28 '15 at 19:41
  • i meant - alias vartype="$VARTYPE"... or just not defining the $VARTYPE at all - it should work, right? you only should need that shopt thing in bash because it breaks with the spec regarding alias expansion in scripts. – mikeserv Nov 28 '15 at 19:42
  • 1
    @mikeserv I'm sure cuonglm is well capable of tweaking this approach to his needs and preferences. ;-) – PSkocik Nov 28 '15 at 19:46
  • ... and security considerations. – PSkocik Nov 28 '15 at 19:47
  • At no point does the above code eval user provided text. It's no less secure than a function. I've never seen you fussing about making functions read-only, but OK, I can mark the variable readonly. – PSkocik Nov 28 '15 at 20:06
6

In zsh

zsh% a=(1 2 3) s=1
zsh% [[ ${(t)a} == *array* ]] && echo array
array
zsh% [[ ${(t)s} == *array* ]] && echo array
zsh%
  • Maybe echo ${(t)var} is simpler. Thanks for this. – user79743 Nov 28 '15 at 21:00
4

To test variable var, with

b=("${!var[@]}")
c="${#b[@]}"

It is possible to test if there are more than one array index:

[[ $c > 1 ]] && echo "Var is an array"

If the first index value is not zero:

[[ ${b[0]} -eq 0 ]] && echo "Var is an array"      ## should be 1 for zsh.

The only hard confusion is when there is only one index value and that value is zero (or one).

For that condition, it is possible to use a side effect of trying to remove an array element from a variable that is not an array:

**bash** reports an error with             unset var[0]
bash: unset: var: not an array variable

**zsh** also reports an error with         $ var[1]=()
attempt to assign array value to non-array

This works correctly for bash:

# Test if the value at index 0 could be unset.
# If it fails, the variable is not an array.
( unset "var[0]" 2>/dev/null; ) && echo "var is an array."

For zsh the index may need to be 1 (unless a compatible mode is active).

The sub-shell is needed to avoid the side effect of erasing index 0 of var.

I have found no way to make it work in ksh.

Edit 1

This function only works in bash4.2+

getVarType(){
    varname=$1;
    case "$(typeset -p "$varname")" in
        "declare -a"*|"typeset -a"*)    echo array; ;;
        "declare -A"*|"typeset -A"*)    echo hash; ;;
        "declare -- "*|"typeset "$varname*| $varname=*) echo scalar; ;;
    esac;
}

var=( foo bar );  getVarType var

Edit 2

This also work only for bash4.2+

{ typeset -p var | grep -qP '(declare|typeset) -a'; } && echo "var is an array"

Note: This will give false positives if var contains the tested strings.

  • How about array with zero elements? – cuonglm Nov 28 '15 at 8:32
  • 1
    Dat edit, tho. Looks very original. :D – PSkocik Nov 28 '15 at 20:30
  • @cuonglm The check ( unset "var[0]" 2>/dev/null; ) && echo "var is an array." correctly reports var is an array when var has been set to var=() an array with zero elements. It acts exactly equal to declare. – user79743 Nov 29 '15 at 12:53
  • The test for scalar won't work if the scalar is exported or marked integer/lowercase/readonly... You can probably safely make it that any other non-empty output means scalar variable. I'd use grep -E instead of grep -P to avoid the dependency on GNU grep. – Stéphane Chazelas Nov 30 '15 at 9:30
  • @StéphaneChazelas The test (in bash) for scalar with integer and/or lowercase and /or readonly always start with -a, like this: declare -airl var='()'. Therefore the grep test will work. – user79743 Nov 30 '15 at 17:32
3

For bash, it's a little bit of a hack (albeit documented): attempt to use typeset to remove the "array" attribute:

$ typeset +a BASH_VERSINFO
bash: typeset: BASH_VERSINFO: cannot destroy array variables in this way
echo $?
1

(You cannot do this in zsh, it allows you to convert an array to a scalar, in bash it's explicitly forbidden.)

So:

 typeset +A myvariable 2>/dev/null || echo is assoc-array
 typeset +a myvariable 2>/dev/null || echo is array

Or in a function, noting the caveats at the end:

function typeof() {
    local _myvar="$1"
    if ! typeset -p $_myvar 2>/dev/null ; then
        echo no-such
    elif ! typeset -g +A  $_myvar 2>/dev/null ; then
        echo is-assoc-array
    elif ! typeset -g +a  $_myvar 2>/dev/null; then
        echo is-array
    else
        echo scalar
    fi
}

Note the use of typeset -g (bash-4.2 or later), this is required within a function so that typeset (syn. declare) doesn't work like local and clobber the value you are trying to inspect. This also does not handle function "variable" types, you can add another branch test using typeset -f if needed.


Another (nearly complete) option is to use this:

    ${!name[*]}
          If name is an array variable, expands to  the  list
          of  array indices (keys) assigned in name.  If name
          is not an array, expands to 0 if name  is  set  and
          null  otherwise.   When @ is used and the expansion
          appears within double quotes, each key expands to a
          separate word.

There's one slight problem though, an array with a single subscript of 0 matches two of the above conditions. This is something that mikeserv also references, bash really doesn't have a hard distinction, and some of this (if you check the Changelog) can be blamed on ksh and compatibilty with how ${name[*]} or ${name[@]} behave on a non-array.

So a partial solution is:

if [[ ${!BASH_VERSINFO[*]} == '' ]]; then
    echo no-such
elif [[ ${!BASH_VERSINFO[*]} == '0' ]]; then 
    echo not-array
elif [[ ${!BASH_VERSINFO[*]} != '0' ]]; 
    echo is-array    
fi

I have used in the past a variation on this:

while read _line; do
   if [[ $_line =~ ^"declare -a" ]]; then 
     ...
   fi 
done < <( declare -p )

this too needs a subshell though.

One more possibly useful technique is compgen:

compgen -A arrayvar

This will list all indexed arrays, however associative arrays are not handled specially (up to bash-4.4) and appear as regular variables (compgen -A variable)

  • The typeset +a also reports an error in ksh. Not in zsh, though. – user79743 Nov 28 '15 at 20:38
1

Short answer:

For the two shells that introduced this notation (bash and ksh93) a scalar variable is just an array with a single element.

Neither needs a special declaration to create an array. Just the assignment is enough, and a plain assignment var=value is identical to var[0]=value.

  • Try: bash -c 'unset var; var=foo; typeset -p var'. Do bash answer report an array (needs an -a)?. Now compare with: bash -c 'unset var; var[12]=foo; typeset -p var'. Why is there a difference?. A: The shell maintains (for good or for bad) a notion of which vars are scalars or arrays. The shell ksh do mix both concepts into one. – user79743 Nov 29 '15 at 23:23
1

yash's array builtin has some options that only work with array variables. Example: the -d option will report an error on non-array variable:

$ a=123
$ array -d a
array: no such array $a

So we can do something like this:

is_array() (
  array -d -- "$1"
) >/dev/null 2>&1

a=(1 2 3)
if is_array a; then
  echo array
fi

b=123
if ! is_array b; then
  echo not array
fi

This approach won't work if array variable is readonly. Trying to modify a readonly variable leading to an error:

$ a=()
$ readonly a
$ array -d a
array: $a is read-only
0
#!/bin/bash

var=BASH_SOURCE

[[ "$(declare -pa)" =~ [^[:alpha:]]$var= ]]

case "$?" in 
  0)
      echo "$var is an array variable"
      ;;
  1)
      echo "$var is not an array variable"
      ;;
  *)
      echo "Unknown exit code"
      ;;
esac

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.