196

I have an array which gets filled with different error messages as my script runs.

I need a way to check if it is empty of not at the end of the script and take a specific action if it is.

I have already tried treating it like a normal VAR and using -z to check it, but that does not seem to work. Is there a way to check if an array is empty or not in Bash?

codeforester
  • 115
  • 1
  • 1
  • 5
Marcos Sander
  • 1,971
  • 2
  • 13
  • 5

9 Answers9

245

Supposing your array is $errors, just check to see if the count of elements is zero.

if [ ${#errors[@]} -eq 0 ]; then
    echo "No errors, hooray"
else
    echo "Oops, something went wrong..."
fi
Michael Hampton
  • 244,070
  • 43
  • 506
  • 972
  • 16
    Does not work with `set -u`: "unbound variable" - if the array is empty. –  Apr 08 '17 at 19:56
  • 1
    @Igor: Works for me in Bash 4.4. `set -u;` `foo=();` `[ ${#foo[@]} -eq 0 ] && echo empty`. If I `unset foo`, then it prints `foo: unbound variable`, but that's different: the array variable doesn't exist at all, rather than existing and being empty. – Peter Cordes Sep 17 '17 at 04:22
  • 1
    Also tested in Bash 3.2 (OSX) when using `set -u` - as long as you declared your variable first, this works perfectly. – zeroimpl Aug 05 '18 at 16:48
57

I generally use arithmetic expansion in this case:

if (( ${#a[@]} )); then
    echo not empty
fi
x-yuri
  • 2,141
  • 2
  • 24
  • 29
  • Nice and clean! I like it. I also note that if the first element of the array is always nonempty, `(( ${#a} ))` (length of the first element) will work also. However, that will fail on `a=('')`, whereas `(( ${#a[@]} ))` given in the answer will succeed. – cxw Aug 15 '18 at 12:36
  • It might have been so once, but I can't reproduce it. If `echo $BASH_VERSION && a=('') && (( ${#a[@]} )) && echo not empty || echo empty` happens to output "empty," fell free to tell your version. – x-yuri Dec 17 '20 at 21:38
  • @x-yuri it works for me: if I run that exact command, the output is `5.0.17(1)-release` / `not empty` – Brandon Dec 15 '21 at 04:09
  • This is the correct answer. You can also write it as `(( ${#a[@]} != 0 ))` if you prefer to make the non-zero check explicit. – dimo414 Apr 14 '23 at 06:24
12

You can also consider the array as a simple variable. In that way, just using

if [ -z "$array" ]; then
    echo "Array empty"
else
    echo "Array non empty"
fi

or using the other side

if [ -n "$array" ]; then
    echo "Array non empty"
else
    echo "Array empty"
fi

The problem with that solution is that if an array is declared like this: array=('' foo). These checks will report the array as empty, while it is clearly not. (thanks @musiphil!)

Using [ -z "$array[@]" ] is clearly not a solution neither. Not specifying curly brackets tries to interpret $array as a string ([@] is in that case a simple literal string) and is therefore always reported as false: "is the literal string [@] empty?" Clearly not.

wget
  • 291
  • 2
  • 8
  • 9
    `[ -z "$array" ]` or `[ -n "$array" ]` doesn't work. Try `array=('' foo); [ -z "$array" ] && echo empty`, and it will print `empty` even though `array` is clearly not empty. – musiphil Aug 17 '15 at 23:25
  • 4
    `[[ -n "${array[*]}" ]]` interpolates the entire array as a string, which you check for non-zero length. If you consider `array=("" "")` to be empty, rather than having two empty elements, this might be useful. – Peter Cordes Sep 19 '17 at 15:21
  • @PeterCordes I don't think that works. The expression evaluates to a single space character, and `[[ -n " " ]]` is "true," which is a pity. Your comment is exactly what I _want_ to do. – Michael Jun 18 '19 at 16:37
  • @Michael: Crap, you're right. It only works with a 1-element array of an empty string, not 2 elements. I even checked older bash and it's still wrong there; like you say `set -x` shows how it expands. I guess I didn't test that comment before posting. >.< You can make it work by setting `IFS=''` (save/restore it around this statement), because `"${array[*]}"` expansion separates elements with the first character of IFS. (Or space if unset). But "*If IFS is null, the parameters are joined without intervening separators.*" (docs for $* positional params, but I assume same for arrays). – Peter Cordes Jun 18 '19 at 16:57
  • @Michael: Added an answer that does this. – Peter Cordes Jun 18 '19 at 17:35
  • 2
    This answer is straight up wrong, why does it have 8 points? It believes the array is incorrectly empty for many different cases – Shardj Jul 24 '19 at 14:22
  • 1
    this is only check if the first element is empty, not the array. – kdubs Jan 20 '23 at 20:52
  • -1, please don't go around treating arrays like scalars, it's just going to confuse people (and will be wrong in all sorts of cases). – dimo414 Apr 14 '23 at 06:22
9

I checked it with bash-4.4.0:

#!/usr/bin/env bash
set -eu
check() {
    if [[ ${array[@]} ]]; then
        echo not empty
    else
        echo empty
    fi
}
check   # empty
array=(a b c d)
check   # not empty
array=()
check   # empty

and bash-4.1.5:

#!/usr/bin/env bash
set -eu
check() {
    if [[ ${array[@]:+${array[@]}} ]]; then
        echo non-empty
    else
        echo empty
    fi
}
check   # empty
array=(a b c d)
check   # not empty
array=()
check   # empty

In the latter case you need the following construct:

${array[@]:+${array[@]}}

for it to not fail on empty or unset array. That's if you do set -eu like I usually do. This provides for more strict error checking. From the docs:

-e

Exit immediately if a pipeline (see Pipelines), which may consist of a single simple command (see Simple Commands), a list (see Lists), or a compound command (see Compound Commands) returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits.

This option applies to the shell environment and each subshell environment separately (see Command Execution Environment), and may cause subshells to exit before executing all the commands in the subshell.

If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.

-u

Treat unset variables and parameters other than the special parameters ‘@’ or ‘*’ as an error when performing parameter expansion. An error message will be written to the standard error, and a non-interactive shell will exit.

If you don't need that, feel free to omit :+${array[@]} part.

Also do note, that it's essential to use [[ operator here, with [ you get:

$ cat 1.sh
#!/usr/bin/env bash
set -eu
array=(a b c d)
if [ "${array[@]}" ]; then
    echo non-empty
else
    echo empty
fi

$ ./1.sh
_/1.sh: line 4: [: too many arguments
empty
x-yuri
  • 2,141
  • 2
  • 24
  • 29
  • With `-u` you should actually use `${array[@]+"${array[@]}"}` cf https://stackoverflow.com/a/34361807/1237617 – Jakub Bochenski Sep 13 '17 at 14:52
  • @JakubBochenski Which version of bash are you talking about? https://gist.github.com/x-yuri/d933972a2f1c42a49fc7999b8d5c50b9 – x-yuri Sep 13 '17 at 18:28
  • The problem in the single brackets example is the `@`, surely. You could use `*` array expansion like `[ "${array[*]}" ]`, could you not? Still, `[[` also works fine. The behaviour of both of these for an array with multiple empty strings is a little surprising. Both `[ ${#array[*]} ]` and `[[ "${array[@]}" ]]` are false for `array=()` and `array=('')` but true for `array=('' '')` (two or more empty strings). If you wanted one or more empty strings to all give true, you could use `[ ${#array[@]} -gt 0 ]`. If you wanted them all false, you could maybe `//` them out. – eisd Sep 02 '18 at 06:42
  • @eisd I could use `[ "${array[*]}" ]`, but if I were to run into such expression, it would be harder for me to understand what it does. Since `[...]` operates in terms of strings on the result of interpolation. As opposed to `[[...]]`, which can be aware of what was interpolated. That is, it can know that it was passed an array. `[[ ${array[@]} ]]` reads to me as "check if array is non-empty", while `[ "${array[*]}" ]` as "check if result of interpolation of all array elements is a non-empty string". – x-yuri Sep 02 '18 at 15:30
  • ...As for behavior with two empty strings it is not at all surprising to me. What is surprising is the behavior with one empty string. But arguably reasonable. Regarding `[ ${#array[*]} ]`, you probably meant `[ "${array[*]}" ]`, since the former is true for any number of elements. Because number of elements is always non-empty string. Regarding the latter with two elements, the expression inside brackets expands to `' '` which is non-empty string. As for `[[ ${array[@]} ]]`, they just think (and rightly so) that any array of two elements is non-empty. – x-yuri Sep 02 '18 at 15:46
3

If you want to detect an array with empty elements, like arr=("" "") as empty, same as arr=()

You can paste all the elements together and check if the result is zero-length. (Building a flattened copy of the array contents is not ideal for performance if the array could be very large. But hopefully you aren't using bash for programs like that...)

But "${arr[*]}" expands with elements separated by the first character of IFS. So you need to save/restore IFS and do IFS='' to make this work, or else check that the string length == # of array elements - 1. (An array of n elements has n-1 separators). To deal with that off-by-one, it's easiest to pad the concatenation by 1

arr=("" "")

## Assuming default non-empty IFS
## TODO: also check for ${#arr[@]} -eq 0
concat="${arr[*]} "      # n-1 separators + 1 space + array elements
[[ "${#concat}" -ne "${#arr[@]}" ]]  && echo not empty array || echo empty array

test case with set -x

### a non-empty element
$ arr=("" "x")
  + arr=("" "x")
$ concat="${arr[*]} ";  [[ "${#concat}" -ne "${#arr[@]}" ]] && echo not empty array || echo empty array
  + concat=' x '
  + [[ 3 -ne 2 ]]
  + echo not empty array
not empty array

### 2 empty elements
$ arr=("" "")
  + arr=("" "")
$ concat="${arr[*]} ";  [[ "${#concat}" -ne "${#arr[@]}" ]] && echo not empty array || echo empty array
  + concat='  '
  + [[ 2 -ne 2 ]]
  + echo empty array
empty array

Unfortunately this fails for arr=() : [[ 1 -ne 0 ]]. So you'd need to check for actually empty arrays first separately.


Or with IFS=''. Probably you'd want to save/restore IFS instead of using a subshell, because you can't get a result out of a subshell easily.

# inside a () subshell so we don't modify our own IFS
(IFS='' ; [[ -n "${arr[*]}" ]] && echo not empty array || echo empty array)

example:

$ arr=("" "")
$ (IFS='' ; [[ -n "${arr[*]}" ]] && echo not empty array || echo empty array)
   + IFS=
   + [[ -n '' ]]
   + echo empty array
empty array

does work with arr=() - it's still just the empty string.

Peter Cordes
  • 457
  • 4
  • 10
2

I prefer to use double brackets:

if [[ ! ${array[@]} ]]
then
    echo "Array is empty"
else
    echo "Array is not empty"
fi

Double Brackets: https://stackoverflow.com/questions/669452/is-preferable-over-in-bash

Edited in order to work as expected

bazeusz
  • 105
  • 4
Nick Tsai
  • 1,318
  • 1
  • 9
  • 8
0

You could also benefit from jq if you got it installed on your system:

if [[ $(echo $array | jq -r 'length') -eq 0 ]]
then
    echo "Array is empty"
else
    echo "Array is not empty"
fi
Nicko Glayre
  • 101
  • 4
0

Sadly I cannot comment yet - Regarding @Nick Tsai - I didn't work for me but the following:

if [[ ! ${arrayName[*]} ]]; then ...
if [[ ! ${arrayOne[*]} && ! ${arrayTwo[*]} ]]; then ...

spacing is important for this to work!

Alkeno
  • 1
0

In my case, the second Answer was not enough because there could be whitespaces. I came along with:

if [ "$(echo -ne ${opts} | wc -m)" -eq 0 ]; then
  echo "No options"
else
  echo "Options found"
fi
Micha
  • 111
  • 3
  • `echo | wc` seems needlessly inefficient compared using shell built-ins. – Peter Cordes Sep 17 '17 at 04:16
  • Not sure if I understand @PeterCordes, can I modify the second answers' `[ ${#errors[@]} -eq 0 ];` in a way to work around the whitespace problem? I'd also prefer the built-in. – Micha Sep 18 '17 at 13:10
  • How exactly does whitespace cause a problem? `$#` expands to a number, and works fine even after `opts+=("")`. e.g. `unset opts;` `opts+=("");opts+=(" "); echo "${#opts[@]}"` and I get `2`. Can you show an example of something that doesn't work? – Peter Cordes Sep 18 '17 at 13:33
  • It's long time ago. IIRC the originating source always printed at least "". Thus, for opts="" or opts=("") I needed 0, not 1, ignoring the empty newline or empty string. – Micha Sep 19 '17 at 14:20
  • Ok, so you need to treat `opts=("")` the same as `opts=()`? That's not an empty array, but you could check for empty array or empty first element with `opts=("");` `[[ "${#opts[@]}" -eq 0 || -z "$opts" ]] && echo empty`. Note that your current answer says "no options" for `opts=("" "-foo")`, which is totally bogus, and this reproduces that behaviour. **You could `[[ -z "${opts[*]}" ]]` I guess, to interpolate all the array elements into a flat string, which `-z` checks for non-zero length.** If checking the first element is sufficient, `-z "$opts"` works. – Peter Cordes Sep 19 '17 at 15:18
  • Just realized there was already an answer that did the same bogus check as this (but without `echo | wc`): https://serverfault.com/questions/477503/check-if-array-is-empty-in-bash/700936#700936. – Peter Cordes Sep 19 '17 at 15:21
  • Oh hmm, I guess if you want to ignore whitespace as well as empty strings, you have to find a way to expand your variable in a way that lets the shell eat whitespace without causing a syntax error. An `echo` command line makes some sense, but you could probably do it by checking "${opts[*]}"` against a glob or regex that matches non-whitespace. (Inside a `[[ ]]`). – Peter Cordes Sep 19 '17 at 15:25
  • Update: `"${opts[*]}"` expands with array elements separated by the first character of `$IFS`. So **I'd need `IFS=''`** (the empty string) to paste them together without space separators, and make `"${opts[*]}"` zero-length even if the array has multiple empty elements. – Peter Cordes Jun 18 '19 at 17:03