Reading Streams Safely with read
Shell automation often fails at the boundary between bytes and records. The read builtin gives Bash controlled stream ingestion, but its defaults must be understood before you use it with whitespace, backslashes, filenames, or structured data.
Learning objectives
By the end of this lesson
- Use IFS= read -r for arbitrary lines.
- Split simple records with an explicit delimiter.
- Process arbitrary filenames with NUL delimiters.
- Read from dedicated descriptors and with timeouts.
- Recognize formats that need real parsers rather than shell splitting.
1. read assigns fields from an input record
printf 'api staging 3\n' |
while read -r service environment replicas; do
printf 'service=%s env=%s replicas=%s\n' "$service" "$environment" "$replicas"
doneThis is appropriate only when whitespace-separated fields are truly the data format.
2. IFS= read -r preserves a line
while IFS= read -r line; do
printf 'line=<%s>\n' "$line"
done < input.txtIFS=Disables normal whitespace trimming/splittingPreserves line whitespace-rDisables backslash escapingPreserves backslasheslineReceives the recordDelimiter itself is removedFor arbitrary line-oriented text, start with IFS= read -r line.
3. Set IFS when the format has a simple delimiter
record='api:staging:3'
IFS=: read -r service environment replicas <<<"$record"
printf '%s | %s | %s\n' "$service" "$environment" "$replicas"This works when the format guarantees that fields themselves cannot contain unescaped colons.
4. Complex formats need format-aware parsers
Real CSV can contain quoted commas and embedded newlines. JSON and YAML have nested syntax and escaping. Do not recreate those parsers with read, cut, or regular expressions.
Use jq for JSON, an appropriate yq implementation for YAML, and a CSV-aware parser for CSV.
5. The final variable receives remaining fields
line='INFO api deployment completed successfully'
read -r level service message <<<"$line"
printf 'level=%s\n' "$level"
printf 'service=%s\n' "$service"
printf 'message=%s\n' "$message"6. NUL is the safe filename-record delimiter
Unix filenames cannot contain the NUL byte, so NUL-delimited streams can represent arbitrary filenames without ambiguity.
find ./artifacts -type f -print0 |
while IFS= read -r -d '' file; do
printf 'file=%q\n' "$file"
doneNewline-delimited filename processing is not fully general. Pair find -print0 with a NUL-aware consumer.
7. Pipeline-fed read loops can lose variable changes
count=0
printf '%s\n' a b c |
while IFS= read -r line; do
((count += 1))
done
printf 'count=%d\n' "$count"The loop commonly runs in a subshell. Use process substitution when state must survive:
count=0
while IFS= read -r line; do
((count += 1))
done < <(printf '%s\n' a b c)
printf 'count=%d\n' "$count"8. read -u consumes a dedicated descriptor
exec 3< services.txt
while IFS= read -r -u 3 service; do
printf 'service=%s\n' "$service"
done
exec 3<&-This preserves stdin for another purpose and makes input ownership explicit.
9. read can wait with a timeout
if IFS= read -r -t 5 response; then
printf 'received=%s\n' "$response"
else
printf 'input timeout\n' >&2
fiTimeouts are useful for controlled interactive or pipe-based workflows. CI automation should generally avoid human prompts.
10. read -a assigns fields into an array
line='api staging 3 enabled'
read -r -a fields <<<"$line"
printf 'count=%d\n' "${#fields[@]}"
printf 'service=%s\n' "${fields[0]}"11. Handle a final unterminated line if your contract requires it
while IFS= read -r line || [[ -n $line ]]; do
printf '%s\n' "$line"
done < input.txt12. Hands-on lab: hostile filenames
mkdir -p "$HOME/devops-academy/bash/chapter05/lesson03/files"
cd "$HOME/devops-academy/bash/chapter05/lesson03"
printf 'a\n' > "files/normal.txt"
printf 'b\n' > "files/space name.txt"
printf 'c\n' > $'files/tab\tname.txt'
count=0
while IFS= read -r -d '' file; do
((count += 1))
printf 'path=%q bytes=%s\n' "$file" "$(wc -c < "$file")"
done < <(find files -type f -print0)
printf 'count=%d\n' "$count"Verification checklist
13. Knowledge check
Question 1. Why use IFS= read -r?
Question 2. What is the safest delimiter for arbitrary Unix filenames?
Question 3. Why can a pipeline-fed read loop lose variable assignments?
Question 4. Should read be used as a complete CSV parser?
14. Summary
Safe stream ingestion begins with a record contract. Use IFS= read -r for lines, explicit delimiters for simple records, NUL for filenames, and a real parser for structured formats.
15. Further reading
- GNU Bash Reference Manual —
read. - GNU Bash Reference Manual — Word Splitting.
- GNU findutils manual —
-print0. - ShellCheck documentation — safe read loops.
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.