Bash Read Skips the Last Line: Preserve a Partial Record at EOF
By ON Lab · Sources checked November 8, 2026
A Bash read can assign a final partial line and still return a failure status because it reached end-of-file before a newline. A loop testing only that status then skips the partial line. If your input contract allows an unterminated final record, test for assigned nonempty content as well:
while IFS= read -r line || [ -n "$line" ]; do
printf '<%s>
' "$line"
done < input.txtReproduce the missing record
Use a disposable text file, not a production job list. This fixture contains two records but no final newline:
printf '%s
%s' 'alpha' 'beta' > input.txt
while IFS= read -r line; do
printf '<%s>
' "$line"
done < input.txtOn our Bash 3.2.57 fixture, this printed only <alpha>. The condition’s final read had received beta, but the unsuccessful status prevented the body from running. Adding the nonempty-content condition printed both records. This is a status-and-data boundary, not evidence that the file’s bytes disappeared.
Keep spaces and backslashes intact
IFS= disables field splitting for this read invocation. -r prevents read from treating backslashes as escape characters. Quoting "$line" in the loop body prevents later shell splitting and wildcard expansion. These choices address independent transformations; keeping the final record does not automatically preserve its spaces or backslashes.
The loop also preserves a newline-terminated blank record. It does not invent one after the final terminator. Use a fixture matrix rather than deciding from one visually blank editor line:
| Bytes represented as text | Plain loop | Loop with partial-line check |
|---|---|---|
alpha\nbeta | alpha | alpha, beta |
alpha\nbeta\n | alpha, beta | alpha, beta |
| Empty file | No records | No records |
| One LF byte | One empty record | One empty record |
We ran all four cases under the same shell. In particular, the extra condition did not create an additional empty record for an empty file or a newline-terminated two-record file.
Scope the recovery condition
This pattern fits ordinary newline-delimited text read from a regular file. A failed read can also involve an invalid descriptor, a timeout or another input error. Do not automatically treat every unsuccessful read in a more complex application as clean EOF. If your script uses timeouts or nonblocking input, give those states a separate error path.
Bash variables cannot preserve NUL bytes. Filename streams also need special care because filenames can contain newlines. For arbitrary filenames, use the separate NUL-delimited filename guide, not this text-line loop. Changing the record separator changes the contract.
Avoid unrelated loop surprises
The redirection after done supplies the file directly. Piping into a loop may put it in a subshell and change whether variable updates remain visible afterward; that is the separate pipeline variable-scope problem. A missing final input record and a counter lost after the loop are different symptoms.
Commands inside the body can also consume the same standard input. If a tool unexpectedly drains your job list, give it the intended input source or redirect its input separately. The partial-line check does not prevent another command from reading records first. Keep the loop body as the simple printf while verifying ingestion, then add real work one step at a time.
Verify before using the records
Compare the emitted records with the exact synthetic input, including a blank line and a backslash-containing value. If the source format requires a final newline, fixing the producer may be the right long-term action; the consumer can still reject malformed input explicitly. If partial final records are valid, document that choice and keep the four-case regression check.
The original public question and GNU’s read documentation are free alternatives. This guide adds the observed matrix and distinguishes text EOF handling from filename framing, subshell scope and standard-input consumption. It does not claim every read failure should be ignored.
Method: current GNU Bash documentation, an inspected historical public user question, and four synthetic cases run with macOS Bash 3.2.57. The examples were not tested under Windows command shells, read timeouts, arbitrary binary input or external command pipelines.