yash_syntax/parser/
compound_command.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 compound command
18//!
19//! Note that the detail parser for each type of compound commands is in another
20//! dedicated module.
21
22use super::core::Parser;
23use super::core::Result;
24use super::error::Error;
25use super::error::SyntaxError;
26use super::lex::Keyword::{Case, Do, Done, For, If, OpenBrace, Until, While};
27use super::lex::Operator::OpenParen;
28use super::lex::TokenId::{Operator, Token};
29use crate::syntax::CompoundCommand;
30use crate::syntax::FullCompoundCommand;
31use crate::syntax::List;
32
33impl Parser<'_, '_> {
34    /// Parses a `do` clause, i.e., a compound list surrounded in `do ... done`.
35    ///
36    /// Returns `Ok(None)` if the first token is not `do`.
37    pub async fn do_clause(&mut self) -> Result<Option<List>> {
38        if self.peek_token().await?.id != Token(Some(Do)) {
39            return Ok(None);
40        }
41
42        let open = self.take_token_raw().await?;
43
44        let list = self.maybe_compound_list_boxed().await?;
45
46        let close = self.take_token_raw().await?;
47        if close.id != Token(Some(Done)) {
48            let opening_location = open.word.location;
49            let cause = SyntaxError::UnclosedDoClause { opening_location }.into();
50            let location = close.word.location;
51            return Err(Error { cause, location });
52        }
53
54        // TODO allow empty do clause if not POSIXly-correct
55        if list.0.is_empty() {
56            let cause = SyntaxError::EmptyDoClause.into();
57            let location = close.word.location;
58            return Err(Error { cause, location });
59        }
60
61        Ok(Some(list))
62    }
63
64    /// Parses a compound command.
65    pub async fn compound_command(&mut self) -> Result<Option<CompoundCommand>> {
66        match self.peek_token().await?.id {
67            Token(Some(OpenBrace)) => self.grouping().await.map(Some),
68            Operator(OpenParen) => self.subshell().await.map(Some),
69            Token(Some(For)) => self.for_loop().await.map(Some),
70            Token(Some(While)) => self.while_loop().await.map(Some),
71            Token(Some(Until)) => self.until_loop().await.map(Some),
72            Token(Some(If)) => self.if_command().await.map(Some),
73            Token(Some(Case)) => self.case_command().await.map(Some),
74            _ => Ok(None),
75        }
76    }
77
78    /// Parses a compound command with optional redirections.
79    pub async fn full_compound_command(&mut self) -> Result<Option<FullCompoundCommand>> {
80        let command = match self.compound_command().await? {
81            Some(command) => command,
82            None => return Ok(None),
83        };
84        let redirs = self.redirections().await?;
85        // TODO Reject `{ { :; } >foo }` and `{ ( : ) }` if POSIXly-correct
86        // (The last `}` is not regarded as a keyword in these cases.)
87        Ok(Some(FullCompoundCommand { command, redirs }))
88    }
89}
90
91#[allow(clippy::bool_assert_comparison)]
92#[cfg(test)]
93mod tests {
94    use super::super::error::ErrorCause;
95    use super::super::lex::Lexer;
96    use super::super::lex::Operator::Semicolon;
97    use super::super::lex::TokenId::EndOfInput;
98    use super::*;
99    use crate::alias::{AliasSet, HashEntry};
100    use crate::source::Location;
101    use crate::source::Source;
102    use crate::syntax::Command;
103    use crate::syntax::ExpansionMode;
104    use crate::syntax::SimpleCommand;
105    use assert_matches::assert_matches;
106    use futures_util::FutureExt;
107
108    #[test]
109    fn parser_do_clause_none() {
110        let mut lexer = Lexer::with_code("done");
111        let mut parser = Parser::new(&mut lexer);
112
113        let result = parser.do_clause().now_or_never().unwrap().unwrap();
114        assert!(result.is_none(), "result should be none: {result:?}");
115    }
116
117    #[test]
118    fn parser_do_clause_short() {
119        let mut lexer = Lexer::with_code("do :; done");
120        let mut parser = Parser::new(&mut lexer);
121
122        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
123        assert_eq!(result.to_string(), ":");
124
125        let next = parser.peek_token().now_or_never().unwrap().unwrap();
126        assert_eq!(next.id, EndOfInput);
127    }
128
129    #[test]
130    fn parser_do_clause_long() {
131        let mut lexer = Lexer::with_code("do foo; bar& done");
132        let mut parser = Parser::new(&mut lexer);
133
134        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
135        assert_eq!(result.to_string(), "foo; bar&");
136
137        let next = parser.peek_token().now_or_never().unwrap().unwrap();
138        assert_eq!(next.id, EndOfInput);
139    }
140
141    #[test]
142    fn parser_do_clause_unclosed() {
143        let mut lexer = Lexer::with_code(" do not close ");
144        let mut parser = Parser::new(&mut lexer);
145
146        let e = parser.do_clause().now_or_never().unwrap().unwrap_err();
147        assert_matches!(e.cause,
148            ErrorCause::Syntax(SyntaxError::UnclosedDoClause { opening_location }) => {
149            assert_eq!(*opening_location.code.value.borrow(), " do not close ");
150            assert_eq!(opening_location.code.start_line_number.get(), 1);
151            assert_eq!(*opening_location.code.source, Source::Unknown);
152            assert_eq!(opening_location.range, 1..3);
153        });
154        assert_eq!(*e.location.code.value.borrow(), " do not close ");
155        assert_eq!(e.location.code.start_line_number.get(), 1);
156        assert_eq!(*e.location.code.source, Source::Unknown);
157        assert_eq!(e.location.range, 14..14);
158    }
159
160    #[test]
161    fn parser_do_clause_empty_posix() {
162        let mut lexer = Lexer::with_code("do done");
163        let mut parser = Parser::new(&mut lexer);
164
165        let e = parser.do_clause().now_or_never().unwrap().unwrap_err();
166        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptyDoClause));
167        assert_eq!(*e.location.code.value.borrow(), "do done");
168        assert_eq!(e.location.code.start_line_number.get(), 1);
169        assert_eq!(*e.location.code.source, Source::Unknown);
170        assert_eq!(e.location.range, 3..7);
171    }
172
173    #[test]
174    fn parser_do_clause_aliasing() {
175        let mut lexer = Lexer::with_code(" do :; end ");
176        #[allow(clippy::mutable_key_type)]
177        let mut aliases = AliasSet::new();
178        let origin = Location::dummy("");
179        aliases.insert(HashEntry::new(
180            "do".to_string(),
181            "".to_string(),
182            false,
183            origin.clone(),
184        ));
185        aliases.insert(HashEntry::new(
186            "done".to_string(),
187            "".to_string(),
188            false,
189            origin.clone(),
190        ));
191        aliases.insert(HashEntry::new(
192            "end".to_string(),
193            "done".to_string(),
194            false,
195            origin,
196        ));
197        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
198
199        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
200        assert_eq!(result.to_string(), ":");
201
202        let next = parser.peek_token().now_or_never().unwrap().unwrap();
203        assert_eq!(next.id, EndOfInput);
204    }
205
206    #[test]
207    fn parser_compound_command_none() {
208        let mut lexer = Lexer::with_code("}");
209        let mut parser = Parser::new(&mut lexer);
210
211        let option = parser.compound_command().now_or_never().unwrap().unwrap();
212        assert_eq!(option, None);
213    }
214
215    #[test]
216    fn parser_full_compound_command_without_redirections() {
217        let mut lexer = Lexer::with_code("(:)");
218        let mut parser = Parser::new(&mut lexer);
219
220        let result = parser.full_compound_command().now_or_never().unwrap();
221        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
222        assert_eq!(command.to_string(), "(:)");
223        assert_eq!(redirs, []);
224    }
225
226    #[test]
227    fn parser_full_compound_command_with_redirections() {
228        let mut lexer = Lexer::with_code("(command) <foo >bar ;");
229        let mut parser = Parser::new(&mut lexer);
230
231        let result = parser.full_compound_command().now_or_never().unwrap();
232        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
233        assert_eq!(command.to_string(), "(command)");
234        assert_eq!(redirs.len(), 2);
235        assert_eq!(redirs[0].to_string(), "<foo");
236        assert_eq!(redirs[1].to_string(), ">bar");
237
238        let next = parser.peek_token().now_or_never().unwrap().unwrap();
239        assert_eq!(next.id, Operator(Semicolon));
240    }
241
242    #[test]
243    fn parser_full_compound_command_none() {
244        let mut lexer = Lexer::with_code("}");
245        let mut parser = Parser::new(&mut lexer);
246
247        let result = parser.full_compound_command().now_or_never().unwrap();
248        assert_eq!(result, Ok(None));
249    }
250
251    #[test]
252    fn parser_short_function_definition_ok() {
253        let mut lexer = Lexer::with_code(" ( ) ( : ) > /dev/null ");
254        let mut parser = Parser::new(&mut lexer);
255        let c = SimpleCommand {
256            assigns: vec![],
257            words: vec![("foo".parse().unwrap(), ExpansionMode::Multiple)],
258            redirs: vec![].into(),
259        };
260
261        let result = parser.short_function_definition(c).now_or_never().unwrap();
262        let command = result.unwrap();
263        assert_matches!(command, Command::Function(f) => {
264            assert_eq!(f.has_keyword, false);
265            assert_eq!(f.name.to_string(), "foo");
266            assert_eq!(f.body.to_string(), "(:) >/dev/null");
267        });
268
269        let next = parser.peek_token().now_or_never().unwrap().unwrap();
270        assert_eq!(next.id, EndOfInput);
271    }
272}