Skip to main content

yash_syntax/parser/lex/
escape.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Parsing escape units and escaped strings
18
19use super::core::Lexer;
20use crate::parser::core::Result;
21use crate::parser::{Error, SyntaxError};
22use crate::syntax::EscapeUnit::{self, *};
23use crate::syntax::EscapedString;
24
25impl Lexer<'_> {
26    /// Parses a hexadecimal digit.
27    async fn hex_digit(&mut self) -> Result<Option<u32>> {
28        if let Some(c) = self.peek_char().await?
29            && let Some(digit) = c.to_digit(16)
30        {
31            self.consume_char();
32            return Ok(Some(digit));
33        }
34        Ok(None)
35    }
36
37    /// Parses a sequence of hexadecimal digits.
38    ///
39    /// This function consumes up to `count` hexadecimal digits and returns the
40    /// value as a single number. If fewer than `count` digits are found, the
41    /// function returns the value of the digits found so far. If no digits are
42    /// found, the function returns `Ok(None)`.
43    async fn hex_digits(&mut self, count: usize) -> Result<Option<u32>> {
44        let Some(digit) = self.hex_digit().await? else {
45            return Ok(None);
46        };
47        let mut value = digit;
48        for _ in 1..count {
49            let Some(digit) = self.hex_digit().await? else {
50                break;
51            };
52            value = (value << 4) | digit;
53        }
54        Ok(Some(value))
55    }
56
57    /// Returns an error for a non-portable escape sequence that starts at the
58    /// given index and ends at the current position.
59    #[must_use]
60    fn non_portable_escape(&self, start_index: usize) -> Error {
61        Error {
62            cause: SyntaxError::NonPortableEscape.into(),
63            location: self.location_range(start_index..self.index()),
64        }
65    }
66
67    /// Parses an escape unit.
68    ///
69    /// This function tests if the next character is an escape sequence and
70    /// returns it if it is. If the next character is not an escape sequence, it
71    /// returns as `EscapeUnit::Literal`. If there is no next character, it
72    /// returns `Ok(None)`. It returns an error if an invalid escape sequence is
73    /// found.
74    ///
75    /// This function should be called in a context where [line continuations are
76    /// disabled](Self::disable_line_continuation), so that backslash-newline
77    /// pairs are not removed before they are parsed as escape sequences.
78    pub async fn escape_unit(&mut self) -> Result<Option<EscapeUnit>> {
79        let Some(c1) = self.peek_char().await? else {
80            return Ok(None);
81        };
82        let start_index = self.index();
83        self.consume_char();
84        if c1 != '\\' {
85            return Ok(Some(Literal(c1)));
86        }
87
88        let Some(c2) = self.peek_char().await? else {
89            let cause = SyntaxError::IncompleteEscape.into();
90            let location = self.location().await?.clone();
91            return Err(Error { cause, location });
92        };
93        self.consume_char();
94
95        let portable = self.mode().portable;
96        match c2 {
97            '"' => Ok(Some(DoubleQuote)),
98            '\'' => Ok(Some(SingleQuote)),
99            '\\' => Ok(Some(Backslash)),
100            '?' if portable => Err(self.non_portable_escape(start_index)),
101            '?' => Ok(Some(Question)),
102            'a' => Ok(Some(Alert)),
103            'b' => Ok(Some(Backspace)),
104            'e' => Ok(Some(Escape)),
105            'E' if portable => Err(self.non_portable_escape(start_index)),
106            'E' => Ok(Some(Escape)),
107            'f' => Ok(Some(FormFeed)),
108            'n' => Ok(Some(Newline)),
109            'r' => Ok(Some(CarriageReturn)),
110            't' => Ok(Some(Tab)),
111            'v' => Ok(Some(VerticalTab)),
112
113            'c' => {
114                let start_index = self.index();
115                let Some(c3) = self.peek_char().await? else {
116                    let cause = SyntaxError::IncompleteControlEscape.into();
117                    let location = self.location().await?.clone();
118                    return Err(Error { cause, location });
119                };
120                self.consume_char();
121                match c3.to_ascii_uppercase() {
122                    '\\' => {
123                        let Some('\\') = self.peek_char().await? else {
124                            let cause = SyntaxError::IncompleteControlBackslashEscape.into();
125                            let location = self.location().await?.clone();
126                            return Err(Error { cause, location });
127                        };
128                        self.consume_char();
129                        Ok(Some(Control(0x1C)))
130                    }
131
132                    // POSIX's `^c` column does not include `@`, so `\c@` (which
133                    // would yield a NUL) is not portable.
134                    // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/stty.html#tag_20_116_05_05
135                    '@' if portable => Err(self.non_portable_escape(start_index)),
136
137                    c3 @ ('\u{3F}'..'\u{60}') => Ok(Some(Control(c3 as u8 ^ 0x40))),
138
139                    _ => {
140                        let cause = SyntaxError::InvalidControlEscape.into();
141                        let location = self.location_range(start_index..self.index());
142                        Err(Error { cause, location })
143                    }
144                }
145            }
146
147            'x' => {
148                // POSIX leaves the result unspecified if more than two
149                // hexadecimal digits follow `\x`, so we only consume at most
150                // two digits.
151                let Some(value) = self.hex_digits(2).await? else {
152                    let cause = SyntaxError::IncompleteHexEscape.into();
153                    let location = self.location().await?.clone();
154                    return Err(Error { cause, location });
155                };
156                // If more digits follow, the escape sequence is non-portable.
157                if portable && self.hex_digits(usize::MAX).await?.is_some() {
158                    let cause = SyntaxError::TooLongHexEscape.into();
159                    let location = self.location_range(start_index..self.index());
160                    return Err(Error { cause, location });
161                }
162                Ok(Some(Hex(value as u8)))
163            }
164
165            'u' if portable => Err(self.non_portable_escape(start_index)),
166            'u' => {
167                let Some(value) = self.hex_digits(4).await? else {
168                    let cause = SyntaxError::IncompleteShortUnicodeEscape.into();
169                    let location = self.location().await?.clone();
170                    return Err(Error { cause, location });
171                };
172                if let Some(c) = char::from_u32(value) {
173                    Ok(Some(Unicode(c)))
174                } else {
175                    let cause = SyntaxError::UnicodeEscapeOutOfRange.into();
176                    let location = self.location_range(start_index..self.index());
177                    Err(Error { cause, location })
178                }
179            }
180
181            'U' if portable => Err(self.non_portable_escape(start_index)),
182            'U' => {
183                let Some(value) = self.hex_digits(8).await? else {
184                    let cause = SyntaxError::IncompleteLongUnicodeEscape.into();
185                    let location = self.location().await?.clone();
186                    return Err(Error { cause, location });
187                };
188                if let Some(c) = char::from_u32(value) {
189                    Ok(Some(Unicode(c)))
190                } else {
191                    let cause = SyntaxError::UnicodeEscapeOutOfRange.into();
192                    let location = self.location_range(start_index..self.index());
193                    Err(Error { cause, location })
194                }
195            }
196
197            _ => {
198                // Consume at most 3 octal digits (including c2)
199                let Some(mut value) = c2.to_digit(8) else {
200                    let cause = SyntaxError::InvalidEscape.into();
201                    let location = self.location_range(start_index..self.index());
202                    return Err(Error { cause, location });
203                };
204                for _ in 0..2 {
205                    let Some(digit) = self.peek_char().await? else {
206                        break;
207                    };
208                    let Some(digit) = digit.to_digit(8) else {
209                        break;
210                    };
211                    value = value * 8 + digit;
212                    self.consume_char();
213                }
214                if let Ok(value) = value.try_into() {
215                    Ok(Some(Octal(value)))
216                } else {
217                    let cause = SyntaxError::OctalEscapeOutOfRange.into();
218                    let location = self.location_range(start_index..self.index());
219                    Err(Error { cause, location })
220                }
221            }
222        }
223    }
224
225    /// Parses an escaped string.
226    ///
227    /// The `is_delimiter` function is called with each character in the string
228    /// to determine if it is a delimiter. If `is_delimiter` returns `true`, the
229    /// character is not consumed and the function returns the string up to that
230    /// point. Otherwise, the character is consumed and the function continues.
231    ///
232    /// The string may contain escape sequences as defined in [`EscapeUnit`].
233    ///
234    /// Escaped strings typically appear as the content of
235    /// [dollar-single-quotes], so `is_delimiter` is usually `|c| c == '\''`.
236    ///
237    /// [dollar-single-quotes]: crate::syntax::WordUnit::DollarSingleQuote
238    pub async fn escaped_string<F>(&mut self, mut is_delimiter: F) -> Result<EscapedString>
239    where
240        F: FnMut(char) -> bool,
241    {
242        self.escaped_string_dyn(&mut is_delimiter).await
243    }
244
245    /// Dynamic version of [`Self::escaped_string`]
246    async fn escaped_string_dyn(
247        &mut self,
248        is_delimiter: &mut dyn FnMut(char) -> bool,
249    ) -> Result<EscapedString> {
250        let mut this = self.disable_line_continuation();
251        let mut units = Vec::new();
252
253        while let Some(c) = this.peek_char().await? {
254            if is_delimiter(c) {
255                break;
256            }
257            let Some(unit) = this.escape_unit().await? else {
258                break;
259            };
260            units.push(unit);
261        }
262
263        Ok(EscapedString(units))
264    }
265
266    /// Parses an escaped string enclosed in single quotes.
267    ///
268    /// This function is meant to be used for parsing dollar-single-quoted
269    /// strings. The initial `$` must have been consumed before calling this
270    /// function, which expects an opening `'` to be the next character. If the
271    /// next character is not `'`, this function returns `None`.
272    ///
273    /// This function consumes up to and including the closing `'`. If the
274    /// closing `'` is not found or an invalid escape sequence is found, this
275    /// function returns an error.
276    pub(super) async fn single_quoted_escaped_string(&mut self) -> Result<Option<EscapedString>> {
277        let is_single_quote = |c| c == '\'';
278
279        // Consume the opening single quote
280        let Some(quote) = self.consume_char_if(is_single_quote).await? else {
281            return Ok(None);
282        };
283        let opening_location = quote.location.clone();
284
285        let content = self.escaped_string(is_single_quote).await?;
286
287        // Consume the closing single quote
288        if let Some(quote) = self.peek_char().await? {
289            debug_assert_eq!(quote, '\'');
290            self.consume_char();
291            Ok(Some(content))
292        } else {
293            let cause = SyntaxError::UnclosedDollarSingleQuote { opening_location }.into();
294            let location = self.location().await?.clone();
295            Err(Error { cause, location })
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use crate::parser::ErrorCause;
304    use crate::source::Source;
305    use assert_matches::assert_matches;
306    use futures_util::FutureExt as _;
307
308    #[test]
309    fn escape_unit_literal() {
310        let mut lexer = Lexer::with_code("bar");
311        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
312        assert_eq!(result, Some(Literal('b')));
313        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('a')));
314    }
315
316    #[test]
317    fn escape_unit_named_escapes() {
318        let mut lexer = Lexer::with_code(r#"\""\'\\\?\a\b\e\E\f\n\r\t\v"#);
319        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
320        assert_eq!(result, Some(DoubleQuote));
321        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
322        assert_eq!(result, Some(Literal('"')));
323        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
324        assert_eq!(result, Some(SingleQuote));
325        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
326        assert_eq!(result, Some(Backslash));
327        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
328        assert_eq!(result, Some(Question));
329        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
330        assert_eq!(result, Some(Alert));
331        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
332        assert_eq!(result, Some(Backspace));
333        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
334        assert_eq!(result, Some(Escape));
335        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
336        assert_eq!(result, Some(Escape));
337        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
338        assert_eq!(result, Some(FormFeed));
339        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
340        assert_eq!(result, Some(Newline));
341        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
342        assert_eq!(result, Some(CarriageReturn));
343        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
344        assert_eq!(result, Some(Tab));
345        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
346        assert_eq!(result, Some(VerticalTab));
347        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(None));
348    }
349
350    #[test]
351    fn escape_unit_incomplete_escapes() {
352        let mut lexer = Lexer::with_code(r"\");
353        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
354        assert_matches!(
355            error.cause,
356            ErrorCause::Syntax(SyntaxError::IncompleteEscape)
357        );
358        assert_eq!(*error.location.code.value.borrow(), r"\");
359        assert_eq!(error.location.code.start_line_number.get(), 1);
360        assert_eq!(*error.location.code.source, Source::Unknown);
361        assert_eq!(error.location.range, 1..1);
362    }
363
364    #[test]
365    fn escape_unit_control_escapes() {
366        let mut lexer = Lexer::with_code(r"\c@\cA\cz\c^\c?\c\\");
367        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
368        assert_eq!(result, Some(Control(0x00)));
369        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
370        assert_eq!(result, Some(Control(0x01)));
371        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
372        assert_eq!(result, Some(Control(0x1A)));
373        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
374        assert_eq!(result, Some(Control(0x1E)));
375        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
376        assert_eq!(result, Some(Control(0x7F)));
377        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
378        assert_eq!(result, Some(Control(0x1C)));
379        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(None));
380    }
381
382    #[test]
383    fn escape_unit_incomplete_control_escape() {
384        let mut lexer = Lexer::with_code(r"\c");
385        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
386        assert_matches!(
387            error.cause,
388            ErrorCause::Syntax(SyntaxError::IncompleteControlEscape)
389        );
390        assert_eq!(*error.location.code.value.borrow(), r"\c");
391        assert_eq!(error.location.code.start_line_number.get(), 1);
392        assert_eq!(*error.location.code.source, Source::Unknown);
393        assert_eq!(error.location.range, 2..2);
394    }
395
396    #[test]
397    fn escape_unit_incomplete_control_backslash_escapes() {
398        let mut lexer = Lexer::with_code(r"\c\");
399        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
400        assert_matches!(
401            error.cause,
402            ErrorCause::Syntax(SyntaxError::IncompleteControlBackslashEscape)
403        );
404        assert_eq!(*error.location.code.value.borrow(), r"\c\");
405        assert_eq!(error.location.code.start_line_number.get(), 1);
406        assert_eq!(*error.location.code.source, Source::Unknown);
407        assert_eq!(error.location.range, 3..3);
408
409        let mut lexer = Lexer::with_code(r"\c\a");
410        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
411        assert_matches!(
412            error.cause,
413            ErrorCause::Syntax(SyntaxError::IncompleteControlBackslashEscape)
414        );
415        assert_eq!(*error.location.code.value.borrow(), r"\c\a");
416        assert_eq!(error.location.code.start_line_number.get(), 1);
417        assert_eq!(*error.location.code.source, Source::Unknown);
418        assert_eq!(error.location.range, 3..4);
419    }
420
421    #[test]
422    fn escape_unit_unknown_control_escape() {
423        let mut lexer = Lexer::with_code(r"\c!`");
424        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
425        assert_matches!(
426            error.cause,
427            ErrorCause::Syntax(SyntaxError::InvalidControlEscape)
428        );
429        assert_eq!(*error.location.code.value.borrow(), r"\c!`");
430        assert_eq!(error.location.code.start_line_number.get(), 1);
431        assert_eq!(*error.location.code.source, Source::Unknown);
432        assert_eq!(error.location.range, 2..3);
433    }
434
435    #[test]
436    fn escape_unit_octal_escapes() {
437        let mut lexer = Lexer::with_code(r"\0\07\234\0123");
438        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
439        assert_eq!(result, Some(Octal(0o0)));
440        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
441        assert_eq!(result, Some(Octal(0o7)));
442        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
443        assert_eq!(result, Some(Octal(0o234)));
444        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
445        assert_eq!(result, Some(Octal(0o12)));
446        // At most 3 octal digits are consumed
447        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('3')));
448
449        let mut lexer = Lexer::with_code(r"\787");
450        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
451        // '8' is not an octal digit
452        assert_eq!(result, Some(Octal(0o7)));
453
454        let mut lexer = Lexer::with_code(r"\12");
455        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
456        // Reaching the end of the input is okay
457        assert_eq!(result, Some(Octal(0o12)));
458    }
459
460    #[test]
461    fn escape_unit_non_byte_octal_escape() {
462        let mut lexer = Lexer::with_code(r"\400");
463        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
464        assert_matches!(
465            error.cause,
466            ErrorCause::Syntax(SyntaxError::OctalEscapeOutOfRange)
467        );
468        assert_eq!(*error.location.code.value.borrow(), r"\400");
469        assert_eq!(error.location.code.start_line_number.get(), 1);
470        assert_eq!(*error.location.code.source, Source::Unknown);
471        assert_eq!(error.location.range, 0..4);
472    }
473
474    #[test]
475    fn escape_unit_hexadecimal_escapes() {
476        let mut lexer = Lexer::with_code(r"\x0\x7F\xd4A");
477        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
478        assert_eq!(result, Some(Hex(0x0)));
479        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
480        assert_eq!(result, Some(Hex(0x7F)));
481        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
482        // At most 2 hexadecimal digits are consumed
483        assert_eq!(result, Some(Hex(0xD4)));
484        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('A')));
485
486        let mut lexer = Lexer::with_code(r"\xb");
487        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
488        // Reaching the end of the input is okay
489        assert_eq!(result, Some(Hex(0xB)));
490    }
491
492    #[test]
493    fn escape_unit_incomplete_hexadecimal_escape() {
494        let mut lexer = Lexer::with_code(r"\x");
495        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
496        assert_matches!(
497            error.cause,
498            ErrorCause::Syntax(SyntaxError::IncompleteHexEscape)
499        );
500        assert_eq!(*error.location.code.value.borrow(), r"\x");
501        assert_eq!(error.location.code.start_line_number.get(), 1);
502        assert_eq!(*error.location.code.source, Source::Unknown);
503        assert_eq!(error.location.range, 2..2);
504    }
505
506    #[test]
507    fn escape_unit_unicode_escapes() {
508        let mut lexer = Lexer::with_code(r"\u20\u4B9d0");
509        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
510        assert_eq!(result, Some(Unicode('\u{20}')));
511        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
512        assert_eq!(result, Some(Unicode('\u{4B9D}')));
513        // At most 4 hexadecimal digits are consumed
514        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('0')));
515
516        let mut lexer = Lexer::with_code(r"\U42\U0001f4A9b");
517        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
518        assert_eq!(result, Some(Unicode('\u{42}')));
519        let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
520        assert_eq!(result, Some(Unicode('\u{1F4A9}')));
521        // At most 8 hexadecimal digits are consumed
522        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('b')));
523    }
524
525    #[test]
526    fn escape_unit_incomplete_unicode_escapes() {
527        let mut lexer = Lexer::with_code(r"\u");
528        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
529        assert_matches!(
530            error.cause,
531            ErrorCause::Syntax(SyntaxError::IncompleteShortUnicodeEscape)
532        );
533        assert_eq!(*error.location.code.value.borrow(), r"\u");
534        assert_eq!(error.location.code.start_line_number.get(), 1);
535        assert_eq!(*error.location.code.source, Source::Unknown);
536        assert_eq!(error.location.range, 2..2);
537
538        let mut lexer = Lexer::with_code(r"\U");
539        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
540        assert_matches!(
541            error.cause,
542            ErrorCause::Syntax(SyntaxError::IncompleteLongUnicodeEscape)
543        );
544        assert_eq!(*error.location.code.value.borrow(), r"\U");
545        assert_eq!(error.location.code.start_line_number.get(), 1);
546        assert_eq!(*error.location.code.source, Source::Unknown);
547        assert_eq!(error.location.range, 2..2);
548    }
549
550    #[test]
551    fn escape_unit_invalid_unicode_escapes() {
552        // U+D800 is not a valid Unicode scalar value
553        let mut lexer = Lexer::with_code(r"\uD800");
554        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
555        assert_matches!(
556            error.cause,
557            ErrorCause::Syntax(SyntaxError::UnicodeEscapeOutOfRange)
558        );
559        assert_eq!(*error.location.code.value.borrow(), r"\uD800");
560        assert_eq!(error.location.code.start_line_number.get(), 1);
561        assert_eq!(*error.location.code.source, Source::Unknown);
562        assert_eq!(error.location.range, 0..6);
563    }
564
565    #[test]
566    fn escape_unit_unknown_escape() {
567        let mut lexer = Lexer::with_code(r"\!");
568        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
569        assert_matches!(error.cause, ErrorCause::Syntax(SyntaxError::InvalidEscape));
570        assert_eq!(*error.location.code.value.borrow(), r"\!");
571        assert_eq!(error.location.code.start_line_number.get(), 1);
572        assert_eq!(*error.location.code.source, Source::Unknown);
573        assert_eq!(error.location.range, 0..2);
574    }
575
576    fn portable_mode() -> yash_env::parser::Mode {
577        let mut mode = yash_env::parser::Mode::default();
578        mode.portable = true;
579        mode
580    }
581
582    #[test]
583    fn escape_unit_non_portable_escapes_rejected_in_portable_mode() {
584        for (code, range) in [
585            (r"\E", 0..2),
586            (r"\?", 0..2),
587            (r"\u0041", 0..2),
588            (r"\U00000041", 0..2),
589            (r"\c@", 2..3),
590        ] {
591            let mut lexer = Lexer::with_code(code);
592            lexer.set_mode(portable_mode());
593            let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
594            assert_matches!(
595                error.cause,
596                ErrorCause::Syntax(SyntaxError::NonPortableEscape),
597                "code={code:?}"
598            );
599            assert_eq!(error.location.range, range, "code={code:?}");
600        }
601    }
602
603    #[test]
604    fn escape_unit_too_long_hex_escape_rejected_in_portable_mode() {
605        // A third hexadecimal digit is unspecified by POSIX.
606        let mut lexer = Lexer::with_code(r"\xABC");
607        lexer.set_mode(portable_mode());
608        let error = lexer.escape_unit().now_or_never().unwrap().unwrap_err();
609        assert_matches!(
610            error.cause,
611            ErrorCause::Syntax(SyntaxError::TooLongHexEscape)
612        );
613        assert_eq!(error.location.range, 0..5);
614    }
615
616    #[test]
617    fn escape_unit_portable_escapes_accepted_in_portable_mode() {
618        // The lowercase `\e`, two-digit `\x`, and other `\cX` are portable.
619        for (code, expected) in [
620            (r"\e", Escape),
621            (r"\xAB", Hex(0xAB)),
622            (r"\cA", Control(1)),
623            (r"\n", Newline),
624        ] {
625            let mut lexer = Lexer::with_code(code);
626            lexer.set_mode(portable_mode());
627            let result = lexer.escape_unit().now_or_never().unwrap().unwrap();
628            assert_eq!(result, Some(expected), "code={code:?}");
629        }
630    }
631
632    #[test]
633    fn escaped_string_literals() {
634        let mut lexer = Lexer::with_code("foo");
635        let EscapedString(content) = lexer
636            .escaped_string(|_| false)
637            .now_or_never()
638            .unwrap()
639            .unwrap();
640        assert_eq!(content, [Literal('f'), Literal('o'), Literal('o')]);
641    }
642
643    #[test]
644    fn escaped_string_mixed() {
645        let mut lexer = Lexer::with_code(r"foo\bar");
646        let EscapedString(content) = lexer
647            .escaped_string(|_| false)
648            .now_or_never()
649            .unwrap()
650            .unwrap();
651        assert_eq!(
652            content,
653            [
654                Literal('f'),
655                Literal('o'),
656                Literal('o'),
657                Backspace,
658                Literal('a'),
659                Literal('r')
660            ]
661        );
662    }
663
664    #[test]
665    fn no_line_continuations_in_escaped_string() {
666        let mut lexer = Lexer::with_code("\\\\\n");
667        let EscapedString(content) = lexer
668            .escaped_string(|_| false)
669            .now_or_never()
670            .unwrap()
671            .unwrap();
672        assert_eq!(content, [Backslash, Literal('\n')]);
673
674        let mut lexer = Lexer::with_code("\\\n");
675        let error = lexer
676            .escaped_string(|_| false)
677            .now_or_never()
678            .unwrap()
679            .unwrap_err();
680        assert_matches!(error.cause, ErrorCause::Syntax(SyntaxError::InvalidEscape));
681        assert_eq!(*error.location.code.value.borrow(), "\\\n");
682        assert_eq!(error.location.code.start_line_number.get(), 1);
683        assert_eq!(*error.location.code.source, Source::Unknown);
684        assert_eq!(error.location.range, 0..2);
685    }
686
687    #[test]
688    fn single_quoted_escaped_string_empty() {
689        let mut lexer = Lexer::with_code("''");
690        let result = lexer
691            .single_quoted_escaped_string()
692            .now_or_never()
693            .unwrap()
694            .unwrap();
695        assert_eq!(result, Some(EscapedString(vec![])));
696    }
697
698    #[test]
699    fn single_quoted_escaped_string_nonempty() {
700        let mut lexer = Lexer::with_code(r"'foo\e'x");
701        let result = lexer
702            .single_quoted_escaped_string()
703            .now_or_never()
704            .unwrap()
705            .unwrap();
706        assert_matches!(result, Some(EscapedString(content)) => {
707            assert_eq!(
708                content,
709                [
710                    Literal('f'),
711                    Literal('o'),
712                    Literal('o'),
713                    Escape,
714                ]
715            );
716        });
717        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('x')));
718    }
719
720    #[test]
721    fn single_quoted_escaped_string_unclosed() {
722        let mut lexer = Lexer::with_code("'foo");
723        let error = lexer
724            .single_quoted_escaped_string()
725            .now_or_never()
726            .unwrap()
727            .unwrap_err();
728        assert_matches!(
729            error.cause,
730            ErrorCause::Syntax(SyntaxError::UnclosedDollarSingleQuote { opening_location }) => {
731                assert_eq!(*opening_location.code.value.borrow(), "'foo");
732                assert_eq!(opening_location.code.start_line_number.get(), 1);
733                assert_eq!(*opening_location.code.source, Source::Unknown);
734                assert_eq!(opening_location.range, 0..1);
735            }
736        );
737        assert_eq!(*error.location.code.value.borrow(), "'foo");
738        assert_eq!(error.location.code.start_line_number.get(), 1);
739        assert_eq!(*error.location.code.source, Source::Unknown);
740        assert_eq!(error.location.range, 4..4);
741    }
742}