Skip to main content

yash_syntax/parser/
for_loop.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 for loop
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::{Do, For, In};
25use super::lex::Operator::{Newline, Semicolon};
26use super::lex::TokenId::{EndOfInput, IoLocation, IoNumber, Operator, Token};
27use super::lex::is_portable_name;
28use crate::source::Location;
29use crate::syntax::CompoundCommand;
30use crate::syntax::List;
31use crate::syntax::MaybeLiteral as _;
32use crate::syntax::Word;
33
34impl Parser<'_, '_> {
35    /// Parses the name of a for loop.
36    async fn for_loop_name(&mut self) -> Result<Word> {
37        let name = self.take_token_auto(&[]).await?;
38
39        // Validate the token type
40        match name.id {
41            EndOfInput | Operator(Newline) | Operator(Semicolon) => {
42                let cause = SyntaxError::MissingForName.into();
43                let location = name.word.location;
44                return Err(Error { cause, location });
45            }
46            Operator(_) => {
47                let cause = SyntaxError::InvalidForName.into();
48                let location = name.word.location;
49                return Err(Error { cause, location });
50            }
51            Token(_) | IoNumber | IoLocation => (),
52        }
53
54        if self.mode().portable
55            && !name
56                .word
57                .to_string_if_literal()
58                .is_some_and(|s| is_portable_name(&s))
59        {
60            let cause = SyntaxError::NonPortableForName.into();
61            let location = name.word.location;
62            return Err(Error { cause, location });
63        }
64
65        Ok(name.word)
66    }
67
68    /// Parses the values of a for loop.
69    ///
70    /// For the values to be parsed, the first token needs to be `in`. Otherwise,
71    /// the result will be `None`.
72    ///
73    /// If successful, `opening_location` is returned intact as the second value
74    /// of the tuple.
75    async fn for_loop_values(
76        &mut self,
77        opening_location: Location,
78    ) -> Result<(Option<Vec<Word>>, Location)> {
79        // Parse the `in`
80        let mut first_line = true;
81        loop {
82            match self.peek_token().await?.id {
83                Operator(Semicolon) if first_line => {
84                    self.take_token_raw().await?;
85                    return Ok((None, opening_location));
86                }
87                Token(Some(Do)) => {
88                    return Ok((None, opening_location));
89                }
90                Operator(Newline) => {
91                    assert!(self.newline_and_here_doc_contents().await?);
92                    first_line = false;
93                }
94                Token(Some(In)) => {
95                    self.take_token_raw().await?;
96                    break;
97                }
98                _ => match self.take_token_manual(false).await? {
99                    Rec::AliasSubstituted => (),
100                    Rec::Parsed(token) => {
101                        let cause = SyntaxError::MissingForBody { opening_location }.into();
102                        let location = token.word.location;
103                        return Err(Error { cause, location });
104                    }
105                },
106            }
107        }
108
109        // Parse values until a delimiter is found
110        let mut values = Vec::new();
111        loop {
112            let next = self.take_token_auto(&[]).await?;
113            match next.id {
114                Token(_) | IoNumber | IoLocation => {
115                    values.push(next.word);
116                }
117                Operator(Semicolon) | Operator(Newline) | EndOfInput => {
118                    return Ok((Some(values), opening_location));
119                }
120                Operator(_) => {
121                    let cause = SyntaxError::InvalidForValue.into();
122                    let location = next.word.location;
123                    return Err(Error { cause, location });
124                }
125            }
126        }
127    }
128
129    /// Parses the body of a for loop, possibly preceded by newlines.
130    async fn for_loop_body(&mut self, opening_location: Location) -> Result<List> {
131        loop {
132            while self.newline_and_here_doc_contents().await? {}
133
134            if let Some(body) = self.do_clause().await? {
135                return Ok(body);
136            }
137
138            match self.take_token_manual(false).await? {
139                Rec::AliasSubstituted => (),
140                Rec::Parsed(token) => {
141                    let cause = SyntaxError::MissingForBody { opening_location }.into();
142                    let location = token.word.location;
143                    return Err(Error { cause, location });
144                }
145            }
146        }
147    }
148
149    /// Parses a for loop.
150    ///
151    /// The next token must be the `for` reserved word.
152    ///
153    /// # Panics
154    ///
155    /// If the first token is not `for`.
156    pub async fn for_loop(&mut self) -> Result<CompoundCommand> {
157        let open = self.take_token_raw().await?;
158        assert_eq!(open.id, Token(Some(For)));
159        let opening_location = open.word.location;
160
161        let name = self.for_loop_name().await?;
162        let (values, opening_location) = self.for_loop_values(opening_location).await?;
163        let body = self.for_loop_body(opening_location).await?;
164        Ok(CompoundCommand::For { name, values, body })
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::super::error::ErrorCause;
171    use super::super::lex::Lexer;
172    use super::*;
173    use crate::alias::{AliasSet, HashEntry};
174    use crate::source::Source;
175    use assert_matches::assert_matches;
176    use futures_util::FutureExt as _;
177
178    #[test]
179    fn parser_for_loop_short() {
180        let mut lexer = Lexer::with_code("for A do :; done");
181        let mut parser = Parser::new(&mut lexer);
182
183        let result = parser.compound_command().now_or_never().unwrap();
184        let compound_command = result.unwrap().unwrap();
185        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
186            assert_eq!(name.to_string(), "A");
187            assert_eq!(values, None);
188            assert_eq!(body.to_string(), ":");
189        });
190
191        let next = parser.peek_token().now_or_never().unwrap().unwrap();
192        assert_eq!(next.id, EndOfInput);
193    }
194
195    #[test]
196    fn parser_for_loop_with_semicolon_before_do() {
197        let mut lexer = Lexer::with_code("for B ; do :; done");
198        let mut parser = Parser::new(&mut lexer);
199
200        let result = parser.compound_command().now_or_never().unwrap();
201        let compound_command = result.unwrap().unwrap();
202        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
203            assert_eq!(name.to_string(), "B");
204            assert_eq!(values, None);
205            assert_eq!(body.to_string(), ":");
206        });
207
208        let next = parser.peek_token().now_or_never().unwrap().unwrap();
209        assert_eq!(next.id, EndOfInput);
210    }
211
212    #[test]
213    fn parser_for_loop_with_semicolon_and_newlines_before_do() {
214        let mut lexer = Lexer::with_code("for B ; \n\t\n do :; done");
215        let mut parser = Parser::new(&mut lexer);
216
217        let result = parser.compound_command().now_or_never().unwrap();
218        let compound_command = result.unwrap().unwrap();
219        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
220            assert_eq!(name.to_string(), "B");
221            assert_eq!(values, None);
222            assert_eq!(body.to_string(), ":");
223        });
224
225        let next = parser.peek_token().now_or_never().unwrap().unwrap();
226        assert_eq!(next.id, EndOfInput);
227    }
228
229    #[test]
230    fn parser_for_loop_with_newlines_before_do() {
231        let mut lexer = Lexer::with_code("for B \n \\\n \n do :; done");
232        let mut parser = Parser::new(&mut lexer);
233
234        let result = parser.compound_command().now_or_never().unwrap();
235        let compound_command = result.unwrap().unwrap();
236        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
237            assert_eq!(name.to_string(), "B");
238            assert_eq!(values, None);
239            assert_eq!(body.to_string(), ":");
240        });
241
242        let next = parser.peek_token().now_or_never().unwrap().unwrap();
243        assert_eq!(next.id, EndOfInput);
244    }
245
246    #[test]
247    fn parser_for_loop_with_zero_values_delimited_by_semicolon() {
248        let mut lexer = Lexer::with_code("for foo in; do :; done");
249        let mut parser = Parser::new(&mut lexer);
250
251        let result = parser.compound_command().now_or_never().unwrap();
252        let compound_command = result.unwrap().unwrap();
253        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
254            assert_eq!(name.to_string(), "foo");
255            assert_eq!(values, Some(vec![]));
256            assert_eq!(body.to_string(), ":");
257        });
258
259        let next = parser.peek_token().now_or_never().unwrap().unwrap();
260        assert_eq!(next.id, EndOfInput);
261    }
262
263    #[test]
264    fn parser_for_loop_with_one_value_delimited_by_semicolon_and_newlines() {
265        let mut lexer = Lexer::with_code("for foo in bar; \n \n do :; done");
266        let mut parser = Parser::new(&mut lexer);
267
268        let result = parser.compound_command().now_or_never().unwrap();
269        let compound_command = result.unwrap().unwrap();
270        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
271            assert_eq!(name.to_string(), "foo");
272            let values = values
273                .unwrap()
274                .iter()
275                .map(ToString::to_string)
276                .collect::<Vec<String>>();
277            assert_eq!(values, vec!["bar"]);
278            assert_eq!(body.to_string(), ":");
279        });
280
281        let next = parser.peek_token().now_or_never().unwrap().unwrap();
282        assert_eq!(next.id, EndOfInput);
283    }
284
285    #[test]
286    fn parser_for_loop_with_many_values_delimited_by_one_newline() {
287        let mut lexer = Lexer::with_code("for in in in a b c\ndo :; done");
288        let mut parser = Parser::new(&mut lexer);
289
290        let result = parser.compound_command().now_or_never().unwrap();
291        let compound_command = result.unwrap().unwrap();
292        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
293            assert_eq!(name.to_string(), "in");
294            let values = values
295                .unwrap()
296                .iter()
297                .map(ToString::to_string)
298                .collect::<Vec<String>>();
299            assert_eq!(values, vec!["in", "a", "b", "c"]);
300            assert_eq!(body.to_string(), ":");
301        });
302
303        let next = parser.peek_token().now_or_never().unwrap().unwrap();
304        assert_eq!(next.id, EndOfInput);
305    }
306
307    #[test]
308    fn parser_for_loop_with_zero_values_delimited_by_many_newlines() {
309        let mut lexer = Lexer::with_code("for foo in \n \n \n do :; done");
310        let mut parser = Parser::new(&mut lexer);
311
312        let result = parser.compound_command().now_or_never().unwrap();
313        let compound_command = result.unwrap().unwrap();
314        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
315            assert_eq!(name.to_string(), "foo");
316            assert_eq!(values, Some(vec![]));
317            assert_eq!(body.to_string(), ":");
318        });
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_for_loop_newlines_before_in() {
326        let mut lexer = Lexer::with_code("for foo\n \n\nin\ndo :; done");
327        let mut parser = Parser::new(&mut lexer);
328
329        let result = parser.compound_command().now_or_never().unwrap();
330        let compound_command = result.unwrap().unwrap();
331        assert_matches!(compound_command, CompoundCommand::For { name, values, body } => {
332            assert_eq!(name.to_string(), "foo");
333            assert_eq!(values, Some(vec![]));
334            assert_eq!(body.to_string(), ":");
335        });
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_for_loop_aliasing_on_semicolon() {
343        let mut lexer = Lexer::with_code(" FOR_A if :; done");
344        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
345        let mut aliases = AliasSet::new();
346        let origin = Location::dummy("");
347        aliases.insert(HashEntry::new(
348            "if".to_string(),
349            " ;\n\ndo".to_string(),
350            false,
351            origin.clone(),
352        ));
353        aliases.insert(HashEntry::new(
354            "FOR_A".to_string(),
355            "for A ".to_string(),
356            false,
357            origin,
358        ));
359        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
360
361        let result = parser.take_token_manual(true).now_or_never().unwrap();
362        assert_matches!(result, Ok(Rec::AliasSubstituted));
363
364        let result = parser.compound_command().now_or_never().unwrap();
365        let compound_command = result.unwrap().unwrap();
366        assert_eq!(compound_command.to_string(), "for A do :; done");
367
368        let next = parser.peek_token().now_or_never().unwrap().unwrap();
369        assert_eq!(next.id, EndOfInput);
370    }
371
372    #[test]
373    fn parser_for_loop_aliasing_on_do() {
374        let mut lexer = Lexer::with_code(" FOR_A if :; done");
375        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
376        let mut aliases = AliasSet::new();
377        let origin = Location::dummy("");
378        aliases.insert(HashEntry::new(
379            "if".to_string(),
380            "\ndo".to_string(),
381            false,
382            origin.clone(),
383        ));
384        aliases.insert(HashEntry::new(
385            "FOR_A".to_string(),
386            "for A ".to_string(),
387            false,
388            origin,
389        ));
390        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
391
392        let result = parser.take_token_manual(true).now_or_never().unwrap();
393        assert_matches!(result, Ok(Rec::AliasSubstituted));
394
395        let result = parser.compound_command().now_or_never().unwrap();
396        let compound_command = result.unwrap().unwrap();
397        assert_eq!(compound_command.to_string(), "for A do :; done");
398
399        let next = parser.peek_token().now_or_never().unwrap().unwrap();
400        assert_eq!(next.id, EndOfInput);
401    }
402
403    #[test]
404    fn parser_for_loop_missing_name_eof() {
405        let mut lexer = Lexer::with_code(" for ");
406        let mut parser = Parser::new(&mut lexer);
407
408        let result = parser.compound_command().now_or_never().unwrap();
409        let e = result.unwrap_err();
410        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingForName));
411        assert_eq!(*e.location.code.value.borrow(), " for ");
412        assert_eq!(e.location.code.start_line_number.get(), 1);
413        assert_eq!(*e.location.code.source, Source::Unknown);
414        assert_eq!(e.location.range, 5..5);
415    }
416
417    #[test]
418    fn parser_for_loop_missing_name_newline() {
419        let mut lexer = Lexer::with_code(" for\ndo :; done");
420        let mut parser = Parser::new(&mut lexer);
421
422        let result = parser.compound_command().now_or_never().unwrap();
423        let e = result.unwrap_err();
424        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingForName));
425        assert_eq!(*e.location.code.value.borrow(), " for\n");
426        assert_eq!(e.location.code.start_line_number.get(), 1);
427        assert_eq!(*e.location.code.source, Source::Unknown);
428        assert_eq!(e.location.range, 4..5);
429    }
430
431    #[test]
432    fn parser_for_loop_missing_name_semicolon() {
433        let mut lexer = Lexer::with_code("for; do :; done");
434        let mut parser = Parser::new(&mut lexer);
435
436        let result = parser.compound_command().now_or_never().unwrap();
437        let e = result.unwrap_err();
438        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::MissingForName));
439        assert_eq!(*e.location.code.value.borrow(), "for; do :; done");
440        assert_eq!(e.location.code.start_line_number.get(), 1);
441        assert_eq!(*e.location.code.source, Source::Unknown);
442        assert_eq!(e.location.range, 3..4);
443    }
444
445    #[test]
446    fn parser_for_loop_invalid_name() {
447        // Alias substitution results in "for & do :; done"
448        let mut lexer = Lexer::with_code("FOR if do :; done");
449        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
450        let mut aliases = AliasSet::new();
451        let origin = Location::dummy("");
452        aliases.insert(HashEntry::new(
453            "FOR".to_string(),
454            "for ".to_string(),
455            false,
456            origin.clone(),
457        ));
458        aliases.insert(HashEntry::new(
459            "if".to_string(),
460            "&".to_string(),
461            false,
462            origin,
463        ));
464        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
465
466        let result = parser.take_token_manual(true).now_or_never().unwrap();
467        assert_matches!(result, Ok(Rec::AliasSubstituted));
468
469        let result = parser.compound_command().now_or_never().unwrap();
470        let e = result.unwrap_err();
471        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::InvalidForName));
472        assert_eq!(*e.location.code.value.borrow(), "&");
473        assert_eq!(e.location.code.start_line_number.get(), 1);
474        assert_eq!(e.location.range, 0..1);
475        assert_matches!(&*e.location.code.source, Source::Alias { original, alias } => {
476            assert_eq!(*original.code.value.borrow(), "FOR if do :; done");
477            assert_eq!(original.code.start_line_number.get(), 1);
478            assert_eq!(*original.code.source, Source::Unknown);
479            assert_eq!(original.range, 4..6);
480            assert_eq!(alias.name, "if");
481        });
482    }
483
484    #[test]
485    fn parser_for_loop_semicolon_after_newline() {
486        let mut lexer = Lexer::with_code("for X\n; do :; done");
487        let mut parser = Parser::new(&mut lexer);
488
489        let result = parser.compound_command().now_or_never().unwrap();
490        let e = result.unwrap_err();
491        assert_matches!(&e.cause,
492            ErrorCause::Syntax(SyntaxError::MissingForBody { opening_location }) => {
493            assert_eq!(*opening_location.code.value.borrow(), "for X\n; do :; done");
494            assert_eq!(opening_location.code.start_line_number.get(), 1);
495            assert_eq!(*opening_location.code.source, Source::Unknown);
496            assert_eq!(opening_location.range, 0..3);
497        });
498        assert_eq!(*e.location.code.value.borrow(), "for X\n; do :; done");
499        assert_eq!(e.location.code.start_line_number.get(), 1);
500        assert_eq!(*e.location.code.source, Source::Unknown);
501        assert_eq!(e.location.range, 6..7);
502    }
503
504    #[test]
505    fn parser_for_loop_invalid_values_delimiter() {
506        // Alias substitution results in "for A in a b & c; do :; done"
507        let mut lexer = Lexer::with_code("for_A_in_a_b if c; do :; done");
508        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
509        let mut aliases = AliasSet::new();
510        let origin = Location::dummy("");
511        aliases.insert(HashEntry::new(
512            "for_A_in_a_b".to_string(),
513            "for A in a b ".to_string(),
514            false,
515            origin.clone(),
516        ));
517        aliases.insert(HashEntry::new(
518            "if".to_string(),
519            "&".to_string(),
520            false,
521            origin,
522        ));
523        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
524
525        let result = parser.take_token_manual(true).now_or_never().unwrap();
526        assert_matches!(result, Ok(Rec::AliasSubstituted));
527
528        let result = parser.compound_command().now_or_never().unwrap();
529        let e = result.unwrap_err();
530        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::InvalidForValue));
531        assert_eq!(*e.location.code.value.borrow(), "&");
532        assert_eq!(e.location.code.start_line_number.get(), 1);
533        assert_eq!(e.location.range, 0..1);
534        assert_matches!(&*e.location.code.source, Source::Alias { original, alias } => {
535            assert_eq!(*original.code.value.borrow(), "for_A_in_a_b if c; do :; done");
536            assert_eq!(original.code.start_line_number.get(), 1);
537            assert_eq!(*original.code.source, Source::Unknown);
538            assert_eq!(original.range, 13..15);
539            assert_eq!(alias.name, "if");
540        });
541    }
542
543    #[test]
544    fn parser_for_loop_invalid_token_after_semicolon() {
545        let mut lexer = Lexer::with_code(" for X; ! do :; done");
546        let mut parser = Parser::new(&mut lexer);
547
548        let result = parser.compound_command().now_or_never().unwrap();
549        let e = result.unwrap_err();
550        assert_matches!(&e.cause,
551            ErrorCause::Syntax(SyntaxError::MissingForBody { opening_location }) => {
552            assert_eq!(*opening_location.code.value.borrow(), " for X; ! do :; done");
553            assert_eq!(opening_location.code.start_line_number.get(), 1);
554            assert_eq!(*opening_location.code.source, Source::Unknown);
555            assert_eq!(opening_location.range, 1..4);
556        });
557        assert_eq!(*e.location.code.value.borrow(), " for X; ! do :; done");
558        assert_eq!(e.location.code.start_line_number.get(), 1);
559        assert_eq!(*e.location.code.source, Source::Unknown);
560        assert_eq!(e.location.range, 8..9);
561    }
562
563    fn portable_mode() -> yash_env::parser::Mode {
564        let mut mode = yash_env::parser::Mode::default();
565        mode.portable = true;
566        mode
567    }
568
569    #[test]
570    fn parser_for_loop_name_starting_with_digit_rejected_in_portable_mode() {
571        let mut lexer = Lexer::with_code("for 1a do :; done");
572        lexer.set_mode(portable_mode());
573        let mut parser = Parser::new(&mut lexer);
574
575        let result = parser.compound_command().now_or_never().unwrap();
576        let e = result.unwrap_err();
577        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::NonPortableForName));
578        assert_eq!(*e.location.code.value.borrow(), "for 1a do :; done");
579        assert_eq!(e.location.code.start_line_number.get(), 1);
580        assert_eq!(*e.location.code.source, Source::Unknown);
581        assert_eq!(e.location.range, 4..6);
582    }
583
584    #[test]
585    fn parser_for_loop_quoted_name_rejected_in_portable_mode() {
586        let mut lexer = Lexer::with_code("for 'A' do :; done");
587        lexer.set_mode(portable_mode());
588        let mut parser = Parser::new(&mut lexer);
589
590        let result = parser.compound_command().now_or_never().unwrap();
591        let e = result.unwrap_err();
592        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::NonPortableForName));
593        assert_eq!(e.location.range, 4..7);
594    }
595
596    #[test]
597    fn parser_for_loop_name_with_expansion_rejected_in_portable_mode() {
598        let mut lexer = Lexer::with_code("for $A do :; done");
599        lexer.set_mode(portable_mode());
600        let mut parser = Parser::new(&mut lexer);
601
602        let result = parser.compound_command().now_or_never().unwrap();
603        let e = result.unwrap_err();
604        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::NonPortableForName));
605        assert_eq!(e.location.range, 4..6);
606    }
607
608    #[test]
609    fn parser_for_loop_portable_name_allowed_in_portable_mode() {
610        let mut lexer = Lexer::with_code("for _Az9 do :; done");
611        lexer.set_mode(portable_mode());
612        let mut parser = Parser::new(&mut lexer);
613
614        let result = parser.compound_command().now_or_never().unwrap();
615        let compound_command = result.unwrap().unwrap();
616        assert_matches!(compound_command, CompoundCommand::For { name, .. } => {
617            assert_eq!(name.to_string(), "_Az9");
618        });
619    }
620
621    #[test]
622    fn parser_for_loop_non_portable_name_allowed_without_portable() {
623        let mut lexer = Lexer::with_code("for 1a do :; done");
624        let mut parser = Parser::new(&mut lexer);
625
626        let result = parser.compound_command().now_or_never().unwrap();
627        let compound_command = result.unwrap().unwrap();
628        assert_matches!(compound_command, CompoundCommand::For { name, .. } => {
629            assert_eq!(name.to_string(), "1a");
630        });
631    }
632}