Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions crates/bashkit/src/builtins/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,13 @@ impl Builtin for Read {
// Split line by IFS (default: space, tab, newline)
// IFS whitespace chars (space, tab, newline) collapse runs and trim.
// Non-whitespace IFS chars preserve empty fields between consecutive delimiters.
let ifs = ctx.env.get("IFS").map(|s| s.as_str()).unwrap_or(" \t\n");
// Check shell variables first (IFS=","), then env, then default.
let ifs = ctx
.variables
.get("IFS")
.or_else(|| ctx.env.get("IFS"))
.map(|s| s.as_str())
.unwrap_or(" \t\n");
let words: Vec<&str> = if ifs.is_empty() {
// Empty IFS means no word splitting
vec![&line]
Expand Down Expand Up @@ -188,9 +194,17 @@ impl Builtin for Read {
continue;
}
let value = if i == var_names.len() - 1 {
// Last variable gets all remaining words
// Last variable gets all remaining words joined by first IFS char
let remaining: Vec<&str> = words.iter().skip(i).copied().collect();
remaining.join(" ")
let ifs_non_ws: Vec<char> = ifs.chars().filter(|c| !" \t\n".contains(*c)).collect();
let join_sep = if !ifs_non_ws.is_empty() {
// Non-whitespace IFS: join with the first non-whitespace IFS char
ifs_non_ws[0].to_string()
} else {
// Whitespace-only IFS: join with space
" ".to_string()
};
remaining.join(&join_sep)
} else if i < words.len() {
words[i].to_string()
} else {
Expand Down Expand Up @@ -548,7 +562,7 @@ mod tests {
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("A").unwrap(), "foo");
assert_eq!(vars.get("B").unwrap(), "bar baz");
assert_eq!(vars.get("B").unwrap(), "bar:baz");
}

#[tokio::test]
Expand Down Expand Up @@ -598,4 +612,53 @@ mod tests {
let vars = extract_vars(&result);
assert_eq!(vars.get("LINE").unwrap(), "no splitting here");
}

#[tokio::test]
async fn read_ifs_from_shell_variables() {
// IFS set as a shell variable (not env) — the common case (IFS=",")
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
variables.insert("IFS".to_string(), ",".to_string());
let args = vec!["A".to_string(), "B".to_string(), "C".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("one,two,three"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("A").unwrap(), "one");
assert_eq!(vars.get("B").unwrap(), "two");
assert_eq!(vars.get("C").unwrap(), "three");
}

#[tokio::test]
async fn read_ifs_from_shell_variables_array() {
// IFS=: with read -ra should split into array
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
variables.insert("IFS".to_string(), ":".to_string());
let args = vec!["-ra".to_string(), "parts".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("a:b:c"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
match &result.side_effects[0] {
BuiltinSideEffect::SetArray { name, elements } => {
assert_eq!(name, "parts");
assert_eq!(elements, &["a", "b", "c"]);
}
_ => panic!("Expected SetArray side effect"),
}
}
}
14 changes: 14 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/read-builtin.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,17 @@ printf "complete\npartial" | {
complete
partial
### end

### read_custom_ifs_comma
# read should split on custom IFS
IFS=","; read -r a b c <<< "one,two,three"; echo "$a|$b|$c"
### expect
one|two|three
### end

### read_custom_ifs_colon
# read -ra should split into array on custom IFS
IFS=":"; read -ra parts <<< "a:b:c"; echo "${#parts[@]} ${parts[1]}"
### expect
3 b
### end
2 changes: 1 addition & 1 deletion supply-chain/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1799,7 +1799,7 @@ version = "0.244.0"
criteria = "safe-to-deploy"

[[exemptions.writeable]]
version = "0.6.2"
version = "0.6.3"
criteria = "safe-to-deploy"

[[exemptions.yansi]]
Expand Down
Loading