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