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        let list = self.maybe_compound_list_boxed().await?;
73
74        let close = self.take_token_raw().await?;
75        if close.id != Operator(CloseParen) {
76            let opening_location = open.word.location;
77            let cause = SyntaxError::UnclosedSubshell { opening_location }.into();
78            let location = close.word.location;
79            return Err(Error { cause, location });
80        }
81
82        // TODO allow empty subshell if not POSIXly-correct
83        if list.0.is_empty() {
84            let cause = SyntaxError::EmptySubshell.into();
85            let location = close.word.location;
86            return Err(Error { cause, location });
87        }
88
89        Ok(CompoundCommand::Subshell {
90            body: Rc::new(list),
91            location: open.word.location,
92        })
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::super::error::ErrorCause;
99    use super::super::lex::Lexer;
100    use super::*;
101    use crate::alias::{AliasSet, HashEntry};
102    use crate::source::Location;
103    use crate::source::Source;
104    use assert_matches::assert_matches;
105    use futures_util::FutureExt;
106
107    #[test]
108    fn parser_grouping_short() {
109        let mut lexer = Lexer::with_code("{ :; }");
110        let mut parser = Parser::new(&mut lexer);
111
112        let result = parser.compound_command().now_or_never().unwrap();
113        let compound_command = result.unwrap().unwrap();
114        assert_matches!(compound_command, CompoundCommand::Grouping(list) => {
115            assert_eq!(list.to_string(), ":");
116        });
117    }
118
119    #[test]
120    fn parser_grouping_long() {
121        let mut lexer = Lexer::with_code("{ foo; bar& }");
122        let mut parser = Parser::new(&mut lexer);
123
124        let result = parser.compound_command().now_or_never().unwrap();
125        let compound_command = result.unwrap().unwrap();
126        assert_matches!(compound_command, CompoundCommand::Grouping(list) => {
127            assert_eq!(list.to_string(), "foo; bar&");
128        });
129    }
130
131    #[test]
132    fn parser_grouping_unclosed() {
133        let mut lexer = Lexer::with_code(" { oh no ");
134        let mut parser = Parser::new(&mut lexer);
135
136        let result = parser.compound_command().now_or_never().unwrap();
137        let e = result.unwrap_err();
138        assert_matches!(e.cause,
139            ErrorCause::Syntax(SyntaxError::UnclosedGrouping { opening_location }) => {
140            assert_eq!(*opening_location.code.value.borrow(), " { oh no ");
141            assert_eq!(opening_location.code.start_line_number.get(), 1);
142            assert_eq!(*opening_location.code.source, Source::Unknown);
143            assert_eq!(opening_location.range, 1..2);
144        });
145        assert_eq!(*e.location.code.value.borrow(), " { oh no ");
146        assert_eq!(e.location.code.start_line_number.get(), 1);
147        assert_eq!(*e.location.code.source, Source::Unknown);
148        assert_eq!(e.location.range, 9..9);
149    }
150
151    #[test]
152    fn parser_grouping_empty_posix() {
153        let mut lexer = Lexer::with_code("{ }");
154        let mut parser = Parser::new(&mut lexer);
155
156        let result = parser.compound_command().now_or_never().unwrap();
157        let e = result.unwrap_err();
158        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptyGrouping));
159        assert_eq!(*e.location.code.value.borrow(), "{ }");
160        assert_eq!(e.location.code.start_line_number.get(), 1);
161        assert_eq!(*e.location.code.source, Source::Unknown);
162        assert_eq!(e.location.range, 2..3);
163    }
164
165    #[test]
166    fn parser_grouping_aliasing() {
167        let mut lexer = Lexer::with_code(" { :; end ");
168        #[allow(clippy::mutable_key_type)]
169        let mut aliases = AliasSet::new();
170        let origin = Location::dummy("");
171        aliases.insert(HashEntry::new(
172            "{".to_string(),
173            "".to_string(),
174            false,
175            origin.clone(),
176        ));
177        aliases.insert(HashEntry::new(
178            "}".to_string(),
179            "".to_string(),
180            false,
181            origin.clone(),
182        ));
183        aliases.insert(HashEntry::new(
184            "end".to_string(),
185            "}".to_string(),
186            false,
187            origin,
188        ));
189        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
190
191        let result = parser.compound_command().now_or_never().unwrap();
192        let compound_command = result.unwrap().unwrap();
193        assert_matches!(compound_command, CompoundCommand::Grouping(list) => {
194            assert_eq!(list.to_string(), ":");
195        });
196    }
197
198    #[test]
199    fn parser_subshell_short() {
200        let mut lexer = Lexer::with_code("(:)");
201        let mut parser = Parser::new(&mut lexer);
202
203        let result = parser.compound_command().now_or_never().unwrap();
204        let compound_command = result.unwrap().unwrap();
205        assert_matches!(compound_command, CompoundCommand::Subshell { body, location } => {
206            assert_eq!(body.to_string(), ":");
207            assert_eq!(*location.code.value.borrow(), "(:)");
208            assert_eq!(location.code.start_line_number.get(), 1);
209            assert_eq!(*location.code.source, Source::Unknown);
210            assert_eq!(location.range, 0..1);
211        });
212    }
213
214    #[test]
215    fn parser_subshell_long() {
216        let mut lexer = Lexer::with_code("( foo& bar; )");
217        let mut parser = Parser::new(&mut lexer);
218
219        let result = parser.compound_command().now_or_never().unwrap();
220        let compound_command = result.unwrap().unwrap();
221        assert_matches!(compound_command, CompoundCommand::Subshell { body, location } => {
222            assert_eq!(body.to_string(), "foo& bar");
223            assert_eq!(*location.code.value.borrow(), "( foo& bar; )");
224            assert_eq!(location.code.start_line_number.get(), 1);
225            assert_eq!(*location.code.source, Source::Unknown);
226            assert_eq!(location.range, 0..1);
227        });
228    }
229
230    #[test]
231    fn parser_subshell_unclosed() {
232        let mut lexer = Lexer::with_code(" ( oh no");
233        let mut parser = Parser::new(&mut lexer);
234
235        let result = parser.compound_command().now_or_never().unwrap();
236        let e = result.unwrap_err();
237        assert_matches!(e.cause,
238            ErrorCause::Syntax(SyntaxError::UnclosedSubshell { opening_location }) => {
239            assert_eq!(*opening_location.code.value.borrow(), " ( oh no");
240            assert_eq!(opening_location.code.start_line_number.get(), 1);
241            assert_eq!(*opening_location.code.source, Source::Unknown);
242            assert_eq!(opening_location.range, 1..2);
243        });
244        assert_eq!(*e.location.code.value.borrow(), " ( oh no");
245        assert_eq!(e.location.code.start_line_number.get(), 1);
246        assert_eq!(*e.location.code.source, Source::Unknown);
247        assert_eq!(e.location.range, 8..8);
248    }
249
250    #[test]
251    fn parser_subshell_empty_posix() {
252        let mut lexer = Lexer::with_code("( )");
253        let mut parser = Parser::new(&mut lexer);
254
255        let result = parser.compound_command().now_or_never().unwrap();
256        let e = result.unwrap_err();
257        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptySubshell));
258        assert_eq!(*e.location.code.value.borrow(), "( )");
259        assert_eq!(e.location.code.start_line_number.get(), 1);
260        assert_eq!(*e.location.code.source, Source::Unknown);
261        assert_eq!(e.location.range, 2..3);
262    }
263}