52

I have a loop over variable names and I need to check if content of a variable is an array or not:

for varname in AA BB CC; do
  local val
  if [ "$varname" is array ]; then # how can I perform this test?
    echo do something with an array
  else
    echo do something with a "'normal'" variable
  fi
done

How do I do it?

codeforester
  • 39,467
  • 16
  • 112
  • 140
wujek
  • 10,112
  • 12
  • 52
  • 88
  • Related and worth reading: https://unix.stackexchange.com/questions/67898/using-the-not-equal-operator-for-string-comparison – Jesse Nickles Apr 23 '23 at 11:32

8 Answers8

47

To avoid a call to grep, you could use:

if [[ "$(declare -p variable_name)" =~ "declare -a" ]]; then
    echo array
else
    echo no array
fi
bricklore
  • 4,125
  • 1
  • 34
  • 62
Reuben W
  • 471
  • 1
  • 4
  • 2
  • 4
    commenting on multiple answers: if you update your regex to also check for 'typeset' instead of just 'declare', this will be more portable. – Sparr Apr 19 '16 at 23:08
  • None of these answers handle empty arrays correctly. Declare gives a 'bash: declare: var: not found' error, despite the fact that the variable appears when you run 'declare -ap'. So a solution that includes empty variables might have to grep the output of 'declare -ap'. – Sam Bull Mar 09 '17 at 23:03
  • @Sparr, sure, `typeset` is available in other shells, but that doesn't mean the output will be identical. – Charles Duffy Mar 18 '17 at 17:05
  • This currently looks for the string **anywhere** in the output from `declare -p` -- so it would match a string containing the literal contents `declare -a` somewhere within. – Charles Duffy Mar 18 '17 at 17:06
  • @CharlesDuffy I think changing the regex to `^declare -a variable_name($|=)` would solve that: `pattern="^declare -a variable_name($|=)"; if [[ "$(declare -p variable_name)" =~ $pattern ]];then ...`. Here `^` anchors `declare` to the start of the line, while `($|=)` causes it to match only if the declaration is followed by a value assignment or end of line. End of line is to match the output if declared but not assigned in bash 4.4.12 (although not in bash 3.2.57, but this patterns works there too because it's output is `=()` in that case). – Rolf W. Sep 23 '17 at 09:18
  • P.S. I first assigned the pattern to a variable because quoting regex patterns [doesn't work in newer versions of bash](https://stackoverflow.com/a/218217/741217) (although the example in Reuben W's answer does work) – Rolf W. Sep 23 '17 at 09:20
  • To also make this pattern work for associative arrays, use this pattern instead: `^declare -(a|A) variable_name($|=)` – Rolf W. Sep 23 '17 at 09:22
  • @RolfW., if the output is multi-line, does `^` match at the beginning of *any* line, or only the first? – Charles Duffy Sep 23 '17 at 15:19
  • @CharlesDuffy I'd say at the beginning of the first; I couldn't find a reference easily, but I'm pretty sure the `=~` operator doesn't support multi-line mode. In this case I don't think it matters though, because `declare -p variable_name` will return only that one item if it was declared, or `declare: variable_name: not found` otherwise. In case the value of the variable contains a line that also matches this pattern, it would still evaluate to `echo array` in the example above. – Rolf W. Sep 26 '17 at 12:33
  • @RolfW. @CharlesDuffy the regular expression will also need to account for the other flags that could appear in the declaration, e.g. `r` (read-only) and `x` (exported) could also appear in the declaration: `declare -arx myArray=...`. – neuralmer Jul 29 '19 at 21:35
  • I'd add a ` 2>/dev/null` to the subshell to avoid error messages when the variable is not declared at all. – stefanct Jul 12 '23 at 16:54
24

According to this wiki page, you can use this command:

declare -p variable-name 2> /dev/null | grep -q '^declare \-a'
Bastien Jansen
  • 8,756
  • 2
  • 35
  • 53
  • 1
    I added the `^` to the pattern to avoid the false positive on `variable-name='declare -a'` mentioned in the examples. – cmbuckley Jan 25 '13 at 16:02
  • 1
    Thanks, it works fine. As an excuse for not googling, I did find the link you mention, but I am forbidden to view it. man bash has all of it there, though, but I didn't think about looking for in under 'declare'... Thanks! – wujek Jan 25 '13 at 16:17
  • 2
    a faster alternative: `str="\`declare -p astr 2>/dev/null\`";if [[ "${str:0:10}" == 'declare -a' ]];then echo array;fi` I wonder if there is anything faster? – Aquarius Power Jun 10 '14 at 22:53
  • 37
    Google is my friend, that's why he took me here. (It's also because he's friend of SO.) – Alois Mahdal Mar 24 '16 at 21:39
  • 1
    commenting on multiple answers: if you update your grep to also check for 'typeset' instead of just 'declare', this will be more portable. – Sparr Apr 19 '16 at 23:08
  • 1
    @Sparr you can suggest an edit to an existing answer or post your own. Notably, none of the existing answers explain why this works, so explaining that (and why it's necessary to check for `typeset` too) would be a very helpful addition. – dimo414 Jan 07 '17 at 07:35
  • 1
    I found that if I do `declare -A x ; declare -p x` I'm told `bash: declare: x: not found`. I have to actually _use_ the array before it's recognised as such: `declare -A x ; x[_]= ; unset x[_] ; declare -p x` returns `declare -A x='()'`. I don't know if this is expected behaviour, but thought it worth mentioning! (This is only for uninitialised arrays, by the way. If I initialise it to empty - `declare -A x='()'` - it works as expected.) – IpsRich Mar 08 '18 at 13:23
  • Downvoted because of the "Google/RTFM it" style remarks, which really are at odds with the very spirit of what SO/SE is about. – ata Jun 28 '21 at 19:00
  • This does not work for namerefs. `declare -n a; b=( c d e ); a=b; declare -p a; echo ${a[2]}` shows `declare -n a="b"; e`. edit: Ah, see other answer. – joepd Oct 15 '21 at 08:09
22

Since bash 4.3 it is not that easy anymore.

With "declare -n" you can add a reference to another variable and you can do this over and over again. As if this was not complicated enough, with "declare -p", you do not get the type or the original variable.

Example:

$ declare -a test=( a b c d e)
$ declare -n mytest=test
$ declare -n newtest=mytest
$ declare -p newtest
declare -n newtest="mytest"
$ declare -p mytest
declare -n mytest="test"

Therefore you have to loop through all the references. In bash-only this would look like this:

vartype() {
    local var=$( declare -p $1 )
    local reg='^declare -n [^=]+=\"([^\"]+)\"$'
    while [[ $var =~ $reg ]]; do
            var=$( declare -p ${BASH_REMATCH[1]} )
    done

    case "${var#declare -}" in
    a*)
            echo "ARRAY"
            ;;
    A*)
            echo "HASH"
            ;;
    i*)
            echo "INT"
            ;;
    x*)
            echo "EXPORT"
            ;;
    *)
            echo "OTHER"
            ;;
    esac
}

With the above example:

$ vartype newtest
ARRAY

To check for array, you can modify the code or use it with grep:

vartype $varname | grep -q "ARRAY"
Marco
  • 824
  • 9
  • 13
  • For a non-`declare`d variable, catch/ignore the error message by changing first line to `local var=$( declare -p $1 2>/dev/null )`. – Cometsong Mar 05 '19 at 15:55
  • I found that using `declare -p ${!theVar}` seemed to do the trick, as in `isRefArray () { local -n refToCheck=$1; testResult="$(declare -p ${!refToCheck})"; [[ $testResult =~ "declare -a" ]];}` – Peter Whittaker Jun 20 '23 at 15:16
7

I started with Reuben's great answer above. I implemented a few of the comments and some of my own improvements and came out with this:

#!/bin/bash
array_test() {
    # no argument passed
    [[ $# -ne 1 ]] && echo 'Supply a variable name as an argument'>&2 && return 2
    local var=$1
    # use a variable to avoid having to escape spaces
    local regex="^declare -[aA] ${var}(=|$)"
    [[ $(declare -p "$var" 2> /dev/null) =~ $regex ]] && return 0
}

Now I can do this:

foo=(lorem ipsum dolor)
bar="declare -a tricky"
declare -A baz

array_test foo && echo "it's an array"
array_test bar && echo "it's an array"
# properly detects empty arrays
array_test baz && echo "it's an array"
# won't throw errors on undeclared variables
array_test foobarbaz && echo "it's an array"
miken32
  • 42,008
  • 16
  • 111
  • 154
1
is_array() {
  local variable_name=$1
  [[ "$(declare -p $variable_name)" =~ "declare -a" ]]
}

is_array BASH_VERSINFO && echo BASH_VERSINFO is an array

is_array() {
    local variable_name=$1
    [[ "$(declare -p $variable_name 2>/dev/null)" =~ "declare -a" ]]
}
Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
dawesh
  • 19
  • 1
  • 8
    Any additional explanation would help improve your answer. – ryanyuyu Jun 02 '15 at 15:14
  • commenting on multiple answers: if you update your regex to also check for 'typeset' instead of just 'declare', this will be more portable. – Sparr Apr 19 '16 at 23:08
  • @Sparr, I disagree that it will be more portable in practice, because a shell that doesn't support `declare -p` won't have `declare -a` in the output of `typeset`. – Charles Duffy Mar 18 '17 at 17:07
  • @CharlesDuffy I can't remember the scenario in which this was relevant to me. Based on the time of my comment, I suspect I was using OSX at the time. – Sparr Mar 22 '17 at 20:40
  • OSX does come with (real David Korn) ksh93, which uses `typeset` rather than `declare`, so that *does* make sense -- but one would still need to adjust the regex and test it in both places. – Charles Duffy Mar 22 '17 at 20:45
  • zsh also appears to reply with typeset too. – Andy May 17 '17 at 22:40
1

Another Way:

Example, create an Array:

Variable=(The Quick Brown Fox...)

Test the Variable:

if [ "${#Variable[@]}" -gt "1" ] ;
 then echo "Variable is an Array"; 
else echo "Variable is NOT an Array" ; 
fi
Laura
  • 8,100
  • 4
  • 40
  • 50
  • 4
    This fails when the array is of length 1 or empty, but still an array. – wujek Feb 20 '19 at 12:44
  • In practice, it is rarely to happen that an Array has only 1 content. There is actually no difference between an ordinary variable and an array with only 1 index value. Example: Var=hello echo ${Var} hello echo ${Var[0]} hello # lets add another index value Var[1]=h3ll0 echo ${Var} hello echo ${Var[0]} hello echo ${Var[1]} h3ll0 echo ${Var[0]} hello echo $Var hello As you can see there is no difference. But I get your point. – Josef Ababat Feb 20 '19 at 20:19
  • this imperfect answer has the benefit of not requiring a fork+exec to get the answer – Sam Liddicott Feb 06 '20 at 12:34
  • @Josef, there is actually a difference between single element array and ordinary variable: "set -u; a=(1);unset a[0];echo ${#a[@];b=1;unset b[0];echo ${#b[@]}. If I use your test to verify if a var is an array, I will just get an error with this code. – Bruno Feb 24 '21 at 09:45
0

I think this does it, it checks that the array indexes don't match "" or "0"

It should be cheap for small arrays and non-arrays.

It works by testing for any non-zero character in the array indexes

is_array() {
  eval [[ "\"\${!$1[*]}\"" =~ "[1-9]" ]]
}

an_array=(1 2 3)
wierd_array[4]=3
non_array="Joe"
unset non_exist

is_array an_array && echo pass || echo fail
is_array wierd_array && echo pass || echo fail
is_array non_array && echo fail || echo pass
is_array non_exist && echo fail || echo pass

output should be

pass
pass
pass
pass
Sam Liddicott
  • 1,265
  • 12
  • 24
0

miken32's improvement actually causes a regression. The following

a=()
export a
readonly a
declare -p a

prints

declare -arx a=()

which would match if you were just checking for declare -a as a substring, but would fail with miken's more complicated regex. I recommend just using

isArray() {
  # why do we need a regex again?
  [[ $(declare -p "$1") == 'declare -'[aA]* ]]
}

For the ksh folks, it's probably [[ $(declare -p $1) ~= '^(declare|typeset) -[aA]' or [[ $(declare -p $1) == @(declare|typeset)\ -[aA].


No, I don't think not throwing an error on undeclared is a good idea. And no, I have no idea why miken checks the variable name on declare output.

Mingye Wang
  • 1,107
  • 9
  • 32