Skip to main content

yash_syntax/parser/
grouping.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//! Syntax parser for grouping and subshell
18
19use super::core::Parser;
20use super::core::Result;
21use super::error::Error;
22use super::error::SyntaxError;
23use super::lex::Keyword::{CloseBrace, OpenBrace};
24use super::lex::Operator::{CloseParen, OpenParen};
25use super::lex::TokenId::{Operator, Token};
26use crate::syntax::CompoundCommand;
27use std::rc::Rc;
28
29impl Parser<'_, '_> {
30    /// Parses a normal grouping.
31    ///
32    /// The next token must be a `{`.
33    ///
34    /// # Panics
35    ///
36    /// If the first token is not a `{`.
37    pub async fn grouping(&mut self) -> Result<CompoundCommand> {
38        let open = self.take_token_raw().await?;
39        assert_eq!(open.id, Token(Some(OpenBrace)));
40
41        let list = self.maybe_compound_list_boxed().await?;
42
43        let close = self.take_token_raw().await?;
44        if close.id != Token(Some(CloseBrace)) {
45            let opening_location = open.word.location;
46            let cause = SyntaxError::UnclosedGrouping { opening_location }.into();
47            let location = close.word.location;
48            return Err(Error { cause, location });
49        }
50
51        // TODO allow empty subshell if not POSIXly-correct
52        if list.0.is_empty() {
53            let cause = SyntaxError::EmptyGrouping.into();
54            let location = close.word.location;
55            return Err(Error { cause, location });
56        }
57
58        Ok(CompoundCommand::Grouping(list))
59    }
60
61    /// Parses a subshell.
62    ///
63    /// The next token must be a `(`.
64    ///
65    /// # Panics
66    ///
67    /// If the first token is not a `(`.
68    pub async fn subshell(&mut self) -> Result<CompoundCommand> {
69        let open = self.take_token_raw().await?;
70        assert_eq!(open.id, Operator(OpenParen));
71
72        // In portable mode, reject `((` (which other shells parse as an
73        // arithmetic command) at the beginning of a command.
74        if self.mode().portable {
75            let next = self.peek_token().await?;
76            if next.id == Operator(OpenParen) && next.index == open.index + 1 {
77                let location = next.word.location.clone();
78                let cause = SyntaxError::UnsupportedArithmeticCommand.into();
79                return Err(Error { cause, location });
80            }
81        }
82
83        let list = self.maybe_compound_list_boxed().await?;
84
85        let close = self.take_token_raw().await?;
86        if close.id != Operator(CloseParen) {
87            let opening_location = open.word.location;
88            let cause = SyntaxError::UnclosedSubshell { opening_location }.into();
89            let location = close.word.location;
90            return Err(Error { cause, location });
91        }
92
93        // TODO allow empty subshell if not POSIXly-correct
94        if list.0.is_empty() {
95            let cause = SyntaxError::EmptySubshell.into();
96            let location = close.word.location;
97            return Err(Error { cause, location });
98        }
99
100        Ok(CompoundCommand::Subshell {
101            body: Rc::new(list),
102            location: open.word.location,
103        })
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::super::error::ErrorCause;
110    use super::super::lex::Lexer;
111    use super::*;
112    use crate::alias::{AliasSet, HashEntry};
113    use crate::source::Location;
114    use crate::source::Source;
115    use assert_matches::assert_matches;
116    use futures_util::FutureExt as _;
117
118    #[test]
119    fn parser_grouping_short() {
120        let mut lexer = Lexer::with_code("{ :; }");
121        let mut parser = Parser::new(&mut lexer);
122
123        let result = parser.compound_command().now_or_never().unwrap();
124        let compound_command = result.unwrap().unwrap();
125        assert_matches!(compound_command, CompoundCommand::Grouping(list) => {
126            assert_eq!(list.to_string(), ":");
127        });
128    }
129
130    #[test]
131    fn parser_grouping_long() {
132        let mut lexer = Lexer::with_code("{ foo; bar& }");
133        let mut parser = Parser::new(&mut lexer);
134
135        let result = parser.compound_command().now_or_never().unwrap();
136        let compound_command = result.unwrap().unwrap();
137        assert_matches!(compound_command, CompoundCommand::Grouping(list) => {
138            assert_eq!(list.to_string(), "foo; bar&");
139        });
140    }
141
142    #[test]
143    fn parser_grouping_unclosed() {
144        let mut lexer = Lexer::with_code(" { oh no ");
145        let mut parser = Parser::new(&mut lexer);
146
147        let result = parser.compound_command().now_or_never().unwrap();
148        let e = result.unwrap_err();
149        assert_matches!(e.cause,
150            ErrorCause::Syntax(SyntaxError::UnclosedGrouping { opening_location }) => {
151            assert_eq!(*opening_location.code.value.borrow(), " { oh no ");
152            assert_eq!(opening_location.code.start_line_number.get(), 1);
153            assert_eq!(*opening_location.code.source, Source::Unknown);
154            assert_eq!(opening_location.range, 1..2);
155        });
156        assert_eq!(*e.location.code.value.borrow(), " { oh no ");
157        assert_eq!(e.location.code.start_line_number.get(), 1);
158        assert_eq!(*e.location.code.source, Source::Unknown);
159        assert_eq!(e.location.range, 9..9);
160    }
161
162    #[test]
163    fn parser_grouping_empty_posix() {
164        let mut lexer = Lexer::with_code("{ }");
165        let mut parser = Parser::new(&mut lexer);
166
167        let result = parser.compound_command().now_or_never().unwrap();
168        let e = result.unwrap_err();
169        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptyGrouping));
170        assert_eq!(*e.location.code.value.borrow(), "{ }");
171        assert_eq!(e.location.code.start_line_number.get(), 1);
172        assert_eq!(*e.location.code.source, Source::Unknown);
173        assert_eq!(e.location.range, 2..3);
174    }
175
176    #[test]
177    fn parser_grouping_aliasing() {
178        let mut lexer = Lexer::with_code(" { :; end ");
179        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
180        let mut aliases = AliasSet::new();
181        let origin = Location::dummy("");
182        aliases.insert(HashEntry::new(
183            "{".to_string(),
184            "".to_string(),
185            false,
186            origin.clone(),
187        ));
188        aliases.insert(HashEntry::new(
189            "}".to_string(),
190            "".to_string(),
191            false,
192            origin.clone(),
193        ));
194        aliases.insert(HashEntry::new(
195            "end".to_string(),
196            "}".to_string(),
197            false,
198            origin,
199        ));
200        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
201
202        let result = parser.compound_command().now_or_never().unwrap();
203        let compound_command = result.unwrap().unwrap();
204        assert_matches!(compound_command, CompoundCommand::Grouping(list) => {
205            assert_eq!(list.to_string(), ":");
206        });
207    }
208
209    #[test]
210    fn parser_subshell_short() {
211        let mut lexer = Lexer::with_code("(:)");
212        let mut parser = Parser::new(&mut lexer);
213
214        let result = parser.compound_command().now_or_never().unwrap();
215        let compound_command = result.unwrap().unwrap();
216        assert_matches!(compound_command, CompoundCommand::Subshell { body, location } => {
217            assert_eq!(body.to_string(), ":");
218            assert_eq!(*location.code.value.borrow(), "(:)");
219            assert_eq!(location.code.start_line_number.get(), 1);
220            assert_eq!(*location.code.source, Source::Unknown);
221            assert_eq!(location.range, 0..1);
222        });
223    }
224
225    #[test]
226    fn parser_subshell_long() {
227        let mut lexer = Lexer::with_code("( foo& bar; )");
228        let mut parser = Parser::new(&mut lexer);
229
230        let result = parser.compound_command().now_or_never().unwrap();
231        let compound_command = result.unwrap().unwrap();
232        assert_matches!(compound_command, CompoundCommand::Subshell { body, location } => {
233            assert_eq!(body.to_string(), "foo& bar");
234            assert_eq!(*location.code.value.borrow(), "( foo& bar; )");
235            assert_eq!(location.code.start_line_number.get(), 1);
236            assert_eq!(*location.code.source, Source::Unknown);
237            assert_eq!(location.range, 0..1);
238        });
239    }
240
241    #[test]
242    fn parser_subshell_unclosed() {
243        let mut lexer = Lexer::with_code(" ( oh no");
244        let mut parser = Parser::new(&mut lexer);
245
246        let result = parser.compound_command().now_or_never().unwrap();
247        let e = result.unwrap_err();
248        assert_matches!(e.cause,
249            ErrorCause::Syntax(SyntaxError::UnclosedSubshell { opening_location }) => {
250            assert_eq!(*opening_location.code.value.borrow(), " ( oh no");
251            assert_eq!(opening_location.code.start_line_number.get(), 1);
252            assert_eq!(*opening_location.code.source, Source::Unknown);
253            assert_eq!(opening_location.range, 1..2);
254        });
255        assert_eq!(*e.location.code.value.borrow(), " ( oh no");
256        assert_eq!(e.location.code.start_line_number.get(), 1);
257        assert_eq!(*e.location.code.source, Source::Unknown);
258        assert_eq!(e.location.range, 8..8);
259    }
260
261    #[test]
262    fn parser_subshell_empty_posix() {
263        let mut lexer = Lexer::with_code("( )");
264        let mut parser = Parser::new(&mut lexer);
265
266        let result = parser.compound_command().now_or_never().unwrap();
267        let e = result.unwrap_err();
268        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptySubshell));
269        assert_eq!(*e.location.code.value.borrow(), "( )");
270        assert_eq!(e.location.code.start_line_number.get(), 1);
271        assert_eq!(*e.location.code.source, Source::Unknown);
272        assert_eq!(e.location.range, 2..3);
273    }
274
275    fn portable_mode() -> yash_env::parser::Mode {
276        let mut mode = yash_env::parser::Mode::default();
277        mode.portable = true;
278        mode
279    }
280
281    #[test]
282    fn parser_subshell_double_open_paren_rejected_in_portable_mode() {
283        let mut lexer = Lexer::with_code("((:))");
284        lexer.set_mode(portable_mode());
285        let mut parser = Parser::new(&mut lexer);
286
287        let e = parser
288            .compound_command()
289            .now_or_never()
290            .unwrap()
291            .unwrap_err();
292        assert_eq!(
293            e.cause,
294            ErrorCause::Syntax(SyntaxError::UnsupportedArithmeticCommand)
295        );
296        // The error points at the second `(`.
297        assert_eq!(e.location.range, 1..2);
298    }
299
300    #[test]
301    fn parser_subshell_spaced_open_parens_accepted_in_portable_mode() {
302        // A space between the parentheses makes it portable nested subshells.
303        let mut lexer = Lexer::with_code("( (:))");
304        lexer.set_mode(portable_mode());
305        let mut parser = Parser::new(&mut lexer);
306
307        let result = parser.compound_command().now_or_never().unwrap();
308        let compound_command = result.unwrap().unwrap();
309        assert_matches!(compound_command, CompoundCommand::Subshell { body, .. } => {
310            assert_eq!(body.to_string(), "(:)");
311        });
312    }
313
314    #[test]
315    fn parser_subshell_double_open_paren_accepted_without_portable() {
316        // Without the portable option, `((` is parsed as nested subshells.
317        let mut lexer = Lexer::with_code("((:))");
318        let mut parser = Parser::new(&mut lexer);
319
320        let result = parser.compound_command().now_or_never().unwrap();
321        let compound_command = result.unwrap().unwrap();
322        assert_matches!(compound_command, CompoundCommand::Subshell { body, .. } => {
323            assert_eq!(body.to_string(), "(:)");
324        });
325    }
326}