use super::ExpandedField;
use crate::env::ShellEnv;
fn get_ifs(env: &ShellEnv) -> String {
match env.vars.get("IFS") {
Some(ifs) => ifs.to_string(),
None => " \t\n".to_string(),
}
}
pub fn split(env: &ShellEnv, fields: Vec<ExpandedField>) -> Vec<ExpandedField> {
let ifs = get_ifs(env);
if ifs.is_empty() {
return fields
.into_iter()
.filter(|f| !f.value.is_empty() || f.was_quoted)
.collect();
}
let ifs_ws: Vec<u8> = ifs
.bytes()
.filter(|b| matches!(*b, b' ' | b'\t' | b'\n'))
.collect();
let ifs_nws: Vec<u8> = ifs
.bytes()
.filter(|b| !matches!(*b, b' ' | b'\t' | b'\n'))
.collect();
let mut result = Vec::new();
for field in fields {
split_field(&field, &ifs_ws, &ifs_nws, &mut result);
}
result
}
fn split_field(field: &ExpandedField, ifs_ws: &[u8], ifs_nws: &[u8], out: &mut Vec<ExpandedField>) {
#[derive(Clone, Copy, PartialEq)]
enum State {
Start,
InField,
AfterWs,
AfterNws,
}
let bytes = field.value.as_bytes();
let len = bytes.len();
if len == 0 && field.was_quoted {
out.push(ExpandedField {
was_quoted: true,
..ExpandedField::new()
});
return;
}
let mut current = ExpandedField::new();
let mut state = State::Start;
let mut i = 0;
while i < len {
let b = bytes[i];
let quoted = field.is_quoted(i);
let is_ws = !quoted && ifs_ws.contains(&b);
let is_nws = !quoted && ifs_nws.contains(&b);
match state {
State::Start | State::AfterNws => {
if is_ws {
i += 1;
} else if is_nws {
out.push(ExpandedField {
was_quoted: true,
..ExpandedField::new()
});
state = State::AfterNws;
i += 1;
} else {
append_byte(&mut current, field, i);
state = State::InField;
i += 1;
}
}
State::InField => {
if is_ws {
emit(&mut current, out);
state = State::AfterWs;
i += 1;
} else if is_nws {
emit(&mut current, out);
state = State::AfterNws;
i += 1;
} else {
append_byte(&mut current, field, i);
i += 1;
}
}
State::AfterWs => {
if is_ws {
i += 1;
} else if is_nws {
state = State::AfterNws;
i += 1;
} else {
append_byte(&mut current, field, i);
state = State::InField;
i += 1;
}
}
}
}
if !current.is_empty() {
emit(&mut current, out);
}
}
#[inline]
fn append_byte(dest: &mut ExpandedField, source: &ExpandedField, i: usize) {
let ch = &source.value[i..i + 1];
if source.is_quoted(i) {
dest.push_quoted(ch);
} else {
dest.push_unquoted(ch);
}
}
#[inline]
fn emit(current: &mut ExpandedField, out: &mut Vec<ExpandedField>) {
let done = std::mem::take(current);
out.push(done);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::env::ShellEnv;
fn env_with_ifs(ifs: &str) -> ShellEnv {
let mut env = ShellEnv::new("yosh", vec![]);
env.vars.set("IFS", ifs).unwrap();
env
}
fn env_no_ifs() -> ShellEnv {
let mut env = ShellEnv::new("yosh", vec![]);
env.vars.unset("IFS").ok();
env
}
fn unquoted(s: &str) -> ExpandedField {
let mut f = ExpandedField::new();
f.push_unquoted(s);
f
}
fn quoted_field(s: &str) -> ExpandedField {
let mut f = ExpandedField::new();
f.push_quoted(s);
f
}
fn values(fields: Vec<ExpandedField>) -> Vec<String> {
fields.into_iter().map(|f| f.value).collect()
}
#[test]
fn test_split_spaces() {
let env = env_with_ifs(" ");
let input = vec![unquoted("hello world foo")];
assert_eq!(values(split(&env, input)), vec!["hello", "world", "foo"]);
}
#[test]
fn test_consecutive_whitespace() {
let env = env_with_ifs(" \t\n");
let input = vec![unquoted(" hello world ")];
assert_eq!(values(split(&env, input)), vec!["hello", "world"]);
}
#[test]
fn test_split_quoted_not_split() {
let env = env_with_ifs(" ");
let input = vec![quoted_field("hello world")];
assert_eq!(values(split(&env, input)), vec!["hello world"]);
}
#[test]
fn test_split_colon_delimiter() {
let env = env_with_ifs(":");
let input = vec![unquoted("a:b:c")];
assert_eq!(values(split(&env, input)), vec!["a", "b", "c"]);
}
#[test]
fn test_colon_with_surrounding_whitespace_absorbed() {
let env = env_with_ifs(" :");
let input = vec![unquoted("a : b : c")];
assert_eq!(values(split(&env, input)), vec!["a", "b", "c"]);
}
#[test]
fn test_empty_ifs_no_split() {
let env = env_with_ifs("");
let input = vec![unquoted("hello world")];
assert_eq!(values(split(&env, input)), vec!["hello world"]);
}
#[test]
fn test_empty_ifs_drops_empty_fields() {
let env = env_with_ifs("");
let mut empty = ExpandedField::new();
empty.push_unquoted("");
let input = vec![empty, unquoted("hello")];
assert_eq!(values(split(&env, input)), vec!["hello"]);
}
#[test]
fn test_unset_ifs_default() {
let env = env_no_ifs();
let input = vec![unquoted("hello\tworld\nfoo")];
assert_eq!(values(split(&env, input)), vec!["hello", "world", "foo"]);
}
#[test]
fn test_mixed_quoted_unquoted() {
let env = env_with_ifs(" ");
let mut f = ExpandedField::new();
f.push_unquoted("foo ");
f.push_quoted("bar baz");
f.push_unquoted(" qux");
let result = split(&env, vec![f]);
assert_eq!(values(result), vec!["foo", "bar baz", "qux"]);
}
#[test]
fn test_double_colon_empty_field() {
let env = env_with_ifs(":");
let input = vec![unquoted("a::b")];
assert_eq!(values(split(&env, input)), vec!["a", "", "b"]);
}
}