Linux Basic — Reading Files in Bash

4 minute read

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 until read returns non-zero (EOF)
  • IFS= — clears the field separator so leading/trailing whitespace is preserved
  • read -r — -r disables 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:

  1. Word-splitting — every whitespace splits a line into multiple “lines”
  2. Glob expansion — * matched an actual file in cwd
  3. 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
  • -t strips 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

Updated: