Skip to main content

yash_syntax/parser/
case.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 case command
18
19use super::core::Parser;
20use super::core::Rec;
21use super::core::Result;
22use super::error::Error;
23use super::error::SyntaxError;
24use super::lex::Keyword::{Case, Esac, In};
25use super::lex::Operator::{Bar, CloseParen, Newline, OpenParen};
26use super::lex::TokenId::{self, EndOfInput, Operator, Token};
27use crate::syntax::CaseContinuation;
28use crate::syntax::CaseItem;
29use crate::syntax::CompoundCommand;
30
31impl Parser<'_, '_> {
32    /// Parses a case item.
33    ///
34    /// If the next token is `esac`, returns `None` without consuming it.
35    /// Otherwise, returns a case item and whether there may be a next item.
36    pub async fn case_item(&mut self) -> Result<Option<(CaseItem, bool)>> {
37        fn pattern_error_cause(token_id: TokenId) -> SyntaxError {
38            match token_id {
39                Token(_) => unreachable!(),
40                Operator(CloseParen) | Operator(Bar) | Operator(Newline) | EndOfInput => {
41                    SyntaxError::MissingPattern
42                }
43                _ => SyntaxError::InvalidPattern,
44            }
45        }
46
47        let first_token = loop {
48            while self.newline_and_here_doc_contents().await? {}
49
50            if self.peek_token().await?.id == Token(Some(Esac)) {
51                return Ok(None);
52            }
53
54            match self.take_token_manual(false).await? {
55                Rec::AliasSubstituted => (),
56                Rec::Parsed(token) => break token,
57            }
58        };
59
60        let first_pattern = match first_token.id {
61            Token(_) => first_token.word,
62            Operator(OpenParen) => {
63                let next_token = self.take_token_auto(&[Esac]).await?;
64                match next_token.id {
65                    Token(_) => next_token.word,
66                    _ => {
67                        let cause = pattern_error_cause(next_token.id).into();
68                        let location = next_token.word.location;
69                        return Err(Error { cause, location });
70                    }
71                }
72            }
73            _ => {
74                let cause = pattern_error_cause(first_token.id).into();
75                let location = first_token.word.location;
76                return Err(Error { cause, location });
77            }
78        };
79
80        let mut patterns = vec![first_pattern];
81        loop {
82            let separator = self.take_token_auto(&[]).await?;
83            match separator.id {
84                Operator(CloseParen) => break,
85                Operator(Bar) => {
86                    let pattern = self.take_token_auto(&[]).await?;
87                    match pattern.id {
88                        Token(_) => patterns.push(pattern.word),
89                        _ => {
90                            let cause = pattern_error_cause(pattern.id).into();
91                            let location = pattern.word.location;
92                            return Err(Error { cause, location });
93                        }
94                    }
95                }
96                _ => {
97                    let cause = SyntaxError::UnclosedPatternList.into();
98                    let location = separator.word.location;
99                    return Err(Error { cause, location });
100                }
101            }
102        }
103
104        let body = self.maybe_compound_list_boxed().await?;
105
106        let continuation = match self.peek_token().await?.id {
107            Operator(op) => op.try_into().ok(),
108            _ => None,
109        };
110        let continued = continuation.is_some();
111        let continuation = continuation.unwrap_or_default();
112        if continued {
113            let token = self.take_token_raw().await?;
114            // The `;;&` and `;|` terminators (both parsed as `Continue`) are not
115            // portable, so reject them when the `portable` option is on.
116            if self.mode().portable && continuation == CaseContinuation::Continue {
117                let Operator(op) = token.id else {
118                    unreachable!("expected operator token, got {:?}", token.id);
119                };
120                return Err(Error {
121                    cause: SyntaxError::NonPortableCaseTerminator(op).into(),
122                    location: token.word.location,
123                });
124            }
125        }
126
127        Ok(Some((
128            CaseItem {
129                patterns,
130                body,
131                continuation,
132            },
133            continued,
134        )))
135    }
136
137    /// Parses a case conditional construct.
138    ///
139    /// The next token must be the `case` reserved word.
140    ///
141    /// # Panics
142    ///
143    /// If the first token is not `case`.
144    pub async fn case_command(&mut self) -> Result<CompoundCommand> {
145        let open = self.take_token_raw().await?;
146        assert_eq!(open.id, Token(Some(Case)));
147
148        let subject = self.take_token_auto(&[]).await?;
149        match subject.id {
150            Token(_) => (),
151            Operator(Newline) | EndOfInput => {
152                let cause = SyntaxError::MissingCaseSubject.into();
153                let location = subject.word.location;
154                return Err(Error { cause, location });
155            }
156            _ => {
157                let cause = SyntaxError::InvalidCaseSubject.into();
158                let location = subject.word.location;
159                return Err(Error { cause, location });
160            }
161        }
162        let subject = subject.word;
163
164        loop {
165            while self.newline_and_here_doc_contents().await? {}
166
167            let next_token = self.take_token_auto(&[In]).await?;
168            match next_token.id {
169                Token(Some(In)) => break,
170                Operator(Newline) => (),
171                _ => {
172                    let opening_location = open.word.location;
173                    let cause = SyntaxError::MissingIn { opening_location }.into();
174                    let location = next_token.word.location;
175                    return Err(Error { cause, location });
176                }
177            }
178        }
179
180        let mut items = Vec::new();
181        while let Some((item, continued)) = self.case_item().await? {
182            items.push(item);
183            if !continued {
184                break;
185            }
186        }
187
188        let close = self.take_token_raw().await?;
189        if close.id != Token(Some(Esac)) {
190            let opening_location = open.word.location;
191            let cause = SyntaxError::UnclosedCase { opening_location }.into();
192            let location = close.word.location;
193            return Err(Error { cause, location });
194        }
195
196        Ok(CompoundCommand::Case { subject, items })
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::super::error::ErrorCause;
203    use super::super::lex::Lexer;
204    use super::super::lex::Operator::{SemicolonBar, SemicolonSemicolonAnd};
205    use super::*;
206    use crate::alias::{AliasSet, HashEntry};
207    use crate::source::Location;
208    use crate::source::Source;
209    use crate::syntax::CaseContinuation;
210    use assert_matches::assert_matches;
211    use futures_util::FutureExt as _;
212
213    #[test]
214    fn parser_case_item_esac() {
215        let mut lexer = Lexer::with_code("\nESAC");
216        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
217        let mut aliases = AliasSet::new();
218        let origin = Location::dummy("");
219        aliases.insert(HashEntry::new(
220            "ESAC".to_string(),
221            "\n\nesac".to_string(),
222            true,
223            origin.clone(),
224        ));
225        aliases.insert(HashEntry::new(
226            "esac".to_string(),
227            "&&".to_string(),
228            true,
229            origin,
230        ));
231        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
232
233        let option = parser.case_item().now_or_never().unwrap().unwrap();
234        assert_eq!(option, None);
235
236        let next = parser.peek_token().now_or_never().unwrap().unwrap();
237        assert_eq!(next.id, Token(Some(Esac)));
238    }
239
240    #[test]
241    fn parser_case_item_minimum() {
242        let mut lexer = Lexer::with_code("foo)");
243        let mut parser = Parser::new(&mut lexer);
244
245        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
246        assert_eq!(item.patterns.len(), 1);
247        assert_eq!(item.patterns[0].to_string(), "foo");
248        assert_eq!(item.body.0, []);
249        assert_eq!(item.continuation, CaseContinuation::Break);
250        assert!(!continued);
251
252        let next = parser.peek_token().now_or_never().unwrap().unwrap();
253        assert_eq!(next.id, EndOfInput);
254    }
255
256    #[test]
257    fn parser_case_item_with_open_paren() {
258        let mut lexer = Lexer::with_code("(foo)");
259        let mut parser = Parser::new(&mut lexer);
260
261        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
262        assert_eq!(item.patterns.len(), 1);
263        assert_eq!(item.patterns[0].to_string(), "foo");
264        assert_eq!(item.body.0, []);
265        assert_eq!(item.continuation, CaseContinuation::Break);
266        assert!(!continued);
267
268        let next = parser.peek_token().now_or_never().unwrap().unwrap();
269        assert_eq!(next.id, EndOfInput);
270    }
271
272    #[test]
273    fn parser_case_item_many_patterns() {
274        let mut lexer = Lexer::with_code("1 | esac | $three)");
275        let mut parser = Parser::new(&mut lexer);
276
277        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
278        assert_eq!(item.patterns.len(), 3);
279        assert_eq!(item.patterns[0].to_string(), "1");
280        assert_eq!(item.patterns[1].to_string(), "esac");
281        assert_eq!(item.patterns[2].to_string(), "$three");
282        assert_eq!(item.body.0, []);
283        assert_eq!(item.continuation, CaseContinuation::Break);
284        assert!(!continued);
285
286        let next = parser.peek_token().now_or_never().unwrap().unwrap();
287        assert_eq!(next.id, EndOfInput);
288    }
289
290    #[test]
291    fn parser_case_item_non_empty_body() {
292        let mut lexer = Lexer::with_code("foo)\necho ok\n:&\n");
293        let mut parser = Parser::new(&mut lexer);
294
295        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
296        assert_eq!(item.patterns.len(), 1);
297        assert_eq!(item.patterns[0].to_string(), "foo");
298        assert_eq!(item.body.0.len(), 2);
299        assert_eq!(item.body.0[0].to_string(), "echo ok");
300        assert_eq!(item.body.0[1].to_string(), ":&");
301        assert_eq!(item.continuation, CaseContinuation::Break);
302        assert!(!continued);
303
304        let next = parser.peek_token().now_or_never().unwrap().unwrap();
305        assert_eq!(next.id, EndOfInput);
306    }
307
308    #[test]
309    fn parser_case_item_with_double_semicolon() {
310        let mut lexer = Lexer::with_code("foo);;");
311        let mut parser = Parser::new(&mut lexer);
312
313        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
314        assert_eq!(item.patterns.len(), 1);
315        assert_eq!(item.patterns[0].to_string(), "foo");
316        assert_eq!(item.body.0, []);
317        assert_eq!(item.continuation, CaseContinuation::Break);
318        assert!(continued);
319
320        let next = parser.peek_token().now_or_never().unwrap().unwrap();
321        assert_eq!(next.id, EndOfInput);
322    }
323
324    #[test]
325    fn parser_case_item_with_non_empty_body_and_double_semicolon() {
326        let mut lexer = Lexer::with_code("foo):;\n;;");
327        let mut parser = Parser::new(&mut lexer);
328
329        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
330        assert_eq!(item.patterns.len(), 1);
331        assert_eq!(item.patterns[0].to_string(), "foo");
332        assert_eq!(item.body.0.len(), 1);
333        assert_eq!(item.body.0[0].to_string(), ":");
334        assert_eq!(item.continuation, CaseContinuation::Break);
335        assert!(continued);
336
337        let next = parser.peek_token().now_or_never().unwrap().unwrap();
338        assert_eq!(next.id, EndOfInput);
339    }
340
341    #[test]
342    fn parser_case_item_with_semicolon_and() {
343        let mut lexer = Lexer::with_code("foo);&");
344        let mut parser = Parser::new(&mut lexer);
345
346        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
347        assert_eq!(item.patterns.len(), 1);
348        assert_eq!(item.patterns[0].to_string(), "foo");
349        assert_eq!(item.body.0, []);
350        assert_eq!(item.continuation, CaseContinuation::FallThrough);
351        assert!(continued);
352
353        let next = parser.peek_token().now_or_never().unwrap().unwrap();
354        assert_eq!(next.id, EndOfInput);
355    }
356
357    fn portable_mode() -> yash_env::parser::Mode {
358        let mut mode = yash_env::parser::Mode::default();
359        mode.portable = true;
360        mode
361    }
362
363    #[test]
364    fn parser_case_item_double_semicolon_and_allowed_without_portable() {
365        let mut lexer = Lexer::with_code("foo);;&");
366        let mut parser = Parser::new(&mut lexer);
367
368        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
369        assert_eq!(item.continuation, CaseContinuation::Continue);
370        assert!(continued);
371    }
372
373    #[test]
374    fn parser_case_item_double_semicolon_and_rejected_in_portable_mode() {
375        let mut lexer = Lexer::with_code("foo);;&");
376        lexer.set_mode(portable_mode());
377        let mut parser = Parser::new(&mut lexer);
378
379        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
380        assert_eq!(
381            e.cause,
382            ErrorCause::Syntax(SyntaxError::NonPortableCaseTerminator(
383                SemicolonSemicolonAnd
384            ))
385        );
386        assert_eq!(*e.location.code.value.borrow(), "foo);;&");
387        assert_eq!(e.location.range, 4..7);
388    }
389
390    #[test]
391    fn parser_case_item_semicolon_bar_rejected_in_portable_mode() {
392        let mut lexer = Lexer::with_code("foo);|");
393        lexer.set_mode(portable_mode());
394        let mut parser = Parser::new(&mut lexer);
395
396        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
397        assert_eq!(
398            e.cause,
399            ErrorCause::Syntax(SyntaxError::NonPortableCaseTerminator(SemicolonBar))
400        );
401        assert_eq!(e.location.range, 4..6);
402    }
403
404    #[test]
405    fn parser_case_item_portable_terminators_allowed_in_portable_mode() {
406        // `;;` (Break) and `;&` (FallThrough) are portable and must be accepted.
407        for code in ["foo);;", "foo);&"] {
408            let mut lexer = Lexer::with_code(code);
409            lexer.set_mode(portable_mode());
410            let mut parser = Parser::new(&mut lexer);
411
412            let result = parser.case_item().now_or_never().unwrap();
413            assert!(result.is_ok(), "code={code:?}, result={result:?}");
414        }
415    }
416
417    #[test]
418    fn parser_case_item_missing_pattern_without_open_paren() {
419        let mut lexer = Lexer::with_code(")");
420        let mut parser = Parser::new(&mut lexer);
421
422        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
423        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingPattern));
424        assert_eq!(*e.location.code.value.borrow(), ")");
425        assert_eq!(e.location.code.start_line_number.get(), 1);
426        assert_eq!(*e.location.code.source, Source::Unknown);
427        assert_eq!(e.location.range, 0..1);
428    }
429
430    #[test]
431    fn parser_case_item_esac_after_paren() {
432        let mut lexer = Lexer::with_code("(esac)");
433        let mut parser = Parser::new(&mut lexer);
434
435        let (item, continued) = parser.case_item().now_or_never().unwrap().unwrap().unwrap();
436        assert_eq!(item.patterns.len(), 1);
437        assert_eq!(item.patterns[0].to_string(), "esac");
438        assert_eq!(item.body.0, []);
439        assert_eq!(item.continuation, CaseContinuation::Break);
440        assert!(!continued);
441    }
442
443    #[test]
444    fn parser_case_item_first_pattern_not_word_after_open_paren() {
445        let mut lexer = Lexer::with_code("(&");
446        let mut parser = Parser::new(&mut lexer);
447
448        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
449        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::InvalidPattern));
450        assert_eq!(*e.location.code.value.borrow(), "(&");
451        assert_eq!(e.location.code.start_line_number.get(), 1);
452        assert_eq!(*e.location.code.source, Source::Unknown);
453        assert_eq!(e.location.range, 1..2);
454    }
455
456    #[test]
457    fn parser_case_item_missing_pattern_after_bar() {
458        let mut lexer = Lexer::with_code("(foo| |");
459        let mut parser = Parser::new(&mut lexer);
460
461        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
462        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingPattern));
463        assert_eq!(*e.location.code.value.borrow(), "(foo| |");
464        assert_eq!(e.location.code.start_line_number.get(), 1);
465        assert_eq!(*e.location.code.source, Source::Unknown);
466        assert_eq!(e.location.range, 6..7);
467    }
468
469    #[test]
470    fn parser_case_item_missing_close_paren() {
471        let mut lexer = Lexer::with_code("(foo bar");
472        let mut parser = Parser::new(&mut lexer);
473
474        let e = parser.case_item().now_or_never().unwrap().unwrap_err();
475        assert_eq!(
476            e.cause,
477            ErrorCause::Syntax(SyntaxError::UnclosedPatternList)
478        );
479        assert_eq!(*e.location.code.value.borrow(), "(foo bar");
480        assert_eq!(e.location.code.start_line_number.get(), 1);
481        assert_eq!(*e.location.code.source, Source::Unknown);
482        assert_eq!(e.location.range, 5..8);
483    }
484
485    #[test]
486    fn parser_case_command_minimum() {
487        let mut lexer = Lexer::with_code("case foo in esac");
488        let mut parser = Parser::new(&mut lexer);
489
490        let result = parser.compound_command().now_or_never().unwrap();
491        let compound_command = result.unwrap().unwrap();
492        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
493            assert_eq!(subject.to_string(), "foo");
494            assert_eq!(items, []);
495        });
496
497        let next = parser.peek_token().now_or_never().unwrap().unwrap();
498        assert_eq!(next.id, EndOfInput);
499    }
500
501    #[test]
502    fn parser_case_command_newline_before_in() {
503        // Alias substitution results in "case x \n\n \nin esac"
504        let mut lexer = Lexer::with_code("CASE_X IN_ESAC");
505        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
506        let mut aliases = AliasSet::new();
507        let origin = Location::dummy("");
508        aliases.insert(HashEntry::new(
509            "CASE_X".to_string(),
510            " case x \n\n ".to_string(),
511            false,
512            origin.clone(),
513        ));
514        aliases.insert(HashEntry::new(
515            "IN_ESAC".to_string(),
516            "\nin esac".to_string(),
517            false,
518            origin,
519        ));
520        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
521
522        let result = parser.take_token_manual(true).now_or_never().unwrap();
523        assert_matches!(result, Ok(Rec::AliasSubstituted));
524
525        let result = parser.compound_command().now_or_never().unwrap();
526        let compound_command = result.unwrap().unwrap();
527        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
528            assert_eq!(subject.to_string(), "x");
529            assert_eq!(items, []);
530        });
531
532        let next = parser.peek_token().now_or_never().unwrap().unwrap();
533        assert_eq!(next.id, EndOfInput);
534    }
535
536    #[test]
537    fn parser_case_command_alias_on_subject() {
538        // Alias substitution results in " case   in in  a|b) esac"
539        let mut lexer = Lexer::with_code("CASE in a|b) esac");
540        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
541        let mut aliases = AliasSet::new();
542        let origin = Location::dummy("");
543        aliases.insert(HashEntry::new(
544            "CASE".to_string(),
545            " case ".to_string(),
546            false,
547            origin.clone(),
548        ));
549        aliases.insert(HashEntry::new(
550            "in".to_string(),
551            " in in ".to_string(),
552            false,
553            origin,
554        ));
555        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
556
557        let result = parser.take_token_manual(true).now_or_never().unwrap();
558        assert_matches!(result, Ok(Rec::AliasSubstituted));
559
560        let result = parser.compound_command().now_or_never().unwrap();
561        let compound_command = result.unwrap().unwrap();
562        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
563            assert_eq!(subject.to_string(), "in");
564            assert_eq!(items.len(), 1);
565            assert_eq!(items[0].to_string(), "(a | b) ;;");
566        });
567
568        let next = parser.peek_token().now_or_never().unwrap().unwrap();
569        assert_eq!(next.id, EndOfInput);
570    }
571
572    #[test]
573    fn parser_case_command_alias_on_in() {
574        // Alias substitution results in "case x  in esac"
575        let mut lexer = Lexer::with_code("CASE_X in esac");
576        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
577        let mut aliases = AliasSet::new();
578        let origin = Location::dummy("");
579        aliases.insert(HashEntry::new(
580            "CASE_X".to_string(),
581            "case x ".to_string(),
582            false,
583            origin.clone(),
584        ));
585        aliases.insert(HashEntry::new(
586            "in".to_string(),
587            "in a)".to_string(),
588            false,
589            origin,
590        ));
591        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
592
593        let result = parser.take_token_manual(true).now_or_never().unwrap();
594        assert_matches!(result, Ok(Rec::AliasSubstituted));
595
596        let result = parser.compound_command().now_or_never().unwrap();
597        let compound_command = result.unwrap().unwrap();
598        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
599            assert_eq!(subject.to_string(), "x");
600            assert_eq!(items, []);
601        });
602
603        let next = parser.peek_token().now_or_never().unwrap().unwrap();
604        assert_eq!(next.id, EndOfInput);
605    }
606
607    #[test]
608    fn parser_case_command_one_item() {
609        let mut lexer = Lexer::with_code("case foo in bar) esac");
610        let mut parser = Parser::new(&mut lexer);
611
612        let result = parser.compound_command().now_or_never().unwrap();
613        let compound_command = result.unwrap().unwrap();
614        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
615            assert_eq!(subject.to_string(), "foo");
616            assert_eq!(items.len(), 1);
617            assert_eq!(items[0].to_string(), "(bar) ;;");
618        });
619
620        let next = parser.peek_token().now_or_never().unwrap().unwrap();
621        assert_eq!(next.id, EndOfInput);
622    }
623
624    #[test]
625    fn parser_case_command_many_items_without_final_double_semicolon() {
626        let mut lexer = Lexer::with_code("case x in\n\na) ;; (b|c):&:; ;;\n d)echo\nesac");
627        let mut parser = Parser::new(&mut lexer);
628
629        let result = parser.compound_command().now_or_never().unwrap();
630        let compound_command = result.unwrap().unwrap();
631        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
632            assert_eq!(subject.to_string(), "x");
633            assert_eq!(items.len(), 3);
634            assert_eq!(items[0].to_string(), "(a) ;;");
635            assert_eq!(items[1].to_string(), "(b | c) :& :;;");
636            assert_eq!(items[2].to_string(), "(d) echo;;");
637        });
638
639        let next = parser.peek_token().now_or_never().unwrap().unwrap();
640        assert_eq!(next.id, EndOfInput);
641    }
642
643    #[test]
644    fn parser_case_command_many_items_with_final_double_semicolon() {
645        let mut lexer = Lexer::with_code("case x in(1);; 2)echo\n\n;;\n\nesac");
646        let mut parser = Parser::new(&mut lexer);
647
648        let result = parser.compound_command().now_or_never().unwrap();
649        let compound_command = result.unwrap().unwrap();
650        assert_matches!(compound_command, CompoundCommand::Case { subject, items } => {
651            assert_eq!(subject.to_string(), "x");
652            assert_eq!(items.len(), 2);
653            assert_eq!(items[0].to_string(), "(1) ;;");
654            assert_eq!(items[1].to_string(), "(2) echo;;");
655        });
656
657        let next = parser.peek_token().now_or_never().unwrap().unwrap();
658        assert_eq!(next.id, EndOfInput);
659    }
660
661    #[test]
662    fn parser_case_command_missing_subject() {
663        let mut lexer = Lexer::with_code(" case  ");
664        let mut parser = Parser::new(&mut lexer);
665
666        let result = parser.compound_command().now_or_never().unwrap();
667        let e = result.unwrap_err();
668        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingCaseSubject));
669        assert_eq!(*e.location.code.value.borrow(), " case  ");
670        assert_eq!(e.location.code.start_line_number.get(), 1);
671        assert_eq!(*e.location.code.source, Source::Unknown);
672        assert_eq!(e.location.range, 7..7);
673    }
674
675    #[test]
676    fn parser_case_command_invalid_subject() {
677        let mut lexer = Lexer::with_code(" case ; ");
678        let mut parser = Parser::new(&mut lexer);
679
680        let result = parser.compound_command().now_or_never().unwrap();
681        let e = result.unwrap_err();
682        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::InvalidCaseSubject));
683        assert_eq!(*e.location.code.value.borrow(), " case ; ");
684        assert_eq!(e.location.code.start_line_number.get(), 1);
685        assert_eq!(*e.location.code.source, Source::Unknown);
686        assert_eq!(e.location.range, 6..7);
687    }
688
689    #[test]
690    fn parser_case_command_missing_in() {
691        let mut lexer = Lexer::with_code(" case x esac");
692        let mut parser = Parser::new(&mut lexer);
693
694        let result = parser.compound_command().now_or_never().unwrap();
695        let e = result.unwrap_err();
696        assert_matches!(e.cause,
697            ErrorCause::Syntax(SyntaxError::MissingIn { opening_location }) => {
698            assert_eq!(*opening_location.code.value.borrow(), " case x esac");
699            assert_eq!(opening_location.code.start_line_number.get(), 1);
700            assert_eq!(*opening_location.code.source, Source::Unknown);
701            assert_eq!(opening_location.range, 1..5);
702        });
703        assert_eq!(*e.location.code.value.borrow(), " case x esac");
704        assert_eq!(e.location.code.start_line_number.get(), 1);
705        assert_eq!(*e.location.code.source, Source::Unknown);
706        assert_eq!(e.location.range, 8..12);
707    }
708
709    #[test]
710    fn parser_case_command_missing_esac() {
711        let mut lexer = Lexer::with_code("case x in a) }");
712        let mut parser = Parser::new(&mut lexer);
713
714        let result = parser.compound_command().now_or_never().unwrap();
715        let e = result.unwrap_err();
716        assert_matches!(e.cause,
717            ErrorCause::Syntax(SyntaxError::UnclosedCase { opening_location }) => {
718            assert_eq!(*opening_location.code.value.borrow(), "case x in a) }");
719            assert_eq!(opening_location.code.start_line_number.get(), 1);
720            assert_eq!(*opening_location.code.source, Source::Unknown);
721            assert_eq!(opening_location.range, 0..4);
722        });
723        assert_eq!(*e.location.code.value.borrow(), "case x in a) }");
724        assert_eq!(e.location.code.start_line_number.get(), 1);
725        assert_eq!(*e.location.code.source, Source::Unknown);
726        assert_eq!(e.location.range, 13..14);
727    }
728}