Skip to main content

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