Bash Arrays: Create, Append, Loop, Slice and Delete

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
Applies to Linux distributions with Bash installed
Privilege Normal user; no root required for the examples
Scope Bash indexed arrays: create, access, "${array[@]}" versus "${array[*]}", length, indices, append, loops, membership checks, slices, negative indexes, unset, sparse holes, command argument arrays, and empty-array checks. Includes one short associative-array example; full key-value maps are in a separate guide. Does not cover associative-array CRUD, string splitting into arrays, or full IFS word-splitting behavior.
Related guides Bash mapfile
Shell scripting course hub
Short on time?Beta Get the gist of this 11-minute read in seconds, then ask follow-up questions

Bash arrays store multiple values under one variable name. Indexed arrays use numeric subscripts and fit file lists, command arguments, job names, and other ordered values.

When elements can contain spaces or empty strings, expand them with "${array[@]}" so each element stays a separate argument. The examples below use printf and declare -p so you can see exactly what Bash stored.


What Kind of Arrays Does Bash Support?

Bash provides two array types:

  • Indexed arrays — values addressed by integer subscripts; compound assignments without explicit subscripts begin at 0, but indices do not have to be contiguous (this article)
  • Associative arrays — string keys declared with declare -A

Indexed arrays fit ordered lists: configuration file paths, CLI flags, batch job names, or rows read from a command. Associative arrays fit lookups by name, such as protocol to port number:

bash
declare -A ports=(
  [http]=80
  [https]=443
)

Read one key:

bash
printf '%s\n' "${ports[https]}"
output
443

The rest of this article covers indexed arrays. For loops over keys, updates, and sorted iteration on associative maps, see Bash associative arrays.

Bash does not offer true nested or multidimensional indexed arrays. A common pattern is an associative array with compound keys such as grid["$row,$col"] when you must map two coordinates to one value.

Indexed array quick reference

Task Syntax
Create arr=(a b c)
Access one ${arr[0]}
Access all "${arr[@]}"
Indices "${!arr[@]}"
Length ${#arr[@]}
Append arr+=(value)
Slice "${arr[@]:1:2}"
Last element ${arr[-1]}
Delete element unset 'arr[1]'
Delete array unset arr

Create an Indexed Array

The usual form assigns elements in parentheses. Each word becomes one element unless you quote a value that contains spaces:

bash
files=( "/etc/hosts" "my config.ini" "" "backup data.tar" )

Inspect what Bash stored:

bash
declare -p files
output
declare -a files=([0]="/etc/hosts" [1]="my config.ini" [2]="" [3]="backup data.tar")

You can also declare the variable explicitly and assign by index:

bash
declare -a paths
paths[0]="/etc/hosts"
paths[3]="backup data.tar"

Index 3 leaves a gap at 1 and 2:

bash
declare -p paths
output
declare -a paths=([0]="/etc/hosts" [3]="backup data.tar")

declare -a marks the variable as an indexed array. Bash also creates one automatically when you use indexed or compound assignment. When you supply an index, Bash uses it as-is; when you omit the index in a compound assignment, Bash picks the next index starting from 0.

An explicit first index does not have to be zero:

bash
items[5]="five"

Bash stores only the index you supplied:

bash
declare -p items
output
declare -a items=([5]="five")

files[2] contains an empty string; it is still a valid array element. Scripts that build file lists from user input or parsed config often need to preserve blank fields, which is one reason arrays beat space-delimited strings.

To turn lines or fields into elements from a string, see split strings into arrays.


Access Array Elements

Read one cell with ${name[index]}. Index 0 is the first element in a contiguous list, but any assigned index works on a sparse array:

bash
printf 'files[0]=%s\n' "${files[0]}"
output
files[0]=/etc/hosts

Read the empty element with quoting so the expansion does not disappear:

bash
printf 'files[2]=%q\n' "${files[2]}"
output
files[2]=''

%q shows an empty quoted value. "${files[2]}" still expands to a single empty argument when passed to a command.

To pass every element to a loop or external command, expand the whole array at once:

bash
printf 'item=%q\n' "${files[@]}"
output
item=/etc/hosts
item=my\ config.ini
item=''
item=backup\ data.tar

Each line is one array element, including the path with a space and the empty slot. Use this form when passing array elements to commands such as tar, curl, or rsync, because each element remains a separate argument.


"${array[@]}" vs "${array[*]}": What's the Difference?

Both forms expand an array inside double quotes, but they do not produce the same word list. Build a small array with a space inside one element and an empty element:

bash
a=( "one two" "" "three" )

Print with "${a[@]}" — each element is a separate argument to printf:

bash
printf '<%s>\n' "${a[@]}"
output
<one two>
<>
<three>

Now print with "${a[*]}" — Bash joins all elements with the first character of IFS (space by default) into one word:

bash
printf '<%s>\n' "${a[*]}"
output
<one two  three>

The empty element is no longer preserved as a separate argument, and "one two" is no longer isolated from "three". All three array members have been joined into one string. For almost every script that forwards data to another command, use "${array[@]}". Reserve "${array[*]}" for when you deliberately want one string, such as logging or building a single message.


Get Array Length and Indices

${#array[@]} counts assigned elements, not highest_index + 1:

bash
printf 'element count=%s\n' "${#a[@]}"
output
element count=3

List which indices currently hold values:

bash
printf 'indices: %s\n' "${!a[@]}"
output
indices: 0
indices: 1
indices: 2

After you delete index 1 from a larger array, ${!array[@]} is how you detect holes without assuming contiguous numbering.

${#array[@]} counts array elements. ${#array[index]} returns the character length of one element:

bash
printf 'length of files[0]=%s\n' "${#files[0]}"
output
length of files[0]=10

Append and Update Elements

Append one or more elements with +=:

bash
files+=( "new file.txt" )

Confirm the new tail element:

bash
printf 'last=%q\n' "${files[-1]}"
output
last=new\ file.txt

Overwrite an existing index with ordinary assignment:

bash
files[1]="updated config.ini"

+= only adds; it does not renumber existing indices.

bash
names=( beta gamma )
names=( alpha "${names[@]}" )

Bash has no separate prepend operator; create a new array with the added value first when you need to insert at the beginning.


Loop Over Elements and Indices

A Bash for loop over "${array[@]}" visits each value:

bash
for item in "${files[@]}"; do
  printf 'item=%q\n' "$item"
done
output
item=/etc/hosts
item=updated\ config.ini
item=''
item=backup\ data.tar
item=new\ file.txt

Loop over indices when you need the position as well as the value:

bash
for i in "${!files[@]}"; do
  printf 'i=%s files[i]=%q\n' "$i" "${files[$i]}"
done
output
i=0 files[i]=/etc/hosts
i=1 files[i]=updated\ config.ini
i=2 files[i]=''
i=3 files[i]=backup\ data.tar
i=4 files[i]=new\ file.txt

If you omit the double quotes and use ${array[@]}, Bash performs word splitting and pathname expansion on the resulting words:

bash
paths=( "/etc/hosts" "my config.ini" )

Without quotes, the loop sees three words instead of two paths:

bash
for p in ${paths[@]}; do printf 'p=%q\n' "$p"; done
output
p=/etc/hosts
p=my
p=config.ini

Quote the expansion so my config.ini stays one element:

bash
for p in "${paths[@]}"; do printf 'p=%q\n' "$p"; done
output
p=/etc/hosts
p=my\ config.ini

To test whether an array contains a value, compare each element and stop on the first match:

bash
target="updated config.ini"

for item in "${files[@]}"; do
  if [[ "$item" == "$target" ]]; then
    echo "found"
    break
  fi
done
output
found

For deleting every copy of a value or subtracting one list from another, see remove array elements by value.


Slice an Array

${array[@]:offset:length} copies a range into a new expansion. Skip the first file and take the next two entries:

bash
batch=( "${files[@]:1:2}" )

Print the slice to confirm which two elements you captured:

bash
printf 'batch: %q\n' "${batch[@]}"
output
batch: updated\ config.ini
batch: ''

Omit :length to take everything from the offset through the end. Slicing does not modify the original array.

On sparse arrays, slicing follows Bash's indexed-array offset rules rather than first reindexing the existing values. If gaps matter, loop over "${!array[@]}" instead of assuming contiguous positions.


Negative Indices and the Last Element

Bash supports negative subscripts on indexed arrays. ${array[-1]} refers to the element at the highest assigned index — Bash counts backward from one greater than the maximum index:

bash
printf 'last=%q\n' "${files[-1]}"
output
last=new\ file.txt

${array[-2]} refers to one index before the highest index. On a sparse array, that index may be unset. ${array[-1]} still refers to the highest assigned index, not ${#array[@]}-1.


Delete Elements with unset

Remove one cell with unset and a quoted subscript:

bash
unset 'files[2]'

Quote files[2] when passing it to unset so the shell does not subject the subscripted name to pathname expansion:

bash
declare -p files
output
declare -a files=([0]="/etc/hosts" [1]="updated config.ini" [3]="backup data.tar" [4]="new file.txt")

Index 2 is gone, but index 3 and 4 are unchanged. unset does not slide later elements down to close the gap.

To remove the entire array variable, omit the subscript:

bash
temp=( one two three )
unset temp

unset temp removes the entire array variable. Use unset 'files[2]' when you only want to delete one element.


Sparse Arrays and Reindexing

After unset, the array is sparse: assigned elements remain at their original indices.

Build a quick example:

bash
tasks=( "pending" "running" "done" )

Remove the middle state and inspect the remaining indices:

bash
unset 'tasks[1]'

declare -p shows the hole at index 1:

bash
declare -p tasks
output
declare -a tasks=([0]="pending" [2]="done")

The length is still the number of assigned elements, not three:

bash
printf 'length=%s indices=%s\n' "${#tasks[@]}" "${!tasks[@]}"
output
length=2 indices=0 2

To pack values back into 0..n-1, assign a fresh array from the existing values:

bash
reindexed=( "${tasks[@]}" )

Bash rebuilds the copy with contiguous indices starting at zero:

bash
declare -p reindexed
output
declare -a reindexed=([0]="pending" [1]="done")

For deleting by value or subtracting one list from another, see remove array elements by value.


Build Command Arguments with Arrays

Arrays are the safe way to assemble flags and operands for external commands. Store each token as its own element:

bash
printf 'demo\n' > "input file.txt"
archive="my data.tar"
args=( --create --file "$archive" "input file.txt" )

Preview the words tar would receive:

bash
printf 'tar'
for token in "${args[@]}"; do printf ' %q' "$token"; done
printf '\n'
output
tar --create --file my\ data.tar input\ file.txt

Run the command with the same expansion:

bash
tar "${args[@]}"

tar sees four arguments: --create, --file, my data.tar, and input file.txt. A single string such as --create --file my data.tar would break at the space inside the filename.

The same pattern works for curl header lists, rsync option bundles, or any command where one logical value contains spaces.


Pass an Array to a Function or Command

Functions receive arguments like any other command. Forward all elements with "${array[@]}":

bash
show_args() {
  printf 'arg=%q\n' "$@"
}

show_args "${files[@]}"
output
arg=/etc/hosts
arg=updated\ config.ini
arg=backup\ data.tar
arg=new\ file.txt

The empty element at the old index 2 is no longer in files after unset, so it does not appear here. When you must mutate an array inside a function and have the caller see the change, pass the array name with a nameref (local -n) — that pattern is covered in Bash functions.


Check an Empty Array

Test ${#array[@]} before you loop:

bash
pending=()
if [ "${#pending[@]}" -eq 0 ]; then
  echo "no pending jobs"
fi
output
no pending jobs

${#array[@]} returns the number of assigned elements, so an empty array reports 0. In scripts that use set -u, initialize arrays with array=() before you conditionally append to them.


Common Array Mistakes

Mistake What goes wrong Safer pattern
${array[@]} without quotes in a loop or command Word splitting breaks elements that contain spaces "${array[@]}"
Using "${array[*]}" when forwarding arguments Multiple elements collapse into one string "${array[@]}"
Assuming indices stay 0..n-1 after unset Loops that use seq 0 $((n-1)) skip or hit empty cells Loop "${!array[@]}" or reindex
Building "$dir/$file $dir/$file2" strings Spaces in names create extra words One element per path in an array
Treating ${#array[@]} as max_index + 1 on sparse arrays Off-by-one logic and wrong counts Use ${!array[@]} or reindex
unset array[1] unquoted The unquoted argument can undergo filename expansion unset 'array[1]'

Summary

Indexed Bash arrays store ordered lists where each value keeps its boundaries when you expand with "${array[@]}". You create them with name=( ...), declare -a, or explicit index assignment, read cells with ${name[i]}, append with +=, and walk them with for item in "${name[@]}" or for i in "${!name[@]}".

The difference between "${array[@]}" and "${array[*]}" matters whenever elements contain spaces or empty strings: the @ form preserves each element as a separate argument, while * joins elements into one word. After unset, indices can leave holes, ${#array[@]} counts assigned elements only, and ${array[-1]} reads the last assigned index even on sparse arrays.

Use arrays to build tar, curl, and rsync argument lists instead of concatenating strings, and quote subscripts when you delete cells. Choose a key-value map instead when string keys are the natural model.


References


Frequently Asked Questions

What is the difference between "${array[@]}" and "${array[*]}"?

Inside double quotes, "${array[@]}" produces one word per element. "${array[*]}" joins all elements into a single word using the first character of IFS. For command arguments and loops, use "${array[@]}".

Can Bash arrays contain spaces or empty strings?

Yes. Quoted elements such as "my config.ini" and "" are stored as single cells. You only lose that guarantee if you expand the array unquoted or use "${array[*]}" when you needed separate words.

Does unset reindex a Bash array?

No. unset 'array[1]' removes index 1 and leaves a hole. Later indices keep their numbers. Rebuild the array with array=( "${array[@]}" ) or copy the values into a new array if you need contiguous 0..n-1 indices.

How do I get the last element of a Bash array?

Use ${array[-1]}. Negative subscripts count backward from one greater than the highest assigned index, so -1 is the last indexed element even when the array is sparse.

How do I check whether a Bash array contains a value?

Loop over "${array[@]}" and compare each element, or break early once you find a match. There is no built-in membership operator for indexed arrays.

Does Bash support multidimensional arrays?

Not as nested indexed arrays. Use separate arrays, an associative array with compound string keys, or parallel arrays indexed by the same counter when you need a grid-like structure.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)