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