Skip to main content

bash_strings/
quoting.rs

1//! How bash spells one word, and the cursor a grammar builds on.
2//!
3//! A word is any of the quoting forms, and adjacent ones concatenate:
4//!
5//! | | |
6//! |---|---|
7//! | `'…'` | single — no escapes at all |
8//! | `"…"` | double — `\$ \" \\ \`` and a line continuation |
9//! | `$'…'` | ANSI-C — the full escape set, including `\nnn` and `\uXXXX` |
10//! | `$"…"` | locale, read as double |
11//! | bare | up to a stop character, `\` escaping the next one |
12//!
13//! `a"b"c'd'$'e'` is one word, `abcde`. Where a word *ends* belongs to the
14//! caller: [`Cursor::word`] takes the stop characters, so a grammar over an
15//! entirely different syntax passes its own and gets bash quoting for free.
16//!
17//! ```
18//! use bash_strings::{parse_with, ParseError};
19//!
20//! // `key: value, key: value` — not a bash value, but the words are bash's.
21//! const STOPS: &[char] = &[':', ',', ' '];
22//!
23//! let pairs = parse_with("a: 'one two', b: $'x\\ty'", |c| {
24//!     let mut out = Vec::new();
25//!     loop {
26//!         c.ws0();
27//!         let key = c.word(STOPS)?;
28//!         c.lit(":")?;
29//!         c.ws0();
30//!         out.push((key, c.word(STOPS)?));
31//!         c.ws0();
32//!         if !c.eat(",") {
33//!             return Ok(out);
34//!         }
35//!     }
36//! })?;
37//!
38//! assert_eq!(pairs, [("a".to_string(), "one two".to_string()),
39//!                    ("b".to_string(), "x\ty".to_string())]);
40//! # Ok::<(), ParseError>(())
41//! ```
42
43use super::error::ParseError;
44
45/// A position in some text, and the bash word grammar over it.
46///
47/// Built by [`parse_with`], which is also what checks that a grammar consumed
48/// the whole input.
49pub struct Cursor<'a> {
50    input: &'a str,
51    rest: &'a str,
52}
53
54/// Run a grammar over the whole of `input`. Anything it leaves behind is an
55/// error, so a grammar cannot quietly match a prefix.
56pub fn parse_with<T>(
57    input: &str,
58    grammar: impl FnOnce(&mut Cursor<'_>) -> Result<T, ParseError>,
59) -> Result<T, ParseError> {
60    let mut cursor = Cursor { input, rest: input };
61    let parsed = grammar(&mut cursor)?;
62
63    match cursor.at_end() {
64        true => Ok(parsed),
65        false => Err(cursor.fail("trailing input")),
66    }
67}
68
69impl<'a> Cursor<'a> {
70    /// The byte offset reached so far.
71    pub fn at(&self) -> usize {
72        self.input.len() - self.rest.len()
73    }
74
75    /// What has not been read yet.
76    pub fn rest(&self) -> &'a str {
77        self.rest
78    }
79
80    pub fn at_end(&self) -> bool {
81        self.rest.is_empty()
82    }
83
84    pub fn peek(&self) -> Option<char> {
85        self.rest.chars().next()
86    }
87
88    pub fn starts_with(&self, text: &str) -> bool {
89        self.rest.starts_with(text)
90    }
91
92    /// A refusal at the current position, carrying the text around it.
93    pub fn fail(&self, message: impl Into<String>) -> ParseError {
94        ParseError::new(self.input, self.at(), message)
95    }
96
97    /// Consume `text` if it is next, and say whether it was.
98    pub fn eat(&mut self, text: &str) -> bool {
99        match self.rest.strip_prefix(text) {
100            Some(rest) => {
101                self.rest = rest;
102                true
103            }
104            None => false,
105        }
106    }
107
108    /// Consume `text`, which must be next.
109    pub fn lit(&mut self, text: &str) -> Result<(), ParseError> {
110        match self.eat(text) {
111            true => Ok(()),
112            false => Err(self.fail(format!("expected {text:?}"))),
113        }
114    }
115
116    /// Every leading character the predicate accepts, possibly none.
117    pub fn take_while(&mut self, accept: impl Fn(char) -> bool) -> &'a str {
118        let end = self
119            .rest
120            .find(|c: char| !accept(c))
121            .unwrap_or(self.rest.len());
122        let (taken, rest) = self.rest.split_at(end);
123
124        self.rest = rest;
125        taken
126    }
127
128    /// Spaces, tabs and newlines, possibly none.
129    pub fn ws0(&mut self) {
130        self.take_while(|c| c == ' ' || c == '\t' || c == '\n');
131    }
132
133    /// One word: a first segment, then every adjacent one that still belongs
134    /// to it. The first must be there; a following one that will not read ends
135    /// the word rather than failing it.
136    pub fn word(&mut self, stops: &[char]) -> Result<String, ParseError> {
137        let mut out = match self.quoted()? {
138            Some(text) => text,
139            None => self.bare(stops)?,
140        };
141
142        loop {
143            let snapshot = self.rest;
144            match self.segment(stops) {
145                Ok(Some(text)) => out.push_str(&text),
146                Ok(None) | Err(_) => {
147                    self.rest = snapshot;
148                    break;
149                }
150            }
151        }
152        Ok(out)
153    }
154
155    fn advance(&mut self, c: char) {
156        self.rest = &self.rest[c.len_utf8()..];
157    }
158
159    /// A segment after the first: a quoting form, more bare text, or the end
160    /// of the word.
161    fn segment(&mut self, stops: &[char]) -> Result<Option<String>, ParseError> {
162        if let Some(text) = self.quoted()? {
163            return Ok(Some(text));
164        }
165
166        match self.peek() {
167            Some('\\') => self.bare(stops).map(Some),
168            Some(c) if !stops.contains(&c) => self.bare(stops).map(Some),
169            _ => Ok(None),
170        }
171    }
172
173    /// One of the quoting forms, or `None` where the cursor is not at one.
174    fn quoted(&mut self) -> Result<Option<String>, ParseError> {
175        if self.eat("$'") {
176            return self.ansi_c().map(Some);
177        }
178        if self.starts_with("'") {
179            return self.single().map(Some);
180        }
181        if self.starts_with("\"") {
182            return self.double().map(Some);
183        }
184        if self.starts_with("$\"") {
185            self.advance('$');
186            return self.double().map(Some);
187        }
188        Ok(None)
189    }
190
191    fn single(&mut self) -> Result<String, ParseError> {
192        self.lit("'")?;
193
194        let body = self.take_while(|c| c != '\'').to_string();
195        if !self.eat("'") {
196            return Err(self.fail("unterminated ' quote"));
197        }
198        Ok(body)
199    }
200
201    fn double(&mut self) -> Result<String, ParseError> {
202        self.lit("\"")?;
203
204        let mut out = String::new();
205        loop {
206            // Reading the character is what says there is one, so the body
207            // below never has to ask again.
208            let Some(c) = self.peek() else {
209                return Err(self.fail("unterminated \" quote"));
210            };
211
212            if c == '"' {
213                self.advance(c);
214                return Ok(out);
215            }
216            if c == '\\' {
217                self.advance(c);
218                match self.peek() {
219                    Some(escaped @ ('$' | '"' | '\\' | '`')) => {
220                        out.push(escaped);
221                        self.advance(escaped);
222                    }
223                    Some('\n') => self.advance('\n'),
224                    Some(escaped) => {
225                        out.push('\\');
226                        out.push(escaped);
227                        self.advance(escaped);
228                    }
229                    None => return Err(self.fail("a backslash at the end of the input")),
230                }
231                continue;
232            }
233            out.push(c);
234            self.advance(c);
235        }
236    }
237
238    fn ansi_c(&mut self) -> Result<String, ParseError> {
239        let mut out = String::new();
240        loop {
241            let Some(c) = self.peek() else {
242                return Err(self.fail("unterminated $' quote"));
243            };
244
245            if c == '\'' {
246                self.advance(c);
247                return Ok(out);
248            }
249            if c == '\\' {
250                self.advance(c);
251                let Some(escaped) = self.peek() else {
252                    return Err(self.fail("a backslash at the end of the input"));
253                };
254
255                self.advance(escaped);
256                self.escape(escaped, &mut out)?;
257                continue;
258            }
259            out.push(c);
260            self.advance(c);
261        }
262    }
263
264    fn escape(&mut self, c: char, out: &mut String) -> Result<(), ParseError> {
265        match c {
266            'a' => out.push('\x07'),
267            'b' => out.push('\x08'),
268            'e' | 'E' => out.push('\x1B'),
269            'f' => out.push('\x0C'),
270            'n' => out.push('\n'),
271            'r' => out.push('\r'),
272            't' => out.push('\t'),
273            'v' => out.push('\x0B'),
274            '\\' => out.push('\\'),
275            '\'' => out.push('\''),
276            '"' => out.push('"'),
277            '?' => out.push('?'),
278            'c' => {
279                let control = self
280                    .peek()
281                    .ok_or_else(|| self.fail(r"\c with nothing after it"))?;
282
283                self.advance(control);
284                out.push(((control as u32) & 0x1F) as u8 as char);
285            }
286            'x' => {
287                let digits = self.hex(2);
288                let byte = u8::from_str_radix(&digits, 16).map_err(|_| self.fail(r"\x wants one or two hex digits"))?;
289
290                out.push(byte as char);
291            }
292            'u' => self.unicode(4, out)?,
293            'U' => self.unicode(8, out)?,
294            first if is_octal(first) => {
295                let mut octal = String::from(first);
296                for _ in 0..2 {
297                    match self.peek() {
298                        Some(digit) if is_octal(digit) => {
299                            octal.push(digit);
300                            self.advance(digit);
301                        }
302                        _ => break,
303                    }
304                }
305
306                // Three octal digits reach 511. Bash prints no escape above
307                // `\377`, so a wider one is not its output.
308                let byte = u8::from_str_radix(&octal, 8).map_err(|_| {
309                    self.fail(format!(
310                        r"octal escape \{octal} is above \377"
311                    ))
312                })?;
313
314                out.push(byte as char);
315            }
316            other => {
317                out.push('\\');
318                out.push(other);
319            }
320        }
321        Ok(())
322    }
323
324    fn unicode(&mut self, width: usize, out: &mut String) -> Result<(), ParseError> {
325        let digits = self.hex(width);
326        let point = u32::from_str_radix(&digits, 16).map_err(|_| self.fail("a unicode escape wants hex digits"))?;
327
328        out.push(char::from_u32(point).ok_or_else(|| self.fail("not a Unicode scalar value"))?);
329        Ok(())
330    }
331
332    fn hex(&mut self, width: usize) -> String {
333        let mut out = String::new();
334        for _ in 0..width {
335            match self.peek() {
336                Some(c) if c.is_ascii_hexdigit() => {
337                    out.push(c);
338                    self.advance(c);
339                }
340                _ => break,
341            }
342        }
343        out
344    }
345
346    /// Unquoted text up to a stop character. A backslash takes the next
347    /// character whatever it is, and a backslash-newline is a line
348    /// continuation. Empty is how [`word`](Cursor::word) learns a segment
349    /// ended, so it is an error the caller catches rather than a value.
350    fn bare(&mut self, stops: &[char]) -> Result<String, ParseError> {
351        let mut out = String::new();
352
353        while let Some(c) = self.peek() {
354            if c == '\'' || c == '"' || c == '$' {
355                break;
356            }
357            if c == '\\' {
358                let after = &self.rest[1..];
359                match after.chars().next() {
360                    Some('\n') => self.rest = &after[1..],
361                    Some(escaped) => {
362                        out.push(escaped);
363                        self.rest = &after[escaped.len_utf8()..];
364                    }
365                    // The backslash stays unread, so the word ends here.
366                    None => break,
367                }
368                continue;
369            }
370            if stops.contains(&c) {
371                break;
372            }
373            out.push(c);
374            self.advance(c);
375        }
376
377        match out.is_empty() {
378            true => Err(self.fail("expected a word")),
379            false => Ok(out),
380        }
381    }
382}
383
384fn is_octal(c: char) -> bool {
385    c.is_ascii_digit() && c != '8' && c != '9'
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    /// The lexer stands on its own: a grammar with no bash value in it still
393    /// gets bash's word rules, its own stop characters, and one error type.
394    #[test]
395    fn a_grammar_over_other_syntax_builds_on_the_word_rules() {
396        const STOPS: &[char] = &['=', ';', ' '];
397
398        let settings = parse_with(
399            r#"a='one two';b=$'x\ty';c=bare\ word"#,
400            |c| {
401                let mut out = Vec::new();
402                loop {
403                    c.ws0();
404                    let key = c.word(STOPS)?;
405                    c.lit("=")?;
406                    out.push((key, c.word(STOPS)?));
407                    if !c.eat(";") {
408                        return Ok(out);
409                    }
410                }
411            },
412        )
413        .unwrap();
414
415        assert_eq!(
416            settings,
417            [
418                ("a".to_string(), "one two".to_string()),
419                ("b".to_string(), "x\ty".to_string()),
420                ("c".to_string(), "bare word".to_string()),
421            ]
422        );
423    }
424
425    #[test]
426    fn a_grammar_that_stops_early_is_an_error() {
427        let failed = parse_with("abc def", |c| c.word(&[' '])).expect_err("trailing input");
428
429        assert_eq!(failed.at, 3);
430        assert!(
431            failed.message.contains("trailing"),
432            "{failed}"
433        );
434    }
435
436    /// An offset is where the parse actually stopped, not where it started.
437    #[test]
438    fn a_refusal_names_the_position_it_reached() {
439        let failed = parse_with("'a' 'b", |c| {
440            c.word(&[' '])?;
441            c.lit(" ")?;
442            c.word(&[' '])
443        })
444        .expect_err("unterminated");
445
446        assert_eq!(
447            failed.at, 6,
448            "the end of the input, inside the open quote"
449        );
450        assert!(
451            failed.message.contains("unterminated"),
452            "{failed}"
453        );
454    }
455
456    /// An octal escape takes at most three digits, so a fourth is text.
457    #[test]
458    fn an_octal_escape_leaves_what_it_does_not_take() {
459        assert_eq!(
460            parse_with(r"$'\1234'", |c| c.word(&[])).unwrap(),
461            "\u{53}4"
462        );
463    }
464}