use async_trait::async_trait;
use super::{Builtin, BuiltinSideEffect, Context};
use crate::error::Result;
use crate::interpreter::{ExecResult, is_internal_variable};
pub struct Read;
#[async_trait]
impl Builtin for Read {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let input = ctx.stdin.map(|s| s.to_string());
let mut raw_mode = false; let mut array_mode = false; let mut delimiter = None::<char>; let mut nchars = None::<usize>; let mut prompt = None::<String>; let mut var_args = Vec::new();
let mut args_iter = ctx.args.iter();
while let Some(arg) = args_iter.next() {
if arg.starts_with('-') && arg.len() > 1 {
let mut chars = arg[1..].chars();
while let Some(flag) = chars.next() {
match flag {
'r' => raw_mode = true,
'a' => array_mode = true,
'd' => {
let rest: String = chars.collect();
let delim_str = if rest.is_empty() {
args_iter.next().map(|s| s.as_str()).unwrap_or("")
} else {
&rest
};
delimiter = delim_str.chars().next();
break;
}
'n' => {
let rest: String = chars.collect();
let n_str = if rest.is_empty() {
args_iter.next().map(|s| s.as_str()).unwrap_or("0")
} else {
&rest
};
nchars = n_str.parse().ok();
break;
}
'p' => {
let rest: String = chars.collect();
prompt = Some(if rest.is_empty() {
args_iter.next().cloned().unwrap_or_default()
} else {
rest
});
break;
}
't' | 's' | 'u' | 'e' | 'i' => {
if matches!(flag, 't' | 'u') {
let rest: String = chars.collect();
if rest.is_empty() {
args_iter.next();
}
break;
}
}
_ => {}
}
}
} else {
var_args.push(arg.as_str());
}
}
let _ = prompt;
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);
}
};
let line = if let Some(n) = nchars {
input.chars().take(n).collect::<String>()
} else if let Some(delim) = delimiter {
input.split(delim).next().unwrap_or("").to_string()
} else if raw_mode {
input.lines().next().unwrap_or("").to_string()
} else {
let mut result = String::new();
for l in input.lines() {
if let Some(stripped) = l.strip_suffix('\\') {
result.push_str(stripped);
} else {
result.push_str(l);
break;
}
}
result
};
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() {
vec![&line]
} else {
let ifs_ws: Vec<char> = ifs.chars().filter(|c| " \t\n".contains(*c)).collect();
let ifs_non_ws: Vec<char> = ifs.chars().filter(|c| !" \t\n".contains(*c)).collect();
if ifs_non_ws.is_empty() {
line.split(|c: char| ifs.contains(c))
.filter(|s| !s.is_empty())
.collect()
} else {
let mut fields: Vec<&str> = line.split(|c: char| ifs_non_ws.contains(&c)).collect();
if !ifs_ws.is_empty() {
fields = fields
.into_iter()
.map(|f| f.trim_matches(|c: char| ifs_ws.contains(&c)))
.collect();
}
fields
}
};
if array_mode {
let arr_name = var_args.first().copied().unwrap_or("REPLY");
if is_internal_variable(arr_name) {
return Ok(ExecResult::ok(String::new()));
}
let mut result = ExecResult::ok(String::new());
result.side_effects.push(BuiltinSideEffect::SetArray {
name: arr_name.to_string(),
elements: words.iter().map(|w| w.to_string()).collect(),
});
return Ok(result);
}
let var_names: Vec<&str> = if var_args.is_empty() {
vec!["REPLY"]
} else {
var_args
};
let mut result = ExecResult::ok(String::new());
for (i, var_name) in var_names.iter().enumerate() {
if is_internal_variable(var_name) {
continue;
}
let value = if i == var_names.len() - 1 {
let remaining: Vec<&str> = words.iter().skip(i).copied().collect();
let ifs_non_ws: Vec<char> = ifs.chars().filter(|c| !" \t\n".contains(*c)).collect();
let join_sep = if !ifs_non_ws.is_empty() {
ifs_non_ws[0].to_string()
} else {
" ".to_string()
};
remaining.join(&join_sep)
} else if i < words.len() {
words[i].to_string()
} else {
String::new()
};
result.side_effects.push(BuiltinSideEffect::SetVariable {
name: var_name.to_string(),
value,
});
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use crate::fs::{FileSystem, InMemoryFs};
async fn setup() -> (Arc<InMemoryFs>, PathBuf, HashMap<String, String>) {
let fs = Arc::new(InMemoryFs::new());
let cwd = PathBuf::from("/home/user");
let variables = HashMap::new();
fs.mkdir(&cwd, true).await.unwrap();
(fs, cwd, variables)
}
fn extract_vars(result: &ExecResult) -> HashMap<String, String> {
let mut map = HashMap::new();
for effect in &result.side_effects {
if let BuiltinSideEffect::SetVariable { name, value } = effect {
map.insert(name.clone(), value.clone());
}
}
map
}
#[tokio::test]
async fn read_no_stdin_returns_error() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
}
#[tokio::test]
async fn read_into_reply() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("hello world"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("REPLY").unwrap(), "hello world");
}
#[tokio::test]
async fn read_into_named_var() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["MY_VAR".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("test_value"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("MY_VAR").unwrap(), "test_value");
}
#[tokio::test]
async fn read_multiple_vars() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
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 four"),
);
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 four");
}
#[tokio::test]
async fn read_more_vars_than_words() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
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"),
);
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(), "");
assert_eq!(vars.get("C").unwrap(), "");
}
#[tokio::test]
async fn read_raw_mode_preserves_backslash() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-r".to_string(), "LINE".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("hello\\world"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("LINE").unwrap(), "hello\\world");
}
#[tokio::test]
async fn read_without_raw_handles_line_continuation() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["LINE".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("hello\\\nworld"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("LINE").unwrap(), "helloworld");
}
#[tokio::test]
async fn read_n_chars() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-n".to_string(), "3".to_string(), "CHUNK".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("abcdefgh"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("CHUNK").unwrap(), "abc");
}
#[tokio::test]
async fn read_n_more_than_input() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-n".to_string(), "100".to_string(), "CHUNK".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("hi"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("CHUNK").unwrap(), "hi");
}
#[tokio::test]
async fn read_custom_delimiter() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-d".to_string(), ",".to_string(), "FIELD".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("first,second,third"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("FIELD").unwrap(), "first");
}
#[tokio::test]
async fn read_array_mode() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-a".to_string(), "ARR".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);
assert_eq!(result.side_effects.len(), 1);
match &result.side_effects[0] {
BuiltinSideEffect::SetArray { name, elements } => {
assert_eq!(name, "ARR");
assert_eq!(elements, &["one", "two", "three"]);
}
_ => panic!("Expected SetArray side effect"),
}
}
#[tokio::test]
async fn read_array_mode_default_name() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-a".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("a b"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.side_effects.len(), 1);
match &result.side_effects[0] {
BuiltinSideEffect::SetArray { name, .. } => assert_eq!(name, "REPLY"),
_ => panic!("Expected SetArray side effect"),
}
}
#[tokio::test]
async fn read_combined_r_flag() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-r".to_string(), "V".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("path\\to\\file"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("V").unwrap(), "path\\to\\file");
}
#[tokio::test]
async fn read_only_first_line() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["-r".to_string(), "LINE".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("first\nsecond\nthird"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("LINE").unwrap(), "first");
}
#[tokio::test]
async fn read_custom_ifs() {
let (fs, mut cwd, mut variables) = setup().await;
let mut env = HashMap::new();
env.insert("IFS".to_string(), ":".to_string());
let args = vec!["A".to_string(), "B".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("foo:bar:baz"),
);
let result = Read.execute(ctx).await.unwrap();
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");
}
#[tokio::test]
async fn read_custom_ifs_preserves_empty_fields() {
let (fs, mut cwd, mut variables) = setup().await;
let mut env = HashMap::new();
env.insert("IFS".to_string(), ":".to_string());
let args = vec![
"A".to_string(),
"B".to_string(),
"C".to_string(),
"D".to_string(),
];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("one::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(), "");
assert_eq!(vars.get("C").unwrap(), "three");
assert_eq!(vars.get("D").unwrap(), "");
}
#[tokio::test]
async fn read_empty_ifs_no_splitting() {
let (fs, mut cwd, mut variables) = setup().await;
let mut env = HashMap::new();
env.insert("IFS".to_string(), String::new());
let args = vec!["LINE".to_string()];
let ctx = Context::new_for_test(
&args,
&env,
&mut variables,
&mut cwd,
fs.clone(),
Some("no splitting here"),
);
let result = Read.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
let vars = extract_vars(&result);
assert_eq!(vars.get("LINE").unwrap(), "no splitting here");
}
#[tokio::test]
async fn read_ifs_from_shell_variables() {
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() {
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"),
}
}
}