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, EmptyGlossary, HashEntry};
100    use crate::source::Location;
101    use crate::source::Source;
102    use crate::syntax::Command;
103    use crate::syntax::SimpleCommand;
104    use assert_matches::assert_matches;
105    use futures_util::FutureExt;
106
107    #[test]
108    fn parser_do_clause_none() {
109        let mut lexer = Lexer::from_memory("done", Source::Unknown);
110        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
111
112        let result = parser.do_clause().now_or_never().unwrap().unwrap();
113        assert!(result.is_none(), "result should be none: {result:?}");
114    }
115
116    #[test]
117    fn parser_do_clause_short() {
118        let mut lexer = Lexer::from_memory("do :; done", Source::Unknown);
119        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
120
121        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
122        assert_eq!(result.to_string(), ":");
123
124        let next = parser.peek_token().now_or_never().unwrap().unwrap();
125        assert_eq!(next.id, EndOfInput);
126    }
127
128    #[test]
129    fn parser_do_clause_long() {
130        let mut lexer = Lexer::from_memory("do foo; bar& done", Source::Unknown);
131        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
132
133        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
134        assert_eq!(result.to_string(), "foo; bar&");
135
136        let next = parser.peek_token().now_or_never().unwrap().unwrap();
137        assert_eq!(next.id, EndOfInput);
138    }
139
140    #[test]
141    fn parser_do_clause_unclosed() {
142        let mut lexer = Lexer::from_memory(" do not close ", Source::Unknown);
143        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
144
145        let e = parser.do_clause().now_or_never().unwrap().unwrap_err();
146        assert_matches!(e.cause,
147            ErrorCause::Syntax(SyntaxError::UnclosedDoClause { opening_location }) => {
148            assert_eq!(*opening_location.code.value.borrow(), " do not close ");
149            assert_eq!(opening_location.code.start_line_number.get(), 1);
150            assert_eq!(*opening_location.code.source, Source::Unknown);
151            assert_eq!(opening_location.range, 1..3);
152        });
153        assert_eq!(*e.location.code.value.borrow(), " do not close ");
154        assert_eq!(e.location.code.start_line_number.get(), 1);
155        assert_eq!(*e.location.code.source, Source::Unknown);
156        assert_eq!(e.location.range, 14..14);
157    }
158
159    #[test]
160    fn parser_do_clause_empty_posix() {
161        let mut lexer = Lexer::from_memory("do done", Source::Unknown);
162        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
163
164        let e = parser.do_clause().now_or_never().unwrap().unwrap_err();
165        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptyDoClause));
166        assert_eq!(*e.location.code.value.borrow(), "do done");
167        assert_eq!(e.location.code.start_line_number.get(), 1);
168        assert_eq!(*e.location.code.source, Source::Unknown);
169        assert_eq!(e.location.range, 3..7);
170    }
171
172    #[test]
173    fn parser_do_clause_aliasing() {
174        let mut lexer = Lexer::from_memory(" do :; end ", Source::Unknown);
175        #[allow(clippy::mutable_key_type)]
176        let mut aliases = AliasSet::new();
177        let origin = Location::dummy("");
178        aliases.insert(HashEntry::new(
179            "do".to_string(),
180            "".to_string(),
181            false,
182            origin.clone(),
183        ));
184        aliases.insert(HashEntry::new(
185            "done".to_string(),
186            "".to_string(),
187            false,
188            origin.clone(),
189        ));
190        aliases.insert(HashEntry::new(
191            "end".to_string(),
192            "done".to_string(),
193            false,
194            origin,
195        ));
196        let mut parser = Parser::new(&mut lexer, &aliases);
197
198        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
199        assert_eq!(result.to_string(), ":");
200
201        let next = parser.peek_token().now_or_never().unwrap().unwrap();
202        assert_eq!(next.id, EndOfInput);
203    }
204
205    #[test]
206    fn parser_compound_command_none() {
207        let mut lexer = Lexer::from_memory("}", Source::Unknown);
208        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
209
210        let option = parser.compound_command().now_or_never().unwrap().unwrap();
211        assert_eq!(option, None);
212    }
213
214    #[test]
215    fn parser_full_compound_command_without_redirections() {
216        let mut lexer = Lexer::from_memory("(:)", Source::Unknown);
217        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
218
219        let result = parser.full_compound_command().now_or_never().unwrap();
220        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
221        assert_eq!(command.to_string(), "(:)");
222        assert_eq!(redirs, []);
223    }
224
225    #[test]
226    fn parser_full_compound_command_with_redirections() {
227        let mut lexer = Lexer::from_memory("(command) <foo >bar ;", Source::Unknown);
228        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
229
230        let result = parser.full_compound_command().now_or_never().unwrap();
231        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
232        assert_eq!(command.to_string(), "(command)");
233        assert_eq!(redirs.len(), 2);
234        assert_eq!(redirs[0].to_string(), "<foo");
235        assert_eq!(redirs[1].to_string(), ">bar");
236
237        let next = parser.peek_token().now_or_never().unwrap().unwrap();
238        assert_eq!(next.id, Operator(Semicolon));
239    }
240
241    #[test]
242    fn parser_full_compound_command_none() {
243        let mut lexer = Lexer::from_memory("}", Source::Unknown);
244        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
245
246        let result = parser.full_compound_command().now_or_never().unwrap();
247        assert_eq!(result, Ok(None));
248    }
249
250    #[test]
251    fn parser_short_function_definition_ok() {
252        let mut lexer = Lexer::from_memory(" ( ) ( : ) > /dev/null ", Source::Unknown);
253        let mut parser = Parser::new(&mut lexer, &EmptyGlossary);
254        let c = SimpleCommand {
255            assigns: vec![],
256            words: vec!["foo".parse().unwrap()],
257            redirs: vec![].into(),
258        };
259
260        let result = parser.short_function_definition(c).now_or_never().unwrap();
261        let command = result.unwrap();
262        assert_matches!(command, Command::Function(f) => {
263            assert_eq!(f.has_keyword, false);
264            assert_eq!(f.name.to_string(), "foo");
265            assert_eq!(f.body.to_string(), "(:) >/dev/null");
266        });
267
268        let next = parser.peek_token().now_or_never().unwrap().unwrap();
269        assert_eq!(next.id, EndOfInput);
270    }
271}