1. Introduction

Bash arrays are powerful data structures and can be very useful when we handle collections of files or strings.

In this tutorial, we’re going to explore how to use them.

2. Types of Arrays

There are two types of arrays in Bash:

  1. indexed arrays – where the values are accessible through an integer index
  2. associative arrays – where the values are accessible through a key (this is also known as a map)

In our examples, we’ll mostly be using the first type, but occasionally, we’ll talk about maps as well.

One particular aspect is that Bash arrays do not have a maximum limit. There is also no requirement regarding the continuous assignment.

We’ll explain the last part a bit later. Now, let’s take a look at how to define arrays.

2.1. Indexed Arrays

We can declare indexed arrays in multiple ways.

Let’s use the declare keyword with the -a option first:

declare -a indexed_array

Additionally, we can initialize the array with some string values:

declare -a indexed_array=("Baeldung" "is" "cool")

Since we provide some initial values, we can skip the declare keyword and flag:

indexed_array=("Baeldung" "is" "cool")

We can also use indexes in the initialization sequence:

indexed_array=([0]="Baeldung" [1]="is" [2]="cool")

Or we can assign the elements one by one:

indexed_array[0]="Baeldung"
indexed_array[1]="is"
indexed_array[2]="cool"

In Bash, all these samples are valid definitions of arrays. However, notice that indexes start at zero.

Remember what we said about no restrictions regarding contiguous assignment? Let’s see what happens:

indexed_array[0]="Baeldung"
indexed_array[2]="cool"

This example is also a valid definition of an array. Unlike the others, the element at index 1 is not assigned, but there’s no harm in that.

2.2. Associative Arrays

Now, let’s take a look at how to declare associative arrays.

Unlike indexed arrays, we must declare associative arrays explicitly using the -A option:

declare -A associative_array=(["one"]="Baeldung" ["two"]="is" ["three"]="cool")

Likewise, we can assign further values by specifying a key and a corresponding value:

associative_array["four"]="yeah"

Of course, now we’ll have four elements in our map.

3. Basic Operations

Next, we’ll take a look at some basic operations we can do with arrays.

3.1. Printing the Array Elements

Printing the array elements is one of the most intuitive and basic operations.

Let’s see what this looks like:

declare -a indexed_array=("Baeldung" "is" "cool")
echo "Array elements : ${indexed_array[@]}"

We get the output:

Array elements : Baeldung is cool

Here, we use the @ symbol as the index to specify all the members of our array.

We also surround the array variable with the ${} construct. This triggers Bash parameter expansion.

We’ll see some interesting possibilities with this a bit later.

Alternatively, we can also use the symbol as an index to obtain the same output:

echo "Array elements : ${indexed_array[*]}"

However, there is a difference between these two options. We’ll talk about this a bit later as well.

3.2. Enhanced Iteration

Enhanced iteration is similar to list iterations in Java:

declare -a indexed_array=("Baeldung" "is" "cool")
for element in ${indexed_array[@]}
    do
       echo "["$element"]"
    done

Here again, we used parameter expansion with ${indexed_array[@]} to return all the elements of the array.

Then, we just looped through it:

[Baeldung]
[is]
[cool]

Of course, this also works with the symbol:

for element in ${indexed_array[*]}
    do
        echo "["$element"]"
    done

We can also use enhanced iteration on associative arrays:

declare -A associative_array=(["one"]="Baeldung" ["two"]="is" ["three"]="cool")
for element in ${associative_array[@]}
    do
        echo "["$element"]"
    done

Let’s take a look at the output:

[is]
[cool]
[Baeldung]

We can see our output is not ordered. The order in which the elements are printed is not the same as the order in the initialization sequence.

3.3. Iterating with Indexes

We can use index-based iteration with enhanced looping constructs on arrays:

declare -a indexed_array=("Baeldung" "is" "cool")
for index in ${!indexed_array[@]}
    do
        echo "["$index"]:["${indexed_array[$index]}"]"
    done

Now, we’re using ${!indexed_array[@]} to return all the indexes of the array:

[0]:[Baeldung]
[1]:[is]
[2]:[cool]

Let’s see what happens if we have an associative array instead:

declare -A associative_array=(["one"]="Baeldung" ["two"]="is" ["three"]="cool")
for key in ${!associative_array[@]}
    do
        echo "["$key"]:["${associative_array[$key]}"]"
    done

Our index values are now actually the keys in the map.

Similarly, there is no particular order in the output:

[two]:[is]
[three]:[cool]
[one]:[Baeldung]

Of course, we can also do a classical incremental looping construct:

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

Unlike the previous approach, now we used ${#indexed_array[@]} to retrieve the number of elements in the array.

We saw earlier that associative arrays return keys rather than indexes. As a result, this type of incremental looping is not suitable for maps.

3.4. Inserting Elements in Arrays

In the beginning, we said Bash arrays have no size and continuity constraints.

As a result, we can insert elements at any index using a simple assignment:

declare -a indexed_array=("Baeldung" "is" "cool")
indexed_array[2]="lorem"
indexed_array[5]="ipsum"
for index in ${!indexed_array[@]}
    do
        echo "["$index"]:["${indexed_array[$index]}"]"
    done

This is not a very intuitive way of adding values, and doing so yields confusing results:

[0]:[Baeldung]
[1]:[is]
[2]:[lorem]
[5]:[ipsum]

In this case, the indexes are not continuous. We just skipped positions 3 and 4.

This makes sense only when trying to replace an existing element in the array.

Assuming our previous example, we can use a cleaner approach to append values to our array:

indexed_array+=("lorem")
for index in ${!indexed_array[@]}
    do
        echo "["$index"]:["${indexed_array[$index]}"]"
    done

Let’s see what happened here:

[0]:[Baeldung]
[1]:[is]
[2]:[cool]
[3]:[lorem]

We used the += construct to append a new value to our array. This can also be used to initialize the array.

3.5. Removing Elements from Arrays

When we want to remove items from an array, we use the unset construct:

declare -a indexed_array=("Baeldung" "is" "cool")
echo "Array elements : ${indexed_array[@]}"
unset indexed_array[1]
echo "Size of array after removal: ${#indexed_array[@]}"
echo "Array elements after removal: ${indexed_array[@]}"

In this sample, we removed the element at index 1:

Array elements : Baeldung is cool
Size of array after removal: 2
Array elements after removal : Baeldung cool

Let’s see what happens if we use unset without providing any index:

declare -a indexed_array=("Baeldung" "is" "cool")
echo "Array elements : ${indexed_array[*]}"
unset indexed_array
echo "Size of array after removal: ${#indexed_array[@]}"
echo "Removed complete array : ${indexed_array[@]}"

We get the output:

Array elements : Baeldung is cool
Size of array after removal: 0
Removed complete array :

This means our array is completely removed. The same thing happens if we use the @ and symbols as index:

declare -a indexed_array=("Baeldung" "is" "cool")
echo "Array elements : ${indexed_array[*]}"
unset indexed_array[@]
echo "Size of array:" ${#indexed_array[@]}
echo "Removed complete array : ${indexed_array[@]}"

Let’s now try to add an element in the array we just removed:

declare -a indexed_array=("Baeldung" "is" "cool")
echo "Array elements : ${indexed_array[@]}"
unset indexed_array[@]
indexed_array+=("lorem ipsum")
echo "Array elements : ${indexed_array[@]}"

This is possible because we’re re-initializing the array:

Array elements : Baeldung is cool
Array elements : lorem ipsum

4. Advanced Operations

Now, let’s take a look at some advanced scenarios involving arrays.

4.1. Using Quotes in Iterations

Remember our first iteration examples? We said there’s a difference between @ and .

Let’s see what that is about:

declare -a indexed_array=("Baeldung" "is" "cool")
for element in "${indexed_array[*]}"
    do
        echo "["$element"]"
    done

And now the output:

[Baeldung is cool]

This is not really what we expected. Now let’s try it again with @ as the index:

for element in "${indexed_array[@]}"
    do
        echo "["$element"]"
    done

We get the output:

[Baeldung]
[is]
[cool]

Let’s explain what happened here. We used double quotes around the ${indexed_array[*]} construct.

As a result, when using the symbol, the elements of the array are a single word separated by the <whitespace> character.

This approach is particularly useful when we have array elements that have <whitespace> characters inside:

declare -a indexed_array=("Baeldung is" "so much" "cool")
echo "Without quotes:"
for element in ${indexed_array[@]}
    do
        echo "["$element"]"
    done
echo "With quotes:"
for element in "${indexed_array[@]}"
    do
        echo "["$element"]"
    done

And the corresponding output:

Without quotes:
[Baeldung]
[is]
[so]
[much]
[cool]
With quotes:
[Baeldung is]
[so much]
[cool]

Notice that when using the unquoted construct, the array elements are subject to further word splitting.

4.2. Transforming Arrays

Normally, when we consider transforming elements of an array, the first thing that comes to mind is iteration.

Luckily, Bash provides some neat tricks with parameter expansions that spare us from iterating.

We’ll only take a look at a few interesting ones, but we can always check the Bash man page for more.

Let’s first search and replace a specific element in our array:

declare -a indexed_array=("Baeldung is" "so much" "cool" "cool cool")
echo "Initial array : ${indexed_array[@]}"
echo "Replacing cool with better: ${indexed_array[@]/cool/better}"

Here we used the ${indexed_array[@]/cool/better} syntax to achieve this replacement:

Initial array :  Baeldung is so much cool cool cool
Replacing cool with better: Baeldung is so much better better cool

However, this only replaces the first occurrence of the string in each element. If we want all strings then we need to use <//>  syntax :

declare -a indexed_array=("Baeldung is" "so much" "cool" "cool cool")
echo "Replacing cool with better: ${indexed_array[@]//cool/better}"

And we get the desired result:

Replacing cool with better: Baeldung is so much better better better

Let’s see what happens if we don’t specify a replacement string:

echo "Replacing cool with nothing: ${indexed_array[@]//cool}"

This removes the matched string from our array:

Replacing cool with nothing: Baeldung is so much

We need to keep in mind that this kind of search is case-sensitive by default unless we modify the optional behavior of our shell.

Now, let’s play around with changing to upper-case:

declare -a indexed_array=("Baeldung" "is" "cool")
echo "Uppercasing sentence case: ${indexed_array[@]^}"
echo "Uppercasing all characters: ${indexed_array[@]^^}"

Here we used the <^> and <^^ > to change to upper-case:

Uppercasing sentence case: Baeldung Is Cool
Uppercasing all characters: BAELDUNG IS COOL

On the other hand, we can also do lower-case transformations with the <,> and <,,> constructs:

indexed_array=("BAeldung" "Is" "COol")
echo "Lowercasing sentence case: ${indexed_array[@],}"
echo "Lowercasing all characters: ${indexed_array[@],,}"

And we get the desired result:

Lowercasing sentence case: bAeldung is cOol
Lowercasing all characters: baeldung is cool

4.3. Assignment Between Arrays

In the previous transformation samples, our initial array was not modified.

Every time we just applied a transformation, but the results were merely printed to the standard output.

If we want to keep the results, we can assign them in a separate array:

indexed_array=("BAeldung" "Is" "COol")
lowercased_array=(${indexed_array[@],})
echo "Lowercasing sentence case: ${lowercased_array[@]}"

And the corresponding output:

Lowercasing sentence case: bAeldung is cOol

Let’s take another example of transformation where this assignment is useful:

indexed_array=("Baeldung is" "so much" "cool")
echo "Uppercasing sentence case1: ${indexed_array[@]^}"
echo "No of elements in first_array: ${#indexed_array[@]}"
second_array=(${indexed_array[@]})
echo "Uppercasing sentence case2: ${second_array[@]^}"
echo "No of elements in second_array: ${#second_array[@]}"

We would expect the two outputs to match. However, this is not the case:

Uppercasing sentence case1: Baeldung is So much Cool
No of elements in first_array: 3
Uppercasing sentence case2: Baeldung Is So Much Cool
No of elements in second_array: 5

So, what happened? Remember what quoting did in the previous iteration examples?

Since we didn’t use quotes, Bash applied word splitting, and the second_array contains more elements than the first.

In some situations, assignment can be useful for merging arrays:

declare -a fist_array=("Baeldung" "is" "cool")
declare -a second_array=("lorem" "ipsum")
declare -a merged=(${fist_array[@]} ${second_array[@]})
echo "First array : ${fist_array[@]}"
echo "Second array : ${second_array[@]}"
echo "Merged array : ${merged[@]}"

Let’s take a closer look at what we did. We initialized the merged array with all the elements of the first and the second array:

First array : Baeldung is cool
Second array : lorem ipsum
Merged array : Baeldung is cool lorem ipsum

4.4. Offset and Length Traversal

In Bash, this is formally known as substring expansion but works as well on indexed arrays. However, for maps, it has undefined behavior.

Sometimes, we need to extract specific parts of an array:

declare -a indexed_array=("Baeldung" "is" "cool" "and" "better" "than" "before")
echo "Offset 1 length 3: ${indexed_array[@]:1:3}"

Let’s see what this does:

Offset 1 length 3: is cool and

This construct takes an input offset and a length. It then and selects length x elements starting at index = offset.

But what if we omit the length:

echo "Offset 1 no length: ${indexed_array[@]:1}"

We then get all the elements of the array starting at the offset until the end:

Offset 1 no length: is cool and better than before

Now let’s see if using a negative offset changes something:

echo "Offset -1 length 3: ${indexed_array[@]: -4:3}"

We get some interesting results:

Offset -1 length 3: and better than

The negative offset is considered relative to the maximum index of the array.

Notice also the <whitespace> character when using negative offsets. If we don’t use it, Bash confuses it with another construct.

5. Conclusion

In this tutorial, we looked at how to work with Bash arrays. We saw there is support for both associative and index-based arrays.

After that, we looked at basic iteration strategies and how to insert and remove elements.

We then played around with transformations and assignments between arrays.

Finally, we showed how to extract specific parts of arrays using offset and length traversal.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments