Skip to content

Read a File Line by Line in Bash

bashreadloopsfilesintermediate
2 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.

The monitoring loop that never checked the last server

We kept a plaintext list of hosts, one per line, and a script read it and pinged each one. app-07 was the last line. app-07 went down on a Sunday, and the alert never fired. The loop had been skipping the last line of the file for months, and app-07 had only recently been appended — by an editor that did not add a trailing newline. while read host returned false on that final, newline-less line and the loop exited one host early, every single run. The list looked complete. The loop quietly read all of it except the part that mattered.

The bug was not ping. It was 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 app-07: 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.

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.

BashSnippets logo

Written by Anguishe

Creator of BashSnippets.xyz

bashsnippets.xyz/about

Run this script on a real Linux server

Get $200 free credit — DigitalOcean

Get $200 Free →

Affiliate link · we earn a commission

Need a domain for your next project?

Register with Namecheap — free WHOIS privacy included

Check Domain Prices →

Affiliate link · we earn a commission

PAID RESOURCE — $9

The Production Bash Toolkit

6 scripts + shared library + 52-page field guide. The production layer the free snippets don't cover.

Get the Toolkit →

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.