Read a File Line by Line in Bash

Tested on RHEL 10.2 (Coughlan)
Package bash 5.2.26-6.el10
Applies to Linux distributions with Bash installed
Privilege Normal user; examples create files in the current directory
Scope Streaming line reads with while IFS= read -r, input redirection, final-line handling, colon-delimited fields per line, process substitution for command output, pipeline subshell pitfalls, here strings, and a brief mapfile comparison. Does not cover interactive read options, full IFS semantics, or a complete mapfile / readarray reference.
Related guides Bash while loop
Shell scripting course hub
Short on time?Beta Get the gist of this 9-minute read in seconds, then ask follow-up questions

The robust Bash pattern for reading a text file line by line is while IFS= read -r line; do ...; done < "$file". It streams one line at a time without loading the whole file into memory and without putting the loop in a pipeline subshell.

That form exists because, with the default IFS, read removes leading and trailing IFS whitespace when reading into a single variable, interprets backslashes unless you pass -r, and returns a non-zero status at EOF even when the final record has no trailing newline. The examples below show why each part matters, including whitespace preservation, missing final newlines, and pipeline subshell behavior.


Read a File Line by Line with while IFS= read -r

Create a small sample file with three colon-separated records:

bash
cat > sample.txt <<'EOF'
alpha:first value
beta:second value
gamma:third value
EOF

Read that file one line at a time with the canonical loop:

bash
while IFS= read -r line; do
  printf 'Line: %s\n' "$line"
done < sample.txt
output
Line: alpha:first value
Line: beta:second value
Line: gamma:third value

Each printf call prints one full input line. Input redirection (< sample.txt) feeds the loop from the file instead of from a pipeline.

The loop has four pieces that work together:

Piece Role
while Keeps running the body while the read command succeeds
IFS= Sets IFS to empty for this read so leading and trailing IFS whitespace is preserved
read -r Reads one line and keeps backslashes literal
< "$file" Redirects the file into the loop's stdin without a pipe

read returns 0 when it reaches its delimiter (a newline by default). If EOF arrives after some characters but before a final newline, read still stores those characters in line but returns non-zero. At EOF with no data left, it returns non-zero and the loop ends.

This article focuses on file streaming. The Bash read command lesson covers interactive prompts, timeouts, and reading from the terminal.


Preserve Whitespace and Backslashes

Create a small config file with ASCII indentation and Windows-style path separators:

bash
printf '%s\n' $'  indented line' > ws.conf

Append a line that contains literal backslashes:

bash
printf '%s\n' 'path=C:\data\logs' >> ws.conf

Read it with IFS= and -r:

bash
while IFS= read -r line; do
  printf 'RAW|%s|END\n' "$line"
done < ws.conf
output
RAW|  indented line|END
RAW|path=C:\data\logs|END

Leading spaces and backslashes survived because IFS= stopped trimming and -r disabled backslash escaping.

Drop IFS= and the leading spaces disappear:

bash
while read -r line; do
  printf 'TRIM|%s|END\n' "$line"
done < ws.conf
output
TRIM|indented line|END
TRIM|path=C:\data\logs|END

Create a line with backslashes using a %s format so printf does not interpret escape sequences in the format string:

bash
printf '%s\n' 'C:\temp\file' > bs.txt

Read that line without -r so backslashes act as escapes:

bash
while IFS= read line; do
  printf 'NOR|%s|END\n' "$line"
done < bs.txt
output
NOR|C:tempfile|END

Use printf '%s\n' "$line" to print the line without interpreting backslashes or option-like text. See Bash echo vs printf for why echo is a poor fit for arbitrary line data.


Handle a Final Line Without a Newline

POSIX text files usually end with a newline, but generators and copy operations sometimes omit it on the last record.

Create a file whose final line has no newline character:

bash
printf 'line one\nline two\nfinal without newline' > no-nl.txt

A naive loop stops before that last record:

bash
while IFS= read -r line; do
  printf 'NAIVE>%s<\n' "$line"
done < no-nl.txt
output
NAIVE>line one<
NAIVE>line two<

The robust guard processes the final partial line even when read returns non-zero at EOF:

bash
while IFS= read -r line || [[ -n $line ]]; do
  printf 'ROBUST>%s<\n' "$line"
done < no-nl.txt
output
ROBUST>line one<
ROBUST>line two<
ROBUST>final without newline<

read still returns non-zero after the last chunk, but [[ -n $line ]] is true when that chunk holds data. Use this pattern when your input may lack a trailing newline; plain files that always end with \n work fine without the guard.


Split Each Line into Fields

Set IFS on the same read that consumes the file when each line is already one record with internal delimiters.

Create a passwd-style row in a file:

bash
cat > users.txt <<'EOF'
alice:x:1000:1000::/home/alice:/bin/bash
EOF

Read colon-separated fields directly into variables:

bash
while IFS=: read -r user _ uid gid _ home shell; do
  printf 'user=%s uid=%s home=%s shell=%s\n' "$user" "$uid" "$home" "$shell"
done < users.txt
output
user=alice uid=1000 home=/home/alice shell=/bin/bash

Setting IFS=: for this read splits each input line at colons and assigns the fields to separate variables. Full IFS rules and word-splitting order belong in a dedicated field-splitting lesson.


Read Command Output Without Losing Variables

Piping into while runs the loop body in a subshell in Bash's default pipeline model. Assignments made inside the loop do not survive.

Demonstrate with a three-line file:

bash
printf 'a\nb\nc\n' > count.txt

Pipe cat into the loop and increment a counter:

bash
count=0
cat count.txt | while IFS= read -r line; do
  ((count++))
done
printf 'After pipe: count=%d\n' "$count"
output
After pipe: count=0

The loop ran three times in a subshell, but the parent shell's count never changed. Redirect from the file instead:

bash
count=0
while IFS= read -r line; do
  ((count++))
done < count.txt
printf 'After redirect: count=%d\n' "$count"
output
After redirect: count=3

When the source is command output rather than a file, use process substitution so the loop still reads from a redirection:

bash
count=0
while IFS= read -r line; do
  ((count++))
done < <(printf 'alpha\nbeta\ngamma\n')
printf 'Lines read: %d\n' "$count"
output
Lines read: 3

< <(command) feeds the while loop from a temporary file descriptor. The loop runs in the current shell, so count keeps its value after the loop ends. shopt -s lastpipe can run the last pipeline stage in the current shell, but redirection or process substitution is the clearer fix for scripts you hand to beginners.

Read a variable line by line

A here string feeds multiline text from a variable without a temporary file:

bash
text=$'alpha\nbeta\ngamma'
while IFS= read -r line; do
  printf '%s\n' "$line"
done <<< "$text"
output
alpha
beta
gamma

A here string adds a trailing newline, so use it for string or variable input—not when you need to preserve the source's exact final-newline state.


while read vs mapfile

while IFS= read -r streams the file: only one line sits in memory at a time. That suits log processing, large inputs, and filters where you handle each line immediately.

mapfile (also called readarray) loads every line into an array in one step. That is convenient when you need random access, line counts up front, or repeated passes over the same data, but it uses more memory on large files.

Pick streaming when the file may be large or you only need a single forward pass. Pick mapfile when you deliberately want an array of lines and the size is bounded. This article stays with streaming; see Bash mapfile for -t, -d, and other array-loading options.


Why for line in $(cat file) Is Wrong

A Bash for loop walks words, not lines. Unquoted command substitution is subject to word splitting, so lines containing spaces or tabs become multiple loop iterations.

Create a file with spaces in each record:

bash
printf 'a file\nb file\n' > spaced.txt

Feed it through for and cat:

bash
for line in $(cat spaced.txt); do
  printf 'FOR>%s<\n' "$line"
done
output
FOR>a<
FOR>file<
FOR>b<
FOR>file<

Four words appeared instead of two lines. Use while IFS= read -r for line-oriented data; use for only when you already have separate words, glob matches, or array elements.


Practical Example: Process a Configuration File

This script prints each non-blank, non-comment line with its line number and preserves the original text:

bash
process_config() {
  local file=$1
  local line_no=0

  [[ -f "$file" ]] || {
    printf 'error: file not found: %s\n' "$file" >&2
    return 1
  }

  local blank_re=$'^[[:space:]]*$'
  local comment_re=$'^[[:space:]]*#'

  while IFS= read -r line || [[ -n $line ]]; do
    line_no=$((line_no + 1))
    [[ $line =~ $blank_re ]] && continue
    [[ $line =~ $comment_re ]] && continue
    printf 'L%d: %s\n' "$line_no" "$line"
  done < "$file"
}

Build a sample config with a comment, a blank line, two ASCII leading spaces, a backslash path, and no newline after the last line:

bash
{
  printf '%s\n' '# database settings'
  printf '\n'
  printf '%s\n' $'  key=value'
  printf '%s\n' 'path=C:\logs'
  printf '%s' 'last line without newline'
} > app.conf

Run the processor:

bash
process_config app.conf
output
L3:   key=value
L4: path=C:\logs
L5: last line without newline

The comment and blank line were skipped. Line 5 was still processed even without a trailing newline.


Common Problems

Symptom Likely cause Fix
Leading spaces vanish Default IFS trims whitespace on read IFS= before read
\ disappears from paths read without -r read -r
Last line missing EOF before newline; naive while read read -r line with `
Counter stays 0 after loop command | while read subshell Redirect with < file or < <(command)
Too many iterations for line in $(cat file) word splitting while IFS= read -r

Summary

Reading a file line by line in Bash means streaming with while IFS= read -r line; do ...; done < "$file". IFS= keeps leading and trailing whitespace on the line; -r keeps backslashes literal; input redirection avoids the pipeline subshell that drops variable updates.

When the last record may lack a newline, add || [[ -n $line ]] so the final chunk is still processed after read returns non-zero at EOF. Print lines with printf '%s\n' "$line" when you need faithful text output.

Do not parse files with for line in $(cat file)—that splits on whitespace, not newlines. Use while read for streaming and mapfile when you need the whole file in an array. Bash variables cannot represent embedded NUL bytes, so line-oriented read is for text records, not arbitrary binary data.


References


Frequently Asked Questions

Why use IFS= read -r?

IFS= stops read from stripping leading and trailing whitespace from each line. -r stops backslash escaping so paths like C:\logs stay literal. Together they preserve leading/trailing whitespace and literal backslashes in ordinary text lines.

How do I keep the last line when the file has no final newline?

Use while IFS= read -r line || [[ -n $line ]]; do ...; done < "$file". After EOF, read fails, but [[ -n $line ]] is still true when the last read stored data in line.

Why is a counter lost after a pipe?

command | while read runs the loop in a subshell. Assignments inside the loop do not update variables in the parent shell. Redirect from the file or use process substitution instead of piping into while.

When should I use while read instead of mapfile?

Use while read to stream large files one line at a time with low memory use. Use mapfile when you need all lines in an array at once and the file size is reasonable.

Can Bash safely read binary files line by line?

No. Bash variables cannot represent embedded NUL bytes, so while read is appropriate for text records, not arbitrary binary data.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)