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;