1. Overview

Unlike many other languages, we can add or remove elements to and from Bash arrays. However, the usual removal process leaves a hole at the original position, making it a sparse array.

In this tutorial, we’ll learn how to completely remove an element from a Bash array.

2. Understanding the Scenario

Let’s say we want to keep track of all the current employees in an organization in a Bash script. For such a use case, let’s define the active_employees array:

$ declare -a active_employees
$ active_employees=("John" "Raymon" "Irene" "Peter")

Further, let’s iterate over the active_employees array using its indices:

$ for index in "${!active_employees[@]}"; 
do 
    echo "active_employees[$index]=${active_employees[index]}"; 
done
active_employees[0]=John
active_employees[1]=Raymon
active_employees[2]=Irene
active_employees[3]=Peter

Next, imagine that Irene leaves the organization, so we must remove active_employees[2] from the active_employees Bash array. Although using the unset command to remove a no longer-required variable is common, we need to see if it works fine for our use case. So, let’s go ahead and verify this:

$ unset active_employees[2]
$ for index in "${!active_employees[@]}";  
do 
    echo "active_employees[$index]=${active_employees[index]}"; 
done
active_employees[0]=John
active_employees[1]=Raymon
active_employees[3]=Peter

At first sight, it looks like this approach worked fine. However, we can see that the remaining elements are left at their original positions. Such results are unintuitive and invite logical errors in code if we neglect the sparse nature of the array.

3. Using Array Positional Offset

We can extract specific parts of a Bash array by specifying a positional offset and length. So, let’s go ahead and see if we can exclude the member at index 2 from the active_employees Bash array and get the remaining employees:

$ for employee in ${active_employees[@]:0:2} ${active_employees[@]:3:1}; 
do 
    echo "Active Employee: $employee"; 
done
Active Employee: John
Active Employee: Raymon
Active Employee: Peter

The result of the iteration looks satisfactory. However, we must note that we didn’t modify the original array yet.

Next, let’s go ahead and change the value for the active_employees array:

$ active_employees="${active_employees[@]:0:2} ${active_employees[@]:3:1}"
$ for employee in "${active_employees[@]}";  do echo $employee; done
John Raymon Peter
Raymon
Irene
Peter

Unfortunately, we didn’t get the desired result. Further, we can see that no members were changed except for the member at index 0 because the name of an array without an index refers to its first element.

Finally, let’s define the members in the active_employees as we did during the initialization phase and verify the change:

$ active_employees=("${active_employees[@]:0:2}" "${active_employees[@]:3:1}")
$ for index in "${!active_employees[@]}";  
do 
    echo "$index -> ${active_employees[$index]}"; 
done
0 -> John
1 -> Raymon
2 -> Peter

Great! It looks like we’ve got our answer now.

4. Removing Element From Command-line Arguments

Bash offers a variety of special constructs, such as command-line arguments ($@) that make it convenient for us to write code. However, we can’t define or change them like other user-defined variables. In this section, let’s learn how to remove an element from $@.

4.1. Using set Command

The set command is commonly used for changing the values of Bash options and variables. However, we can also use it with to define positional arguments.

First, let’s go ahead and set the names of active employees as positional arguments:

$ set -- John Raymon Irene Peter
$ echo $1
John
$ echo $2
Raymon
$ echo $3
Irene
$ echo $4
Peter

We must note that the indexing of positional arguments starts from 1 instead of 0.

Next, let’s see how we can use the array offset values to extract specific elements from the positional arguments such that we select all employees except Irene:

$ echo "${@}"
John Raymon Irene Peter
$ echo "${@:1:2}"
John Raymon
$ echo "${@:4:1}"
Peter

Over here, we must note that ${@} represents all the positional arguments.

Finally, let’s use the set — command to redefine the positional arguments with the new values and verify the outcome:

$ set -- "${@:1:2}" "${@:4:1}"
$ for employee in "${@}"
do
    echo $employee
done
John
Raymon
Peter

Perfect! We’ve got this one right.

4.2. Using shift Command

We can also use an indirect approach involving the shift command to remove an element from the positional arguments.

First, we start by defining the positional arguments by using the set command:

$ set -- "John" "Raymon" "Irene" "Peter"
$ echo "$@"
John Raymon Irene Peter

Now, let’s use the tac command to define the reverse array containing the positional arguments in reverse order:

$ reverse=($(printf '%s\n' "${@}" | tac | tr '\n' ' '; echo))
$ echo "${reverse[@]}"
Peter Irene Raymon John

Next, let’s use the shift command to shift the positional arguments by 3 towards the left and save the remaining positional arguments in the suffix array:

$ shift 3
$ suffix=(echo "$@")
$ echo "${suffix[@]}"
Peter

Moving on, let’s redefine the positional arguments using the reverse array and keep the positional arguments after a left-shift of 2 in the prefix array, but in the reverse order:

$ set -- "${reverse[@]}"
$ echo "$@"
Peter Irene Raymon John
$ shift 2
$ echo "$@"
Raymon John
$ prefix=($(printf '%s\n' "${@}" | tac | tr '\n' ' '; echo))
$ echo "${prefix[@]}"
John Raymon

Finally, let’s redefine the positional arguments by using the prefix and suffix arrays:

$ set -- "${prefix[@]}" "${suffix[@]}"
$ echo "$@"
John Raymon Peter

It looks like we managed to get this right too.

5. Selective Copy

We can also see removing an element of an array as a selective copy operation wherein we copy all elements except one. After the copy operation, we set the copied array as the original one.

Let’s start by looking at the selective_copy.sh Bash script in its entirety:

$ cat selective_copy.sh
#!/bin/bash

declare -a active_employees=("John" "Raymon" "Irene" "Peter")
declare -a active_employees_v2

declare -i count=0

for index in "${!active_employees[@]}"
do
    if [[ "${active_employees[$index]}" != "Irene" ]]
    then
        active_employees_v2[count]=${active_employees[$index]}
        count=count+1
    fi
done

active_employees=("${active_employees_v2[@]}")
echo "${active_employees[@]}"
exit 0

Let’s break this down to understand the nitty gritty of the logic. First, we defined the active_employees_v2 as a placeholder array where we’ll selectively copy the contents from the active_employees array. Then, we iterated over the active_employees array and used an integer variable, count, to selectively add members to the active_employees_v2 array. Lastly, we replace the content of the active_employees array with that of the active_employees_v2 array.

Finally, let’s see this in action:

$ ./selective_copy.sh
John Raymon Peter

6. Conclusion

In this article, we learned how to remove an element from a Bash array. Furthermore, we solved the use case using the concept of positional offsets, the selective-copying technique, and utilities such as set, shift, and tac.