use super::emit::emit_array;
use super::error::ParseError;
use super::parser::{inside, parse_q_words};
#[derive(Debug, Clone, PartialEq)]
pub enum BashVal {
Str(String),
Arr(Vec<BashVal>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Schema {
Scalar,
Arr(Box<Schema>),
}
impl Schema {
pub fn n_d(n: usize) -> Self {
let mut schema = Schema::Scalar;
for _ in 0..n {
schema = Schema::Arr(Box::new(schema));
}
schema
}
}
impl BashVal {
pub fn row(words: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self::Arr(
words
.into_iter()
.map(|word| Self::Str(word.into()))
.collect(),
)
}
pub fn words(self) -> Option<Vec<String>> {
let Self::Arr(items) = self else { return None };
items
.into_iter()
.map(|item| match item {
Self::Str(word) => Some(word),
Self::Arr(_) => None,
})
.collect()
}
pub fn rows(self) -> Option<Vec<Vec<String>>> {
let Self::Arr(rows) = self else { return None };
rows.into_iter().map(Self::words).collect()
}
}
fn scalar_expected(words: &[String]) -> ParseError {
ParseError::new(
&words.join(" "),
0,
format!("expected one word, got {}", words.len()),
)
}
pub trait BashCodec {
fn emit(&self, val: &BashVal) -> Vec<String>;
fn parse(&self, words: &[String], schema: &Schema) -> Result<BashVal, ParseError>;
fn emit_literal(&self, val: &BashVal) -> String {
emit_array(&self.emit(val))
}
fn parse_literal(&self, input: &str, schema: &Schema) -> Result<BashVal, ParseError> {
self.parse(&parse_q_words(inside(input)?)?, schema)
}
fn rows(&self, input: &str) -> Result<Vec<Vec<String>>, ParseError> {
self.parse_literal(input, &Schema::n_d(2))?
.rows()
.ok_or_else(|| ParseError::new(input, 0, "expected rows"))
}
}
pub struct QuotedNest;
impl BashCodec for QuotedNest {
fn emit(&self, val: &BashVal) -> Vec<String> {
match val {
BashVal::Str(word) => vec![word.clone()],
BashVal::Arr(items) => items
.iter()
.map(|item| match item {
BashVal::Str(word) => word.clone(),
BashVal::Arr(_) => self.emit_literal(item),
})
.collect(),
}
}
fn parse(&self, words: &[String], schema: &Schema) -> Result<BashVal, ParseError> {
match schema {
Schema::Scalar => match words {
[only] => Ok(BashVal::Str(only.clone())),
_ => Err(scalar_expected(words)),
},
Schema::Arr(inner) => words
.iter()
.map(|word| match **inner {
Schema::Scalar => Ok(BashVal::Str(word.clone())),
Schema::Arr(_) => self.parse_literal(word, inner),
})
.collect::<Result<_, _>>()
.map(BashVal::Arr),
}
}
}
pub struct LinkedArr;
impl BashCodec for LinkedArr {
fn emit(&self, val: &BashVal) -> Vec<String> {
match val {
BashVal::Str(word) => vec![word.clone()],
BashVal::Arr(items) => {
let nested = matches!(items.first(), Some(BashVal::Arr(_)));
let mut out = Vec::new();
for item in items {
let body = self.emit(item);
if nested {
out.push(body.len().to_string());
}
out.extend(body);
}
out
}
}
}
fn parse(&self, words: &[String], schema: &Schema) -> Result<BashVal, ParseError> {
match schema {
Schema::Scalar => match words {
[only] => Ok(BashVal::Str(only.clone())),
_ => Err(scalar_expected(words)),
},
Schema::Arr(_) => {
let (val, consumed) = parse_body(words, schema)?;
if consumed != words.len() {
return Err(ParseError::new(
&words.join(" "),
0,
format!(
"trailing words: consumed {consumed} of {}",
words.len()
),
));
}
Ok(val)
}
}
}
}
fn parse_body(words: &[String], schema: &Schema) -> Result<(BashVal, usize), ParseError> {
let Schema::Arr(inner) = schema else {
return match words.first() {
Some(word) => Ok((BashVal::Str(word.clone()), 1)),
None => Err(ParseError::new(
"",
0,
"a scalar position with no word",
)),
};
};
let grouped = matches!(**inner, Schema::Arr(_));
let mut items = Vec::new();
let mut at = 0;
while at < words.len() {
if !grouped {
items.push(BashVal::Str(words[at].clone()));
at += 1;
continue;
}
let width: usize = words[at].parse().map_err(|_| {
ParseError::new(
&words.join(" "),
0,
format!(
"length prefix not numeric at pos {at}: {:?}",
words[at]
),
)
})?;
at += 1;
let end = at + width;
if end > words.len() {
return Err(ParseError::new(
&words.join(" "),
0,
format!(
"group claims {width} words; only {} available",
words.len() - at
),
));
}
let (item, consumed) = parse_body(&words[at..end], inner)?;
if consumed != end - at {
return Err(ParseError::new(
&words.join(" "),
0,
format!(
"nested group: consumed {consumed} of {} body words",
end - at
),
));
}
items.push(item);
at = end;
}
Ok((BashVal::Arr(items), at))
}
#[cfg(test)]
mod tests {
#[test]
fn rows_round_trip_through_one_flat_array() {
let rows = vec![
vec!["AspectRequire".to_string(), "env".into(), "mod a".into()],
vec!["Accumulate".to_string()],
Vec::new(),
];
let text = QuotedNest.emit_literal(&BashVal::Arr(
rows.iter()
.map(|row| BashVal::row(row.iter().cloned()))
.collect(),
));
let outer = crate::parse_array(&text).unwrap();
assert_eq!(
outer.len(),
3,
"three words at the outer level, one per row"
);
assert_eq!(
outer[0], "('AspectRequire' 'env' 'mod a')",
"each one an array literal"
);
assert_eq!(
crate::parse_array(&outer[0]).unwrap(),
rows[0],
"which reads back on its own"
);
assert_eq!(
QuotedNest.rows(&text).unwrap(),
rows,
"or in one step"
);
}
use super::*;
fn row(words: &[&str]) -> BashVal {
BashVal::row(words.iter().copied())
}
fn arr(items: Vec<BashVal>) -> BashVal {
BashVal::Arr(items)
}
fn words(items: &[&str]) -> Vec<String> {
items.iter().map(|word| word.to_string()).collect()
}
#[test]
fn quoted_nest_wraps_a_level_per_dimension() {
let two_d = arr(vec![
row(&["a", "b"]),
row(&["c", "d", "e"]),
]);
assert_eq!(
QuotedNest.emit(&two_d),
words(&["('a' 'b')", "('c' 'd' 'e')"])
);
assert_eq!(
QuotedNest
.parse(
&QuotedNest.emit(&two_d),
&Schema::n_d(2)
)
.unwrap(),
two_d
);
}
#[test]
fn linked_arr_prefixes_each_group_with_its_width() {
assert_eq!(
LinkedArr.emit(&arr(vec![
row(&["a", "b"]),
row(&["c", "d", "e"])
])),
words(&["2", "a", "b", "3", "c", "d", "e"])
);
assert_eq!(
LinkedArr.emit(&arr(vec![arr(vec![
row(&["a", "b"]),
row(&["c"])
])])),
words(&["5", "2", "a", "b", "1", "c"])
);
assert_eq!(
LinkedArr.emit(&arr(vec![
arr(vec![row(&["a", "b"])]),
arr(vec![row(&["c"])])
])),
words(&["3", "2", "a", "b", "2", "1", "c"])
);
}
#[test]
fn quoted_nest_round_trips_at_three_dimensions() {
let three_d = arr(vec![
arr(vec![row(&["a", "b"]), row(&["c"])]),
arr(vec![row(&["d", "e"])]),
]);
let text = QuotedNest.emit_literal(&three_d);
assert_eq!(
QuotedNest.parse_literal(&text, &Schema::n_d(3)).unwrap(),
three_d
);
assert_eq!(
QuotedNest
.parse_literal(&text, &Schema::n_d(2))
.unwrap()
.rows()
.unwrap()
.len(),
2,
"read one level shallower it is still two rows, of one word each"
);
}
#[test]
fn linked_arr_round_trips_at_three_dimensions() {
let three_d = arr(vec![
arr(vec![row(&["a", "b"]), row(&["c"])]),
arr(vec![row(&["d", "e"])]),
]);
assert_eq!(
LinkedArr
.parse(
&LinkedArr.emit(&three_d),
&Schema::n_d(3)
)
.unwrap(),
three_d
);
}
}