Arrays, Command Substitution, and Input Handling
Represent argument lists as arrays, capture command output without corrupting boundaries, and read text or path data through explicit delimiters and trusted channels.
Learning objectives
By the end of this lesson
- Create and expand indexed and associative Bash arrays safely.
- Distinguish one string from an argument vector.
- Understand command substitution’s newline and splitting behavior.
- Read text with
IFS= read -rand paths with NUL delimiters. - Avoid
eval, unsafe parsing, and accidental code execution from input.
1. Shell automation needs explicit data boundaries
Many Bash defects are data-model defects. A filename list is stored in one string; a command and its arguments are concatenated; command output is split on whitespace; or user input is reinterpreted as shell source. Arrays, delimiters, and separate input channels let scripts carry structure instead of guessing it later.
flowchart LR
S["Source: arguments, file, or command"] --> D{"What is the data model?"}
D -- one scalar --> V["Quoted variable"]
D -- argument list --> A["Indexed array"]
D -- key/value map --> M["Associative array"]
D -- text records --> R["IFS= read -r"]
D -- path records --> N["NUL delimiter"]
V --> E["Expand with preserved boundary"]
A --> E
M --> E
R --> E
N --> EIf you already have separate arguments, keep them in an array. Turning them into a string and later reparsing that string creates ambiguity and injection risk.
2. Indexed arrays preserve ordered argument lists
Indexed arrays use integer subscripts. Append with array+=(value), inspect length with ${#array[@]}, and expand every element with "${array[@]}". Quoted [@] preserves each element as a separate argument.
declare -a curl_args=(
--fail
--silent
--show-error
--connect-timeout 5
)
url='https://example.com/health?name=blue team'
curl_args+=(--header 'Accept: application/json')
printf 'argument_count=%d\n' "${#curl_args[@]}"
printf 'arg=<%s>\n' "${curl_args[@]}"
# The command receives the URL as one argument and every option separately.
# curl "${curl_args[@]}" "$url"
for index in "${!curl_args[@]}"; do
printf 'curl_args[%d]=%q\n' "$index" "${curl_args[$index]}"
done"${array[*]}" joins all elements into one word using the first character of IFS. That can be useful for display, but it is not the correct way to pass an argument list.
3. Associative arrays model small key/value maps
Bash associative arrays use string keys. They are useful for small in-memory mappings such as environment-to-endpoint or status counters. They are not a replacement for JSON, YAML, databases, or ordered records when interoperability and schema matter.
declare -A endpoints=(
[development]='http://127.0.0.1:8080'
[staging]='https://staging.example.net'
[production]='https://api.example.net'
)
environment=${1:-staging}
if [[ -v 'endpoints[$environment]' ]]; then
printf 'endpoint=%s\n' "${endpoints[$environment]}"
else
printf 'Unknown environment: %s\n' "$environment" >&2
exit 2
fi
for key in "${!endpoints[@]}"; do
printf '%s\t%s\n' "$key" "${endpoints[$key]}"
done | sortIteration order is not a reliable configuration contract. Sort keys for human reports or use a format designed for ordered data.
4. Command substitution captures text, not arbitrary records
$(command) captures standard output and removes trailing newline characters. When the substitution is unquoted, the result is then subject to word splitting and pathname expansion. Quoted substitution preserves one argument, but it is still one text value—not an array of records.
kernel=$(uname -r)
printf 'kernel=<%s>\n' "$kernel"
# Capture multiple lines as array elements.
mapfile -t shells < <(awk -F: '{print $7}' /etc/passwd | sort -u)
printf 'shell=<%s>\n' "${shells[@]}"
# Do not use: files=($(find ...))
# Pathnames may contain spaces, tabs, wildcard characters, or newlines.
declare -a config_files=()
while IFS= read -r -d '' path; do
config_files+=("$path")
done < <(find /etc -maxdepth 2 -type f -name '*.conf' -print0 2>/dev/null)
printf 'config_count=%d\n' "${#config_files[@]}"Process substitution, such as < <(command), provides a file-like input to a command or loop while keeping the loop in the current shell. It is a Bash feature and therefore belongs in scripts that explicitly target Bash.
5. Input handling requires an explicit delimiter and trust model
read is designed for one record at a time. Use IFS= to prevent trimming at the edges and -r to keep backslashes literal. For pathnames, newline is not a complete delimiter because Linux filenames may contain newlines; NUL is the robust separator.
# Read ordinary text lines exactly, except for the line-ending delimiter.
while IFS= read -r line || [[ -n $line ]]; do
printf 'line=<%s>\n' "$line"
done < input.txt
# Split a controlled colon-delimited record.
while IFS=: read -r name _ uid gid comment home shell; do
printf 'user=%s uid=%s home=%s shell=%s\n' \
"$name" "$uid" "$home" "$shell"
done < /etc/passwd
# Read pathnames safely from find.
while IFS= read -r -d '' path; do
printf 'path=%q\n' "$path"
done < <(find . -type f -print0)Do not pass input to eval, embed it in shell source, or use it as an unquoted format string. Validate according to the expected grammar and pass it as quoted arguments.
6. Here-documents and here-strings create explicit input streams
A here-document feeds a block of text to standard input. Quoting the delimiter controls whether parameter, command, and arithmetic expansion occur inside the body. A here-string feeds one expanded string followed by a newline.
service='payments-api'
# Quoted delimiter: body remains literal.
cat > template.txt <<'EOF'
service=${service}
host=$(hostname)
EOF
# Unquoted delimiter: selected expansions occur before input is delivered.
cat > rendered.txt <<EOF
service=${service}
host=$(hostname)
EOF
# Feed one value as standard input.
grep -q '^payments-' <<< "$service" && printf 'expected prefix\n' Never include secrets in shell tracing or generated here-documents without understanding file permissions, process arguments, logs, and cleanup. Chapter 15 develops temporary-resource and cleanup controls.
7. Hands-on lab: inventory difficult filenames safely
The lab creates filenames containing spaces, wildcard characters, tabs, and a newline, then inventories them using NUL-delimited records and arrays.
lab="$HOME/devops-academy/linux/chapter14/lesson04"
rm -rf "$lab"
mkdir -p "$lab/input"
cd "$lab"
printf 'alpha\n' > 'input/normal.txt'
printf 'beta\n' > 'input/two words.txt'
printf 'gamma\n' > 'input/*.literal'
printf 'delta\n' > $'input/tab\tname.txt'
printf 'epsilon\n' > $'input/line\nbreak.txt'
cat > inventory.sh <<'SCRIPT'
#!/usr/bin/env bash
main() {
if (( $# != 1 )); then
printf 'Usage: %s DIRECTORY\n' "${0##*/}" >&2
return 2
fi
local root=$1 path
if [[ ! -d $root ]]; then
printf 'Not a directory: %s\n' "$root" >&2
return 3
fi
local -a files=()
mapfile -d '' -t files < <(find "$root" -maxdepth 1 -type f -print0)
printf 'count=%d\n' "${#files[@]}"
for path in "${files[@]}"; do
printf 'path=%q\tsize=%s\n' "$path" "$(stat -c %s -- "$path")"
done
}
main "$@"
SCRIPT
chmod u+x inventory.sh
./inventory.sh input | tee inventory.txt
bash -n inventory.sh
printf 'filesystem_count=%s\n' "$(find input -maxdepth 1 -type f -printf '.' | wc -c)"Verification checklist
8. Common input and array mistakes
“A space-separated string is an argument list.”
It cannot preserve embedded spaces or empty elements. Use an indexed array.
“Quoted command substitution creates an array.”
It creates one string. Use mapfile or a read loop with an explicit delimiter.
“One filename per line is always safe.”
Filenames may contain newline characters. NUL-delimited records are the robust pathname interface.
“eval is needed for dynamic commands.”
Usually the command name and arguments can be represented separately and invoked with an array, avoiding source-code reinterpretation.
9. Knowledge check
Question 1. What is the difference between "${items[@]}" and "${items[*]}"?
[@] expands each array element as a separate argument. Quoted [*] joins all elements into one argument using the first character of IFS.Question 2. Why is files=($(find ...)) unsafe?
Question 3. Why use IFS= read -r?
-r prevents backslashes from acting as escapes.10. Summary
Safe input handling starts by choosing the correct representation. Use quoted scalars for one value, arrays for argument lists, associative arrays for small maps, text reads with explicit delimiters, and NUL records for pathnames. Command substitution captures text, not structured records, and untrusted input must remain data rather than shell source.
11. Further reading
- GNU Bash Reference Manual — Arrays.
- GNU Bash Reference Manual — Command Substitution.
- GNU Bash Reference Manual —
readandmapfilebuiltins. - GNU Findutils manual —
-print0and safe pathname processing.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.