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    ///
147    /// # Errors
148    ///
149    /// - [`crate::ParseError::Tokenizing`] if the input cannot be tokenized
150    ///   (see [`crate::TokenizerError`]).
151    /// - [`crate::ParseError::ParsingNear`] if the tokens contain a syntax
152    ///   error; it carries the position of the offending token.
153    /// - [`crate::ParseError::ParsingAtEndOfInput`] if the input ends before
154    ///   the program is complete.
155    ///
156    /// # Panics
157    ///
158    /// Panics if the parser was built with the `winnow` implementation (the
159    /// `winnow-parser` feature), because that parser is not yet implemented.
160    pub fn parse_program(&mut self) -> Result<ast::Program, crate::error::ParseError> {
161        //
162        // References:
163        //   * https://www.gnu.org/software/bash/manual/bash.html#Shell-Syntax
164        //   * https://mywiki.wooledge.org/BashParser
165        //   * https://aosabook.org/en/v1/bash.html
166        //   * https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
167        //
168        match self.options.parser_impl {
169            ParserImpl::Peg => {
170                let tokens = self.tokenize()?;
171                parse_tokens(&tokens, &self.options)
172            }
173            #[cfg(feature = "winnow-parser")]
174            ParserImpl::Winnow => {
175                // Read entire input to string for winnow_str parser
176                let mut input_str = String::new();
177                std::io::Read::read_to_string(&mut self.reader, &mut input_str).map_err(|e| {
178                    crate::error::ParseError::Tokenizing {
179                        inner: crate::tokenizer::TokenizerError::from(e),
180                        position: None,
181                    }
182                })?;
183
184                winnow_str::parse_program(&input_str, &self.options, &SourceInfo::default())
185                    .map_err(|_e| {
186                        // Convert winnow error to ParseError
187                        // TODO: Extract position information from winnow error
188                        crate::error::ParseError::ParsingAtEndOfInput
189                    })
190            }
191        }
192    }
193
194    /// Parses a function definition body from the input. The body is expected
195    /// to be preceded by "()", but no function name.
196    ///
197    /// # Errors
198    ///
199    /// - [`crate::ParseError::Tokenizing`] if the input cannot be tokenized
200    ///   (see [`crate::TokenizerError`]).
201    /// - [`crate::ParseError::ParsingNear`] if the tokens contain a syntax
202    ///   error; it carries the position of the offending token.
203    /// - [`crate::ParseError::ParsingAtEndOfInput`] if the input ends before
204    ///   the program is complete.
205    pub fn parse_function_parens_and_body(
206        &mut self,
207    ) -> Result<ast::FunctionBody, crate::error::ParseError> {
208        let tokens = self.tokenize()?;
209        let parse_result =
210            peg::token_parser::function_parens_and_body(&Tokens { tokens: &tokens }, &self.options);
211        parse_result_to_error(parse_result, &tokens)
212    }
213
214    fn tokenize(&mut self) -> Result<Vec<Token>, crate::error::ParseError> {
215        // First we tokenize the input, according to the policy implied by
216        // provided options.
217        let mut tokenizer = Tokenizer::new(&mut self.reader, &self.options.tokenizer_options());
218
219        tracing::debug!(target: "tokenize", "Tokenizing...");
220
221        let mut tokens = vec![];
222        loop {
223            let result = match tokenizer.next_token() {
224                Ok(result) => result,
225                Err(e) => {
226                    return Err(crate::error::ParseError::Tokenizing {
227                        inner: e,
228                        position: tokenizer.current_location(),
229                    });
230                }
231            };
232
233            let reason = result.reason;
234            if let Some(token) = result.token {
235                tracing::debug!(target: "tokenize", "TOKEN {}: {:?} {reason:?}", tokens.len(), token);
236                tokens.push(token);
237            }
238
239            if matches!(reason, TokenEndReason::EndOfInput) {
240                break;
241            }
242        }
243
244        tracing::debug!(target: "tokenize", "  => {} token(s)", tokens.len());
245
246        Ok(tokens)
247    }
248}
249
250/// Parses a sequence of tokens into the abstract syntax tree (AST) of a shell
251/// program.
252///
253/// # Arguments
254///
255/// * `tokens` - The tokens to parse.
256/// * `options` - The options to use when parsing.
257///
258/// # Errors
259///
260/// - [`crate::ParseError::ParsingNear`] if the tokens contain a syntax error;
261///   it carries the position of the offending token.
262/// - [`crate::ParseError::ParsingAtEndOfInput`] if the tokens end before the
263///   program is complete.
264pub fn parse_tokens(
265    tokens: &[Token],
266    options: &ParserOptions,
267) -> Result<ast::Program, crate::error::ParseError> {
268    let parse_result = peg::token_parser::program(&Tokens { tokens }, options);
269    parse_result_to_error(parse_result, tokens)
270}
271
272/// Tokenizes and parses text as a compound assignment value, returning its
273/// element words. Returns `None` if the text is not a well-formed compound
274/// value.
275///
276/// # Arguments
277///
278/// * `input` - The text to parse.
279/// * `options` - The options to use when parsing.
280pub(crate) fn parse_compound_assignment_value(
281    input: &str,
282    options: &ParserOptions,
283) -> Option<Vec<String>> {
284    let mut parser = Parser::new(input.as_bytes(), options);
285    let tokens = parser.tokenize().ok()?;
286    let tokens = Tokens { tokens: &tokens };
287    let elements = peg::token_parser::compound_assignment_value(&tokens, options).ok()?;
288    Some(elements.into_iter().cloned().collect())
289}
290
291fn parse_result_to_error<R>(
292    parse_result: Result<R, ::peg::error::ParseError<usize>>,
293    tokens: &[Token],
294) -> Result<R, crate::error::ParseError>
295where
296    R: std::fmt::Debug,
297{
298    match parse_result {
299        Ok(program) => {
300            tracing::debug!(target: "parse", "PROG: {:?}", program);
301            Ok(program)
302        }
303        Err(parse_error) => {
304            tracing::debug!(target: "parse", "Parse error: {:?}", parse_error);
305            Err(crate::error::convert_peg_parse_error(&parse_error, tokens))
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests;