Chapter 05Lesson 03~70 minutes

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.

BeginnerLoops & functionsHands-on lab

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"
done

This 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.txt
ConceptMeaningOperational note
IFS=Disables normal whitespace trimming/splittingPreserves line whitespace
-rDisables backslash escapingPreserves backslashes
lineReceives the recordDelimiter itself is removed
Canonical line reader

For 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 the right parser

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"
done
Filename safety

Newline-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
fi

Timeouts 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.txt

12. 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.
Next lesson

Functions, Local Variables, and Return Status

Continue Chapter 5 by building the next layer of reusable shell logic.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.