yash_syntax/parser/lex/
backquote.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2020 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//! Part of the lexer that parses backquotes
18
19use super::core::WordContext;
20use super::core::WordLexer;
21use crate::parser::core::Result;
22use crate::parser::error::Error;
23use crate::parser::error::SyntaxError;
24use crate::syntax::BackquoteUnit;
25use crate::syntax::TextUnit;
26
27impl WordLexer<'_, '_> {
28    /// Parses a backquote unit.
29    async fn backquote_unit(&mut self) -> Result<Option<BackquoteUnit>> {
30        if self.skip_if(|c| c == '\\').await? {
31            let double_quote_escapable = match self.context {
32                WordContext::Word => false,
33                WordContext::Text => true,
34            };
35            let is_escapable =
36                |c| matches!(c, '$' | '`' | '\\') || c == '"' && double_quote_escapable;
37            if let Some(c) = self.consume_char_if(is_escapable).await? {
38                return Ok(Some(BackquoteUnit::Backslashed(c.value)));
39            } else {
40                return Ok(Some(BackquoteUnit::Literal('\\')));
41            }
42        }
43
44        if let Some(c) = self.consume_char_if(|c| c != '`').await? {
45            return Ok(Some(BackquoteUnit::Literal(c.value)));
46        }
47
48        Ok(None)
49    }
50
51    /// Parses a command substitution of the form `` `...` ``.
52    ///
53    /// If the next character is a backquote, the command substitution is parsed
54    /// up to the closing backquote (inclusive). It is a syntax error if there is
55    /// no closing backquote.
56    ///
57    /// Between the backquotes, only backslashes can have special meanings. A
58    /// backslash is an escape character if it precedes a dollar, backquote, or
59    /// another backslash. If `self.context` is `Text`, double quotes can also
60    /// be backslash-escaped.
61    pub async fn backquote(&mut self) -> Result<Option<TextUnit>> {
62        let start = self.index();
63        let opening_location = match self.consume_char_if(|c| c == '`').await? {
64            None => return Ok(None),
65            Some(c) => c.location.clone(),
66        };
67
68        let mut content = Vec::new();
69        while let Some(unit) = self.backquote_unit().await? {
70            content.push(unit);
71        }
72
73        if self.skip_if(|c| c == '`').await? {
74            let location = self.location_range(start..self.index());
75            Ok(Some(TextUnit::Backquote { content, location }))
76        } else {
77            let cause = SyntaxError::UnclosedBackquote { opening_location }.into();
78            let location = self.location().await?.clone();
79            Err(Error { cause, location })
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::parser::error::ErrorCause;
88    use crate::parser::lex::Lexer;
89    use crate::source::Source;
90    use assert_matches::assert_matches;
91    use futures_util::FutureExt;
92
93    #[test]
94    fn lexer_backquote_not_backquote() {
95        let mut lexer = Lexer::with_code("X");
96        let mut lexer = WordLexer {
97            lexer: &mut lexer,
98            context: WordContext::Word,
99        };
100        let result = lexer.backquote().now_or_never().unwrap().unwrap();
101        assert_eq!(result, None);
102    }
103
104    #[test]
105    fn lexer_backquote_empty() {
106        let mut lexer = Lexer::with_code("``");
107        let mut lexer = WordLexer {
108            lexer: &mut lexer,
109            context: WordContext::Word,
110        };
111        let result = lexer.backquote().now_or_never().unwrap().unwrap().unwrap();
112        assert_matches!(result, TextUnit::Backquote { content, location } => {
113            assert_eq!(content, []);
114            assert_eq!(location.range, 0..2);
115        });
116
117        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(None));
118    }
119
120    #[test]
121    fn lexer_backquote_literals() {
122        let mut lexer = Lexer::with_code("`echo`");
123        let mut lexer = WordLexer {
124            lexer: &mut lexer,
125            context: WordContext::Word,
126        };
127        let result = lexer.backquote().now_or_never().unwrap().unwrap().unwrap();
128        assert_matches!(result, TextUnit::Backquote { content, location } => {
129            assert_eq!(
130                content,
131                [
132                    BackquoteUnit::Literal('e'),
133                    BackquoteUnit::Literal('c'),
134                    BackquoteUnit::Literal('h'),
135                    BackquoteUnit::Literal('o')
136                ]
137            );
138            assert_eq!(location.range, 0..6);
139        });
140
141        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(None));
142    }
143
144    #[test]
145    fn lexer_backquote_with_escapes_double_quote_escapable() {
146        let mut lexer = Lexer::with_code(r#"`a\a\$\`\\\"\'`"#);
147        let mut lexer = WordLexer {
148            lexer: &mut lexer,
149            context: WordContext::Text,
150        };
151        let result = lexer.backquote().now_or_never().unwrap().unwrap().unwrap();
152        assert_matches!(result, TextUnit::Backquote { content, location } => {
153            assert_eq!(
154                content,
155                [
156                    BackquoteUnit::Literal('a'),
157                    BackquoteUnit::Literal('\\'),
158                    BackquoteUnit::Literal('a'),
159                    BackquoteUnit::Backslashed('$'),
160                    BackquoteUnit::Backslashed('`'),
161                    BackquoteUnit::Backslashed('\\'),
162                    BackquoteUnit::Backslashed('"'),
163                    BackquoteUnit::Literal('\\'),
164                    BackquoteUnit::Literal('\'')
165                ]
166            );
167            assert_eq!(location.range, 0..15);
168        });
169
170        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(None));
171    }
172
173    #[test]
174    fn lexer_backquote_with_escapes_double_quote_not_escapable() {
175        let mut lexer = Lexer::with_code(r#"`a\a\$\`\\\"\'`"#);
176        let mut lexer = WordLexer {
177            lexer: &mut lexer,
178            context: WordContext::Word,
179        };
180        let result = lexer.backquote().now_or_never().unwrap().unwrap().unwrap();
181        assert_matches!(result, TextUnit::Backquote { content, location } => {
182            assert_eq!(
183                content,
184                [
185                    BackquoteUnit::Literal('a'),
186                    BackquoteUnit::Literal('\\'),
187                    BackquoteUnit::Literal('a'),
188                    BackquoteUnit::Backslashed('$'),
189                    BackquoteUnit::Backslashed('`'),
190                    BackquoteUnit::Backslashed('\\'),
191                    BackquoteUnit::Literal('\\'),
192                    BackquoteUnit::Literal('"'),
193                    BackquoteUnit::Literal('\\'),
194                    BackquoteUnit::Literal('\'')
195                ]
196            );
197            assert_eq!(location.range, 0..15);
198        });
199
200        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(None));
201    }
202
203    #[test]
204    fn lexer_backquote_line_continuation() {
205        let mut lexer = Lexer::with_code("`\\\na\\\n\\\nb\\\n`");
206        let mut lexer = WordLexer {
207            lexer: &mut lexer,
208            context: WordContext::Word,
209        };
210        let result = lexer.backquote().now_or_never().unwrap().unwrap().unwrap();
211        assert_matches!(result, TextUnit::Backquote { content, location } => {
212            assert_eq!(
213                content,
214                [BackquoteUnit::Literal('a'), BackquoteUnit::Literal('b')]
215            );
216            assert_eq!(location.range, 0..12);
217        });
218
219        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(None));
220    }
221
222    #[test]
223    fn lexer_backquote_unclosed_empty() {
224        let mut lexer = Lexer::with_code("`");
225        let mut lexer = WordLexer {
226            lexer: &mut lexer,
227            context: WordContext::Word,
228        };
229        let e = lexer.backquote().now_or_never().unwrap().unwrap_err();
230        assert_matches!(e.cause,
231            ErrorCause::Syntax(SyntaxError::UnclosedBackquote { opening_location }) => {
232            assert_eq!(*opening_location.code.value.borrow(), "`");
233            assert_eq!(opening_location.code.start_line_number.get(), 1);
234            assert_eq!(*opening_location.code.source, Source::Unknown);
235            assert_eq!(opening_location.range, 0..1);
236        });
237        assert_eq!(*e.location.code.value.borrow(), "`");
238        assert_eq!(e.location.code.start_line_number.get(), 1);
239        assert_eq!(*e.location.code.source, Source::Unknown);
240        assert_eq!(e.location.range, 1..1);
241    }
242
243    #[test]
244    fn lexer_backquote_unclosed_nonempty() {
245        let mut lexer = Lexer::with_code("`foo");
246        let mut lexer = WordLexer {
247            lexer: &mut lexer,
248            context: WordContext::Word,
249        };
250        let e = lexer.backquote().now_or_never().unwrap().unwrap_err();
251        assert_matches!(e.cause, ErrorCause::Syntax(SyntaxError::UnclosedBackquote { opening_location }) => {
252            assert_eq!(*opening_location.code.value.borrow(), "`foo");
253            assert_eq!(opening_location.code.start_line_number.get(), 1);
254            assert_eq!(*opening_location.code.source, Source::Unknown);
255            assert_eq!(opening_location.range, 0..1);
256        });
257        assert_eq!(*e.location.code.value.borrow(), "`foo");
258        assert_eq!(e.location.code.start_line_number.get(), 1);
259        assert_eq!(*e.location.code.source, Source::Unknown);
260        assert_eq!(e.location.range, 4..4);
261    }
262}