use super::{Expansion, ExpansionPiece, WordField};
enum SplitState {
InField,
AfterWhitespace,
AfterDelimiter,
}
const fn is_ifs_whitespace(c: char) -> bool {
matches!(
c,
' ' | '\t' | '\n' | '\r'
| '\u{0b}' | '\u{0c}' )
}
pub(super) fn split_fields(ifs: &str, expansion: Expansion) -> Vec<WordField> {
let mut fields: Vec<WordField> = vec![];
let mut current_field = WordField::new();
let has_hard_delimiter = ifs.chars().any(|c| !is_ifs_whitespace(c));
for existing_field in expansion.fields {
let mut state = SplitState::AfterDelimiter;
let preserve_empty_list_element = matches!(
existing_field.0.as_slice(),
[ExpansionPiece::EmptyListElement {
has_following: true
}]
);
for piece in existing_field.0 {
match piece {
ExpansionPiece::EmptyListElement { .. } => {}
ExpansionPiece::UnquotedLiteral(s) if s.is_empty() => {}
ExpansionPiece::Unsplittable(_) | ExpansionPiece::UnquotedLiteral(_) => {
current_field.0.push(piece);
state = SplitState::InField;
}
ExpansionPiece::Splittable(s) => {
for c in s.chars() {
if !ifs.contains(c) {
match current_field.0.last_mut() {
Some(ExpansionPiece::Splittable(last)) => last.push(c),
_ => current_field
.0
.push(ExpansionPiece::Splittable(c.to_string())),
}
state = SplitState::InField;
continue;
}
state = match (state, is_ifs_whitespace(c)) {
(SplitState::InField, whitespace) => {
fields.push(std::mem::take(&mut current_field));
if whitespace {
SplitState::AfterWhitespace
} else {
SplitState::AfterDelimiter
}
}
(SplitState::AfterWhitespace, false) => SplitState::AfterDelimiter,
(SplitState::AfterDelimiter, false) => {
fields.push(WordField::new());
SplitState::AfterDelimiter
}
(state, true) => state,
};
}
}
}
}
if matches!(state, SplitState::InField) {
fields.push(std::mem::take(&mut current_field));
} else if preserve_empty_list_element && has_hard_delimiter {
fields.push(WordField::new());
}
}
fields
}
#[cfg(test)]
mod tests {
use super::*;
type Pieces = Vec<Vec<ExpansionPiece>>;
fn s(text: &str) -> ExpansionPiece {
ExpansionPiece::Splittable(text.into())
}
fn u(text: &str) -> ExpansionPiece {
ExpansionPiece::Unsplittable(text.into())
}
fn l(text: &str) -> ExpansionPiece {
ExpansionPiece::UnquotedLiteral(text.into())
}
const fn e(has_following: bool) -> ExpansionPiece {
ExpansionPiece::EmptyListElement { has_following }
}
fn split(ifs: &str, fields: Pieces) -> Pieces {
let expansion = Expansion {
fields: fields.into_iter().map(WordField).collect(),
..Expansion::default()
};
split_fields(ifs, expansion)
.into_iter()
.map(|field| field.0)
.collect()
}
#[test]
fn splits_like_bash() {
let cases: Vec<(&str, Pieces, Pieces)> = vec![
(
" \t\n",
vec![vec![s(" a \t b\n")]],
vec![vec![s("a")], vec![s("b")]],
),
(":", vec![vec![s("a:b")]], vec![vec![s("a")], vec![s("b")]]),
(
":",
vec![vec![s("a::b")]],
vec![vec![s("a")], vec![], vec![s("b")]],
),
(":", vec![vec![s(":a")]], vec![vec![], vec![s("a")]]),
(":", vec![vec![s("a:")]], vec![vec![s("a")]]),
(":", vec![vec![s(":")]], vec![vec![]]),
(":", vec![vec![s("::")]], vec![vec![], vec![]]),
(
": ",
vec![vec![s("a : b")]],
vec![vec![s("a")], vec![s("b")]],
),
(
": ",
vec![vec![s("a :: b")]],
vec![vec![s("a")], vec![], vec![s("b")]],
),
(": ", vec![vec![s(" :a")]], vec![vec![], vec![s("a")]]),
(": ", vec![vec![s(": a")]], vec![vec![], vec![s("a")]]),
(
": ",
vec![vec![s(": :a")]],
vec![vec![], vec![], vec![s("a")]],
),
(": ", vec![vec![s("a: ")]], vec![vec![s("a")]]),
(":", vec![vec![l("a:b")]], vec![vec![l("a:b")]]),
(
":",
vec![vec![s("1:2"), l(":3")]],
vec![vec![s("1")], vec![s("2"), l(":3")]],
),
(
":",
vec![vec![s("1:"), l(":3")]],
vec![vec![s("1")], vec![l(":3")]],
),
(
":",
vec![vec![u("a:b"), s(":c")]],
vec![vec![u("a:b")], vec![s("c")]],
),
(
":",
vec![vec![u("a:b"), s("c")]],
vec![vec![u("a:b"), s("c")]],
),
(":", vec![vec![u("")]], vec![vec![u("")]]),
(
":",
vec![vec![u("A")], vec![u("")]],
vec![vec![u("A")], vec![u("")]],
),
(
":",
vec![vec![s("1:"), s(":4")]],
vec![vec![s("1")], vec![], vec![s("4")]],
),
(
":",
vec![vec![s("a:b")], vec![s("c::d")]],
vec![
vec![s("a")],
vec![s("b")],
vec![s("c")],
vec![],
vec![s("d")],
],
),
(":", vec![vec![s("")]], vec![]),
("", vec![vec![s("a b")]], vec![vec![s("a b")]]),
(
"\r",
vec![vec![s("a\rb\r\rc")]],
vec![vec![s("a")], vec![s("b")], vec![s("c")]],
),
(
"\u{a0}",
vec![vec![s("a\u{a0}\u{a0}b")]],
vec![vec![s("a")], vec![], vec![s("b")]],
),
];
for (ifs, input, expected) in cases {
let actual = split(ifs, input.clone());
assert_eq!(actual, expected, "ifs={ifs:?} input={input:?}");
}
}
#[test]
fn preserves_non_trailing_empty_list_elements_with_hard_ifs() {
assert_eq!(
split(":", vec![vec![e(true)], vec![s("2")]]),
vec![vec![], vec![s("2")]]
);
assert_eq!(
split(":", vec![vec![s("2")], vec![e(false)]]),
vec![vec![s("2")]]
);
assert_eq!(
split(" ", vec![vec![e(true)], vec![s("2")]]),
vec![vec![s("2")]]
);
}
}