How do I shift a bash array at some index in the middle?

unset removes an element. It doesn't renumber the remaining elements.

We can use declare -p to see exactly what happens to numbers:

$ unset "numbers[i]"
$ declare -p numbers
declare -a numbers=([0]="53" [1]="8" [2]="12" [3]="9" [5]="69" [6]="8" [7]="7" [8]="1")

Observe the numbers no longer has an element 4.

Another example

Observe:

$ a=()
$ a[1]="element 1"
$ a[22]="element 22"
$ declare -p a
declare -a a=([1]="element 1" [22]="element 22")

Array a has no elements 2 through 21. Bash does not require that array indices be consecutive.

Suggested method to force a renumbering of the indices

Let's start with the numbers array with the missing element 4:

$ declare -p numbers
declare -a numbers=([0]="53" [1]="8" [2]="12" [3]="9" [5]="69" [6]="8" [7]="7" [8]="1")

If we would like the indices to change, then:

$ numbers=("${numbers[@]}")
$ declare -p numbers
declare -a numbers=([0]="53" [1]="8" [2]="12" [3]="9" [4]="69" [5]="8" [6]="7" [7]="1")

There is now an element number 4 and it has value 69.

Alternate method to remove an element & renumber array in one step

Again, let's define numbers:

$ numbers=(53 8 12 9 784 69 8 7 1)

As suggested by Toby Speight in the comments, a method to remove the fourth element and renumber the remaining elements all in one step:

$ numbers=("${numbers[@]:0:4}" "${numbers[@]:5}")
$ declare -p numbers
declare -a numbers=([0]="53" [1]="8" [2]="12" [3]="9" [4]="69" [5]="8" [6]="7" [7]="1")

As you can see, the fourth element was removed and all remaining elements were renumbered.

${numbers[@]:0:4} slices array numbers: it takes the first four elements starting with element 0.

Similarly, ${numbers[@]:5} slice array numbers: it takes all elements starting with element 5 and continuing to the end of the array.

Obtaining the indices of an array

The values of an array can be obtained with ${a[@]}. To find the indices (or keys) that correspond to those values, use ${!a[@]}.

For example, consider again our array numbers with the missing element 4:

$ declare -p numbers
declare -a numbers=([0]="53" [1]="8" [2]="12" [3]="9" [5]="69" [6]="8" [7]="7" [8]="1")

To see which indices are assigned:

$ echo "${!numbers[@]}"
0 1 2 3 5 6 7 8

Again, 4 is missing from the list of indices.

Documentation

From man bash:

The unset builtin is used to destroy arrays. unset name[subscript] destroys the array element at index subscript. Negative subscripts to indexed arrays are interpreted as described above. Care must be taken to avoid unwanted side effects caused by pathname expansion. unset name, where name is an array, or unset name[subscript], where subscript is * or @, removes the entire array.


bash arrays like in ksh, are not really arrays, they're more like associative arrays with keys limited to positive integers (or so called sparse arrays). For a shell with real arrays, you can have a look at shells like rc, es, fish, yash, zsh (or even csh/tcsh though those shells have so many issues they're better avoided).

In zsh:

a=(1 2 3 4 5)
a[3]=() # remove the 3rd element
a[1,3]=() # remove the first 3 elements
a[-1]=() # remove the last element

(Note that in zsh, unset 'a[3]' actually sets it to the empty string for improved compatibility with ksh)

in yash:

a=(1 2 3 4 5)
array -d a 3 # remove the 3rd element
array -d a 1 2 3 # remove the first 3 elements
array -d a -1 # remove the last element

in fish (not a Bourne-like shell contrary to bash/zsh):

set a 1 2 3 4 5
set -e a[3] # remove the 3rd element
set -e a[1..3] # remove the first 3 elements
set -e a[-1] # remove the last element

in es (based on rc, not Bourne-like)

a = 1 2 3 4 5
a = $a(... 2 4 ...) # remove the 3rd element
a = $a(4 ...) # remove the first 3 elements
a = $a(... `{expr $#a - 1}) # remove the last element
# or a convoluted way that avoids forking expr:
a = $a(... <={@{*=$*(2 ...); return $#*} $a})

in ksh and bash

You can use the arrays as normal arrays if you do:

a=("${a[@]}")

after each delete or insert operations that may have made the list of indexes not contiguous or not start at 0. Also note that ksh/bash arrays start at 0, not 1 (except for $@ (in some ways)).

That will in effect tidy the elements and move them to index 0, 1, 2... in sequence.

Also note that you need to quote the number[i] in:

unset 'number[i]'

Otherwise, that would be treated as unset numberi is there was a file called numberi in the current directory.