1use indexmap::IndexMap;
17
18use super::error::ParseError;
19use super::quoting::{Cursor, parse_with};
20
21const VALUE_STOPS: &[char] = &[' ', '\t', '\n', ')'];
23
24const KEY_STOPS: &[char] = &[']'];
26
27pub fn parse_scalar(text: &str) -> Result<String, ParseError> {
28 parse_with(trimmed(text), |c| c.word(VALUE_STOPS))
29}
30
31pub fn parse_q_words(text: &str) -> Result<Vec<String>, ParseError> {
32 parse_with(trimmed(text), q_words)
33}
34
35pub fn parse_array(text: &str) -> Result<Vec<String>, ParseError> {
41 parse_q_words(inside(text)?)
42}
43
44pub fn parse_indexed(text: &str) -> Result<IndexMap<usize, String>, ParseError> {
45 parse_with(trimmed(text), indexed_compound)
46}
47
48pub fn parse_assoc(text: &str) -> Result<IndexMap<String, String>, ParseError> {
49 parse_with(trimmed(text), assoc_compound)
50}
51
52pub(super) fn inside(text: &str) -> Result<&str, ParseError> {
56 let trimmed = text.trim();
57
58 trimmed
59 .strip_prefix('(')
60 .and_then(|rest| rest.strip_suffix(')'))
61 .ok_or_else(|| {
62 ParseError::new(
63 trimmed,
64 0,
65 "expected a (…) array literal",
66 )
67 })
68}
69
70fn trimmed(text: &str) -> &str {
73 text.trim_end_matches('\n')
74}
75
76fn q_words(c: &mut Cursor<'_>) -> Result<Vec<String>, ParseError> {
79 let mut out = Vec::new();
80 if c.at_end() {
81 return Ok(out);
82 }
83
84 out.push(c.word(VALUE_STOPS)?);
85 while !c.at_end() {
86 c.lit(" ")?;
87 out.push(c.word(VALUE_STOPS)?);
88 }
89 Ok(out)
90}
91
92fn indexed_compound(c: &mut Cursor<'_>) -> Result<IndexMap<usize, String>, ParseError> {
93 c.lit("(")?;
94 c.ws0();
95
96 let mut out = IndexMap::new();
97 while !c.starts_with(")") {
98 let index = bracket_index(c)?;
99 c.lit("=")?;
100 out.insert(index, c.word(VALUE_STOPS)?);
101 c.ws0();
102 }
103 c.lit(")")?;
104
105 Ok(out)
106}
107
108fn assoc_compound(c: &mut Cursor<'_>) -> Result<IndexMap<String, String>, ParseError> {
109 c.lit("(")?;
110 c.ws0();
111
112 let mut out = IndexMap::new();
113 while !c.starts_with(")") {
114 c.lit("[")?;
115 let key = c.word(KEY_STOPS)?;
116 c.lit("]")?;
117 c.lit("=")?;
118 out.insert(key, c.word(VALUE_STOPS)?);
119 c.ws0();
120 }
121 c.lit(")")?;
122
123 Ok(out)
124}
125
126fn bracket_index(c: &mut Cursor<'_>) -> Result<usize, ParseError> {
127 c.lit("[")?;
128
129 let digits = c.take_while(|d| d.is_ascii_digit());
130 if digits.is_empty() {
131 return Err(c.fail("expected a subscript"));
132 }
133
134 let index = digits.parse().map_err(|_| {
137 c.fail(format!(
138 "subscript {digits:?} is not an index"
139 ))
140 })?;
141 c.lit("]")?;
142
143 Ok(index)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::super::codec::{BashCodec, QuotedNest};
149 use super::*;
150 use crate::{BashVal, LinkedArr, emit_array};
151
152 fn ix<I: IntoIterator<Item = (usize, &'static str)>>(it: I) -> IndexMap<usize, String> {
153 it.into_iter().map(|(k, v)| (k, v.to_string())).collect()
154 }
155 fn ax<I: IntoIterator<Item = (&'static str, &'static str)>>(it: I) -> IndexMap<String, String> {
156 it.into_iter()
157 .map(|(k, v)| (k.to_string(), v.to_string()))
158 .collect()
159 }
160
161 #[test]
162 fn scalar_canonical_forms() {
163 assert_eq!(
164 parse_scalar("'hello world'").unwrap(),
165 "hello world"
166 );
167 assert_eq!(
168 parse_scalar(r#""hello \$VAR""#).unwrap(),
169 "hello $VAR"
170 );
171 assert_eq!(
172 parse_scalar(r"$'a\nb'").unwrap(),
173 "a\nb"
174 );
175 assert_eq!(parse_scalar("''").unwrap(), "");
176 }
177
178 #[test]
179 fn scalar_concat() {
180 assert_eq!(parse_scalar("'a''b'").unwrap(), "ab");
181 }
182
183 #[test]
184 fn scalar_rejects_non_canonical() {
185 assert!(parse_scalar("").is_err());
186 assert!(parse_scalar("'a' 'b'").is_err());
187 assert!(parse_scalar(" 'a'").is_err());
188 }
189
190 #[test]
191 fn q_words_canonical() {
192 assert_eq!(
193 parse_q_words("'a' 'b'").unwrap(),
194 vec!["a", "b"]
195 );
196 assert_eq!(
197 parse_q_words("'a b' $'c\\nd'").unwrap(),
198 vec!["a b", "c\nd"]
199 );
200 assert_eq!(
201 parse_q_words("").unwrap(),
202 Vec::<String>::new()
203 );
204 }
205
206 #[test]
207 fn q_words_rejects_non_canonical_spacing() {
208 assert!(parse_q_words("'a' 'b'").is_err());
209 assert!(parse_q_words(" 'a' 'b'").is_err());
210 assert!(parse_q_words("'a' 'b' ").is_err());
211 }
212
213 #[test]
214 fn ansi_c_escapes() {
215 assert_eq!(
216 parse_q_words(r"$'\t\r\\\''").unwrap(),
217 vec!["\t\r\\'"]
218 );
219 assert_eq!(
220 parse_q_words(r"$'\x41' $'\101'").unwrap(),
221 vec!["A", "A"]
222 );
223 }
224
225 #[test]
226 fn array_round_trips_and_needs_its_parentheses() {
227 let words = vec!["a".to_string(), "b c".into(), "d\ne".into(), String::new()];
228
229 assert_eq!(
230 emit_array(&words),
231 "('a' 'b c' $'d\\ne' '')"
232 );
233 assert_eq!(
234 parse_array(&emit_array(&words)).unwrap(),
235 words
236 );
237 assert_eq!(
238 parse_array("()").unwrap(),
239 Vec::<String>::new()
240 );
241
242 let bare = parse_array("'a' 'b'").expect_err("no parentheses");
243 assert!(
244 bare.message.contains("array literal"),
245 "{bare}"
246 );
247 }
248
249 #[test]
252 fn one_dimension_is_the_same_under_either_codec() {
253 let words = vec!["a".to_string(), "b c".into(), "2".into()];
254 let value = BashVal::row(words.clone());
255
256 assert_eq!(
257 QuotedNest.emit_literal(&value),
258 LinkedArr.emit_literal(&value)
259 );
260 assert_eq!(
261 QuotedNest.emit_literal(&value),
262 emit_array(&words)
263 );
264 assert_eq!(
265 parse_array(&emit_array(&words)).unwrap(),
266 words
267 );
268 }
269
270 #[test]
271 fn indexed_canonical() {
272 assert_eq!(
273 parse_indexed("([0]='a' [1]='b')").unwrap(),
274 ix([(0, "a"), (1, "b")])
275 );
276 assert_eq!(parse_indexed("()").unwrap(), ix([]));
277 assert_eq!(
278 parse_indexed(r#"([0]="a" [1]="b c" [2]=$'d\ne')"#).unwrap(),
279 ix([(0, "a"), (1, "b c"), (2, "d\ne")])
280 );
281 }
282
283 #[test]
284 fn indexed_sparse_ascending() {
285 assert_eq!(
286 parse_indexed(r#"([0]="zero" [2]="two" [5]="five")"#).unwrap(),
287 ix([(0, "zero"), (2, "two"), (5, "five")])
288 );
289 }
290
291 #[test]
292 fn indexed_rejects_non_canonical() {
293 assert!(parse_indexed("").is_err());
294 assert!(parse_indexed("[0]='a' [1]='b'").is_err());
295 assert!(parse_indexed("([0]=)").is_err());
296 assert!(parse_indexed("([0]='a'").is_err());
297 }
298
299 #[test]
300 fn assoc_canonical() {
301 assert_eq!(
302 parse_assoc(r#"([k]="v")"#).unwrap(),
303 ax([("k", "v")])
304 );
305 assert_eq!(parse_assoc("()").unwrap(), ax([]));
306 assert_eq!(
307 parse_assoc(r#"([foo]="1" [c]="3" )"#).unwrap(),
308 ax([("foo", "1"), ("c", "3")])
309 );
310 }
311
312 #[test]
313 fn assoc_quoted_key_ansi_value() {
314 assert_eq!(
315 parse_assoc(r#"([foo]="1" ["k 2"]=$'v\n2' [c]="3" )"#).unwrap(),
316 ax([("foo", "1"), ("k 2", "v\n2"), ("c", "3")])
317 );
318 }
319
320 #[test]
321 fn assoc_rejects_non_canonical() {
322 assert!(parse_assoc("").is_err());
323 assert!(parse_assoc("[k]='v'").is_err());
324 assert!(parse_assoc("([k]=)").is_err());
325 }
326
327 #[test]
332 fn an_octal_escape_stops_at_a_byte() {
333 assert_eq!(
334 parse_q_words(r"$'\377'").unwrap(),
335 vec!["\u{ff}"]
336 );
337 assert_eq!(
338 parse_q_words(r"$'\0'").unwrap(),
339 vec!["\0"]
340 );
341 assert!(parse_q_words(r"$'\400'").is_err());
342 assert!(parse_q_words(r"$'\777'").is_err());
343 }
344
345 #[test]
348 fn a_subscript_too_wide_to_be_one_is_refused() {
349 assert!(parse_indexed("([99999999999999999999999]='a')").is_err());
350 assert_eq!(
351 parse_indexed(&format!("([{}]='a')", usize::MAX)).unwrap(),
352 ix([(usize::MAX, "a")]),
353 "the widest one that is still an index"
354 );
355 }
356
357 #[test]
360 fn an_error_reports_the_text_around_it() {
361 let long = format!("'{}' trailing", "é".repeat(30));
362 let failed = parse_scalar(&long).expect_err("trailing input");
363
364 assert!(!failed.snippet.is_empty(), "{failed}");
365 assert!(
366 long.contains(&failed.snippet),
367 "{failed}"
368 );
369 }
370}