64

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?

1

10 Answers 10

58

To avoid a call to grep, you could use:

if [[ "$(declare -p variable_name)" =~ "declare -a" ]]; then
    echo array
else
    echo no array
fi
Sign up to request clarification or add additional context in comments.

12 Comments

commenting on multiple answers: if you update your regex to also check for 'typeset' instead of just 'declare', this will be more portable.
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'.
@Sparr, sure, typeset is available in other shells, but that doesn't mean the output will be identical.
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.
@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).
|
26

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

declare -p variable-name 2> /dev/null | grep -q '^declare \-a'

9 Comments

I added the ^ to the pattern to avoid the false positive on variable-name='declare -a' mentioned in the examples.
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!
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?
Google is my friend, that's why he took me here. (It's also because he's friend of SO.)
commenting on multiple answers: if you update your grep to also check for 'typeset' instead of just 'declare', this will be more portable.
|
25

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"

3 Comments

For a non-declared variable, catch/ignore the error message by changing first line to local var=$( declare -p $1 2>/dev/null ).
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" ]];}
I believe the grep example should be replaced with anything Bash-only considering the initial Bash regex processing, too. Still awesome Grep is a third-party. Nevertheless, my... dear... This a genius and so simply great approach! Thank you very much! Bellissimo! ✨
8

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"

Comments

2

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

4 Comments

This fails when the array is of length 1 or empty, but still an array.
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.
this imperfect answer has the benefit of not requiring a fork+exec to get the answer
@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.
2

I usually use the following functions to check for arrays/dicts (e.g. to verify a sourced configuration):

# Check if variable is an array [$1: variable name]
function is_array() {
    [[ "$(declare -p -- "$1")" == "declare -a "* ]] && return 0 || return 1
}

# Check if variable is a dict [$1: variable name]
function is_dict() {
    [[ "$(declare -p -- "$1")" == "declare -A "* ]] && return 0 || return 1
}

I don't see any benefit in using regular expressions here, simple string matching is quite sufficient.

As others already pointed out, this approach will NOT work for namerefs and/or additional flags like readonly (which is sufficient in my experience -> KISS).

NOTE: && return 0 || return 1 may actually be omitted, I just added that to make the code self-explanatory.

Comments

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" ]]
}

6 Comments

Any additional explanation would help improve your answer.
commenting on multiple answers: if you update your regex to also check for 'typeset' instead of just 'declare', this will be more portable.
@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.
@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.
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.
|
1

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.

Comments

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

Comments

0

This method can detect, bash hash arrays, even if empty, based on the fact that 1/0 is computed for a list index causing an error division by 0 (error token is "0"), but not computed for a hash index.

Therefore we can detect a hash by using a key expression that would cause an error for a list.

It also naturally follows -n for aliases

Note that this is a subshell function because dividing by zero quits bash.

is-array() (
  eval : '$'"{$1"'[1/0]}' 2>/dev/null
)

Other syntax errors also work such as [0 0] but they also result in quitting bash, so the subshell function is required

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.