Reading Command Output into Arrays with Bash mapfile and readarray
Slurp command output and files into Bash arrays the safe way. Use mapfile and readarray to dodge word-splitting bugs and handle null-delimited filenames cleanly.
- #automation
- #ai
- #bash
- #arrays
- #shell
Sooner or later every automation script needs to take the output of a command and treat it as a list. You ran kubectl get pods, you globbed a directory, you pulled hostnames from an inventory file, and now you want to iterate over the results. The reflex most of us learned years ago was for x in $(command), and it works right up until a filename has a space in it or an IFS surprise splits a single line into three. Those bugs are quiet. They pass code review, they pass the demo, and they fail at 2 a.m. when a directory finally contains a file named release notes.txt.
mapfile (and its synonym readarray) is the right tool for this job, and it has been a Bash builtin since 4.0. It reads lines straight into an array without going through word splitting or glob expansion. If you are writing Bash that other people run, this should be in your muscle memory. Below I walk through the patterns I actually use, where they bite, and how I let AI draft the gnarlier flag combinations while I keep the verification in my own hands.
Why $(command) Splitting Is the Wrong Default
Consider the classic mistake:
# DON'T: word-splitting and glob expansion both fire here
files=$(ls /var/log)
for f in $files; do
echo "processing $f"
done
This breaks on filenames with spaces, breaks on filenames containing glob characters, and silently collapses empty results. The unquoted $files is expanded by the shell, splitting on every space and tab and re-globbing anything that looks like a pattern. You did not ask for any of that.
mapfile sidesteps the whole mess by reading line by line:
mapfile -t files < <(ls /var/log)
for f in "${files[@]}"; do
printf 'processing %s\n' "$f"
done
The -t flag strips the trailing newline from each element, which is almost always what you want. The < <(...) is process substitution feeding the command output into mapfile on stdin. Each line becomes exactly one array element, spaces and all.
The Core Patterns
Reading a file into an array, one line per element:
mapfile -t hosts < /etc/ansible/host_list.txt
echo "Loaded ${#hosts[@]} hosts"
Reading command output, which is the same idea with process substitution:
mapfile -t pods < <(kubectl get pods -o name)
for pod in "${pods[@]}"; do
kubectl logs "$pod" --tail=5
done
A subtle but important point: piping into mapfile does not work the way you might hope.
# BROKEN: mapfile runs in a subshell, array vanishes after the pipe
kubectl get pods -o name | mapfile -t pods
echo "${#pods[@]}" # prints 0
The pipeline puts mapfile in a subshell, so the array it populates dies when the subshell exits. Always use < <(...) process substitution instead of a pipe when the array needs to survive.
Handling Filenames Safely: Null Delimiters
Line-delimited input still has one failure mode: filenames can legally contain newlines. Rare, but a security-relevant edge case, and find lets you defend against it with -print0. Pair it with mapfile -d '' to split on the null byte:
mapfile -d '' -t files < <(find /srv/data -type f -name '*.bak' -print0)
printf 'Found %d backup files\n' "${#files[@]}"
for f in "${files[@]}"; do
rm -v -- "$f"
done
The -d '' sets the delimiter to the null character (an empty string argument to -d means NUL). Combined with -print0, this is the only fully robust way to move a list of arbitrary filenames between two commands in Bash. Note the -- before "$f" so a filename starting with - is not mistaken for an option.
Useful Flags You Will Reach For
mapfile has a handful of flags worth knowing:
-tstrips trailing newlines (use it almost always).-d delimsets a custom delimiter;-d ''for NUL.-n countreads at mostcountlines.-O indexstarts assigning at array indexindexinstead of zero.-s countskips the firstcountlines, handy for dropping a header row.-C callback -c quantumruns a callback everyquantumlines, useful for progress reporting on huge inputs.
Skipping a CSV header while loading the rest:
mapfile -t -s 1 rows < inventory.csv
printf 'Loaded %d data rows (header skipped)\n' "${#rows[@]}"
Where AI Helps, and Where It Doesn’t
The flag soup above is exactly the kind of thing that is easy to get subtly wrong from memory. This is a good spot to let an assistant draft and then verify against real input. I keep prompts like the one on the bulk-ingest pattern handy, and when I am untangling an existing word-splitting bug I lean on the array safe-quoting fix prompt to get a second opinion on the rewrite.
A representative exchange when I asked an assistant to convert a fragile loop:
Prompt: Rewrite this safely:
for f in $(find . -name '*.log'); do gzip "$f"; doneResponse: Use null-delimited input to survive spaces and newlines in paths:
mapfile -d '' -t logs < <(find . -name '*.log' -print0) for f in "${logs[@]}"; do gzip -- "$f" doneNote: piping
find | while readwould also work, butmapfilekeeps the full list in memory so you can report a count or sort it before acting.
That draft is correct, but I still verified it. The rule on every one of these is the same: AI drafts, human verifies. I ran the rewrite against a throwaway directory seeded with a file named weird name.log and a file named -rf.log to confirm both the spaces case and the leading-dash case behaved. Only then did it go near a real host.
A Quick Verification Harness
Before trusting any list-handling script, prove it survives hostile input. This little harness creates the nasty cases for you:
#!/usr/bin/env bash
set -euo pipefail
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
# seed adversarial filenames
touch -- "$tmp/normal.log" "$tmp/with space.log" "$tmp/-rf.log"
mapfile -d '' -t files < <(find "$tmp" -type f -print0)
printf 'Recovered %d files:\n' "${#files[@]}"
printf ' [%s]\n' "${files[@]}"
# expect 3 entries, each intact
[[ ${#files[@]} -eq 3 ]] && echo "PASS" || { echo "FAIL"; exit 1; }
If that prints PASS and shows three bracketed names with the spaces and dashes intact, your ingestion logic is sound.
Takeaways
mapfile -t should be your default for turning command output or file contents into a Bash array. Reach for process substitution (< <(...)) rather than a pipe so the array survives. When filenames are involved, go all the way to -print0 and mapfile -d ''. And treat AI as a fast way to draft the flag combinations you do not use often, while you own the part that matters: running it against deliberately ugly input before it touches anything real.
For more patterns in this vein, browse the rest of the Bash and Python automation guides.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.