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 crate::ast;
13use crate::error;
14use crate::ParserOptions;
15
16/// Encapsulates a `WordPiece` together with its position in the string it came from.
17#[derive(Clone, Debug)]
18pub struct WordPieceWithSource {
19    /// The word piece.
20    pub piece: WordPiece,
21    /// The start index of the piece in the source string.
22    pub start_index: usize,
23    /// The end index of the piece in the source string.
24    pub end_index: usize,
25}
26
27/// Represents a piece of a word.
28#[derive(Clone, Debug)]
29pub enum WordPiece {
30    /// A simple unquoted, unescaped string.
31    Text(String),
32    /// A string that is single-quoted.
33    SingleQuotedText(String),
34    /// A string that is ANSI-C quoted.
35    AnsiCQuotedText(String),
36    /// A sequence of pieces that are embedded in double quotes.
37    DoubleQuotedSequence(Vec<WordPieceWithSource>),
38    /// A tilde prefix.
39    TildePrefix(String),
40    /// A parameter expansion.
41    ParameterExpansion(ParameterExpr),
42    /// A command substitution.
43    CommandSubstitution(String),
44    /// A backquoted command substitution.
45    BackquotedCommandSubstitution(String),
46    /// An escape sequence.
47    EscapeSequence(String),
48    /// An arithmetic expression.
49    ArithmeticExpression(ast::UnexpandedArithmeticExpr),
50}
51
52/// Type of a parameter test.
53#[derive(Clone, Debug)]
54pub enum ParameterTestType {
55    /// Check for unset or null.
56    UnsetOrNull,
57    /// Check for unset.
58    Unset,
59}
60
61/// A parameter, used in a parameter expansion.
62#[derive(Clone, Debug)]
63pub enum Parameter {
64    /// A 0-indexed positional parameter.
65    Positional(u32),
66    /// A special parameter.
67    Special(SpecialParameter),
68    /// A named variable.
69    Named(String),
70    /// An index into a named variable.
71    NamedWithIndex {
72        /// Variable name.
73        name: String,
74        /// Index.
75        index: String,
76    },
77    /// A named array variable with all indices.
78    NamedWithAllIndices {
79        /// Variable name.
80        name: String,
81        /// Whether to concatenate the values.
82        concatenate: bool,
83    },
84}
85
86/// A special parameter, used in a parameter expansion.
87#[derive(Clone, Debug)]
88pub enum SpecialParameter {
89    /// All positional parameters.
90    AllPositionalParameters {
91        /// Whether to concatenate the values.
92        concatenate: bool,
93    },
94    /// The count of positional parameters.
95    PositionalParameterCount,
96    /// The last exit status in the shell.
97    LastExitStatus,
98    /// The current shell option flags.
99    CurrentOptionFlags,
100    /// The current shell process ID.
101    ProcessId,
102    /// The last background process ID managed by the shell.
103    LastBackgroundProcessId,
104    /// The name of the shell.
105    ShellName,
106}
107
108/// A parameter expression, used in a parameter expansion.
109#[derive(Clone, Debug)]
110pub enum ParameterExpr {
111    /// A parameter, with optional indirection.
112    Parameter {
113        /// The parameter.
114        parameter: Parameter,
115        /// Whether to treat the expanded parameter as an indirect
116        /// reference, which should be subsequently dereferenced
117        /// for the expansion.
118        indirect: bool,
119    },
120    /// Conditionally use default values.
121    UseDefaultValues {
122        /// The parameter.
123        parameter: Parameter,
124        /// Whether to treat the expanded parameter as an indirect
125        /// reference, which should be subsequently dereferenced
126        /// for the expansion.
127        indirect: bool,
128        /// The type of test to perform.
129        test_type: ParameterTestType,
130        /// Default value to conditionally use.
131        default_value: Option<String>,
132    },
133    /// Conditionally assign default values.
134    AssignDefaultValues {
135        /// The parameter.
136        parameter: Parameter,
137        /// Whether to treat the expanded parameter as an indirect
138        /// reference, which should be subsequently dereferenced
139        /// for the expansion.
140        indirect: bool,
141        /// The type of test to perform.
142        test_type: ParameterTestType,
143        /// Default value to conditionally assign.
144        default_value: Option<String>,
145    },
146    /// Indicate error if null or unset.
147    IndicateErrorIfNullOrUnset {
148        /// The parameter.
149        parameter: Parameter,
150        /// Whether to treat the expanded parameter as an indirect
151        /// reference, which should be subsequently dereferenced
152        /// for the expansion.
153        indirect: bool,
154        /// The type of test to perform.
155        test_type: ParameterTestType,
156        /// Error message to conditionally yield.
157        error_message: Option<String>,
158    },
159    /// Conditionally use an alternative value.
160    UseAlternativeValue {
161        /// The parameter.
162        parameter: Parameter,
163        /// Whether to treat the expanded parameter as an indirect
164        /// reference, which should be subsequently dereferenced
165        /// for the expansion.
166        indirect: bool,
167        /// The type of test to perform.
168        test_type: ParameterTestType,
169        /// Alternative value to conditionally use.
170        alternative_value: Option<String>,
171    },
172    /// Compute the length of the given parameter.
173    ParameterLength {
174        /// The parameter.
175        parameter: Parameter,
176        /// Whether to treat the expanded parameter as an indirect
177        /// reference, which should be subsequently dereferenced
178        /// for the expansion.
179        indirect: bool,
180    },
181    /// Remove the smallest suffix from the given string matching the given pattern.
182    RemoveSmallestSuffixPattern {
183        /// The parameter.
184        parameter: Parameter,
185        /// Whether to treat the expanded parameter as an indirect
186        /// reference, which should be subsequently dereferenced
187        /// for the expansion.
188        indirect: bool,
189        /// Optionally provides a pattern to match.
190        pattern: Option<String>,
191    },
192    /// Remove the largest suffix from the given string matching the given pattern.
193    RemoveLargestSuffixPattern {
194        /// The parameter.
195        parameter: Parameter,
196        /// Whether to treat the expanded parameter as an indirect
197        /// reference, which should be subsequently dereferenced
198        /// for the expansion.
199        indirect: bool,
200        /// Optionally provides a pattern to match.
201        pattern: Option<String>,
202    },
203    /// Remove the smallest prefix from the given string matching the given pattern.
204    RemoveSmallestPrefixPattern {
205        /// The parameter.
206        parameter: Parameter,
207        /// Whether to treat the expanded parameter as an indirect
208        /// reference, which should be subsequently dereferenced
209        /// for the expansion.
210        indirect: bool,
211        /// Optionally provides a pattern to match.
212        pattern: Option<String>,
213    },
214    /// Remove the largest prefix from the given string matching the given pattern.
215    RemoveLargestPrefixPattern {
216        /// The parameter.
217        parameter: Parameter,
218        /// Whether to treat the expanded parameter as an indirect
219        /// reference, which should be subsequently dereferenced
220        /// for the expansion.
221        indirect: bool,
222        /// Optionally provides a pattern to match.
223        pattern: Option<String>,
224    },
225    /// Extract a substring from the given parameter.
226    Substring {
227        /// The parameter.
228        parameter: Parameter,
229        /// Whether to treat the expanded parameter as an indirect
230        /// reference, which should be subsequently dereferenced
231        /// for the expansion.
232        indirect: bool,
233        /// Arithmetic expression that will be expanded to compute the offset
234        /// at which the substring should be extracted.
235        offset: ast::UnexpandedArithmeticExpr,
236        /// Optionally provides an arithmetic expression that will be expanded
237        /// to compute the length of substring to be extracted; if left
238        /// unspecified, the remainder of the string will be extracted.
239        length: Option<ast::UnexpandedArithmeticExpr>,
240    },
241    /// Transform the given parameter.
242    Transform {
243        /// The parameter.
244        parameter: Parameter,
245        /// Whether to treat the expanded parameter as an indirect
246        /// reference, which should be subsequently dereferenced
247        /// for the expansion.
248        indirect: bool,
249        /// Type of transformation to apply.
250        op: ParameterTransformOp,
251    },
252    /// Uppercase the first character of the given parameter.
253    UppercaseFirstChar {
254        /// The parameter.
255        parameter: Parameter,
256        /// Whether to treat the expanded parameter as an indirect
257        /// reference, which should be subsequently dereferenced
258        /// for the expansion.
259        indirect: bool,
260        /// Optionally provides a pattern to match.
261        pattern: Option<String>,
262    },
263    /// Uppercase the portion of the given parameter matching the given pattern.
264    UppercasePattern {
265        /// The parameter.
266        parameter: Parameter,
267        /// Whether to treat the expanded parameter as an indirect
268        /// reference, which should be subsequently dereferenced
269        /// for the expansion.
270        indirect: bool,
271        /// Optionally provides a pattern to match.
272        pattern: Option<String>,
273    },
274    /// Lowercase the first character of the given parameter.
275    LowercaseFirstChar {
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        /// Optionally provides a pattern to match.
283        pattern: Option<String>,
284    },
285    /// Lowercase the portion of the given parameter matching the given pattern.
286    LowercasePattern {
287        /// The parameter.
288        parameter: Parameter,
289        /// Whether to treat the expanded parameter as an indirect
290        /// reference, which should be subsequently dereferenced
291        /// for the expansion.
292        indirect: bool,
293        /// Optionally provides a pattern to match.
294        pattern: Option<String>,
295    },
296    /// Replace occurrences of the given pattern in the given parameter.
297    ReplaceSubstring {
298        /// The parameter.
299        parameter: Parameter,
300        /// Whether to treat the expanded parameter as an indirect
301        /// reference, which should be subsequently dereferenced
302        /// for the expansion.
303        indirect: bool,
304        /// Pattern to match.
305        pattern: String,
306        /// Replacement string.
307        replacement: Option<String>,
308        /// Kind of match to perform.
309        match_kind: SubstringMatchKind,
310    },
311    /// Select variable names from the environment with a given prefix.
312    VariableNames {
313        /// The prefix to match.
314        prefix: String,
315        /// Whether to concatenate the results.
316        concatenate: bool,
317    },
318    /// Select member keys from the named array.
319    MemberKeys {
320        /// Name of the array variable.
321        variable_name: String,
322        /// Whether to concatenate the results.
323        concatenate: bool,
324    },
325}
326
327/// Kind of substring match.
328#[derive(Clone, Debug)]
329pub enum SubstringMatchKind {
330    /// Match the prefix of the string.
331    Prefix,
332    /// Match the suffix of the string.
333    Suffix,
334    /// Match the first occurrence in the string.
335    FirstOccurrence,
336    /// Match all instances in the string.
337    Anywhere,
338}
339
340/// Kind of operation to apply to a parameter.
341#[derive(Clone, Debug)]
342pub enum ParameterTransformOp {
343    /// Capitalizate initials.
344    CapitalizeInitial,
345    /// Expand escape sequences.
346    ExpandEscapeSequences,
347    /// Possibly quote with arrays expanded.
348    PossiblyQuoteWithArraysExpanded {
349        /// Whether or not to yield separate words.
350        separate_words: bool,
351    },
352    /// Apply prompt expansion.
353    PromptExpand,
354    /// Quote the parameter.
355    Quoted,
356    /// Translate to a format usable in an assignment/declaration.
357    ToAssignmentLogic,
358    /// Translate to the parameter's attribute flags.
359    ToAttributeFlags,
360    /// Translate to lowercase.
361    ToLowerCase,
362    /// Translate to uppercase.
363    ToUpperCase,
364}
365
366/// Represents a sub-word that is either a brace expression or some other word text.
367#[derive(Clone, Debug)]
368pub enum BraceExpressionOrText {
369    /// A brace expression.
370    Expr(BraceExpression),
371    /// Other word text.
372    Text(String),
373}
374
375/// Represents a brace expression to be expanded.
376pub type BraceExpression = Vec<BraceExpressionMember>;
377
378impl BraceExpressionOrText {
379    /// Generates expansions for this value.
380    pub fn generate(self) -> Box<dyn Iterator<Item = String>> {
381        match self {
382            BraceExpressionOrText::Expr(members) => {
383                let mut iters = vec![];
384                for m in members {
385                    iters.push(m.generate());
386                }
387                Box::new(iters.into_iter().flatten())
388            }
389            BraceExpressionOrText::Text(text) => Box::new(std::iter::once(text)),
390        }
391    }
392}
393
394/// Member of a brace expression.
395#[derive(Clone, Debug)]
396pub enum BraceExpressionMember {
397    /// An inclusive numerical sequence.
398    NumberSequence {
399        /// Start of the sequence.
400        low: i64,
401        /// Inclusive end of the sequence.
402        high: i64,
403        /// Increment value.
404        increment: i64,
405    },
406    /// An inclusive character sequence.
407    CharSequence {
408        /// Start of the sequence.
409        low: char,
410        /// Inclusive end of the sequence.
411        high: char,
412        /// Increment value.
413        increment: i64,
414    },
415    /// Text.
416    Text(String),
417}
418
419impl BraceExpressionMember {
420    /// Generates expansions for this member.
421    #[allow(clippy::cast_possible_truncation)]
422    #[allow(clippy::cast_sign_loss)]
423    pub fn generate(self) -> Box<dyn Iterator<Item = String>> {
424        match self {
425            BraceExpressionMember::NumberSequence {
426                low,
427                high,
428                increment,
429            } => Box::new(
430                (low..=high)
431                    .step_by(increment as usize)
432                    .map(|n| n.to_string()),
433            ),
434            BraceExpressionMember::CharSequence {
435                low,
436                high,
437                increment,
438            } => Box::new(
439                (low..=high)
440                    .step_by(increment as usize)
441                    .map(|c| c.to_string()),
442            ),
443            BraceExpressionMember::Text(text) => Box::new(std::iter::once(text)),
444        }
445    }
446}
447
448/// Parse a word into its constituent pieces.
449///
450/// # Arguments
451///
452/// * `word` - The word to parse.
453/// * `options` - The parser options to use.
454pub fn parse(
455    word: &str,
456    options: &ParserOptions,
457) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
458    cacheable_parse(word.to_owned(), options.to_owned())
459}
460
461#[cached::proc_macro::cached(size = 64, result = true)]
462fn cacheable_parse(
463    word: String,
464    options: ParserOptions,
465) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
466    tracing::debug!(target: "expansion", "Parsing word '{}'", word);
467
468    let pieces = expansion_parser::unexpanded_word(word.as_str(), &options)
469        .map_err(|err| error::WordParseError::Word(word.clone(), err))?;
470
471    tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces);
472
473    Ok(pieces)
474}
475
476/// Parse the given word into a parameter expression.
477///
478/// # Arguments
479///
480/// * `word` - The word to parse.
481/// * `options` - The parser options to use.
482pub fn parse_parameter(
483    word: &str,
484    options: &ParserOptions,
485) -> Result<Parameter, error::WordParseError> {
486    expansion_parser::parameter(word, options)
487        .map_err(|err| error::WordParseError::Parameter(word.to_owned(), err))
488}
489
490/// Parse brace expansion from a given word .
491///
492/// # Arguments
493///
494/// * `word` - The word to parse.
495/// * `options` - The parser options to use.
496pub fn parse_brace_expansions(
497    word: &str,
498    options: &ParserOptions,
499) -> Result<Option<Vec<BraceExpressionOrText>>, error::WordParseError> {
500    expansion_parser::brace_expansions(word, options)
501        .map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err))
502}
503
504peg::parser! {
505    grammar expansion_parser(parser_options: &ParserOptions) for str {
506        // Helper rule that enables pegviz to be used to visualize debug peg traces.
507        rule traced<T>(e: rule<T>) -> T =
508            &(input:$([_]*) {
509                #[cfg(feature = "debug-tracing")]
510                println!("[PEG_INPUT_START]\n{input}\n[PEG_TRACE_START]");
511            })
512            e:e()? {?
513                #[cfg(feature = "debug-tracing")]
514                println!("[PEG_TRACE_STOP]");
515                e.ok_or("")
516            }
517
518        pub(crate) rule unexpanded_word() -> Vec<WordPieceWithSource> = traced(<word(<![_]>)>)
519
520        rule word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
521            tilde:tilde_prefix_with_source()? pieces:word_piece_with_source(<stop_condition()>, false /*in_command*/)* {
522                let mut all_pieces = Vec::new();
523                if let Some(tilde) = tilde {
524                    all_pieces.push(tilde);
525                }
526                all_pieces.extend(pieces);
527                all_pieces
528            }
529
530        pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
531            pieces:(brace_expansion_piece()+) { Some(pieces) } /
532            [_]* { None }
533
534        rule brace_expansion_piece() -> BraceExpressionOrText =
535            expr:brace_expr() {
536                BraceExpressionOrText::Expr(expr)
537            } /
538            text:$(non_brace_expr_text()+) { BraceExpressionOrText::Text(text.to_owned()) }
539
540        rule non_brace_expr_text() -> () =
541            !"{" word_piece(<['{']>, false) {} /
542            !brace_expr() "{" {}
543
544        rule brace_expr() -> BraceExpression =
545            "{" inner:brace_expr_inner() "}" { inner }
546
547        rule brace_expr_inner() -> BraceExpression =
548            brace_text_list_expr() /
549            seq:brace_sequence_expr() { vec![seq] }
550
551        rule brace_text_list_expr() -> BraceExpression =
552            members:(brace_text_list_member() **<2,> ",") {
553                members.into_iter().flatten().collect()
554            }
555
556        rule brace_text_list_member() -> BraceExpression =
557            &[',' | '}'] { vec![BraceExpressionMember::Text(String::new())] } /
558            brace_expr() /
559            text:$(word_piece(<[',' | '}']>, false)) {
560                vec![BraceExpressionMember::Text(text.to_owned())]
561            }
562
563        rule brace_sequence_expr() -> BraceExpressionMember =
564            low:number() ".." high:number() increment:(".." n:number() { n })? {
565                BraceExpressionMember::NumberSequence { low, high, increment: increment.unwrap_or(1) }
566            } /
567            low:character() ".." high:character() increment:(".." n:number() { n })? {
568                BraceExpressionMember::CharSequence { low, high, increment: increment.unwrap_or(1) }
569            }
570
571        rule number() -> i64 = n:$(['0'..='9']+) { n.parse().unwrap() }
572        rule character() -> char = ['a'..='z' | 'A'..='Z']
573
574        pub(crate) rule is_arithmetic_word() =
575            arithmetic_word(<![_]>)
576
577            // N.B. We don't bother returning the word pieces, as all users of this rule
578        // only try to extract the consumed input string and not the parse result.
579        rule arithmetic_word<T>(stop_condition: rule<T>) =
580            arithmetic_word_piece(<stop_condition()>)* {}
581
582        pub(crate) rule is_arithmetic_word_piece() =
583            arithmetic_word_piece(<![_]>)
584
585        // This rule matches an individual "piece" of an arithmetic expression. It needs to handle
586        // matching nested parenthesized expressions as well. We stop consuming the input when
587        // we reach the provided stop condition, which typically denotes the end of the containing
588        // arithmetic expression.
589        rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
590            // This branch matches a parenthesized piece; we consume the opening parenthesis and
591            // delegate the rest to a helper rule. We don't worry about the stop condition passed
592            // into us, because if we see an opening parenthesis then we *must* find its closing
593            // partner.
594            "(" arithmetic_word_plus_right_paren() {} /
595            // This branch matches any standard piece of a word, stopping as soon as we reach
596            // either the overall stop condition *OR* an opening parenthesis. We add this latter
597            // condition to ensure that *we* handle matching parentheses.
598            !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>, false /*in_command*/) {}
599
600        // This is a helper rule that matches either the provided stop condition or an opening parenthesis.
601        rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
602            stop_condition() {} /
603            "(" {}
604
605        // This rule matches an arithmetic word followed by a right parenthesis. It must consume the right parenthesis.
606        rule arithmetic_word_plus_right_paren() =
607            arithmetic_word(<[')']>) ")" /
608
609        rule word_piece_with_source<T>(stop_condition: rule<T>, in_command: bool) -> WordPieceWithSource =
610            start_index:position!() piece:word_piece(<stop_condition()>, in_command) end_index:position!() {
611                WordPieceWithSource { piece, start_index, end_index }
612            }
613
614        rule word_piece<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
615            arithmetic_expansion() /
616            command_substitution() /
617            parameter_expansion() /
618            unquoted_text(<stop_condition()>, in_command)
619
620        rule double_quoted_word_piece() -> WordPiece =
621            arithmetic_expansion() /
622            command_substitution() /
623            parameter_expansion() /
624            double_quoted_escape_sequence() /
625            double_quoted_text()
626
627        rule unquoted_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
628            s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
629            s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
630            s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
631            normal_escape_sequence() /
632            unquoted_literal_text(<stop_condition()>, in_command)
633
634        rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
635            "\"" i:double_quoted_sequence_inner()* "\"" { i }
636
637        rule double_quoted_sequence_inner() -> WordPieceWithSource =
638            start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
639                WordPieceWithSource {
640                    piece,
641                    start_index,
642                    end_index
643                }
644            }
645
646        rule single_quoted_literal_text() -> &'input str =
647            "\'" inner:$([^'\'']*) "\'" { inner }
648
649        rule ansi_c_quoted_text() -> &'input str =
650            "$\'" inner:$([^'\'']*) "\'" { inner }
651
652        rule unquoted_literal_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
653            s:$(unquoted_literal_text_piece(<stop_condition()>, in_command)+) { WordPiece::Text(s.to_owned()) }
654
655        // TODO: Find a way to remove the special-case logic for extglob + subshell commands
656        rule unquoted_literal_text_piece<T>(stop_condition: rule<T>, in_command: bool) =
657            is_true(in_command) extglob_pattern() /
658            is_true(in_command) subshell_command() /
659            !stop_condition() !normal_escape_sequence() [^'$' | '\'' | '\"' | '`'] {}
660
661        rule is_true(value: bool) = &[_] {? if value { Ok(()) } else { Err("not true") } }
662
663        rule extglob_pattern() =
664            ("@" / "!" / "?" / "+" / "*") "(" extglob_body_piece()* ")" {}
665
666        rule extglob_body_piece() =
667            word_piece(<[')']>, true /*in_command*/) {}
668
669        rule subshell_command() =
670            "(" command() ")" {}
671
672        rule double_quoted_text() -> WordPiece =
673            s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
674
675        rule double_quote_body_text() -> &'input str =
676            $((!double_quoted_escape_sequence() [^'$' | '\"' | '`'])+)
677
678        rule normal_escape_sequence() -> WordPiece =
679            s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
680
681        rule double_quoted_escape_sequence() -> WordPiece =
682            s:$("\\" ['$' | '`' | '\"' | '\'' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
683
684        rule tilde_prefix_with_source() -> WordPieceWithSource =
685            start_index:position!() piece:tilde_prefix() end_index:position!() {
686                WordPieceWithSource {
687                    piece,
688                    start_index,
689                    end_index
690                }
691            }
692
693        // TODO: Handle colon syntax
694        rule tilde_prefix() -> WordPiece =
695            tilde_parsing_enabled() "~" cs:$((!['/' | ':' | ';'] [c])*) { WordPiece::TildePrefix(cs.to_owned()) }
696
697        // TODO: Deal with fact that there may be a quoted word or escaped closing brace chars.
698        // TODO: Improve on how we handle a '$' not followed by a valid variable name or parameter.
699        rule parameter_expansion() -> WordPiece =
700            "${" e:parameter_expression() "}" {
701                WordPiece::ParameterExpansion(e)
702            } /
703            "$" parameter:unbraced_parameter() {
704                WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false })
705            } /
706            "$" !['\''] {
707                WordPiece::Text("$".to_owned())
708            }
709
710        rule parameter_expression() -> ParameterExpr =
711            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? {
712                ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value }
713            } /
714            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? {
715                ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value }
716            } /
717            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? {
718                ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message }
719            } /
720            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? {
721                ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value }
722            } /
723            "#" parameter:parameter() {
724                ParameterExpr::ParameterLength { parameter, indirect: false }
725            } /
726            indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? {
727                ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern }
728            } /
729            indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? {
730                ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern }
731            } /
732            indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? {
733                ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern }
734            } /
735            indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? {
736                ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern }
737            } /
738            // N.B. The following case is for non-sh extensions.
739            non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } /
740            indirect:parameter_indirection() parameter:parameter() {
741                ParameterExpr::Parameter { parameter, indirect }
742            }
743
744        rule parameter_test_type() -> ParameterTestType =
745            colon:":"? {
746                if colon.is_some() {
747                    ParameterTestType::UnsetOrNull
748                } else {
749                    ParameterTestType::Unset
750                }
751            }
752
753        rule non_posix_parameter_expression() -> ParameterExpr =
754            "!" variable_name:variable_name() "[*]" {
755                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true }
756            } /
757            "!" variable_name:variable_name() "[@]" {
758                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false }
759            } /
760            "!" prefix:variable_name() "*" {
761                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true }
762            } /
763            "!" prefix:variable_name() "@" {
764                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false }
765            } /
766            indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? {
767                ParameterExpr::Substring { parameter, indirect, offset, length }
768            } /
769            indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() {
770                ParameterExpr::Transform { parameter, indirect, op }
771            } /
772            indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
773                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix }
774            } /
775            indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
776                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix }
777            } /
778            indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
779                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere }
780            } /
781            indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
782                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence }
783            } /
784            indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? {
785                ParameterExpr::UppercasePattern { parameter, indirect, pattern }
786            } /
787            indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? {
788                ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern }
789            } /
790            indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? {
791                ParameterExpr::LowercasePattern { parameter, indirect, pattern }
792            } /
793            indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? {
794                ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern }
795            }
796
797        rule parameter_indirection() -> bool =
798            non_posix_extensions_enabled() "!" { true } /
799            { false }
800
801        rule non_posix_parameter_transformation_op() -> ParameterTransformOp =
802            "U" { ParameterTransformOp::ToUpperCase } /
803            "u" { ParameterTransformOp::CapitalizeInitial } /
804            "L" { ParameterTransformOp::ToLowerCase } /
805            "Q" { ParameterTransformOp::Quoted } /
806            "E" { ParameterTransformOp::ExpandEscapeSequences } /
807            "P" { ParameterTransformOp::PromptExpand } /
808            "A" { ParameterTransformOp::ToAssignmentLogic } /
809            "K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } /
810            "a" { ParameterTransformOp::ToAttributeFlags } /
811            "k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } }
812
813
814        rule unbraced_parameter() -> Parameter =
815            p:unbraced_positional_parameter() { Parameter::Positional(p) } /
816            p:special_parameter() { Parameter::Special(p) } /
817            p:variable_name() { Parameter::Named(p.to_owned()) }
818
819        // N.B. The indexing syntax is not a standard sh-ism.
820        pub(crate) rule parameter() -> Parameter =
821            p:positional_parameter() { Parameter::Positional(p) } /
822            p:special_parameter() { Parameter::Special(p) } /
823            non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } /
824            non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } /
825            non_posix_extensions_enabled() p:variable_name() "[" index:$(arithmetic_word(<"]">)) "]" {?
826                Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() })
827            } /
828            p:variable_name() { Parameter::Named(p.to_owned()) }
829
830        rule positional_parameter() -> u32 =
831            n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) }
832        rule unbraced_positional_parameter() -> u32 =
833            n:$(['1'..='9']) {? n.parse().or(Err("u32")) }
834
835        rule special_parameter() -> SpecialParameter =
836            "@" { SpecialParameter::AllPositionalParameters { concatenate: false } } /
837            "*" { SpecialParameter::AllPositionalParameters { concatenate: true } } /
838            "#" { SpecialParameter::PositionalParameterCount } /
839            "?" { SpecialParameter::LastExitStatus } /
840            "-" { SpecialParameter::CurrentOptionFlags } /
841            "$" { SpecialParameter::ProcessId } /
842            "!" { SpecialParameter::LastBackgroundProcessId } /
843            "0" { SpecialParameter::ShellName }
844
845        rule variable_name() -> &'input str =
846            $(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+)
847
848        pub(crate) rule command_substitution() -> WordPiece =
849            "$(" c:command() ")" { WordPiece::CommandSubstitution(c.to_owned()) } /
850            "`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) }
851
852        pub(crate) rule command() -> &'input str =
853            $(command_piece()*)
854
855        pub(crate) rule command_piece() -> () =
856            word_piece(<[')']>, true /*in_command*/) {} /
857            ([' ' | '\t'])+ {}
858
859        rule backquoted_command() -> String =
860            chars:(backquoted_char()*) { chars.into_iter().collect() }
861
862        rule backquoted_char() -> char =
863            "\\`" { '`' } /
864            [^'`']
865
866        rule arithmetic_expansion() -> WordPiece =
867            "$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
868
869        rule substring_offset() -> ast::UnexpandedArithmeticExpr =
870            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
871
872        rule substring_length() -> ast::UnexpandedArithmeticExpr =
873            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
874
875        rule parameter_replacement_str() -> String =
876            "/" s:$(word(<['}']>)) { s.to_owned() }
877
878        rule parameter_search_pattern() -> String =
879            s:$(word(<['}' | '/']>)) { s.to_owned() }
880
881        rule parameter_expression_word() -> String =
882            s:$(word(<['}']>)) { s.to_owned() }
883
884        rule extglob_enabled() -> () =
885            &[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } }
886
887        rule non_posix_extensions_enabled() -> () =
888            &[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
889
890        rule tilde_parsing_enabled() -> () =
891            &[_] {? if parser_options.tilde_expansion { Ok(()) } else { Err("no tilde expansion") } }
892    }
893}
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898    use anyhow::Result;
899    use assert_matches::assert_matches;
900
901    #[test]
902    fn parse_command_substitution() -> Result<()> {
903        super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
904        super::expansion_parser::command_piece("hi", &ParserOptions::default())?;
905        super::expansion_parser::command("echo hi", &ParserOptions::default())?;
906        super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?;
907
908        let parsed = super::parse("$(echo hi)", &ParserOptions::default())?;
909        assert_matches!(
910            &parsed[..],
911            [WordPieceWithSource { piece: WordPiece::CommandSubstitution(s), .. }] if s.as_str() == "echo hi"
912        );
913
914        Ok(())
915    }
916
917    #[test]
918    fn parse_command_substitution_with_embedded_quotes() -> Result<()> {
919        super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
920        super::expansion_parser::command_piece(r#""hi""#, &ParserOptions::default())?;
921        super::expansion_parser::command(r#"echo "hi""#, &ParserOptions::default())?;
922        super::expansion_parser::command_substitution(
923            r#"$(echo "hi")"#,
924            &ParserOptions::default(),
925        )?;
926
927        let parsed = super::parse(r#"$(echo "hi")"#, &ParserOptions::default())?;
928        assert_matches!(
929            &parsed[..],
930            [WordPieceWithSource { piece: WordPiece::CommandSubstitution(s), .. }] if s.as_str() == r#"echo "hi""#
931        );
932
933        Ok(())
934    }
935
936    #[test]
937    fn parse_command_substitution_with_embedded_extglob() -> Result<()> {
938        let parsed = super::parse("$(echo !(x))", &ParserOptions::default())?;
939        assert_matches!(
940            &parsed[..],
941            [WordPieceWithSource { piece: WordPiece::CommandSubstitution(s), .. }] if s.as_str() == "echo !(x)"
942        );
943
944        Ok(())
945    }
946
947    #[test]
948    fn parse_backquoted_command() -> Result<()> {
949        let parsed = super::parse("`echo hi`", &ParserOptions::default())?;
950        assert_matches!(
951            &parsed[..],
952            [WordPieceWithSource { piece: WordPiece::BackquotedCommandSubstitution(s), .. }] if s == "echo hi"
953        );
954
955        Ok(())
956    }
957
958    #[test]
959    fn parse_backquoted_command_in_double_quotes() -> Result<()> {
960        let parsed = super::parse(r#""`echo hi`""#, &ParserOptions::default())?;
961        assert_matches!(
962            &parsed[..],
963            [WordPieceWithSource { piece: WordPiece::DoubleQuotedSequence(inner), .. }]
964                if matches!(
965                    &inner[..],
966                    [WordPieceWithSource { piece: WordPiece::BackquotedCommandSubstitution(s), .. }] if s == "echo hi"
967                )
968        );
969
970        Ok(())
971    }
972
973    #[test]
974    fn parse_extglob_with_embedded_parameter() -> Result<()> {
975        let parsed = super::parse("+([$var])", &ParserOptions::default())?;
976        assert_matches!(
977            &parsed[..],
978            [WordPieceWithSource { piece: WordPiece::Text(s1), .. },
979             WordPieceWithSource { piece: WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter: Parameter::Named(s2), .. }), ..},
980             WordPieceWithSource { piece: WordPiece::Text(s3), .. }] if s1 == "+([" && s2 == "var" && s3 == "])"
981        );
982
983        Ok(())
984    }
985
986    #[test]
987    fn parse_arithmetic_expansion() -> Result<()> {
988        const EXPECTED_RESULT: &str = "0";
989
990        let parsed = super::parse("$((0))", &ParserOptions::default())?;
991        assert_matches!(
992            &parsed[..],
993            [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value }), .. }] if value == EXPECTED_RESULT
994        );
995
996        Ok(())
997    }
998
999    #[test]
1000    fn parse_arithmetic_expansion_with_parens() -> Result<()> {
1001        const EXPECTED_RESULT: &str = "((1+2)*3)";
1002
1003        let parsed = super::parse("$((((1+2)*3)))", &ParserOptions::default())?;
1004        assert_matches!(
1005            &parsed[..],
1006            [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value }), .. }] if value == EXPECTED_RESULT
1007        );
1008
1009        Ok(())
1010    }
1011
1012    #[test]
1013    fn test_arithmetic_word_parsing() {
1014        let options = ParserOptions::default();
1015
1016        assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok());
1017        assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok());
1018        assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok());
1019        assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok());
1020        assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok());
1021        assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok());
1022        assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok());
1023        assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok());
1024        assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok());
1025    }
1026
1027    #[test]
1028    fn test_arithmetic_word_piece_parsing() {
1029        let options = ParserOptions::default();
1030
1031        assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok());
1032        assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok());
1033        assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok());
1034        assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok());
1035        assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok());
1036        assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok());
1037        assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok());
1038        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok());
1039        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok());
1040        assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err());
1041        assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err());
1042        assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err());
1043    }
1044}