| 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 |
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:
declare -A ports=(
[http]=80
[https]=443
)Read one key:
printf '%s\n' "${ports[https]}"443The 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:
files=( "/etc/hosts" "my config.ini" "" "backup data.tar" )Inspect what Bash stored:
declare -p filesdeclare -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:
declare -a paths
paths[0]="/etc/hosts"
paths[3]="backup data.tar"Index 3 leaves a gap at 1 and 2:
declare -p pathsdeclare -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:
items[5]="five"Bash stores only the index you supplied:
declare -p itemsdeclare -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:
printf 'files[0]=%s\n' "${files[0]}"files[0]=/etc/hostsRead the empty element with quoting so the expansion does not disappear:
printf 'files[2]=%q\n' "${files[2]}"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:
printf 'item=%q\n' "${files[@]}"item=/etc/hosts
item=my\ config.ini
item=''
item=backup\ data.tarEach 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:
a=( "one two" "" "three" )Print with "${a[@]}" — each element is a separate argument to printf:
printf '<%s>\n' "${a[@]}"<one two>
<>
<three>Now print with "${a[*]}" — Bash joins all elements with the first character of IFS (space by default) into one word:
printf '<%s>\n' "${a[*]}"<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:
printf 'element count=%s\n' "${#a[@]}"element count=3List which indices currently hold values:
printf 'indices: %s\n' "${!a[@]}"indices: 0
indices: 1
indices: 2After 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:
printf 'length of files[0]=%s\n' "${#files[0]}"length of files[0]=10Append and Update Elements
Append one or more elements with +=:
files+=( "new file.txt" )Confirm the new tail element:
printf 'last=%q\n' "${files[-1]}"last=new\ file.txtOverwrite an existing index with ordinary assignment:
files[1]="updated config.ini"+= only adds; it does not renumber existing indices.
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:
for item in "${files[@]}"; do
printf 'item=%q\n' "$item"
doneitem=/etc/hosts
item=updated\ config.ini
item=''
item=backup\ data.tar
item=new\ file.txtLoop over indices when you need the position as well as the value:
for i in "${!files[@]}"; do
printf 'i=%s files[i]=%q\n' "$i" "${files[$i]}"
donei=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.txtIf you omit the double quotes and use ${array[@]}, Bash performs word splitting and pathname expansion on the resulting words:
paths=( "/etc/hosts" "my config.ini" )Without quotes, the loop sees three words instead of two paths:
for p in ${paths[@]}; do printf 'p=%q\n' "$p"; donep=/etc/hosts
p=my
p=config.iniQuote the expansion so my config.ini stays one element:
for p in "${paths[@]}"; do printf 'p=%q\n' "$p"; donep=/etc/hosts
p=my\ config.iniTo test whether an array contains a value, compare each element and stop on the first match:
target="updated config.ini"
for item in "${files[@]}"; do
if [[ "$item" == "$target" ]]; then
echo "found"
break
fi
donefoundFor 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:
batch=( "${files[@]:1:2}" )Print the slice to confirm which two elements you captured:
printf 'batch: %q\n' "${batch[@]}"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:
printf 'last=%q\n' "${files[-1]}"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:
unset 'files[2]'Quote files[2] when passing it to unset so the shell does not subject the subscripted name to pathname expansion:
declare -p filesdeclare -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:
temp=( one two three )
unset tempunset 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:
tasks=( "pending" "running" "done" )Remove the middle state and inspect the remaining indices:
unset 'tasks[1]'declare -p shows the hole at index 1:
declare -p tasksdeclare -a tasks=([0]="pending" [2]="done")The length is still the number of assigned elements, not three:
printf 'length=%s indices=%s\n' "${#tasks[@]}" "${!tasks[@]}"length=2 indices=0 2To pack values back into 0..n-1, assign a fresh array from the existing values:
reindexed=( "${tasks[@]}" )Bash rebuilds the copy with contiguous indices starting at zero:
declare -p reindexeddeclare -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:
printf 'demo\n' > "input file.txt"
archive="my data.tar"
args=( --create --file "$archive" "input file.txt" )Preview the words tar would receive:
printf 'tar'
for token in "${args[@]}"; do printf ' %q' "$token"; done
printf '\n'tar --create --file my\ data.tar input\ file.txtRun the command with the same expansion:
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[@]}":
show_args() {
printf 'arg=%q\n' "$@"
}
show_args "${files[@]}"arg=/etc/hosts
arg=updated\ config.ini
arg=backup\ data.tar
arg=new\ file.txtThe 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:
pending=()
if [ "${#pending[@]}" -eq 0 ]; then
echo "no pending jobs"
fino 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.

