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
86        // POSIX recognizes a reserved word only when it is the first word of a
87        // command or follows another reserved word. This compound command ends
88        // with a reserved word (`}`, `done`, `fi`, `esac`) unless it is a
89        // subshell (which ends with the `)` operator) or has a redirection
90        // (which ends with a word). In those cases, a clause-delimiting reserved
91        // word that immediately follows (such as the `}` in `{ ( : ) }`) is not
92        // portably recognized, so reject it in portable mode.
93        let ends_with_reserved_word =
94            redirs.is_empty() && !matches!(command, CompoundCommand::Subshell { .. });
95        if self.mode().portable && !ends_with_reserved_word {
96            let next = self.peek_token().await?;
97            if let Token(Some(keyword)) = next.id
98                && keyword.is_clause_delimiter()
99            {
100                let location = next.word.location.clone();
101                return Err(Error {
102                    cause: SyntaxError::MissingSeparatorBeforeReservedWord.into(),
103                    location,
104                });
105            }
106        }
107
108        Ok(Some(FullCompoundCommand { command, redirs }))
109    }
110}
111
112#[allow(
113    clippy::bool_assert_comparison,
114    reason = "to make the expected values clearer"
115)]
116#[cfg(test)]
117mod tests {
118    use super::super::error::ErrorCause;
119    use super::super::lex::Keyword::CloseBrace;
120    use super::super::lex::Lexer;
121    use super::super::lex::Operator::Semicolon;
122    use super::super::lex::TokenId::EndOfInput;
123    use super::*;
124    use crate::alias::{AliasSet, HashEntry};
125    use crate::source::Location;
126    use crate::source::Source;
127    use crate::syntax::Command;
128    use crate::syntax::ExpansionMode;
129    use crate::syntax::SimpleCommand;
130    use assert_matches::assert_matches;
131    use futures_util::FutureExt as _;
132
133    #[test]
134    fn parser_do_clause_none() {
135        let mut lexer = Lexer::with_code("done");
136        let mut parser = Parser::new(&mut lexer);
137
138        let result = parser.do_clause().now_or_never().unwrap().unwrap();
139        assert!(result.is_none(), "result should be none: {result:?}");
140    }
141
142    #[test]
143    fn parser_do_clause_short() {
144        let mut lexer = Lexer::with_code("do :; done");
145        let mut parser = Parser::new(&mut lexer);
146
147        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
148        assert_eq!(result.to_string(), ":");
149
150        let next = parser.peek_token().now_or_never().unwrap().unwrap();
151        assert_eq!(next.id, EndOfInput);
152    }
153
154    #[test]
155    fn parser_do_clause_long() {
156        let mut lexer = Lexer::with_code("do foo; bar& done");
157        let mut parser = Parser::new(&mut lexer);
158
159        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
160        assert_eq!(result.to_string(), "foo; bar&");
161
162        let next = parser.peek_token().now_or_never().unwrap().unwrap();
163        assert_eq!(next.id, EndOfInput);
164    }
165
166    #[test]
167    fn parser_do_clause_unclosed() {
168        let mut lexer = Lexer::with_code(" do not close ");
169        let mut parser = Parser::new(&mut lexer);
170
171        let e = parser.do_clause().now_or_never().unwrap().unwrap_err();
172        assert_matches!(e.cause,
173            ErrorCause::Syntax(SyntaxError::UnclosedDoClause { opening_location }) => {
174            assert_eq!(*opening_location.code.value.borrow(), " do not close ");
175            assert_eq!(opening_location.code.start_line_number.get(), 1);
176            assert_eq!(*opening_location.code.source, Source::Unknown);
177            assert_eq!(opening_location.range, 1..3);
178        });
179        assert_eq!(*e.location.code.value.borrow(), " do not close ");
180        assert_eq!(e.location.code.start_line_number.get(), 1);
181        assert_eq!(*e.location.code.source, Source::Unknown);
182        assert_eq!(e.location.range, 14..14);
183    }
184
185    #[test]
186    fn parser_do_clause_empty_posix() {
187        let mut lexer = Lexer::with_code("do done");
188        let mut parser = Parser::new(&mut lexer);
189
190        let e = parser.do_clause().now_or_never().unwrap().unwrap_err();
191        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::EmptyDoClause));
192        assert_eq!(*e.location.code.value.borrow(), "do done");
193        assert_eq!(e.location.code.start_line_number.get(), 1);
194        assert_eq!(*e.location.code.source, Source::Unknown);
195        assert_eq!(e.location.range, 3..7);
196    }
197
198    #[test]
199    fn parser_do_clause_aliasing() {
200        let mut lexer = Lexer::with_code(" do :; end ");
201        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
202        let mut aliases = AliasSet::new();
203        let origin = Location::dummy("");
204        aliases.insert(HashEntry::new(
205            "do".to_string(),
206            "".to_string(),
207            false,
208            origin.clone(),
209        ));
210        aliases.insert(HashEntry::new(
211            "done".to_string(),
212            "".to_string(),
213            false,
214            origin.clone(),
215        ));
216        aliases.insert(HashEntry::new(
217            "end".to_string(),
218            "done".to_string(),
219            false,
220            origin,
221        ));
222        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
223
224        let result = parser.do_clause().now_or_never().unwrap().unwrap().unwrap();
225        assert_eq!(result.to_string(), ":");
226
227        let next = parser.peek_token().now_or_never().unwrap().unwrap();
228        assert_eq!(next.id, EndOfInput);
229    }
230
231    #[test]
232    fn parser_compound_command_none() {
233        let mut lexer = Lexer::with_code("}");
234        let mut parser = Parser::new(&mut lexer);
235
236        let option = parser.compound_command().now_or_never().unwrap().unwrap();
237        assert_eq!(option, None);
238    }
239
240    #[test]
241    fn parser_full_compound_command_without_redirections() {
242        let mut lexer = Lexer::with_code("(:)");
243        let mut parser = Parser::new(&mut lexer);
244
245        let result = parser.full_compound_command().now_or_never().unwrap();
246        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
247        assert_eq!(command.to_string(), "(:)");
248        assert_eq!(redirs, []);
249    }
250
251    #[test]
252    fn parser_full_compound_command_with_redirections() {
253        let mut lexer = Lexer::with_code("(command) <foo >bar ;");
254        let mut parser = Parser::new(&mut lexer);
255
256        let result = parser.full_compound_command().now_or_never().unwrap();
257        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
258        assert_eq!(command.to_string(), "(command)");
259        assert_eq!(redirs.len(), 2);
260        assert_eq!(redirs[0].to_string(), "<foo");
261        assert_eq!(redirs[1].to_string(), ">bar");
262
263        let next = parser.peek_token().now_or_never().unwrap().unwrap();
264        assert_eq!(next.id, Operator(Semicolon));
265    }
266
267    fn portable_mode() -> yash_env::parser::Mode {
268        let mut mode = yash_env::parser::Mode::default();
269        mode.portable = true;
270        mode
271    }
272
273    #[test]
274    fn parser_full_compound_command_close_brace_after_subshell_rejected_in_portable_mode() {
275        // The `}` follows `)`, which is not a reserved word.
276        let mut lexer = Lexer::with_code("( : ) }");
277        lexer.set_mode(portable_mode());
278        let mut parser = Parser::new(&mut lexer);
279
280        let e = parser
281            .full_compound_command()
282            .now_or_never()
283            .unwrap()
284            .unwrap_err();
285        assert_eq!(
286            e.cause,
287            ErrorCause::Syntax(SyntaxError::MissingSeparatorBeforeReservedWord)
288        );
289        assert_eq!(*e.location.code.value.borrow(), "( : ) }");
290        assert_eq!(e.location.range, 6..7);
291    }
292
293    #[test]
294    fn parser_full_compound_command_done_after_subshell_rejected_in_portable_mode() {
295        // The rule applies to any clause-delimiting reserved word, not just `}`.
296        let mut lexer = Lexer::with_code("( : ) done");
297        lexer.set_mode(portable_mode());
298        let mut parser = Parser::new(&mut lexer);
299
300        let e = parser
301            .full_compound_command()
302            .now_or_never()
303            .unwrap()
304            .unwrap_err();
305        assert_eq!(
306            e.cause,
307            ErrorCause::Syntax(SyntaxError::MissingSeparatorBeforeReservedWord)
308        );
309        assert_eq!(e.location.range, 6..10);
310    }
311
312    #[test]
313    fn parser_full_compound_command_then_after_subshell_rejected_in_portable_mode() {
314        let mut lexer = Lexer::with_code("( : ) then");
315        lexer.set_mode(portable_mode());
316        let mut parser = Parser::new(&mut lexer);
317
318        let e = parser
319            .full_compound_command()
320            .now_or_never()
321            .unwrap()
322            .unwrap_err();
323        assert_eq!(
324            e.cause,
325            ErrorCause::Syntax(SyntaxError::MissingSeparatorBeforeReservedWord)
326        );
327    }
328
329    #[test]
330    fn parser_full_compound_command_close_brace_after_redirection_rejected_in_portable_mode() {
331        let mut lexer = Lexer::with_code("{ :; } >foo }");
332        lexer.set_mode(portable_mode());
333        let mut parser = Parser::new(&mut lexer);
334
335        let e = parser
336            .full_compound_command()
337            .now_or_never()
338            .unwrap()
339            .unwrap_err();
340        assert_eq!(
341            e.cause,
342            ErrorCause::Syntax(SyntaxError::MissingSeparatorBeforeReservedWord)
343        );
344    }
345
346    #[test]
347    fn parser_full_compound_command_close_brace_allowed_without_portable() {
348        let mut lexer = Lexer::with_code("( : ) }");
349        let mut parser = Parser::new(&mut lexer);
350
351        let result = parser.full_compound_command().now_or_never().unwrap();
352        let FullCompoundCommand { command, .. } = result.unwrap().unwrap();
353        assert_eq!(command.to_string(), "(:)");
354
355        // The `}` is left unconsumed.
356        let next = parser.peek_token().now_or_never().unwrap().unwrap();
357        assert_eq!(next.id, Token(Some(CloseBrace)));
358    }
359
360    #[test]
361    fn parser_full_compound_command_separator_before_close_brace_allowed_in_portable_mode() {
362        let mut lexer = Lexer::with_code("( : ) ;");
363        lexer.set_mode(portable_mode());
364        let mut parser = Parser::new(&mut lexer);
365
366        let result = parser.full_compound_command().now_or_never().unwrap();
367        let FullCompoundCommand { command, .. } = result.unwrap().unwrap();
368        assert_eq!(command.to_string(), "(:)");
369    }
370
371    #[test]
372    fn parser_full_compound_command_close_brace_after_grouping_allowed_in_portable_mode() {
373        // The `}` follows the inner grouping's `}`, which is a reserved word, so
374        // it is portable and must be accepted.
375        let mut lexer = Lexer::with_code("{ :; } }");
376        lexer.set_mode(portable_mode());
377        let mut parser = Parser::new(&mut lexer);
378
379        let result = parser.full_compound_command().now_or_never().unwrap();
380        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
381        assert_matches!(command, CompoundCommand::Grouping(_));
382        assert_eq!(redirs, []);
383
384        // The outer `}` is left unconsumed for the enclosing grouping.
385        let next = parser.peek_token().now_or_never().unwrap().unwrap();
386        assert_eq!(next.id, Token(Some(CloseBrace)));
387    }
388
389    #[test]
390    fn parser_full_compound_command_close_brace_after_if_allowed_in_portable_mode() {
391        // The `}` follows `fi`, which is a reserved word.
392        let mut lexer = Lexer::with_code("if true; then :; fi }");
393        lexer.set_mode(portable_mode());
394        let mut parser = Parser::new(&mut lexer);
395
396        let result = parser.full_compound_command().now_or_never().unwrap();
397        let FullCompoundCommand { command, redirs } = result.unwrap().unwrap();
398        assert_matches!(command, CompoundCommand::If { .. });
399        assert_eq!(redirs, []);
400
401        let next = parser.peek_token().now_or_never().unwrap().unwrap();
402        assert_eq!(next.id, Token(Some(CloseBrace)));
403    }
404
405    #[test]
406    fn parser_full_compound_command_none() {
407        let mut lexer = Lexer::with_code("}");
408        let mut parser = Parser::new(&mut lexer);
409
410        let result = parser.full_compound_command().now_or_never().unwrap();
411        assert_eq!(result, Ok(None));
412    }
413
414    #[test]
415    fn parser_short_function_definition_ok() {
416        let mut lexer = Lexer::with_code(" ( ) ( : ) > /dev/null ");
417        let mut parser = Parser::new(&mut lexer);
418        let c = SimpleCommand {
419            assigns: vec![],
420            words: vec![("foo".parse().unwrap(), ExpansionMode::Multiple)],
421            redirs: vec![].into(),
422        };
423
424        let result = parser.short_function_definition(c).now_or_never().unwrap();
425        let command = result.unwrap();
426        assert_matches!(command, Command::Function(f) => {
427            assert_eq!(f.has_keyword, false);
428            assert_eq!(f.name.to_string(), "foo");
429            assert_eq!(f.body.to_string(), "(:) >/dev/null");
430        });
431
432        let next = parser.peek_token().now_or_never().unwrap().unwrap();
433        assert_eq!(next.id, EndOfInput);
434    }
435}