Inconsistencies when removing elements from a Bash array

Viewed 110

When using bash arrays, you can remove an element in the following manner:

unset array[i]   # where i is the array index

The problem with this is after the unset ${array[@]] is not truly valid. Yes it does give you the number of active array elements, but not the actual array depth. The index of the removed index in the array still exists. It simply has been set inactive/null. For example:

declare -a array=( a b c d e )
unset array[2]

for ((i=0; i < ${#array[@]}; i++)) ; do
    echo "$i: ${array[$i]}"
done

outputs the following:

0: a
1: b
2:
3: d

array[2] is still there but set to null or inactive

array[4] (e) does not show because ${#array[@]} is the number of active array elements and not the true array element depth. This gets very messy very quickly each time an element is unset

As an example consider the following code:

# array contains 0 or more strings # remove looks for and removes a given string

remove () {

    local str=$1

    for (( i = 0 ; i < ${#array[@]}; i++ )) ; do
        if [[ "${array[$i]}" == "$str" ]] ; then
            unset array[$i]
            return 0
        fi
    done

    echo "$str: not registered"

    return 0
}

This is only valid the first time remove is called. After that, valid elements may be missed.

One fix for this is to added the following line after the unset:

     unset array[$i]
  +  array=( "${array[@]}" )

This re-initializes array with the element completely removed.

The problem, this feels kludgy.

So my questions are this:

1) is there a way of getting the true array element depth?

2) is there a way of detecting the end of an array when iterating through it?

3) is there another cleaner solution?

1 Answers

Understanding Why for ((i=0; i<${#array[@]}; i++)) is broken

There's no such thing as "true array element depth" in the sense that you're asking for here. Bash arrays aren't really arrays -- they're hash maps (numerically indexed for regular arrays, indexed by strings for bash 4.0's new "associative arrays"). As such, there's absolutely no reason for the keys to start from 0 -- you can have an array like the following: declare -a array=( [1000]="one thousand" [2000]="two thousand" [3000]="three thousand" ), and its length is exactly 3; there aren't a bunch of NUL/empty elements sitting between those items (looked up with keys 1000, 2000 and 3000, respectively).


Removing Elements From A Sparse Array Safely

Iterate over indices, if you want to remove by index. Whereas "${array[@]}" iterates over items in the array, "${!array[@]}" (note the !) iterates over by the indices by which those items can be looked up.

As you've observed, it's unsafe to assume that the indices range from 0 to the total number of items in an array, as bash arrays are allowed to be sparse -- but there's no reason to write your code to make that assumption in the first place.

for array_idx in "${!array[@]}"; do
  unset "array[$array_idx]"
done
Related