Linux Basic — Array vs String
Array vs String
This is the single concept most shell programmers get wrong. Get it right and your scripts handle filenames with spaces, newlines, and quotes effortlessly. Get it wrong and you spend years patching mysterious bugs.
The Core Idea
A string in Bash is one value. A list of values must be an array — or be passed as the positional parameters $@. Trying to store a list as a space-separated string and iterate over it is the most common bug in shell scripts.
# ❌ String pretending to be a list — breaks on filenames with spaces
files="a.txt b.txt 'my doc.txt'"
for f in $files; do
echo "[$f]"
done
# [a.txt]
# [b.txt]
# ['my]
# [doc.txt']
# ✅ Real array
files=(a.txt b.txt "my doc.txt")
for f in "${files[@]}"; do
echo "[$f]"
done
# [a.txt]
# [b.txt]
# [my doc.txt]
The string version word-splits on $IFS (spaces, tabs, newlines) and then performs glob expansion. The array version preserves each element exactly.
"$@" vs $* vs "$*" vs $@
Four expansions of the positional parameters that look almost identical and behave totally differently:
set -- "alice" "bob carol" "dan"
# Show each in a way that makes word-splitting visible
echo '--- "$@" ---'
for x in "$@"; do printf '[%s]\n' "$x"; done
# [alice]
# [bob carol] ← preserved as one
# [dan]
echo '--- $@ ---'
for x in $@; do printf '[%s]\n' "$x"; done
# [alice]
# [bob]
# [carol] ← split!
# [dan]
echo '--- "$*" ---'
for x in "$*"; do printf '[%s]\n' "$x"; done
# [alice bob carol dan] ← all joined into one string
echo '--- $* ---'
for x in $*; do printf '[%s]\n' "$x"; done
# [alice]
# [bob]
# [carol] ← also split
# [dan]
| Form | Behaviour |
|---|---|
"$@" |
N values, each preserved as-is — the safe form |
$@ |
N values, each subject to word-splitting and globbing |
"$*" |
1 string, joined by first char of IFS |
$* |
N values, each subject to word-splitting and globbing |
Rule: always use "$@" when iterating or passing args along. Use "$*" only when you want a single joined string (e.g. for logging).
The same applies to arrays: "${arr[@]}" is the iterate-safely form; "${arr[*]}" is the join-into-one-string form.
When Do You Actually Want a String?
When the data is a single value, even if it contains spaces:
greeting="Hello, World!" # one string
name="John Smith" # one string
# Iterating over a string makes no sense — there's nothing to iterate
echo "$greeting"
When you’re printing:
arr=(alice bob carol)
echo "Users: ${arr[*]}" # Users: alice bob carol
${arr[*]} (joined) is fine for display. For everything that consumes elements one-at-a-time, use "${arr[@]}".
The “Build a Command” Pattern
Suppose you’re constructing a command with optional flags:
# ❌ String concatenation — breaks if any flag value has spaces
cmd="curl"
[[ -n "$token" ]] && cmd="$cmd -H 'Authorization: Bearer $token'"
$cmd "$url"
# ✅ Use an array
cmd=(curl)
[[ -n "$token" ]] && cmd+=(-H "Authorization: Bearer $token")
"${cmd[@]}" "$url"
The array version handles spaces in $token correctly — and you can see by inspection how many arguments you’re actually passing.
Pass-Through of Arguments
Functions / scripts that forward their args:
wrap() {
echo "Wrapping: $*" # display only
real_cmd "$@" # iterate — preserves grouping
}
wrap echo "hello world" "foo bar"
# Wrapping: echo hello world foo bar
# (real_cmd receives: echo, "hello world", "foo bar")
"$@" is the right way to forward. "$*" would merge everything into one string.
When IFS Bites You
# Default IFS is space + tab + newline
list="apple banana cherry"
for x in $list; do
echo "$x"
done
# apple
# banana
# cherry — works, because no spaces in the values
# But add a value with a space:
list="apple banana 'red cherry'"
for x in $list; do
echo "$x"
done
# apple
# banana
# 'red
# cherry' — quotes are literal characters, not parsed by shell
Shell does not re-parse single quotes in a variable — for x in $list only word-splits. If your data has values containing spaces, you must use an array.
A Real Example — Filenames With Spaces
# ❌ The classic broken loop
for f in $(ls); do
rm "$f"
done
# Filename "my doc.txt" becomes two iterations: "my" and "doc.txt" → both fail
# ✅ Glob (no fork, handles all chars except newlines)
for f in *; do
[[ -e "$f" ]] || continue
rm "$f"
done
# ✅ find + read for newline-safe iteration
while IFS= read -r -d '' f; do
rm "$f"
done < <(find . -maxdepth 1 -type f -print0)
Globs work for almost everything (they handle spaces fine; newlines in filenames are rare and broken).
Quick Decision Checklist
When holding multiple values:
- Use an array (
arr=(…)), never a space-separated string - Iterate with
for x in "${arr[@]}"(always quoted, always@) - Forward args with
"$@"(always quoted) - Join into a string with
"$*"or"${arr[*]}"only for display - To populate an array from command output, use
mapfile -t arr < <(cmd), neverarr=( $(cmd) )
Memorise those five and you’ll write robust shell forever.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
for x in $list (string) |
use an array | word-splits on IFS |
for x in $@ or for x in "$*" |
for x in "$@" |
unquoted splits; "$*" joins |
cmd="real_cmd -a $arg"; $cmd |
cmd=(real_cmd -a "$arg"); "${cmd[@]}" |
quoting of $arg is lost |
arr=( $(cmd) ) |
mapfile -t arr < <(cmd) |
splits on IFS, glob-expands |
if [[ "$list" == "$item" ]] for “contains” |
use assoc set or grep | strings aren’t lists |
Next → Practice Exercises