16

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?

cuonglm
  • 150,973
  • 38
  • 327
  • 406

9 Answers9

11

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.

mikeserv
  • 57,448
  • 9
  • 113
  • 229
  • So I guess checking if `${a[1]-not array}` can do the task, can't it? – cuonglm Nov 28 '15 at 04: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 05: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
Petr Skocik
  • 28,176
  • 14
  • 81
  • 141
  • @mikeserv Good point. Aliases make it look prettier. +1 – Petr Skocik 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. ;-) – Petr Skocik Nov 28 '15 at 19:46
  • ... and security considerations. – Petr Skocik 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. – Petr Skocik Nov 28 '15 at 20:06
  • well, true, and it didn't matter when you just used `eval`, because you only targeted the one definition, but when you `alias x='eval "$x"'` it may not be immediately apparent to some that the `alias` and `$x` are connected *over time*. you know what I mean? i mean that a lot of people may think the `alias` definition sticks even if the variable doesn't - that's all i'm getting at... but you can't portably make functions `readonly`, and i've only now learned you can at all... – mikeserv Nov 28 '15 at 20:09
6

In zsh

zsh% a=(1 2 3) s=1
zsh% [[ ${(t)a} == *array* ]] && echo array
array
zsh% [[ ${(t)s} == *array* ]] && echo array
zsh%
llua
  • 6,760
  • 24
  • 30
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 08:32
  • 1
    Dat edit, tho. Looks very original. :D – Petr Skocik 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. –  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 09: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**. –  Nov 30 '15 at 17:32
  • I meant that `getVarType HOME` would not report `scalar` for instance. Nitpick: `getVarType -a`, `getVarType varname`. – Stéphane Chazelas Nov 30 '15 at 18:10
  • @StéphaneChazelas I believe that getVarType will not work in most corner cases. The limited pattern matching being the reason. I plan on changing to regex (some form of it) to resolve the issue. I'll ping you when done. –  Nov 30 '15 at 18:26
  • If you do it `...array/hash;; ("") ;; (*) scalar`, I think it should work. A variable should be either scalar, array or hash in `bash` (even namerefs can be considered as scalar I'd say). – Stéphane Chazelas Nov 30 '15 at 18:30
4

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)

mr.spuratic
  • 9,721
  • 26
  • 41
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.

Henk Langeveld
  • 752
  • 5
  • 16
  • 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. –  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
cuonglm
  • 150,973
  • 38
  • 327
  • 406
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
0

In newer versions of bash (4.4+), you can just use the attribute operator on the variable expansion.

e.g.

bob=()
declare -A rob
rob[mob]=bob

echo ${bob@a}
>>> a
echo ${rob@a}
>>> A

Note simply declaring the associative array will not print its attribute -- you must assign something to it first.

Jon
  • 121
  • 3