Skip to content

Read a File Line by Line in Bash

bashreadloopsfilesintermediate
3 min read

Quick Answer

The correct way to read a file line by line in bash is while IFS= read -r line; do ... done < file.txt. Three parts matter. IFS= (empty) stops bash from trimming leading and trailing whitespace from each line. The -r flag stops read from treating backslashes as escape characters, so a path like C:\\temp survives intact. And redirecting the file in with < at the done keyword feeds the loop without spawning a subshell, so variables you set inside the loop are still set after it. The trap that bites everyone: if the file's last line has no trailing newline, read returns false on that final line and the loop skips it. Guard it with while IFS= read -r line || [[ -n "$line" ]]; do, which processes the leftover line when read hits end-of-file mid-line. Never loop for line in $(cat file) — that word-splits on spaces and reads words, not lines.

Reading a file is usually grep or awk instead

The text-processing guide shows when a while read loop is the wrong tool — most line-by-line work is a grep filter or an awk field extraction, and both are faster and harder to get wrong. Full guide: Bash Text Processing: find, grep, sed, and awk.

The loop that skips the last line

A plain while read loop has a failure mode built for host lists and config files: when the last line of the file has no trailing newline — and plenty of editors, generators, and printf calls write files that way — read returns false on that final line and the loop exits one line early, every single run. A monitoring script that pings every host in a list silently skips the last host, which is the one somebody most recently appended. The list looks complete. The loop quietly reads all of it except the part that matters.

The bug is not in the command inside the loop. It is reading lines without guarding the no-trailing-newline case, and using a bare read that mangles input besides.

The correct read loop

bash
#!/bin/bash # Script: check-hosts.sh # Purpose: ping every host in a list, including a newline-less last line # Usage: ./check-hosts.sh hosts.txt set -euo pipefail CHECK="✓" CROSS="✗" HOST_FILE="${1:?Usage: check-hosts.sh <host-file>}" while IFS= read -r host || [[ -n "$host" ]]; do [[ -z "$host" || "$host" == \#* ]] && continue # skip blanks and comments if ping -c1 -W2 "$host" >/dev/null 2>&1; then echo "$CHECK up: $host" else echo "$CROSS down: $host" fi done < "$HOST_FILE"

Every piece earns its place. IFS= keeps a host like app-07 from being silently trimmed into something else. -r keeps any backslash in the data literal. The || [[ -n "$host" ]] is the fix for the newline-less last line: when read hits end-of-file with a partial line still in $host, it returns false, but the || clause sees the non-empty buffer and runs the loop body one last time. Redirecting with done < "$HOST_FILE" keeps the loop in the current shell — which matters the moment you want to count failures.

Why not pipe into the loop

bash
# WRONG — the count is always 0 fails=0 cat hosts.txt | while IFS= read -r host; do ping -c1 "$host" >/dev/null 2>&1 || ((fails++)) done echo "failures: $fails" # prints 0, always

The pipe runs the while in a subshell. fails increments inside that subshell and dies with it, so the parent shell still sees 0. Redirect the file (done < hosts.txt) and the loop runs in the current shell, so the count survives. The other while-loop traps (commands that eat the loop's stdin, set -e with (( i++ )), polling without a deadline) are on Bash While Loop Examples.

Reading fields from each line

bash
# Split each line on a delimiter into named variables while IFS=, read -r name ip role; do echo "$CHECK $name ($role) -> $ip" done < servers.csv

Setting IFS=, for the read splits each line on commas into name, ip, and role in one step — the clean way to parse a simple CSV without reaching for cut.

Glob over parse, quote on use, and guard the last line. For iterating a known list of files rather than a file's contents, reach for a for loop instead, and wrap anything that acts on what it reads in set -euo pipefail.

Raw script, MIT licensed: scripts/bash-read-file-line-by-line.sh on GitHub

PAID RESOURCE — $9

The Production Bash Toolkit

An operational script system + a 30-function shared library + a 52-page field guide. The production layer the free snippets don't cover.

Get the Toolkit →
curl -O bashlib-starter.sh

Get the bashlib starter

Ten functions I source into every script on my own boxes — strict-mode setup, an ERR trap that names the failing line, lock and timeout wrappers, and cleanup that runs on every exit path. After that, a short email only when something breaks on a real box and I've worked out why, about twice a month. One-click unsubscribe.

BashSnippets logo

Written by Travis

Creator of BashSnippets.xyz

bashsnippets.xyz/about

Related Snippets

Frequently Asked Questions

faq — snippet

Why does my bash loop skip the last line of the file?

Because the last line has no trailing newline, so read returns a non-zero status on it and the while loop exits before processing it. Add the guard: while IFS= read -r line || [[ -n "$line" ]]; do. The || clause runs the loop body one more time when read hit end-of-file with a partial line still buffered.

faq — snippet

What does IFS= and -r do in read?

IFS= sets the field separator to empty for that command, which prevents read from stripping leading and trailing whitespace. -r prevents read from interpreting backslashes as escapes, so backslashes in the data are preserved literally. Together they read the line exactly as written.

faq — snippet

Why are my variables empty after the while loop?

You probably piped into the loop — cat file | while read line. The pipe runs the loop in a subshell, so variables set inside it vanish when the subshell exits. Redirect the file instead with done < file.txt, which keeps the loop in the current shell.

faq — snippet

Can I read a file line by line with a for loop?

No — for line in $(cat file) word-splits on spaces and tabs, so it iterates words, not lines, and a line with spaces becomes several iterations. A while IFS= read -r loop is the only form that reads one full line at a time.