Skip to content

fix(read): assign the variables at end of input, and never a readonly one - #156

Merged
LESdylan merged 1 commit into
developfrom
fix/read-eof-assigns
Sep 26, 2026
Merged

LESdylan merged 1 commit into
developfrom
fix/read-eof-assigns

Conversation

@LESdylan

Copy link
Copy Markdown
Member

What & why

This is the loop that also reads a last line with no newline:

while IFS= read -r line || [ -n "$line" ]; do ...; done

On hellish it never ended. At end of input, with nothing read, read returned 1 without assigning anything. $line still held the last line, so the || kept the loop going. bash assigns the variables empty (and empties an -a array) whenever read returns, EOF included. The hellishrc_plugins envload/envdiff had to work around this with an explicit line= before each read.

Root cause. read_one_line returns NULL for "EOF, nothing read", and builtin_read returned on NULL before rd_dispatch. It now dispatches an empty line, so every variable is assigned. The status is still 1.

A second bug this made reachable. read never checked readonly: readonly r=keep; echo x | read r changed r. rd_set_var now refuses a readonly name with bash's r: readonly variable. The statuses are bash's:

  • a readonly variable before the last one: 2, and the variables after it are not assigned;
  • the last one: 1;
  • REPLY: 2;
  • an -a array: 1.

read is not a special builtin, so the shell goes on.

How I verified it

  • Reproduced first. tests/scripts/58_read_eof_readonly.sh is graded against bash --posix 5.3.9. It covers:

    • EOF with nothing read, for named variables, REPLY, -a, -d, -n and -N;
    • a last line without a newline, and the || [ -n "$line" ] loop (guarded, so a regression fails instead of hanging);
    • readonly in each position, REPLY, -a, and the messages.

    develop diverges on 16 of its 26 lines, and without the guard it loops forever. This branch matches bash byte for byte.

  • Local gates on this commit (ASan debug build):

    • golden tests/tester: 5363/5363;
    • tests/run_scripts.sh against bash --posix: 125/125;
    • verify_alloc.sh: identical output on both heaps;
    • alloc_stress.sh: all clean;
    • tests/pty_suite.sh: 110 ok, 6 skipped, 3 failed. The three are prompt_compat_matrix, prompt_drift_matrix and prompt_jobs_badge. They expect the non-root %/$ prompt and get #, because the container runs as root; they fail the same way on develop, and CI runs them as a normal user.
  • norminette is OK on every touched file.

Notes / trade-offs

  • Found while writing the hellishrc_plugins test suites, not from an issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG


Generated by Claude Code

… one

    while IFS= read -r line || [ -n "$line" ]; do ...; done

is the loop that also reads a last line with no newline. On hellish it
never ended. At EOF with nothing read, builtin_read returned 1 without
assigning anything, so $line still held the last line and the `||`
kept the loop going. bash assigns the variables empty (and empties an
-a array) whenever read returns, EOF included. The plugins' envload and
envdiff had to work around this with an explicit `line=`.

Root cause: read_one_line returns NULL for "EOF, nothing read", and
builtin_read returned on NULL before rd_dispatch. It now dispatches an
empty line, so every variable is assigned, and still returns 1.

Assigning at EOF made a second bug reachable on a new path: read never
checked readonly. `readonly r=keep; echo x | read r` changed r. rd_set_var
now refuses a readonly name with bash's "r: readonly variable" message.
The statuses are bash's:
  - a readonly variable before the last one: 2, and the ones after it
    are not assigned;
  - the last one: 1;
  - REPLY: 2; an -a array: 1.
read is not a special builtin, so the shell goes on.

tests/scripts/58_read_eof_readonly.sh, graded against bash --posix:
  - EOF with nothing read, for named variables, REPLY, -a, -d, -n and -N;
  - a last line without a newline, and the `|| [ -n "$line" ]` loop
    (guarded, so a regression fails instead of hanging);
  - readonly in each position, REPLY, -a, and the messages.
develop diverges on 16 of its 26 lines and loops without the guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAmeHfJNm7XjYbMNrQqvkG
@LESdylan
LESdylan merged commit 5720a95 into develop Sep 26, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants