Skip to main content

shrub_parser/parser/
mod.rs

1use std::path::PathBuf;
2
3use bon::bon;
4
5use crate::ast;
6use crate::tokenizer::{Token, TokenEndReason, Tokenizer, TokenizerOptions, Tokens};
7
8pub mod peg;
9#[cfg(feature = "winnow-parser")]
10pub mod winnow_str;
11
12/// Parser implementation to use
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)]
14pub enum ParserImpl {
15    /// PEG-based parser (token-based)
16    #[default]
17    Peg,
18    /// Winnow-based parser (string-based, direct)
19    #[cfg(feature = "winnow-parser")]
20    Winnow,
21}
22
23/// Options used to control the behavior of the parser.
24#[derive(Clone, Eq, Hash, PartialEq)]
25pub struct ParserOptions {
26    /// Whether or not to enable extended globbing (a.k.a. `extglob`).
27    pub enable_extended_globbing: bool,
28    /// Whether or not to enable POSIX compliance mode.
29    pub posix_mode: bool,
30    /// Whether or not to enable maximal compatibility with the `sh` shell.
31    pub sh_mode: bool,
32    /// Whether or not to perform tilde expansion for tildes at the start of
33    /// words.
34    pub tilde_expansion_at_word_start: bool,
35    /// Whether or not to perform tilde expansion for tildes after colons.
36    pub tilde_expansion_after_colon: bool,
37    /// Select the parser internal implementation
38    pub parser_impl: ParserImpl,
39}
40
41impl Default for ParserOptions {
42    fn default() -> Self {
43        Self {
44            enable_extended_globbing: true,
45            posix_mode: false,
46            sh_mode: false,
47            tilde_expansion_at_word_start: true,
48            tilde_expansion_after_colon: false,
49            parser_impl: ParserImpl::default(),
50        }
51    }
52}
53
54impl ParserOptions {
55    /// Returns the tokenizer options implied by these parser options.
56    pub const fn tokenizer_options(&self) -> TokenizerOptions {
57        TokenizerOptions {
58            enable_extended_globbing: self.enable_extended_globbing,
59            posix_mode: self.posix_mode,
60            sh_mode: self.sh_mode,
61        }
62    }
63}
64
65/// Information about the source of tokens.
66#[derive(Clone, Debug, Default)]
67#[allow(dead_code)]
68pub struct SourceInfo {
69    /// The source of the tokens.
70    pub source: String,
71}
72
73impl From<PathBuf> for SourceInfo {
74    fn from(path: PathBuf) -> Self {
75        Self {
76            source: path.to_string_lossy().to_string(),
77        }
78    }
79}
80
81/// Implements parsing for shell programs.
82pub struct Parser<R: std::io::BufRead> {
83    /// The reader to use for input
84    reader: R,
85    /// Parsing options
86    options: ParserOptions,
87}
88
89#[bon]
90impl<R: std::io::BufRead> Parser<R> {
91    ///
92    /// # Arguments
93    ///
94    /// * `reader` - The reader to use for input.
95    /// * `options` - The options to use when parsing.
96    pub fn new(reader: R, options: &ParserOptions) -> Self {
97        Self {
98            reader,
99            options: options.clone(),
100        }
101    }
102
103    /// Create a new parser instance through a builder
104    #[builder(
105        finish_fn(doc {
106            /// Instantiate a parser with the provided reader as input
107        })
108    )]
109    pub const fn builder(
110        /// The reader to use for input
111        #[builder(finish_fn)]
112        reader: R,
113
114        #[builder(default = true)]
115        /// Whether or not to enable extended globbing (a.k.a. `extglob`).
116        enable_extended_globbing: bool,
117        #[builder(default = false)]
118        /// Whether or not to enable POSIX compliance mode.
119        posix_mode: bool,
120        #[builder(default = false)]
121        /// Whether or not to enable maximal compatibility with the `sh` shell.
122        sh_mode: bool,
123        #[builder(default = true)]
124        /// Whether or not to perform tilde expansion for tildes at the start of
125        /// words.
126        tilde_expansion_at_word_start: bool,
127        #[builder(default = false)]
128        /// Whether or not to perform tilde expansion for tildes after colons.
129        tilde_expansion_after_colon: bool,
130        #[builder(default)]
131        /// Select the parser internal implementation
132        parser_impl: ParserImpl,
133    ) -> Self {
134        let options = ParserOptions {
135            enable_extended_globbing,
136            posix_mode,
137            sh_mode,
138            tilde_expansion_at_word_start,
139            tilde_expansion_after_colon,
140            parser_impl,
141        };
142        Self { reader, options }
143    }
144
145    /// Parses the input into an abstract syntax tree (AST) of a shell program.
146    pub fn parse_program(&mut self) -> Result<ast::Program, crate::error::ParseError> {
147        //
148        // References:
149        //   * https://www.gnu.org/software/bash/manual/bash.html#Shell-Syntax
150        //   * https://mywiki.wooledge.org/BashParser
151        //   * https://aosabook.org/en/v1/bash.html
152        //   * https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
153        //
154        match self.options.parser_impl {
155            ParserImpl::Peg => {
156                let tokens = self.tokenize()?;
157                parse_tokens(&tokens, &self.options)
158            }
159            #[cfg(feature = "winnow-parser")]
160            ParserImpl::Winnow => {
161                // Read entire input to string for winnow_str parser
162                let mut input_str = String::new();
163                std::io::Read::read_to_string(&mut self.reader, &mut input_str).map_err(|e| {
164                    crate::error::ParseError::Tokenizing {
165                        inner: crate::tokenizer::TokenizerError::from(e),
166                        position: None,
167                    }
168                })?;
169
170                winnow_str::parse_program(&input_str, &self.options, &SourceInfo::default())
171                    .map_err(|_e| {
172                        // Convert winnow error to ParseError
173                        // TODO: Extract position information from winnow error
174                        crate::error::ParseError::ParsingAtEndOfInput
175                    })
176            }
177        }
178    }
179
180    /// Parses a function definition body from the input. The body is expected
181    /// to be preceded by "()", but no function name.
182    pub fn parse_function_parens_and_body(
183        &mut self,
184    ) -> Result<ast::FunctionBody, crate::error::ParseError> {
185        let tokens = self.tokenize()?;
186        let parse_result =
187            peg::token_parser::function_parens_and_body(&Tokens { tokens: &tokens }, &self.options);
188        parse_result_to_error(parse_result, &tokens)
189    }
190
191    fn tokenize(&mut self) -> Result<Vec<Token>, crate::error::ParseError> {
192        // First we tokenize the input, according to the policy implied by
193        // provided options.
194        let mut tokenizer = Tokenizer::new(&mut self.reader, &self.options.tokenizer_options());
195
196        tracing::debug!(target: "tokenize", "Tokenizing...");
197
198        let mut tokens = vec![];
199        loop {
200            let result = match tokenizer.next_token() {
201                Ok(result) => result,
202                Err(e) => {
203                    return Err(crate::error::ParseError::Tokenizing {
204                        inner: e,
205                        position: tokenizer.current_location(),
206                    });
207                }
208            };
209
210            let reason = result.reason;
211            if let Some(token) = result.token {
212                tracing::debug!(target: "tokenize", "TOKEN {}: {:?} {reason:?}", tokens.len(), token);
213                tokens.push(token);
214            }
215
216            if matches!(reason, TokenEndReason::EndOfInput) {
217                break;
218            }
219        }
220
221        tracing::debug!(target: "tokenize", "  => {} token(s)", tokens.len());
222
223        Ok(tokens)
224    }
225}
226
227/// Parses a sequence of tokens into the abstract syntax tree (AST) of a shell
228/// program.
229///
230/// # Arguments
231///
232/// * `tokens` - The tokens to parse.
233/// * `options` - The options to use when parsing.
234pub fn parse_tokens(
235    tokens: &[Token],
236    options: &ParserOptions,
237) -> Result<ast::Program, crate::error::ParseError> {
238    let parse_result = peg::token_parser::program(&Tokens { tokens }, options);
239    parse_result_to_error(parse_result, tokens)
240}
241
242/// Tokenizes and parses text as a compound assignment value, returning its
243/// element words. Returns `None` if the text is not a well-formed compound
244/// value.
245///
246/// # Arguments
247///
248/// * `input` - The text to parse.
249/// * `options` - The options to use when parsing.
250pub(crate) fn parse_compound_assignment_value(
251    input: &str,
252    options: &ParserOptions,
253) -> Option<Vec<String>> {
254    let mut parser = Parser::new(input.as_bytes(), options);
255    let tokens = parser.tokenize().ok()?;
256    let tokens = Tokens { tokens: &tokens };
257    let elements = peg::token_parser::compound_assignment_value(&tokens, options).ok()?;
258    Some(elements.into_iter().cloned().collect())
259}
260
261fn parse_result_to_error<R>(
262    parse_result: Result<R, ::peg::error::ParseError<usize>>,
263    tokens: &[Token],
264) -> Result<R, crate::error::ParseError>
265where
266    R: std::fmt::Debug,
267{
268    match parse_result {
269        Ok(program) => {
270            tracing::debug!(target: "parse", "PROG: {:?}", program);
271            Ok(program)
272        }
273        Err(parse_error) => {
274            tracing::debug!(target: "parse", "Parse error: {:?}", parse_error);
275            Err(crate::error::convert_peg_parse_error(&parse_error, tokens))
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests;