Linux Basic — Array vs String

4 minute read

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:

  1. Use an array (arr=(…)), never a space-separated string
  2. Iterate with for x in "${arr[@]}" (always quoted, always @)
  3. Forward args with "$@" (always quoted)
  4. Join into a string with "$*" or "${arr[*]}" only for display
  5. To populate an array from command output, use mapfile -t arr < <(cmd), never arr=( $(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

Updated: