Linux Basic — Reading Files in Bash
Reading Files
There is exactly one correct way to read a file line-by-line in Bash, and several wrong ones that look right but silently break on whitespace, globs, or empty lines. This page covers the right way, the wrong way, and a few helpful variants.
The Correct Pattern — while IFS= read -r
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hostname
# Line: ubuntu-host
Every word in that command matters:
-
while ... do ... done— loop untilreadreturns non-zero (EOF) -
IFS=— clears the field separator so leading/trailing whitespace is preserved -
read -r—-rdisables backslash interpretation (always use it) -
line— the variable that receives each line -
< /etc/hostname— redirect file to loop’s stdin
Why for line in $(cat file) Is Broken
# Sample file
cat > demo.txt <<'EOF'
hello world
leading spaces
file with *
empty next line
last line
EOF
# ❌ The broken pattern
for line in $(cat demo.txt); do
echo "Got: [$line]"
done
# Got: [hello]
# Got: [world]
# Got: [leading]
# Got: [spaces]
# Got: [file]
# Got: [with]
# Got: [01-reading-files.md] ← * expanded to a real filename!
# Got: [empty]
# Got: [next]
# Got: [line]
# Got: [last]
# Got: [line]
Three failures at once:
- Word-splitting — every whitespace splits a line into multiple “lines”
-
Glob expansion —
*matched an actual file in cwd - Empty lines lost — there’s no record of the blank line
# ✅ Correct: same file, expected output
while IFS= read -r line; do
echo "Got: [$line]"
done < demo.txt
# Got: [hello world]
# Got: [ leading spaces]
# Got: [file with *]
# Got: []
# Got: [last line]
Slurping a Whole File into an Array — mapfile
For files small enough to fit in memory, Bash 4+ has mapfile (alias readarray):
mapfile -t lines < /etc/passwd
echo "${#lines[@]} lines read"
# 32 lines read
echo "First line: ${lines[0]}"
# First line: root:x:0:0:root:/root:/bin/bash
echo "Last line: ${lines[-1]}"
# Last line: ubuntu:x:1000:1000::/home/ubuntu:/bin/bash
-
-tstrips the trailing newline from each element - Each line becomes one array element — indices, length, slicing all work
When to prefer mapfile:
- You need to revisit lines (count, jump to line N, slice ranges)
- The file is small (everything is held in memory)
- You want to count lines without spawning
wc
When to prefer while read:
- The file is large (streamed — constant memory)
- You only need each line once, in order
Reading From a Pipeline (Carefully)
# ❌ Variables set inside the loop are LOST after it ends
count=0
grep -c '^' /etc/passwd | while read n; do
count=$n
done
echo "count=$count"
# count=0 ← the loop ran in a subshell
# ✅ Use process substitution to keep the loop in the current shell
count=0
while IFS= read -r line; do
((count++))
done < <(grep '^' /etc/passwd)
echo "count=$count"
# count=32
while ... done < <(cmd) runs the loop in the current shell — variable assignments persist.
Reading From Standard Input
read -p "Enter your name: " name
echo "Hello, $name!"
# Enter your name: Ada
# Hello, Ada!
Useful flags:
| Flag | Effect |
|---|---|
-p "prompt " |
Display a prompt |
-s |
Silent (no echo) — for passwords |
-n N |
Read exactly N characters (no Enter needed) |
-t SECS |
Timeout in seconds |
-r |
Raw (don’t interpret backslashes) — always use |
-a arr |
Read words into array arr
|
-d DELIM |
Use DELIM as line terminator (e.g. '' for null) |
# Password prompt
read -s -p "Password: " pw; echo
echo "Password is ${#pw} characters long."
# Password: ********
# Password is 8 characters long.
# Read a single keypress (no Enter)
read -n 1 -p "Continue? (y/n) " ans; echo
Reading Null-Delimited Records — Filenames With Newlines
Yes, filenames on Linux can contain newlines. Treat external file lists with -print0 and -d '':
find . -type f -print0 | while IFS= read -r -d '' file; do
echo "Got: $file"
done
-print0 and -d '' use a NUL byte as a separator — the only byte that cannot appear in a filename — so this loop is impossible to break.
Hard rule: Any time a script accepts a list of filenames from outside (find, user input, a config file), assume the filenames contain spaces, newlines, glob chars, and even leading dashes. Use -print0 + read -d '' or mapfile -d '' to be safe.
Reading Specific Lines
# Line 5 only
sed -n '5p' /etc/passwd
# Lines 5–10
sed -n '5,10p' /etc/passwd
# First 3
head -n 3 /etc/passwd
# Last 3
tail -n 3 /etc/passwd
# Random line
shuf -n 1 /etc/passwd
Common Mistakes Recap
| ❌ Wrong | ✅ Right | Why |
|---|---|---|
for line in $(cat f) |
while IFS= read -r line; do … done < f |
word-split + glob bugs |
read line |
IFS= read -r line |
loses whitespace, eats backslashes |
cat f \| while read |
while read; do …; done < <(cmd) or done < f
|
subshell loses variable changes |
find . \| while read f |
find . -print0 \| while IFS= read -r -d '' f |
newlines in filenames break it |
ls file*.log to iterate |
for f in file*.log; do (with shopt -s nullglob) |
ls output is for humans, not scripts |
Next → Writing Files