Skip to main content

clash_brush_parser/
word.rs

1//! Parser for shell words, used in expansion and other contexts.
2//!
3//! Implements support for:
4//!
5//! - Text quoting (single, double, ANSI C).
6//! - Escape sequences.
7//! - Tilde prefixes.
8//! - Parameter expansion expressions.
9//! - Command substitution expressions.
10//! - Arithmetic expansion expressions.
11
12use std::fmt::Debug;
13use std::fmt::Display;
14
15use crate::ParserOptions;
16use crate::SourceSpan;
17use crate::ast;
18use crate::error;
19
20/// Encapsulates a `WordPiece` together with its position in the string it came from.
21#[derive(Clone, Debug)]
22#[cfg_attr(
23    any(test, feature = "serde"),
24    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
25)]
26pub struct WordPieceWithSource {
27    /// The word piece.
28    pub piece: WordPiece,
29    /// The start index of the piece in the source string.
30    pub start_index: usize,
31    /// The end index of the piece in the source string.
32    pub end_index: usize,
33}
34
35/// Represents a piece of a word.
36#[derive(Clone, Debug)]
37#[cfg_attr(
38    any(test, feature = "serde"),
39    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
40)]
41pub enum WordPiece {
42    /// A simple unquoted, unescaped string.
43    Text(String),
44    /// A string that is single-quoted.
45    SingleQuotedText(String),
46    /// A string that is ANSI-C quoted.
47    AnsiCQuotedText(String),
48    /// A sequence of pieces that are embedded in double quotes.
49    DoubleQuotedSequence(Vec<WordPieceWithSource>),
50    /// Gettext enabled variant of [`WordPiece::DoubleQuotedSequence`].
51    GettextDoubleQuotedSequence(Vec<WordPieceWithSource>),
52    /// A tilde expansion.
53    TildeExpansion(TildeExpr),
54    /// A parameter expansion.
55    ParameterExpansion(ParameterExpr),
56    /// A command substitution.
57    CommandSubstitution(String),
58    /// A backquoted command substitution.
59    BackquotedCommandSubstitution(String),
60    /// An escape sequence.
61    EscapeSequence(String),
62    /// An arithmetic expression.
63    ArithmeticExpression(ast::UnexpandedArithmeticExpr),
64}
65
66/// Represents an expandable tilde expression (e.g., ~).
67#[derive(Clone, Debug)]
68#[cfg_attr(
69    any(test, feature = "serde"),
70    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
71)]
72pub enum TildeExpr {
73    /// `~`
74    Home,
75    /// `~<user>`
76    UserHome(String),
77    /// `~+`
78    WorkingDir,
79    /// `~-`
80    OldWorkingDir,
81    /// Represents a tilde expansion of the form `~+N`, referring to the Nth directory in
82    /// the shell's directory stack, starting at the top of the stack. Note that the directory
83    /// stack is expected to contains the current working directory as its topmost entry.
84    NthDirFromTopOfDirStack {
85        /// Index into the directory stack (zero-based: 0 is the top of the stack).
86        n: usize,
87        /// Whether the '+' prefix was explicitly used.
88        plus_used: bool,
89    },
90    /// Represents a tilde expansion of the form `~-N`, referring to the Nth directory in
91    /// the shell's directory stack, starting at the bottom of the stack.
92    NthDirFromBottomOfDirStack {
93        /// Index into the directory stack (zero-based: 0 is the bottom of the stack).
94        n: usize,
95    },
96}
97
98/// Type of a parameter test.
99#[derive(Clone, Debug)]
100#[cfg_attr(
101    any(test, feature = "serde"),
102    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
103)]
104pub enum ParameterTestType {
105    /// Check for unset or null.
106    UnsetOrNull,
107    /// Check for unset.
108    Unset,
109}
110
111/// A parameter, used in a parameter expansion.
112#[derive(Clone, Debug)]
113#[cfg_attr(
114    any(test, feature = "serde"),
115    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
116)]
117pub enum Parameter {
118    /// A 0-indexed positional parameter.
119    Positional(u32),
120    /// A special parameter.
121    Special(SpecialParameter),
122    /// A named variable.
123    Named(String),
124    /// An index into a named variable.
125    NamedWithIndex {
126        /// Variable name.
127        name: String,
128        /// Index.
129        index: String,
130    },
131    /// A named array variable with all indices.
132    NamedWithAllIndices {
133        /// Variable name.
134        name: String,
135        /// Whether to concatenate the values.
136        concatenate: bool,
137    },
138}
139
140impl Display for Parameter {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            Self::Positional(n) => write!(f, "${n}"),
144            Self::Special(s) => write!(f, "${s}"),
145            Self::Named(name) => write!(f, "${{{name}}}"),
146            Self::NamedWithIndex { name, index } => {
147                write!(f, "${{{name}[{index}]}}")
148            }
149            Self::NamedWithAllIndices { name, concatenate } => {
150                if *concatenate {
151                    write!(f, "${{{name}[*]}}")
152                } else {
153                    write!(f, "${{{name}[@]}}")
154                }
155            }
156        }
157    }
158}
159
160/// A special parameter, used in a parameter expansion.
161#[derive(Clone, Debug)]
162#[cfg_attr(
163    any(test, feature = "serde"),
164    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
165)]
166pub enum SpecialParameter {
167    /// All positional parameters.
168    AllPositionalParameters {
169        /// Whether to concatenate the values.
170        concatenate: bool,
171    },
172    /// The count of positional parameters.
173    PositionalParameterCount,
174    /// The last exit status in the shell.
175    LastExitStatus,
176    /// The current shell option flags.
177    CurrentOptionFlags,
178    /// The current shell process ID.
179    ProcessId,
180    /// The last background process ID managed by the shell.
181    LastBackgroundProcessId,
182    /// The name of the shell.
183    ShellName,
184}
185
186impl Display for SpecialParameter {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            Self::AllPositionalParameters { concatenate } => {
190                if *concatenate {
191                    write!(f, "*")
192                } else {
193                    write!(f, "@")
194                }
195            }
196            Self::PositionalParameterCount => write!(f, "#"),
197            Self::LastExitStatus => write!(f, "?"),
198            Self::CurrentOptionFlags => write!(f, "-"),
199            Self::ProcessId => write!(f, "$"),
200            Self::LastBackgroundProcessId => write!(f, "!"),
201            Self::ShellName => write!(f, "0"),
202        }
203    }
204}
205
206/// A parameter expression, used in a parameter expansion.
207#[derive(Clone, Debug)]
208#[cfg_attr(
209    any(test, feature = "serde"),
210    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
211)]
212pub enum ParameterExpr {
213    /// A parameter, with optional indirection.
214    Parameter {
215        /// The parameter.
216        parameter: Parameter,
217        /// Whether to treat the expanded parameter as an indirect
218        /// reference, which should be subsequently dereferenced
219        /// for the expansion.
220        indirect: bool,
221    },
222    /// Conditionally use default values.
223    UseDefaultValues {
224        /// The parameter.
225        parameter: Parameter,
226        /// Whether to treat the expanded parameter as an indirect
227        /// reference, which should be subsequently dereferenced
228        /// for the expansion.
229        indirect: bool,
230        /// The type of test to perform.
231        test_type: ParameterTestType,
232        /// Default value to conditionally use.
233        default_value: Option<String>,
234    },
235    /// Conditionally assign default values.
236    AssignDefaultValues {
237        /// The parameter.
238        parameter: Parameter,
239        /// Whether to treat the expanded parameter as an indirect
240        /// reference, which should be subsequently dereferenced
241        /// for the expansion.
242        indirect: bool,
243        /// The type of test to perform.
244        test_type: ParameterTestType,
245        /// Default value to conditionally assign.
246        default_value: Option<String>,
247    },
248    /// Indicate error if null or unset.
249    IndicateErrorIfNullOrUnset {
250        /// The parameter.
251        parameter: Parameter,
252        /// Whether to treat the expanded parameter as an indirect
253        /// reference, which should be subsequently dereferenced
254        /// for the expansion.
255        indirect: bool,
256        /// The type of test to perform.
257        test_type: ParameterTestType,
258        /// Error message to conditionally yield.
259        error_message: Option<String>,
260    },
261    /// Conditionally use an alternative value.
262    UseAlternativeValue {
263        /// The parameter.
264        parameter: Parameter,
265        /// Whether to treat the expanded parameter as an indirect
266        /// reference, which should be subsequently dereferenced
267        /// for the expansion.
268        indirect: bool,
269        /// The type of test to perform.
270        test_type: ParameterTestType,
271        /// Alternative value to conditionally use.
272        alternative_value: Option<String>,
273    },
274    /// Compute the length of the given parameter.
275    ParameterLength {
276        /// The parameter.
277        parameter: Parameter,
278        /// Whether to treat the expanded parameter as an indirect
279        /// reference, which should be subsequently dereferenced
280        /// for the expansion.
281        indirect: bool,
282    },
283    /// Remove the smallest suffix from the given string matching the given pattern.
284    RemoveSmallestSuffixPattern {
285        /// The parameter.
286        parameter: Parameter,
287        /// Whether to treat the expanded parameter as an indirect
288        /// reference, which should be subsequently dereferenced
289        /// for the expansion.
290        indirect: bool,
291        /// Optionally provides a pattern to match.
292        pattern: Option<String>,
293    },
294    /// Remove the largest suffix from the given string matching the given pattern.
295    RemoveLargestSuffixPattern {
296        /// The parameter.
297        parameter: Parameter,
298        /// Whether to treat the expanded parameter as an indirect
299        /// reference, which should be subsequently dereferenced
300        /// for the expansion.
301        indirect: bool,
302        /// Optionally provides a pattern to match.
303        pattern: Option<String>,
304    },
305    /// Remove the smallest prefix from the given string matching the given pattern.
306    RemoveSmallestPrefixPattern {
307        /// The parameter.
308        parameter: Parameter,
309        /// Whether to treat the expanded parameter as an indirect
310        /// reference, which should be subsequently dereferenced
311        /// for the expansion.
312        indirect: bool,
313        /// Optionally provides a pattern to match.
314        pattern: Option<String>,
315    },
316    /// Remove the largest prefix from the given string matching the given pattern.
317    RemoveLargestPrefixPattern {
318        /// The parameter.
319        parameter: Parameter,
320        /// Whether to treat the expanded parameter as an indirect
321        /// reference, which should be subsequently dereferenced
322        /// for the expansion.
323        indirect: bool,
324        /// Optionally provides a pattern to match.
325        pattern: Option<String>,
326    },
327    /// Extract a substring from the given parameter.
328    Substring {
329        /// The parameter.
330        parameter: Parameter,
331        /// Whether to treat the expanded parameter as an indirect
332        /// reference, which should be subsequently dereferenced
333        /// for the expansion.
334        indirect: bool,
335        /// Arithmetic expression that will be expanded to compute the offset
336        /// at which the substring should be extracted.
337        offset: ast::UnexpandedArithmeticExpr,
338        /// Optionally provides an arithmetic expression that will be expanded
339        /// to compute the length of substring to be extracted; if left
340        /// unspecified, the remainder of the string will be extracted.
341        length: Option<ast::UnexpandedArithmeticExpr>,
342    },
343    /// Transform the given parameter.
344    Transform {
345        /// The parameter.
346        parameter: Parameter,
347        /// Whether to treat the expanded parameter as an indirect
348        /// reference, which should be subsequently dereferenced
349        /// for the expansion.
350        indirect: bool,
351        /// Type of transformation to apply.
352        op: ParameterTransformOp,
353    },
354    /// Uppercase the first character of the given parameter.
355    UppercaseFirstChar {
356        /// The parameter.
357        parameter: Parameter,
358        /// Whether to treat the expanded parameter as an indirect
359        /// reference, which should be subsequently dereferenced
360        /// for the expansion.
361        indirect: bool,
362        /// Optionally provides a pattern to match.
363        pattern: Option<String>,
364    },
365    /// Uppercase the portion of the given parameter matching the given pattern.
366    UppercasePattern {
367        /// The parameter.
368        parameter: Parameter,
369        /// Whether to treat the expanded parameter as an indirect
370        /// reference, which should be subsequently dereferenced
371        /// for the expansion.
372        indirect: bool,
373        /// Optionally provides a pattern to match.
374        pattern: Option<String>,
375    },
376    /// Lowercase the first character of the given parameter.
377    LowercaseFirstChar {
378        /// The parameter.
379        parameter: Parameter,
380        /// Whether to treat the expanded parameter as an indirect
381        /// reference, which should be subsequently dereferenced
382        /// for the expansion.
383        indirect: bool,
384        /// Optionally provides a pattern to match.
385        pattern: Option<String>,
386    },
387    /// Lowercase the portion of the given parameter matching the given pattern.
388    LowercasePattern {
389        /// The parameter.
390        parameter: Parameter,
391        /// Whether to treat the expanded parameter as an indirect
392        /// reference, which should be subsequently dereferenced
393        /// for the expansion.
394        indirect: bool,
395        /// Optionally provides a pattern to match.
396        pattern: Option<String>,
397    },
398    /// Replace occurrences of the given pattern in the given parameter.
399    ReplaceSubstring {
400        /// The parameter.
401        parameter: Parameter,
402        /// Whether to treat the expanded parameter as an indirect
403        /// reference, which should be subsequently dereferenced
404        /// for the expansion.
405        indirect: bool,
406        /// Pattern to match.
407        pattern: String,
408        /// Replacement string.
409        replacement: Option<String>,
410        /// Kind of match to perform.
411        match_kind: SubstringMatchKind,
412    },
413    /// Select variable names from the environment with a given prefix.
414    VariableNames {
415        /// The prefix to match.
416        prefix: String,
417        /// Whether to concatenate the results.
418        concatenate: bool,
419    },
420    /// Select member keys from the named array.
421    MemberKeys {
422        /// Name of the array variable.
423        variable_name: String,
424        /// Whether to concatenate the results.
425        concatenate: bool,
426    },
427}
428
429/// Kind of substring match.
430#[derive(Clone, Debug)]
431#[cfg_attr(
432    any(test, feature = "serde"),
433    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
434)]
435pub enum SubstringMatchKind {
436    /// Match the prefix of the string.
437    Prefix,
438    /// Match the suffix of the string.
439    Suffix,
440    /// Match the first occurrence in the string.
441    FirstOccurrence,
442    /// Match all instances in the string.
443    Anywhere,
444}
445
446/// Kind of operation to apply to a parameter.
447#[derive(Clone, Debug)]
448#[cfg_attr(
449    any(test, feature = "serde"),
450    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
451)]
452pub enum ParameterTransformOp {
453    /// Capitalizate initials.
454    CapitalizeInitial,
455    /// Expand escape sequences.
456    ExpandEscapeSequences,
457    /// Possibly quote with arrays expanded.
458    PossiblyQuoteWithArraysExpanded {
459        /// Whether or not to yield separate words.
460        separate_words: bool,
461    },
462    /// Apply prompt expansion.
463    PromptExpand,
464    /// Quote the parameter.
465    Quoted,
466    /// Translate to a format usable in an assignment/declaration.
467    ToAssignmentLogic,
468    /// Translate to the parameter's attribute flags.
469    ToAttributeFlags,
470    /// Translate to lowercase.
471    ToLowerCase,
472    /// Translate to uppercase.
473    ToUpperCase,
474}
475
476/// Represents a sub-word that is either a brace expression or some other word text.
477#[derive(Clone, Debug)]
478#[cfg_attr(
479    any(test, feature = "serde"),
480    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
481)]
482pub enum BraceExpressionOrText {
483    /// A brace expression.
484    Expr(BraceExpression),
485    /// Other word text.
486    Text(String),
487}
488
489/// Represents a brace expression to be expanded.
490pub type BraceExpression = Vec<BraceExpressionMember>;
491
492/// Member of a brace expression.
493#[derive(Clone, Debug)]
494#[cfg_attr(
495    any(test, feature = "serde"),
496    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
497)]
498pub enum BraceExpressionMember {
499    /// An inclusive numerical sequence.
500    NumberSequence {
501        /// Start of the sequence.
502        start: i64,
503        /// Inclusive end of the sequence.
504        end: i64,
505        /// Increment value.
506        increment: i64,
507    },
508    /// An inclusive character sequence.
509    CharSequence {
510        /// Start of the sequence.
511        start: char,
512        /// Inclusive end of the sequence.
513        end: char,
514        /// Increment value.
515        increment: i64,
516    },
517    /// Child text or expressions.
518    Child(Vec<BraceExpressionOrText>),
519}
520
521/// Parse a word into its constituent pieces.
522///
523/// # Arguments
524///
525/// * `word` - The word to parse.
526/// * `options` - The parser options to use.
527pub fn parse(
528    word: &str,
529    options: &ParserOptions,
530) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
531    cacheable_parse(word.to_owned(), options.to_owned())
532}
533
534#[cached::proc_macro::cached(size = 64, result = true)]
535fn cacheable_parse(
536    word: String,
537    options: ParserOptions,
538) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
539    tracing::debug!(target: "expansion", "Parsing word '{}'", word);
540
541    let pieces = expansion_parser::unexpanded_word(word.as_str(), &options)
542        .map_err(|err| error::WordParseError::Word(word.clone(), err.into()))?;
543
544    tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces);
545
546    Ok(pieces)
547}
548
549/// Parse a heredoc body, treating `"` and `'` as literal characters.
550///
551/// # Arguments
552///
553/// * `word` - The heredoc body to parse.
554/// * `options` - The parser options to use.
555pub fn parse_heredoc(
556    word: &str,
557    options: &ParserOptions,
558) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
559    expansion_parser::unexpanded_heredoc_word(word, options)
560        .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))
561}
562
563/// Parse the given word into a parameter expression.
564///
565/// # Arguments
566///
567/// * `word` - The word to parse.
568/// * `options` - The parser options to use.
569pub fn parse_parameter(
570    word: &str,
571    options: &ParserOptions,
572) -> Result<Parameter, error::WordParseError> {
573    expansion_parser::parameter(word, options)
574        .map_err(|err| error::WordParseError::Parameter(word.to_owned(), err.into()))
575}
576
577/// Parse brace expansion from a given word .
578///
579/// # Arguments
580///
581/// * `word` - The word to parse.
582/// * `options` - The parser options to use.
583pub fn parse_brace_expansions(
584    word: &str,
585    options: &ParserOptions,
586) -> Result<Option<Vec<BraceExpressionOrText>>, error::WordParseError> {
587    expansion_parser::brace_expansions(word, options)
588        .map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err.into()))
589}
590
591pub(crate) fn parse_assignment_word(
592    word: &str,
593) -> Result<ast::Assignment, peg::error::ParseError<peg::str::LineCol>> {
594    expansion_parser::name_equals_scalar_value(word, &ParserOptions::default())
595}
596
597pub(crate) fn parse_array_assignment(
598    word: &str,
599    elements: &[&String],
600) -> Result<ast::Assignment, &'static str> {
601    let (assignment_name, append) = expansion_parser::name_equals(word, &ParserOptions::default())
602        .map_err(|_| "not array assignment word")?;
603
604    let elements = elements
605        .iter()
606        .map(|element| expansion_parser::literal_array_element(element, &ParserOptions::default()))
607        .collect::<Result<Vec<_>, _>>()
608        .map_err(|_| "invalid array element in literal")?;
609
610    let elements_as_words = elements
611        .into_iter()
612        .map(|(key, value)| {
613            (
614                key.map(|k| ast::Word::new(k.as_str())),
615                ast::Word::new(value.as_str()),
616            )
617        })
618        .collect();
619
620    Ok(ast::Assignment {
621        name: assignment_name,
622        value: ast::AssignmentValue::Array(elements_as_words),
623        append,
624        loc: SourceSpan::default(),
625    })
626}
627
628peg::parser! {
629    grammar expansion_parser(parser_options: &ParserOptions) for str {
630        // Helper rule that enables pegviz to be used to visualize debug peg traces.
631        rule traced<T>(e: rule<T>) -> T =
632            &(input:$([_]*) {
633                #[cfg(feature = "debug-tracing")]
634                println!("[PEG_INPUT_START]\n{input}\n[PEG_TRACE_START]");
635            })
636            e:e()? {?
637                #[cfg(feature = "debug-tracing")]
638                println!("[PEG_TRACE_STOP]");
639                e.ok_or("")
640            }
641
642        pub(crate) rule unexpanded_word() -> Vec<WordPieceWithSource> = traced(<word(<![_]>)>)
643
644        rule word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
645            tilde:tilde_expr_prefix_with_source()? pieces:word_piece_with_source(<stop_condition()>, false /*in_command*/)* {
646                let mut all_pieces = Vec::new();
647                if let Some(tilde) = tilde {
648                    all_pieces.push(tilde);
649                }
650                all_pieces.extend(pieces);
651                all_pieces
652            }
653
654        // Takes a word as input.
655        pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
656            pieces:(brace_expansion_piece(<![_]>)+) { Some(pieces) } /
657            [_]* { None }
658
659        // Returns either a complete brace expression (without any prefix or suffix), or a
660        // non-brace-expression string.
661        rule brace_expansion_piece<T>(stop_condition: rule<T>) -> BraceExpressionOrText =
662            expr:brace_expr() {
663                BraceExpressionOrText::Expr(expr)
664            } /
665            text:$(non_brace_expr_text(<stop_condition()>)+) { BraceExpressionOrText::Text(text.to_owned()) }
666
667        // Parses text that is not considered to contain a brace expression.
668        rule non_brace_expr_text<T>(stop_condition: rule<T>) -> () =
669            !"{" word_piece(<['{'] {} / stop_condition() {}>, false) {} /
670            !brace_expr() !stop_condition() "{" {}
671
672        // Parses a complete brace expression, with no prefix or suffix.
673        pub(crate) rule brace_expr() -> BraceExpression =
674            "{" inner:brace_expr_inner() "}" { inner }
675
676        // Parses the text inside a complete brace expression; basically the complete brace
677        // expression without the opening and closing brace characters.
678        pub(crate) rule brace_expr_inner() -> BraceExpression =
679            brace_text_list_expr() /
680            seq:brace_sequence_expr() { vec![seq] }
681
682        // Parses a list of brace expression members, including the separating commas; does
683        // not include the opening and closing braces.
684        pub(crate) rule brace_text_list_expr() -> BraceExpression =
685            brace_text_list_member() **<2,> ","
686
687        // Parses an element that can occur in a brace expression member list, not including the
688        // terminating comma or closing brace.
689        pub(crate) rule brace_text_list_member() -> BraceExpressionMember =
690            // Matches an empty-string member, without consuming the comma or closing brace that terminates it.
691            &[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } /
692            // Matches a nested string that may include some combination of concatenated textual strings
693            // and brace expressions.
694            child_pieces:(brace_expansion_piece(<[',' | '}']>)+) {
695                BraceExpressionMember::Child(child_pieces)
696            }
697
698        pub(crate) rule brace_sequence_expr() -> BraceExpressionMember =
699            start:number() ".." end:number() increment:(".." n:number() { n })? {
700                BraceExpressionMember::NumberSequence { start, end, increment: increment.unwrap_or(1) }
701            } /
702            start:character() ".." end:character() increment:(".." n:number() { n })? {
703                BraceExpressionMember::CharSequence { start, end, increment: increment.unwrap_or(1) }
704            }
705
706        rule number() -> i64 = sign:number_sign()? n:$(['0'..='9']+) {
707            let sign = sign.unwrap_or(1);
708            let num: i64 = n.parse().unwrap();
709            num * sign
710        }
711
712        rule number_sign() -> i64 =
713            ['-'] { -1 } /
714            ['+'] { 1 }
715
716        rule character() -> char = ['a'..='z' | 'A'..='Z']
717
718        pub(crate) rule is_arithmetic_word() =
719            arithmetic_word(<![_]>)
720
721        // N.B. We don't bother returning the word pieces, as all users of this rule
722        // only try to extract the consumed input string and not the parse result.
723        rule arithmetic_word<T>(stop_condition: rule<T>) =
724            arithmetic_word_piece(<stop_condition()>)* {}
725
726        pub(crate) rule is_arithmetic_word_piece() =
727            arithmetic_word_piece(<![_]>)
728
729        // This rule matches an individual "piece" of an arithmetic expression. It needs to handle
730        // matching nested parenthesized expressions as well. We stop consuming the input when
731        // we reach the provided stop condition, which typically denotes the end of the containing
732        // arithmetic expression.
733        rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
734            // This branch matches a parenthesized piece; we consume the opening parenthesis and
735            // delegate the rest to a helper rule. We don't worry about the stop condition passed
736            // into us, because if we see an opening parenthesis then we *must* find its closing
737            // partner.
738            "(" arithmetic_word_plus_right_paren() {} /
739            // This branch handles the case where we have an array element name with square brackets,
740            // which may (legitimately) contain the stop condition.
741            array_element_name() {} /
742            // This branch matches any standard piece of a word, stopping as soon as we reach
743            // either the overall stop condition *OR* an opening parenthesis. We add this latter
744            // condition to ensure that *we* handle matching parentheses.
745            !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>, false /*in_command*/) {}
746
747        // This is a helper rule that matches either the provided stop condition or an opening parenthesis.
748        rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
749            stop_condition() {} /
750            "(" {}
751
752        // This rule matches an arithmetic word followed by a right parenthesis. It must consume the right parenthesis.
753        rule arithmetic_word_plus_right_paren() =
754            arithmetic_word(<[')']>) ")"
755
756        rule word_piece_with_source<T>(stop_condition: rule<T>, in_command: bool) -> WordPieceWithSource =
757            start_index:position!() piece:word_piece(<stop_condition()>, in_command) end_index:position!() {
758                WordPieceWithSource { piece, start_index, end_index }
759            }
760
761        rule word_piece<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
762            // Rules that match quoted text.
763            s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
764            s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
765            s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
766            s:gettext_double_quoted_sequence() { WordPiece::GettextDoubleQuotedSequence(s) } /
767            // Rules that match pieces starting with a dollar sign ('$').
768            dollar_sign_word_piece() /
769            // Rules that match unquoted text that doesn't start with an unescaped dollar sign.
770            normal_escape_sequence() /
771            // Allow tilde expression to be matched as a word piece (for tilde-after-colon expansion)
772            enabled_tilde_expr_after_colon() /
773            // Finally, match unquoted literal text.
774            unquoted_literal_text(<stop_condition()>, in_command)
775
776        rule dollar_sign_word_piece() -> WordPiece =
777            arithmetic_expansion() /
778            legacy_arithmetic_expansion() /
779            command_substitution() /
780            parameter_expansion()
781
782        rule double_quoted_word_piece() -> WordPiece =
783            arithmetic_expansion() /
784            legacy_arithmetic_expansion() /
785            command_substitution() /
786            parameter_expansion() /
787            double_quoted_escape_sequence() /
788            double_quoted_text()
789
790        rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
791            "\"" i:double_quoted_sequence_inner()* "\"" { i }
792
793        rule gettext_double_quoted_sequence() -> Vec<WordPieceWithSource> =
794            "$\"" i:double_quoted_sequence_inner()* "\"" { i }
795
796        rule double_quoted_sequence_inner() -> WordPieceWithSource =
797            start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
798                WordPieceWithSource {
799                    piece,
800                    start_index,
801                    end_index
802                }
803            }
804
805        rule single_quoted_literal_text() -> &'input str =
806            "\'" inner:$([^'\'']*) "\'" { inner }
807
808        rule ansi_c_quoted_text() -> &'input str =
809            r"$'" inner:$((r"\\" / r"\'" / [^'\''])*) r"'" { inner }
810
811        rule unquoted_literal_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
812            s:$(unquoted_literal_text_piece(<stop_condition()>, in_command)+) { WordPiece::Text(s.to_owned()) }
813
814        // TODO(parser): Find a way to remove the special-case logic for extglob + subshell commands
815        rule unquoted_literal_text_piece<T>(stop_condition: rule<T>, in_command: bool) =
816            is_true(in_command) extglob_pattern() /
817            is_true(in_command) subshell_command() /
818            !stop_condition() !normal_escape_sequence() !enabled_tilde_expr_after_colon() [^'\'' | '\"' | '$' | '`'] {}
819
820        rule enabled_tilde_expr_after_colon() -> WordPiece =
821            tilde_exprs_after_colon_enabled() last_char_is_colon() piece:tilde_expression_piece() { piece }
822
823        rule last_char_is_colon() = #{|input, pos| {
824            if pos == 0 {
825                // No preceding character - can't be preceded by ':'
826                peg::RuleResult::Failed
827            } else {
828                // Check the byte directly (`:` is ASCII, single byte)
829                if input.as_bytes()[pos - 1] == b':' {
830                    peg::RuleResult::Matched(pos, ())
831                } else {
832                    peg::RuleResult::Failed
833                }
834            }
835        }}
836
837        rule is_true(value: bool) = &[_] {? if value { Ok(()) } else { Err("not true") } }
838
839        rule extglob_pattern() =
840            ("@" / "!" / "?" / "+" / "*") "(" extglob_body_piece()* ")" {}
841
842        rule extglob_body_piece() =
843            word_piece(<[')']>, true /*in_command*/) {}
844
845        rule subshell_command() =
846            "(" command() ")" {}
847
848        rule double_quoted_text() -> WordPiece =
849            s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
850
851        rule double_quote_body_text() -> &'input str =
852            $((!double_quoted_escape_sequence() !dollar_sign_word_piece() [^'\"'])+)
853
854        // Heredoc body parsing: like double-quoted content, but " and ' are literal characters.
855        pub(crate) rule unexpanded_heredoc_word() -> Vec<WordPieceWithSource> =
856            traced(<heredoc_word(<![_]>)>)
857
858        rule heredoc_word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
859            pieces:heredoc_word_piece_with_source(<stop_condition()>)* { pieces }
860
861        rule heredoc_word_piece_with_source<T>(stop_condition: rule<T>) -> WordPieceWithSource =
862            !stop_condition() start_index:position!() piece:heredoc_word_piece() end_index:position!() {
863                WordPieceWithSource { piece, start_index, end_index }
864            }
865
866        rule heredoc_word_piece() -> WordPiece =
867            arithmetic_expansion() /
868            legacy_arithmetic_expansion() /
869            command_substitution() /
870            parameter_expansion() /
871            heredoc_escape_sequence() /
872            heredoc_literal_text()
873
874        rule heredoc_escape_sequence() -> WordPiece =
875            s:$("\\" ['$' | '`' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
876
877        rule heredoc_literal_text() -> WordPiece =
878            s:$((!heredoc_escape_sequence() !dollar_sign_word_piece() [^'`'])+) {
879                WordPiece::Text(s.to_owned())
880            }
881
882        rule normal_escape_sequence() -> WordPiece =
883            s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
884
885        rule double_quoted_escape_sequence() -> WordPiece =
886            s:$("\\" ['$' | '`' | '\"' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
887
888        rule tilde_expr_prefix_with_source() -> WordPieceWithSource =
889            start_index:position!() piece:tilde_expr_prefix() end_index:position!() {
890                WordPieceWithSource {
891                    piece,
892                    start_index,
893                    end_index
894                }
895            }
896
897        rule tilde_expr_prefix() -> WordPiece =
898            tilde_exprs_at_word_start_enabled() piece:tilde_expression_piece() { piece }
899
900        rule tilde_expr_after_colon() -> WordPiece =
901            tilde_exprs_after_colon_enabled() piece:tilde_expression_piece() { piece }
902
903        rule tilde_expression_piece() -> WordPiece =
904            "~" expr:tilde_expression() { WordPiece::TildeExpansion(expr) }
905
906        rule tilde_expression() -> TildeExpr =
907            &tilde_terminator() { TildeExpr::Home } /
908            "+" &tilde_terminator() { TildeExpr::WorkingDir } /
909            plus:("+"?) n:$(['0'..='9']*) &tilde_terminator() { TildeExpr::NthDirFromTopOfDirStack { n: n.parse().unwrap(), plus_used: plus.is_some() } } /
910            "-" &tilde_terminator() { TildeExpr::OldWorkingDir } /
911            "-" n:$(['0'..='9']*) &tilde_terminator() { TildeExpr::NthDirFromBottomOfDirStack { n: n.parse().unwrap() } } /
912            user:$(portable_filename_char()*) &tilde_terminator() { TildeExpr::UserHome(user.to_owned()) }
913
914        rule tilde_terminator() = ['/' | ':' | ';' | '}'] / ![_]
915
916        rule portable_filename_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '-']
917
918        // TODO(parser): Deal with fact that there may be a quoted word or escaped closing brace chars.
919        // TODO(parser): Improve on how we handle a '$' not followed by a valid variable name or parameter.
920        rule parameter_expansion() -> WordPiece =
921            "${" e:parameter_expression() "}" {
922                WordPiece::ParameterExpansion(e)
923            } /
924            "$" parameter:unbraced_parameter() {
925                WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false })
926            } /
927            "$" !['\''] {
928                WordPiece::Text("$".to_owned())
929            }
930
931        rule parameter_expression() -> ParameterExpr =
932            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? {
933                ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value }
934            } /
935            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? {
936                ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value }
937            } /
938            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? {
939                ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message }
940            } /
941            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? {
942                ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value }
943            } /
944            "#" parameter:parameter() {
945                ParameterExpr::ParameterLength { parameter, indirect: false }
946            } /
947            indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? {
948                ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern }
949            } /
950            indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? {
951                ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern }
952            } /
953            indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? {
954                ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern }
955            } /
956            indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? {
957                ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern }
958            } /
959            // N.B. The following case is for non-sh extensions.
960            non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } /
961            indirect:parameter_indirection() parameter:parameter() {
962                ParameterExpr::Parameter { parameter, indirect }
963            }
964
965        rule parameter_test_type() -> ParameterTestType =
966            colon:":"? {
967                if colon.is_some() {
968                    ParameterTestType::UnsetOrNull
969                } else {
970                    ParameterTestType::Unset
971                }
972            }
973
974        rule non_posix_parameter_expression() -> ParameterExpr =
975            "!" variable_name:variable_name() "[*]" {
976                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true }
977            } /
978            "!" variable_name:variable_name() "[@]" {
979                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false }
980            } /
981            indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? {
982                ParameterExpr::Substring { parameter, indirect, offset, length }
983            } /
984            indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() {
985                ParameterExpr::Transform { parameter, indirect, op }
986            } /
987            "!" prefix:variable_name() "*" {
988                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true }
989            } /
990            "!" prefix:variable_name() "@" {
991                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false }
992            } /
993            indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
994                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix }
995            } /
996            indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
997                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix }
998            } /
999            indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1000                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere }
1001            } /
1002            indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1003                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence }
1004            } /
1005            indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? {
1006                ParameterExpr::UppercasePattern { parameter, indirect, pattern }
1007            } /
1008            indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? {
1009                ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern }
1010            } /
1011            indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? {
1012                ParameterExpr::LowercasePattern { parameter, indirect, pattern }
1013            } /
1014            indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? {
1015                ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern }
1016            }
1017
1018        rule parameter_indirection() -> bool =
1019            non_posix_extensions_enabled() "!" { true } /
1020            { false }
1021
1022        rule non_posix_parameter_transformation_op() -> ParameterTransformOp =
1023            "U" { ParameterTransformOp::ToUpperCase } /
1024            "u" { ParameterTransformOp::CapitalizeInitial } /
1025            "L" { ParameterTransformOp::ToLowerCase } /
1026            "Q" { ParameterTransformOp::Quoted } /
1027            "E" { ParameterTransformOp::ExpandEscapeSequences } /
1028            "P" { ParameterTransformOp::PromptExpand } /
1029            "A" { ParameterTransformOp::ToAssignmentLogic } /
1030            "K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } /
1031            "a" { ParameterTransformOp::ToAttributeFlags } /
1032            "k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } }
1033
1034
1035        rule unbraced_parameter() -> Parameter =
1036            p:unbraced_positional_parameter() { Parameter::Positional(p) } /
1037            p:special_parameter() { Parameter::Special(p) } /
1038            p:variable_name() { Parameter::Named(p.to_owned()) }
1039
1040        // N.B. The indexing syntax is not a standard sh-ism.
1041        pub(crate) rule parameter() -> Parameter =
1042            p:positional_parameter() { Parameter::Positional(p) } /
1043            p:special_parameter() { Parameter::Special(p) } /
1044            non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } /
1045            non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } /
1046            non_posix_extensions_enabled() p:variable_name() "[" index:array_index() "]" {?
1047                Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() })
1048            } /
1049            p:variable_name() { Parameter::Named(p.to_owned()) }
1050
1051        rule positional_parameter() -> u32 =
1052            n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) }
1053        rule unbraced_positional_parameter() -> u32 =
1054            n:$(['1'..='9']) {? n.parse().or(Err("u32")) }
1055
1056        rule special_parameter() -> SpecialParameter =
1057            "@" { SpecialParameter::AllPositionalParameters { concatenate: false } } /
1058            "*" { SpecialParameter::AllPositionalParameters { concatenate: true } } /
1059            "#" { SpecialParameter::PositionalParameterCount } /
1060            "?" { SpecialParameter::LastExitStatus } /
1061            "-" { SpecialParameter::CurrentOptionFlags } /
1062            "$" { SpecialParameter::ProcessId } /
1063            "!" { SpecialParameter::LastBackgroundProcessId } /
1064            "0" { SpecialParameter::ShellName }
1065
1066        rule variable_name() -> &'input str =
1067            $(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+)
1068
1069        pub(crate) rule command_substitution() -> WordPiece =
1070            "$(" c:command() ")" { WordPiece::CommandSubstitution(c.to_owned()) } /
1071            "`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) }
1072
1073        pub(crate) rule command() -> &'input str =
1074            $(command_piece()*)
1075
1076        pub(crate) rule command_piece() -> () =
1077            word_piece(<[')']>, true /*in_command*/) {} /
1078            ([' ' | '\t'])+ {}
1079
1080        rule backquoted_command() -> String =
1081            chars:(backquoted_char()*) { chars.into_iter().collect() }
1082
1083        rule backquoted_char() -> &'input str =
1084            "\\`" { "`" } /
1085            "\\\\" { "\\\\" } /
1086            s:$([^'`']) { s }
1087
1088        rule arithmetic_expansion() -> WordPiece =
1089            "$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1090
1091        rule legacy_arithmetic_expansion() -> WordPiece =
1092            "$[" e:$(arithmetic_word(<"]">)) "]" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1093
1094        rule substring_offset() -> ast::UnexpandedArithmeticExpr =
1095            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1096
1097        rule substring_length() -> ast::UnexpandedArithmeticExpr =
1098            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1099
1100        rule parameter_replacement_str() -> String =
1101            "/" s:$(word(<['}']>)) { s.to_owned() }
1102
1103        rule parameter_search_pattern() -> String =
1104            s:$(word(<['}' | '/']>)) { s.to_owned() }
1105
1106        rule parameter_expression_word() -> String =
1107            s:$(word(<['}']>)) { s.to_owned() }
1108
1109        rule extglob_enabled() -> () =
1110            &[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } }
1111
1112        rule non_posix_extensions_enabled() -> () =
1113            &[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
1114
1115        rule tilde_exprs_at_word_start_enabled() -> () =
1116            &[_] {? if parser_options.tilde_expansion_at_word_start { Ok(()) } else { Err("no tilde expansion at word start") } }
1117
1118        rule tilde_exprs_after_colon_enabled() -> () =
1119            &[_] {? if parser_options.tilde_expansion_after_colon { Ok(()) } else { Err("no tilde expansion after colon") } }
1120
1121        // Assignment rules.
1122
1123        pub(crate) rule name_equals_scalar_value() -> ast::Assignment =
1124            nae:name_equals() value:assigned_scalar_value() {
1125                let (name, append) = nae;
1126                ast::Assignment { name, value, append, loc: SourceSpan::default() }
1127            }
1128
1129        pub(crate) rule name_equals() -> (ast::AssignmentName, bool) =
1130            name:assignment_name() append:("+"?) "=" {
1131                (name, append.is_some())
1132            }
1133
1134        pub(crate) rule literal_array_element() -> (Option<String>, String) =
1135            "[" inner:$((!"]" [_])*) "]=" value:$([_]*) {
1136                (Some(inner.to_owned()), value.to_owned())
1137            } /
1138            value:$([_]+) {
1139                (None, value.to_owned())
1140            }
1141
1142        rule assignment_name() -> ast::AssignmentName =
1143            aen:array_element_name() {
1144                let (name, index) = aen;
1145                ast::AssignmentName::ArrayElementName(name.to_owned(), index.to_owned())
1146            } /
1147            name:assigned_scalar_name() {
1148                ast::AssignmentName::VariableName(name.to_owned())
1149            }
1150
1151        rule array_element_name() -> (&'input str, &'input str) =
1152            name:assigned_scalar_name() "[" ai:array_index() "]" { (name, ai) }
1153
1154        rule array_index() -> &'input str =
1155            $(arithmetic_word(<"]">))
1156
1157        rule assigned_scalar_name() -> &'input str =
1158            $(alpha_or_underscore() non_first_variable_char()*)
1159
1160        rule non_first_variable_char() -> () =
1161            ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z'] {}
1162
1163        rule alpha_or_underscore() -> () =
1164            ['_' | 'a'..='z' | 'A'..='Z'] {}
1165
1166        rule assigned_scalar_value() -> ast::AssignmentValue =
1167            v:$([_]*) { ast::AssignmentValue::Scalar(ast::Word::from(v.to_owned())) }
1168    }
1169}
1170
1171#[cfg(test)]
1172#[allow(clippy::panic_in_result_fn)]
1173mod tests {
1174    use super::*;
1175    use anyhow::Result;
1176    use insta::assert_ron_snapshot;
1177    use pretty_assertions::assert_matches;
1178
1179    #[derive(serde::Serialize, serde::Deserialize)]
1180    struct ParseTestResults<'a> {
1181        input: &'a str,
1182        result: Vec<WordPieceWithSource>,
1183    }
1184
1185    fn test_parse(word: &str) -> Result<ParseTestResults<'_>> {
1186        let parsed = super::parse(word, &ParserOptions::default())?;
1187        Ok(ParseTestResults {
1188            input: word,
1189            result: parsed,
1190        })
1191    }
1192
1193    #[test]
1194    fn parse_ansi_c_quoted_text() -> Result<()> {
1195        assert_ron_snapshot!(test_parse(r"$'hi\nthere\t'")?);
1196        Ok(())
1197    }
1198
1199    #[test]
1200    fn parse_ansi_c_quoted_escape_seq() -> Result<()> {
1201        assert_ron_snapshot!(test_parse(r"$'\\'")?);
1202        Ok(())
1203    }
1204
1205    #[test]
1206    fn parse_tilde_after_colon() -> Result<()> {
1207        let opts = ParserOptions {
1208            tilde_expansion_after_colon: true,
1209            ..ParserOptions::default()
1210        };
1211
1212        let parsed = super::parse("a:~", &opts)?;
1213
1214        // Should have: Text("a:"), TildeExpansion("")
1215        assert_eq!(parsed.len(), 2);
1216        assert_matches!(parsed[0].piece, WordPiece::Text(_));
1217        assert_matches!(parsed[1].piece, WordPiece::TildeExpansion(_));
1218
1219        Ok(())
1220    }
1221
1222    #[test]
1223    fn parse_double_quoted_text() -> Result<()> {
1224        assert_ron_snapshot!(test_parse(r#""a ${b} c""#)?);
1225        Ok(())
1226    }
1227
1228    #[test]
1229    fn parse_gettext_double_quoted_text() -> Result<()> {
1230        assert_ron_snapshot!(test_parse(r#"$"a ${b} c""#)?);
1231        Ok(())
1232    }
1233
1234    #[test]
1235    fn parse_command_substitution() -> Result<()> {
1236        super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
1237        super::expansion_parser::command_piece("hi", &ParserOptions::default())?;
1238        super::expansion_parser::command("echo hi", &ParserOptions::default())?;
1239        super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?;
1240
1241        assert_ron_snapshot!(test_parse("$(echo hi)")?);
1242
1243        Ok(())
1244    }
1245
1246    #[test]
1247    fn parse_command_substitution_with_embedded_quotes() -> Result<()> {
1248        super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
1249        super::expansion_parser::command_piece(r#""hi""#, &ParserOptions::default())?;
1250        super::expansion_parser::command(r#"echo "hi""#, &ParserOptions::default())?;
1251        super::expansion_parser::command_substitution(
1252            r#"$(echo "hi")"#,
1253            &ParserOptions::default(),
1254        )?;
1255
1256        assert_ron_snapshot!(test_parse(r#"$(echo "hi")"#)?);
1257        Ok(())
1258    }
1259
1260    #[test]
1261    fn parse_command_substitution_with_embedded_extglob() -> Result<()> {
1262        assert_ron_snapshot!(test_parse("$(echo !(x))")?);
1263        Ok(())
1264    }
1265
1266    #[test]
1267    fn parse_backquoted_command() -> Result<()> {
1268        assert_ron_snapshot!(test_parse("`echo hi`")?);
1269        Ok(())
1270    }
1271
1272    #[test]
1273    fn parse_backquoted_command_in_double_quotes() -> Result<()> {
1274        assert_ron_snapshot!(test_parse(r#""`echo hi`""#)?);
1275        Ok(())
1276    }
1277
1278    #[test]
1279    fn parse_extglob_with_embedded_parameter() -> Result<()> {
1280        assert_ron_snapshot!(test_parse("+([$var])")?);
1281        Ok(())
1282    }
1283
1284    #[test]
1285    fn parse_arithmetic_expansion() -> Result<()> {
1286        assert_ron_snapshot!(test_parse("$((0))")?);
1287        Ok(())
1288    }
1289
1290    #[test]
1291    fn parse_arithmetic_expansion_with_parens() -> Result<()> {
1292        assert_ron_snapshot!(test_parse("$((((1+2)*3)))")?);
1293        Ok(())
1294    }
1295
1296    #[test]
1297    fn test_arithmetic_word_parsing() {
1298        let options = ParserOptions::default();
1299
1300        assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok());
1301        assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok());
1302        assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok());
1303        assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok());
1304        assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok());
1305        assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok());
1306        assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok());
1307        assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok());
1308        assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok());
1309    }
1310
1311    #[test]
1312    fn test_arithmetic_word_piece_parsing() {
1313        let options = ParserOptions::default();
1314
1315        assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok());
1316        assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok());
1317        assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok());
1318        assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok());
1319        assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok());
1320        assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok());
1321        assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok());
1322        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok());
1323        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok());
1324        assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err());
1325        assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err());
1326        assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err());
1327    }
1328
1329    #[test]
1330    fn test_brace_expansion_parsing() -> Result<()> {
1331        let options = ParserOptions::default();
1332
1333        let inputs = ["x{a,b}y", "{a,b{1,2}}"];
1334
1335        for input in inputs {
1336            assert_ron_snapshot!(super::parse_brace_expansions(input, &options)?.ok_or_else(
1337                || anyhow::anyhow!("Expected brace expansion to be parsed successfully")
1338            )?);
1339        }
1340
1341        Ok(())
1342    }
1343
1344    #[test]
1345    fn parse_assignment_word() -> Result<()> {
1346        super::parse_assignment_word("x=3")?;
1347        super::parse_assignment_word("x=")?;
1348        super::parse_assignment_word("x[3]=a")?;
1349        super::parse_assignment_word("x[${y[3]}]=a")?;
1350        super::parse_assignment_word("x[y[3]]=a")?;
1351        Ok(())
1352    }
1353}