From 3e836889b6aff94a06c9e6f977f70d78131a78ea Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 2 Apr 2026 06:17:09 +0000 Subject: [PATCH] fix(builtins): clear variable on read at EOF with no remaining data When read reaches EOF and there is no partial line data remaining, clear the target variable to empty string before returning 1. This prevents the common `while read line || [[ -n "$line" ]]` pattern from looping infinitely on the stale last value. Closes #953 --- crates/bashkit/src/builtins/read.rs | 30 ++++++++++++++++--- .../spec_cases/bash/read-builtin.test.sh | 30 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/crates/bashkit/src/builtins/read.rs b/crates/bashkit/src/builtins/read.rs index a346a57e..aac0f5a2 100644 --- a/crates/bashkit/src/builtins/read.rs +++ b/crates/bashkit/src/builtins/read.rs @@ -13,10 +13,7 @@ pub struct Read; impl Builtin for Read { async fn execute(&self, ctx: Context<'_>) -> Result { // Get the input to read from stdin - let input = match ctx.stdin { - Some(s) => s.to_string(), - None => return Ok(ExecResult::err("", 1)), - }; + let input = ctx.stdin.map(|s| s.to_string()); // Parse flags let mut raw_mode = false; // -r: don't interpret backslashes @@ -82,6 +79,31 @@ impl Builtin for Read { } let _ = prompt; // prompt is for interactive use, ignored in non-interactive + // EOF with no data: clear all target variables to empty and return 1. + // This prevents the common `while read line || [[ -n "$line" ]]` + // pattern from looping infinitely on the stale last value. + let input = match input { + Some(s) => s, + None => { + let var_names: Vec<&str> = if var_args.is_empty() { + vec!["REPLY"] + } else { + var_args + }; + let mut result = ExecResult::err("", 1); + for var_name in &var_names { + if is_internal_variable(var_name) { + continue; + } + result.side_effects.push(BuiltinSideEffect::SetVariable { + name: var_name.to_string(), + value: String::new(), + }); + } + return Ok(result); + } + }; + // Extract input based on delimiter or nchars let line = if let Some(n) = nchars { // -n N: read at most N chars diff --git a/crates/bashkit/tests/spec_cases/bash/read-builtin.test.sh b/crates/bashkit/tests/spec_cases/bash/read-builtin.test.sh index cdcff272..79bf67d2 100644 --- a/crates/bashkit/tests/spec_cases/bash/read-builtin.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/read-builtin.test.sh @@ -82,3 +82,33 @@ echo "$var" ### expect hel ### end + +### read_eof_clears_variable +# read at EOF with no data should clear the variable +printf "one\ntwo" | { + count=0 + while IFS= read -r line || [[ -n "$line" ]]; do + echo "$line" + count=$((count + 1)) + [[ $count -gt 5 ]] && break + done +} +### expect +one +two +### end + +### read_eof_partial_line +# read returns 1 but captures partial line without trailing newline +printf "complete\npartial" | { + lines=() + while IFS= read -r line || [[ -n "$line" ]]; do + lines+=("$line") + [[ ${#lines[@]} -gt 5 ]] && break + done + printf '%s\n' "${lines[@]}" +} +### expect +complete +partial +### end