use indexmap::IndexMap;
use super::error::ParseError;
use super::quoting::{Cursor, parse_with};
const VALUE_STOPS: &[char] = &[' ', '\t', '\n', ')'];
const KEY_STOPS: &[char] = &[']'];
pub fn parse_scalar(text: &str) -> Result<String, ParseError> {
parse_with(trimmed(text), |c| c.word(VALUE_STOPS))
}
pub fn parse_q_words(text: &str) -> Result<Vec<String>, ParseError> {
parse_with(trimmed(text), q_words)
}
pub fn parse_array(text: &str) -> Result<Vec<String>, ParseError> {
parse_q_words(inside(text)?)
}
pub fn parse_indexed(text: &str) -> Result<IndexMap<usize, String>, ParseError> {
parse_with(trimmed(text), indexed_compound)
}
pub fn parse_assoc(text: &str) -> Result<IndexMap<String, String>, ParseError> {
parse_with(trimmed(text), assoc_compound)
}
pub(super) fn inside(text: &str) -> Result<&str, ParseError> {
let trimmed = text.trim();
trimmed
.strip_prefix('(')
.and_then(|rest| rest.strip_suffix(')'))
.ok_or_else(|| {
ParseError::new(
trimmed,
0,
"expected a (…) array literal",
)
})
}
fn trimmed(text: &str) -> &str {
text.trim_end_matches('\n')
}
fn q_words(c: &mut Cursor<'_>) -> Result<Vec<String>, ParseError> {
let mut out = Vec::new();
if c.at_end() {
return Ok(out);
}
out.push(c.word(VALUE_STOPS)?);
while !c.at_end() {
c.lit(" ")?;
out.push(c.word(VALUE_STOPS)?);
}
Ok(out)
}
fn indexed_compound(c: &mut Cursor<'_>) -> Result<IndexMap<usize, String>, ParseError> {
c.lit("(")?;
c.ws0();
let mut out = IndexMap::new();
while !c.starts_with(")") {
let index = bracket_index(c)?;
c.lit("=")?;
out.insert(index, c.word(VALUE_STOPS)?);
c.ws0();
}
c.lit(")")?;
Ok(out)
}
fn assoc_compound(c: &mut Cursor<'_>) -> Result<IndexMap<String, String>, ParseError> {
c.lit("(")?;
c.ws0();
let mut out = IndexMap::new();
while !c.starts_with(")") {
c.lit("[")?;
let key = c.word(KEY_STOPS)?;
c.lit("]")?;
c.lit("=")?;
out.insert(key, c.word(VALUE_STOPS)?);
c.ws0();
}
c.lit(")")?;
Ok(out)
}
fn bracket_index(c: &mut Cursor<'_>) -> Result<usize, ParseError> {
c.lit("[")?;
let digits = c.take_while(|d| d.is_ascii_digit());
if digits.is_empty() {
return Err(c.fail("expected a subscript"));
}
let index = digits.parse().map_err(|_| {
c.fail(format!(
"subscript {digits:?} is not an index"
))
})?;
c.lit("]")?;
Ok(index)
}
#[cfg(test)]
mod tests {
use super::super::codec::{BashCodec, QuotedNest};
use super::*;
use crate::{BashVal, LinkedArr, emit_array};
fn ix<I: IntoIterator<Item = (usize, &'static str)>>(it: I) -> IndexMap<usize, String> {
it.into_iter().map(|(k, v)| (k, v.to_string())).collect()
}
fn ax<I: IntoIterator<Item = (&'static str, &'static str)>>(it: I) -> IndexMap<String, String> {
it.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn scalar_canonical_forms() {
assert_eq!(
parse_scalar("'hello world'").unwrap(),
"hello world"
);
assert_eq!(
parse_scalar(r#""hello \$VAR""#).unwrap(),
"hello $VAR"
);
assert_eq!(
parse_scalar(r"$'a\nb'").unwrap(),
"a\nb"
);
assert_eq!(parse_scalar("''").unwrap(), "");
}
#[test]
fn scalar_concat() {
assert_eq!(parse_scalar("'a''b'").unwrap(), "ab");
}
#[test]
fn scalar_rejects_non_canonical() {
assert!(parse_scalar("").is_err());
assert!(parse_scalar("'a' 'b'").is_err());
assert!(parse_scalar(" 'a'").is_err());
}
#[test]
fn q_words_canonical() {
assert_eq!(
parse_q_words("'a' 'b'").unwrap(),
vec!["a", "b"]
);
assert_eq!(
parse_q_words("'a b' $'c\\nd'").unwrap(),
vec!["a b", "c\nd"]
);
assert_eq!(
parse_q_words("").unwrap(),
Vec::<String>::new()
);
}
#[test]
fn q_words_rejects_non_canonical_spacing() {
assert!(parse_q_words("'a' 'b'").is_err());
assert!(parse_q_words(" 'a' 'b'").is_err());
assert!(parse_q_words("'a' 'b' ").is_err());
}
#[test]
fn ansi_c_escapes() {
assert_eq!(
parse_q_words(r"$'\t\r\\\''").unwrap(),
vec!["\t\r\\'"]
);
assert_eq!(
parse_q_words(r"$'\x41' $'\101'").unwrap(),
vec!["A", "A"]
);
}
#[test]
fn array_round_trips_and_needs_its_parentheses() {
let words = vec!["a".to_string(), "b c".into(), "d\ne".into(), String::new()];
assert_eq!(
emit_array(&words),
"('a' 'b c' $'d\\ne' '')"
);
assert_eq!(
parse_array(&emit_array(&words)).unwrap(),
words
);
assert_eq!(
parse_array("()").unwrap(),
Vec::<String>::new()
);
let bare = parse_array("'a' 'b'").expect_err("no parentheses");
assert!(
bare.message.contains("array literal"),
"{bare}"
);
}
#[test]
fn one_dimension_is_the_same_under_either_codec() {
let words = vec!["a".to_string(), "b c".into(), "2".into()];
let value = BashVal::row(words.clone());
assert_eq!(
QuotedNest.emit_literal(&value),
LinkedArr.emit_literal(&value)
);
assert_eq!(
QuotedNest.emit_literal(&value),
emit_array(&words)
);
assert_eq!(
parse_array(&emit_array(&words)).unwrap(),
words
);
}
#[test]
fn indexed_canonical() {
assert_eq!(
parse_indexed("([0]='a' [1]='b')").unwrap(),
ix([(0, "a"), (1, "b")])
);
assert_eq!(parse_indexed("()").unwrap(), ix([]));
assert_eq!(
parse_indexed(r#"([0]="a" [1]="b c" [2]=$'d\ne')"#).unwrap(),
ix([(0, "a"), (1, "b c"), (2, "d\ne")])
);
}
#[test]
fn indexed_sparse_ascending() {
assert_eq!(
parse_indexed(r#"([0]="zero" [2]="two" [5]="five")"#).unwrap(),
ix([(0, "zero"), (2, "two"), (5, "five")])
);
}
#[test]
fn indexed_rejects_non_canonical() {
assert!(parse_indexed("").is_err());
assert!(parse_indexed("[0]='a' [1]='b'").is_err());
assert!(parse_indexed("([0]=)").is_err());
assert!(parse_indexed("([0]='a'").is_err());
}
#[test]
fn assoc_canonical() {
assert_eq!(
parse_assoc(r#"([k]="v")"#).unwrap(),
ax([("k", "v")])
);
assert_eq!(parse_assoc("()").unwrap(), ax([]));
assert_eq!(
parse_assoc(r#"([foo]="1" [c]="3" )"#).unwrap(),
ax([("foo", "1"), ("c", "3")])
);
}
#[test]
fn assoc_quoted_key_ansi_value() {
assert_eq!(
parse_assoc(r#"([foo]="1" ["k 2"]=$'v\n2' [c]="3" )"#).unwrap(),
ax([("foo", "1"), ("k 2", "v\n2"), ("c", "3")])
);
}
#[test]
fn assoc_rejects_non_canonical() {
assert!(parse_assoc("").is_err());
assert!(parse_assoc("[k]='v'").is_err());
assert!(parse_assoc("([k]=)").is_err());
}
#[test]
fn an_octal_escape_stops_at_a_byte() {
assert_eq!(
parse_q_words(r"$'\377'").unwrap(),
vec!["\u{ff}"]
);
assert_eq!(
parse_q_words(r"$'\0'").unwrap(),
vec!["\0"]
);
assert!(parse_q_words(r"$'\400'").is_err());
assert!(parse_q_words(r"$'\777'").is_err());
}
#[test]
fn a_subscript_too_wide_to_be_one_is_refused() {
assert!(parse_indexed("([99999999999999999999999]='a')").is_err());
assert_eq!(
parse_indexed(&format!("([{}]='a')", usize::MAX)).unwrap(),
ix([(usize::MAX, "a")]),
"the widest one that is still an index"
);
}
#[test]
fn an_error_reports_the_text_around_it() {
let long = format!("'{}' trailing", "é".repeat(30));
let failed = parse_scalar(&long).expect_err("trailing input");
assert!(!failed.snippet.is_empty(), "{failed}");
assert!(
long.contains(&failed.snippet),
"{failed}"
);
}
}