Skip to main content

rippy_cli/
parser.rs

1use rable::Node;
2
3use crate::error::RippyError;
4
5/// Wrapper around rable bash parser.
6pub struct BashParser;
7
8impl BashParser {
9    /// Create a new parser.
10    ///
11    /// # Errors
12    ///
13    /// Always succeeds — rable is stateless.
14    pub const fn new() -> Result<Self, RippyError> {
15        Ok(Self)
16    }
17
18    /// Parse a bash command string into a list of AST nodes.
19    ///
20    /// # Errors
21    ///
22    /// Returns `RippyError::Parse` if the source cannot be parsed.
23    pub fn parse(&mut self, source: &str) -> Result<Vec<Node>, RippyError> {
24        rable::parse(source, false).map_err(|e| RippyError::Parse(format!("parse error: {e}")))
25    }
26}
27
28#[cfg(test)]
29#[allow(clippy::unwrap_used)]
30mod tests {
31    use rable::NodeKind;
32
33    use super::*;
34
35    #[test]
36    fn parse_simple_command() {
37        let mut parser = BashParser::new().unwrap();
38        let nodes = parser.parse("echo hello").unwrap();
39        assert!(!nodes.is_empty());
40        assert!(matches!(nodes[0].kind, NodeKind::Command { .. }));
41    }
42
43    #[test]
44    fn parse_pipeline() {
45        let mut parser = BashParser::new().unwrap();
46        let nodes = parser.parse("cat file | grep pattern").unwrap();
47        assert!(matches!(nodes[0].kind, NodeKind::Pipeline { .. }));
48    }
49
50    #[test]
51    fn parse_list() {
52        let mut parser = BashParser::new().unwrap();
53        let nodes = parser.parse("cd /tmp && ls").unwrap();
54        assert!(matches!(nodes[0].kind, NodeKind::List { .. }));
55    }
56
57    #[test]
58    fn parse_redirect() {
59        let mut parser = BashParser::new().unwrap();
60        let nodes = parser.parse("echo foo > output.txt").unwrap();
61        assert!(
62            matches!(&nodes[0].kind, NodeKind::Command { redirects, .. } if !redirects.is_empty())
63        );
64    }
65
66    #[test]
67    fn parse_command_substitution() {
68        let mut parser = BashParser::new().unwrap();
69        let nodes = parser.parse("echo $(whoami)").unwrap();
70        assert!(!nodes.is_empty());
71        assert!(crate::ast::has_expansions(&nodes[0]));
72    }
73
74    #[test]
75    fn parse_if_statement() {
76        let mut parser = BashParser::new().unwrap();
77        let nodes = parser.parse("if true; then echo yes; fi").unwrap();
78        assert!(matches!(nodes[0].kind, NodeKind::If { .. }));
79    }
80
81    #[test]
82    fn parse_for_loop() {
83        let mut parser = BashParser::new().unwrap();
84        let nodes = parser.parse("for i in 1 2 3; do echo $i; done").unwrap();
85        assert!(matches!(nodes[0].kind, NodeKind::For { .. }));
86    }
87
88    #[test]
89    fn parse_subshell() {
90        let mut parser = BashParser::new().unwrap();
91        let nodes = parser.parse("(echo hello)").unwrap();
92        assert!(matches!(nodes[0].kind, NodeKind::Subshell { .. }));
93    }
94}