Skip to main content

yash_syntax/parser/
pipeline.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 pipeline
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::Bang;
25use super::lex::Operator::{Bar, OpenParen};
26use super::lex::TokenId::{Operator, Token};
27use crate::syntax::Pipeline;
28use std::rc::Rc;
29
30impl Parser<'_, '_> {
31    /// Parses a pipeline.
32    ///
33    /// If there is no valid pipeline at the current position, this function
34    /// returns `Ok(Rec::Parsed(None))`.
35    pub async fn pipeline(&mut self) -> Result<Rec<Option<Pipeline>>> {
36        // Parse the first command
37        let (first, negation) = match self.command().await? {
38            Rec::AliasSubstituted => return Ok(Rec::AliasSubstituted),
39            Rec::Parsed(Some(first)) => (first, false),
40            Rec::Parsed(None) => {
41                // Parse the `!` reserved word
42                if self.peek_token().await?.id != Token(Some(Bang)) {
43                    return Ok(Rec::Parsed(None));
44                }
45                let bang = self.take_token_raw().await?;
46
47                // In portable mode, reject `!(` (which other shells parse as an
48                // extended glob) at the beginning of a command.
49                if self.mode().portable {
50                    let next = self.peek_token().await?;
51                    if next.id == Operator(OpenParen) && next.index == bang.index + 1 {
52                        let location = next.word.location.clone();
53                        let cause = SyntaxError::UnsupportedExtendedGlob.into();
54                        return Err(Error { cause, location });
55                    }
56                }
57
58                // Parse the command after the `!`
59                loop {
60                    match self.command().await? {
61                        Rec::AliasSubstituted => continue,
62                        Rec::Parsed(Some(first)) => break (first, true),
63                        Rec::Parsed(None) => {
64                            // Error: the command is missing
65                            let next = self.take_token_raw().await?;
66                            let cause = if next.id == Token(Some(Bang)) {
67                                SyntaxError::DoubleNegation.into()
68                            } else {
69                                SyntaxError::MissingCommandAfterBang.into()
70                            };
71                            let location = next.word.location;
72                            return Err(Error { cause, location });
73                        }
74                    }
75                }
76            }
77        };
78
79        // Parse `|`
80        let mut commands = vec![Rc::new(first)];
81        while self.peek_token().await?.id == Operator(Bar) {
82            self.take_token_raw().await?;
83
84            // Parse the next command
85            let next = loop {
86                while self.newline_and_here_doc_contents().await? {}
87
88                match self.command().await? {
89                    Rec::AliasSubstituted => continue,
90                    Rec::Parsed(Some(next)) => break next,
91                    Rec::Parsed(None) => {
92                        // Error: the command is missing
93                        let next = self.take_token_raw().await?;
94                        let cause = if next.id == Token(Some(Bang)) {
95                            SyntaxError::BangAfterBar.into()
96                        } else {
97                            SyntaxError::MissingCommandAfterBar.into()
98                        };
99                        let location = next.word.location;
100                        return Err(Error { cause, location });
101                    }
102                }
103            };
104            commands.push(Rc::new(next));
105        }
106
107        Ok(Rec::Parsed(Some(Pipeline { commands, negation })))
108    }
109}
110
111#[allow(
112    clippy::bool_assert_comparison,
113    reason = "to make the expected values clearer"
114)]
115#[cfg(test)]
116mod tests {
117    use super::super::error::ErrorCause;
118    use super::super::lex::Lexer;
119    use super::*;
120    use crate::alias::{AliasSet, HashEntry};
121    use crate::source::Location;
122    use crate::source::Source;
123    use futures_util::FutureExt as _;
124
125    #[test]
126    fn parser_pipeline_eof() {
127        let mut lexer = Lexer::with_code("");
128        let mut parser = Parser::new(&mut lexer);
129
130        let option = parser.pipeline().now_or_never().unwrap().unwrap().unwrap();
131        assert_eq!(option, None);
132    }
133
134    #[test]
135    fn parser_pipeline_one() {
136        let mut lexer = Lexer::with_code("foo");
137        let mut parser = Parser::new(&mut lexer);
138
139        let result = parser.pipeline().now_or_never().unwrap();
140        let p = result.unwrap().unwrap().unwrap();
141        assert_eq!(p.negation, false);
142        assert_eq!(p.commands.len(), 1);
143        assert_eq!(p.commands[0].to_string(), "foo");
144    }
145
146    #[test]
147    fn parser_pipeline_many() {
148        let mut lexer = Lexer::with_code("one | two | \n\t\n three");
149        let mut parser = Parser::new(&mut lexer);
150
151        let result = parser.pipeline().now_or_never().unwrap();
152        let p = result.unwrap().unwrap().unwrap();
153        assert_eq!(p.negation, false);
154        assert_eq!(p.commands.len(), 3);
155        assert_eq!(p.commands[0].to_string(), "one");
156        assert_eq!(p.commands[1].to_string(), "two");
157        assert_eq!(p.commands[2].to_string(), "three");
158    }
159
160    #[test]
161    fn parser_pipeline_negated() {
162        let mut lexer = Lexer::with_code("! foo");
163        let mut parser = Parser::new(&mut lexer);
164
165        let result = parser.pipeline().now_or_never().unwrap();
166        let p = result.unwrap().unwrap().unwrap();
167        assert_eq!(p.negation, true);
168        assert_eq!(p.commands.len(), 1);
169        assert_eq!(p.commands[0].to_string(), "foo");
170    }
171
172    #[test]
173    fn parser_pipeline_double_negation() {
174        let mut lexer = Lexer::with_code(" !  !");
175        let mut parser = Parser::new(&mut lexer);
176
177        let e = parser.pipeline().now_or_never().unwrap().unwrap_err();
178        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::DoubleNegation));
179        assert_eq!(*e.location.code.value.borrow(), " !  !");
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, 4..5);
183    }
184
185    #[test]
186    fn parser_pipeline_missing_command_after_negation() {
187        let mut lexer = Lexer::with_code("!\nfoo");
188        let mut parser = Parser::new(&mut lexer);
189
190        let e = parser.pipeline().now_or_never().unwrap().unwrap_err();
191        assert_eq!(
192            e.cause,
193            ErrorCause::Syntax(SyntaxError::MissingCommandAfterBang)
194        );
195        assert_eq!(*e.location.code.value.borrow(), "!\n");
196        assert_eq!(e.location.code.start_line_number.get(), 1);
197        assert_eq!(*e.location.code.source, Source::Unknown);
198        assert_eq!(e.location.range, 1..2);
199    }
200
201    #[test]
202    fn parser_pipeline_missing_command_after_bar() {
203        let mut lexer = Lexer::with_code("foo | ;");
204        let mut parser = Parser::new(&mut lexer);
205
206        let e = parser.pipeline().now_or_never().unwrap().unwrap_err();
207        assert_eq!(
208            e.cause,
209            ErrorCause::Syntax(SyntaxError::MissingCommandAfterBar)
210        );
211        assert_eq!(*e.location.code.value.borrow(), "foo | ;");
212        assert_eq!(e.location.code.start_line_number.get(), 1);
213        assert_eq!(*e.location.code.source, Source::Unknown);
214        assert_eq!(e.location.range, 6..7);
215    }
216
217    #[test]
218    fn parser_pipeline_bang_after_bar() {
219        let mut lexer = Lexer::with_code("foo | !");
220        let mut parser = Parser::new(&mut lexer);
221
222        let e = parser.pipeline().now_or_never().unwrap().unwrap_err();
223        assert_eq!(e.cause, ErrorCause::Syntax(SyntaxError::BangAfterBar));
224        assert_eq!(*e.location.code.value.borrow(), "foo | !");
225        assert_eq!(e.location.code.start_line_number.get(), 1);
226        assert_eq!(*e.location.code.source, Source::Unknown);
227        assert_eq!(e.location.range, 6..7);
228    }
229
230    #[test]
231    fn parser_pipeline_no_aliasing_of_bang() {
232        let mut lexer = Lexer::with_code("! ok");
233        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
234        let mut aliases = AliasSet::new();
235        let origin = Location::dummy("");
236        aliases.insert(HashEntry::new(
237            "!".to_string(),
238            "; ; ;".to_string(),
239            true,
240            origin,
241        ));
242        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
243
244        let result = parser.pipeline().now_or_never().unwrap();
245        let p = result.unwrap().unwrap().unwrap();
246        assert_eq!(p.negation, true);
247        assert_eq!(p.commands.len(), 1);
248        assert_eq!(p.commands[0].to_string(), "ok");
249    }
250
251    #[test]
252    fn parser_alias_substitution_to_newline_after_bar() {
253        let mut lexer = Lexer::with_code("foo | X\n bar");
254        #[allow(clippy::mutable_key_type, reason = "AliasSet is defined as such")]
255        let mut aliases = AliasSet::new();
256        aliases.insert(HashEntry::new(
257            "X".to_string(),
258            "\n".to_string(),
259            false,
260            Location::dummy(""),
261        ));
262        let mut parser = Parser::config().aliases(&aliases).input(&mut lexer);
263
264        let result = parser.pipeline().now_or_never().unwrap();
265        let p = result.unwrap().unwrap().unwrap();
266        assert_eq!(p.negation, false);
267        assert_eq!(p.commands.len(), 2);
268        assert_eq!(p.commands[0].to_string(), "foo");
269        assert_eq!(p.commands[1].to_string(), "bar");
270    }
271
272    fn portable_mode() -> yash_env::parser::Mode {
273        let mut mode = yash_env::parser::Mode::default();
274        mode.portable = true;
275        mode
276    }
277
278    #[test]
279    fn parser_pipeline_bang_open_paren_rejected_in_portable_mode() {
280        let mut lexer = Lexer::with_code("!(:)");
281        lexer.set_mode(portable_mode());
282        let mut parser = Parser::new(&mut lexer);
283
284        let e = parser.pipeline().now_or_never().unwrap().unwrap_err();
285        assert_eq!(
286            e.cause,
287            ErrorCause::Syntax(SyntaxError::UnsupportedExtendedGlob)
288        );
289        // The error points at the `(`.
290        assert_eq!(e.location.range, 1..2);
291    }
292
293    #[test]
294    fn parser_pipeline_bang_spaced_open_paren_accepted_in_portable_mode() {
295        // A space after `!` makes it a portable negated subshell.
296        let mut lexer = Lexer::with_code("! (:)");
297        lexer.set_mode(portable_mode());
298        let mut parser = Parser::new(&mut lexer);
299
300        let result = parser.pipeline().now_or_never().unwrap();
301        let p = result.unwrap().unwrap().unwrap();
302        assert_eq!(p.negation, true);
303        assert_eq!(p.commands[0].to_string(), "(:)");
304    }
305
306    #[test]
307    fn parser_pipeline_bang_open_paren_accepted_without_portable() {
308        // Without the portable option, `!(` is parsed as a negated subshell.
309        let mut lexer = Lexer::with_code("!(:)");
310        let mut parser = Parser::new(&mut lexer);
311
312        let result = parser.pipeline().now_or_never().unwrap();
313        let p = result.unwrap().unwrap().unwrap();
314        assert_eq!(p.negation, true);
315        assert_eq!(p.commands[0].to_string(), "(:)");
316    }
317}