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    pub fn generate(self) -> Box<dyn Iterator<Item = String>> {
422        match self {
423            BraceExpressionMember::NumberSequence {
424                low,
425                high,
426                increment,
427            } => Box::new(
428                (low..=high)
429                    .step_by(increment as usize)
430                    .map(|n| n.to_string()),
431            ),
432            BraceExpressionMember::CharSequence {
433                low,
434                high,
435                increment,
436            } => Box::new(
437                (low..=high)
438                    .step_by(increment as usize)
439                    .map(|c| c.to_string()),
440            ),
441            BraceExpressionMember::Text(text) => Box::new(std::iter::once(text)),
442        }
443    }
444}
445
446/// Parse a word into its constituent pieces.
447///
448/// # Arguments
449///
450/// * `word` - The word to parse.
451/// * `options` - The parser options to use.
452pub fn parse(
453    word: &str,
454    options: &ParserOptions,
455) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
456    cacheable_parse(word.to_owned(), options.to_owned())
457}
458
459#[cached::proc_macro::cached(size = 64, result = true)]
460fn cacheable_parse(
461    word: String,
462    options: ParserOptions,
463) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
464    tracing::debug!(target: "expansion", "Parsing word '{}'", word);
465
466    let pieces = expansion_parser::unexpanded_word(word.as_str(), &options)
467        .map_err(|err| error::WordParseError::Word(word.to_owned(), err))?;
468
469    tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces);
470
471    Ok(pieces)
472}
473
474/// Parse the given word into a parameter expression.
475///
476/// # Arguments
477///
478/// * `word` - The word to parse.
479/// * `options` - The parser options to use.
480pub fn parse_parameter(
481    word: &str,
482    options: &ParserOptions,
483) -> Result<Parameter, error::WordParseError> {
484    expansion_parser::parameter(word, options)
485        .map_err(|err| error::WordParseError::Parameter(word.to_owned(), err))
486}
487
488/// Parse brace expansion from a given word .
489///
490/// # Arguments
491///
492/// * `word` - The word to parse.
493/// * `options` - The parser options to use.
494pub fn parse_brace_expansions(
495    word: &str,
496    options: &ParserOptions,
497) -> Result<Option<Vec<BraceExpressionOrText>>, error::WordParseError> {
498    expansion_parser::brace_expansions(word, options)
499        .map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err))
500}
501
502peg::parser! {
503    grammar expansion_parser(parser_options: &ParserOptions) for str {
504        // Helper rule that enables pegviz to be used to visualize debug peg traces.
505        rule traced<T>(e: rule<T>) -> T =
506            &(input:$([_]*) {
507                #[cfg(feature = "debug-tracing")]
508                println!("[PEG_INPUT_START]\n{}\n[PEG_TRACE_START]", input);
509            })
510            e:e()? {?
511                #[cfg(feature = "debug-tracing")]
512                println!("[PEG_TRACE_STOP]");
513                e.ok_or("")
514            }
515
516        pub(crate) rule unexpanded_word() -> Vec<WordPieceWithSource> = traced(<word(<![_]>)>)
517
518        rule word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
519            tilde:tilde_prefix_with_source()? pieces:word_piece_with_source(<stop_condition()>, false /*in_command*/)* {
520                let mut all_pieces = Vec::new();
521                if let Some(tilde) = tilde {
522                    all_pieces.push(tilde);
523                }
524                all_pieces.extend(pieces);
525                all_pieces
526            }
527
528        pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
529            pieces:(brace_expansion_piece()+) { Some(pieces) } /
530            [_]* { None }
531
532        rule brace_expansion_piece() -> BraceExpressionOrText =
533            expr:brace_expr() {
534                BraceExpressionOrText::Expr(expr)
535            } /
536            text:$(non_brace_expr_text()+) { BraceExpressionOrText::Text(text.to_owned()) }
537
538        rule non_brace_expr_text() -> () =
539            !"{" word_piece(<['{']>, false) {} /
540            !brace_expr() "{" {}
541
542        rule brace_expr() -> BraceExpression =
543            "{" inner:brace_expr_inner() "}" { inner }
544
545        rule brace_expr_inner() -> BraceExpression =
546            brace_text_list_expr() /
547            seq:brace_sequence_expr() { vec![seq] }
548
549        rule brace_text_list_expr() -> BraceExpression =
550            members:(brace_text_list_member() **<2,> ",") {
551                members.into_iter().flatten().collect()
552            }
553
554        rule brace_text_list_member() -> BraceExpression =
555            &[',' | '}'] { vec![BraceExpressionMember::Text(String::new())] } /
556            brace_expr() /
557            text:$(word_piece(<[',' | '}']>, false)) {
558                vec![BraceExpressionMember::Text(text.to_owned())]
559            }
560
561        rule brace_sequence_expr() -> BraceExpressionMember =
562            low:number() ".." high:number() increment:(".." n:number() { n })? {
563                BraceExpressionMember::NumberSequence { low, high, increment: increment.unwrap_or(1) }
564            } /
565            low:character() ".." high:character() increment:(".." n:number() { n })? {
566                BraceExpressionMember::CharSequence { low, high, increment: increment.unwrap_or(1) }
567            }
568
569        rule number() -> i64 = n:$(['0'..='9']+) { n.parse().unwrap() }
570        rule character() -> char = ['a'..='z' | 'A'..='Z']
571
572        pub(crate) rule is_arithmetic_word() =
573            arithmetic_word(<![_]>)
574
575            // N.B. We don't bother returning the word pieces, as all users of this rule
576        // only try to extract the consumed input string and not the parse result.
577        rule arithmetic_word<T>(stop_condition: rule<T>) =
578            arithmetic_word_piece(<stop_condition()>)* {}
579
580        pub(crate) rule is_arithmetic_word_piece() =
581            arithmetic_word_piece(<![_]>)
582
583        // This rule matches an individual "piece" of an arithmetic expression. It needs to handle
584        // matching nested parenthesized expressions as well. We stop consuming the input when
585        // we reach the provided stop condition, which typically denotes the end of the containing
586        // arithmetic expression.
587        rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
588            // This branch matches a parenthesized piece; we consume the opening parenthesis and
589            // delegate the rest to a helper rule. We don't worry about the stop condition passed
590            // into us, because if we see an opening parenthesis then we *must* find its closing
591            // partner.
592            "(" arithmetic_word_plus_right_paren() {} /
593            // This branch matches any standard piece of a word, stopping as soon as we reach
594            // either the overall stop condition *OR* an opening parenthesis. We add this latter
595            // condition to ensure that *we* handle matching parentheses.
596            !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>, false /*in_command*/) {}
597
598        // This is a helper rule that matches either the provided stop condition or an opening parenthesis.
599        rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
600            stop_condition() {} /
601            "(" {}
602
603        // This rule matches an arithmetic word followed by a right parenthesis. It must consume the right parenthesis.
604        rule arithmetic_word_plus_right_paren() =
605            arithmetic_word(<[')']>) ")" /
606
607        rule word_piece_with_source<T>(stop_condition: rule<T>, in_command: bool) -> WordPieceWithSource =
608            start_index:position!() piece:word_piece(<stop_condition()>, in_command) end_index:position!() {
609                WordPieceWithSource { piece, start_index, end_index }
610            }
611
612        rule word_piece<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
613            arithmetic_expansion() /
614            command_substitution() /
615            parameter_expansion() /
616            unquoted_text(<stop_condition()>, in_command)
617
618        rule double_quoted_word_piece() -> WordPiece =
619            arithmetic_expansion() /
620            command_substitution() /
621            parameter_expansion() /
622            double_quoted_escape_sequence() /
623            double_quoted_text()
624
625        rule unquoted_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
626            s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
627            s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
628            s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
629            normal_escape_sequence() /
630            unquoted_literal_text(<stop_condition()>, in_command)
631
632        rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
633            "\"" i:double_quoted_sequence_inner()* "\"" { i }
634
635        rule double_quoted_sequence_inner() -> WordPieceWithSource =
636            start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
637                WordPieceWithSource {
638                    piece,
639                    start_index,
640                    end_index
641                }
642            }
643
644        rule single_quoted_literal_text() -> &'input str =
645            "\'" inner:$([^'\'']*) "\'" { inner }
646
647        rule ansi_c_quoted_text() -> &'input str =
648            "$\'" inner:$([^'\'']*) "\'" { inner }
649
650        rule unquoted_literal_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
651            s:$(unquoted_literal_text_piece(<stop_condition()>, in_command)+) { WordPiece::Text(s.to_owned()) }
652
653        // TODO: Find a way to remove the special-case logic for extglob + subshell commands
654        rule unquoted_literal_text_piece<T>(stop_condition: rule<T>, in_command: bool) =
655            is_true(in_command) extglob_pattern() /
656            is_true(in_command) subshell_command() /
657            !stop_condition() !normal_escape_sequence() [^'$' | '\'' | '\"'] {}
658
659        rule is_true(value: bool) = &[_] {? if value { Ok(()) } else { Err("not true") } }
660
661        rule extglob_pattern() =
662            ("@" / "!" / "?" / "+" / "*") "(" extglob_body_piece()* ")" {}
663
664        rule extglob_body_piece() =
665            word_piece(<[')']>, true /*in_command*/) {}
666
667        rule subshell_command() =
668            "(" command() ")" {}
669
670        rule double_quoted_text() -> WordPiece =
671            s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
672
673        rule double_quote_body_text() -> &'input str =
674            $((!double_quoted_escape_sequence() [^'$' | '\"'])+)
675
676        rule normal_escape_sequence() -> WordPiece =
677            s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
678
679        rule double_quoted_escape_sequence() -> WordPiece =
680            s:$("\\" ['$' | '`' | '\"' | '\'' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
681
682        rule tilde_prefix_with_source() -> WordPieceWithSource =
683            start_index:position!() piece:tilde_prefix() end_index:position!() {
684                WordPieceWithSource {
685                    piece,
686                    start_index,
687                    end_index
688                }
689            }
690
691        // TODO: Handle colon syntax
692        rule tilde_prefix() -> WordPiece =
693            tilde_parsing_enabled() "~" cs:$((!['/' | ':' | ';'] [c])*) { WordPiece::TildePrefix(cs.to_owned()) }
694
695        // TODO: Deal with fact that there may be a quoted word or escaped closing brace chars.
696        // TODO: Improve on how we handle a '$' not followed by a valid variable name or parameter.
697        rule parameter_expansion() -> WordPiece =
698            "${" e:parameter_expression() "}" {
699                WordPiece::ParameterExpansion(e)
700            } /
701            "$" parameter:unbraced_parameter() {
702                WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false })
703            } /
704            "$" !['\''] {
705                WordPiece::Text("$".to_owned())
706            }
707
708        rule parameter_expression() -> ParameterExpr =
709            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? {
710                ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value }
711            } /
712            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? {
713                ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value }
714            } /
715            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? {
716                ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message }
717            } /
718            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? {
719                ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value }
720            } /
721            "#" parameter:parameter() {
722                ParameterExpr::ParameterLength { parameter, indirect: false }
723            } /
724            indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? {
725                ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern }
726            } /
727            indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? {
728                ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern }
729            } /
730            indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? {
731                ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern }
732            } /
733            indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? {
734                ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern }
735            } /
736            // N.B. The following case is for non-sh extensions.
737            non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } /
738            indirect:parameter_indirection() parameter:parameter() {
739                ParameterExpr::Parameter { parameter, indirect }
740            }
741
742        rule parameter_test_type() -> ParameterTestType =
743            colon:":"? {
744                if colon.is_some() {
745                    ParameterTestType::UnsetOrNull
746                } else {
747                    ParameterTestType::Unset
748                }
749            }
750
751        rule non_posix_parameter_expression() -> ParameterExpr =
752            "!" variable_name:variable_name() "[*]" {
753                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true }
754            } /
755            "!" variable_name:variable_name() "[@]" {
756                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false }
757            } /
758            "!" prefix:variable_name() "*" {
759                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true }
760            } /
761            "!" prefix:variable_name() "@" {
762                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false }
763            } /
764            indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? {
765                ParameterExpr::Substring { parameter, indirect, offset, length }
766            } /
767            indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() {
768                ParameterExpr::Transform { parameter, indirect, op }
769            } /
770            indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
771                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix }
772            } /
773            indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
774                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix }
775            } /
776            indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
777                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere }
778            } /
779            indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
780                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence }
781            } /
782            indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? {
783                ParameterExpr::UppercasePattern { parameter, indirect, pattern }
784            } /
785            indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? {
786                ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern }
787            } /
788            indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? {
789                ParameterExpr::LowercasePattern { parameter, indirect, pattern }
790            } /
791            indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? {
792                ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern }
793            }
794
795        rule parameter_indirection() -> bool =
796            non_posix_extensions_enabled() "!" { true } /
797            { false }
798
799        rule non_posix_parameter_transformation_op() -> ParameterTransformOp =
800            "U" { ParameterTransformOp::ToUpperCase } /
801            "u" { ParameterTransformOp::CapitalizeInitial } /
802            "L" { ParameterTransformOp::ToLowerCase } /
803            "Q" { ParameterTransformOp::Quoted } /
804            "E" { ParameterTransformOp::ExpandEscapeSequences } /
805            "P" { ParameterTransformOp::PromptExpand } /
806            "A" { ParameterTransformOp::ToAssignmentLogic } /
807            "K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } /
808            "a" { ParameterTransformOp::ToAttributeFlags } /
809            "k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } }
810
811
812        rule unbraced_parameter() -> Parameter =
813            p:unbraced_positional_parameter() { Parameter::Positional(p) } /
814            p:special_parameter() { Parameter::Special(p) } /
815            p:variable_name() { Parameter::Named(p.to_owned()) }
816
817        // N.B. The indexing syntax is not a standard sh-ism.
818        pub(crate) rule parameter() -> Parameter =
819            p:positional_parameter() { Parameter::Positional(p) } /
820            p:special_parameter() { Parameter::Special(p) } /
821            non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } /
822            non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } /
823            non_posix_extensions_enabled() p:variable_name() "[" index:$(arithmetic_word(<"]">)) "]" {?
824                Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() })
825            } /
826            p:variable_name() { Parameter::Named(p.to_owned()) }
827
828        rule positional_parameter() -> u32 =
829            n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) }
830        rule unbraced_positional_parameter() -> u32 =
831            n:$(['1'..='9']) {? n.parse().or(Err("u32")) }
832
833        rule special_parameter() -> SpecialParameter =
834            "@" { SpecialParameter::AllPositionalParameters { concatenate: false } } /
835            "*" { SpecialParameter::AllPositionalParameters { concatenate: true } } /
836            "#" { SpecialParameter::PositionalParameterCount } /
837            "?" { SpecialParameter::LastExitStatus } /
838            "-" { SpecialParameter::CurrentOptionFlags } /
839            "$" { SpecialParameter::ProcessId } /
840            "!" { SpecialParameter::LastBackgroundProcessId } /
841            "0" { SpecialParameter::ShellName }
842
843        rule variable_name() -> &'input str =
844            $(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+)
845
846        pub(crate) rule command_substitution() -> WordPiece =
847            "$(" c:command() ")" { WordPiece::CommandSubstitution(c.to_owned()) } /
848            "`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) }
849
850        pub(crate) rule command() -> &'input str =
851            $(command_piece()*)
852
853        pub(crate) rule command_piece() -> () =
854            word_piece(<[')']>, true /*in_command*/) {} /
855            ([' ' | '\t'])+ {}
856
857        rule backquoted_command() -> String =
858            chars:(backquoted_char()*) { chars.into_iter().collect() }
859
860        rule backquoted_char() -> char =
861            "\\`" { '`' } /
862            [^'`']
863
864        rule arithmetic_expansion() -> WordPiece =
865            "$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
866
867        rule substring_offset() -> ast::UnexpandedArithmeticExpr =
868            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
869
870        rule substring_length() -> ast::UnexpandedArithmeticExpr =
871            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
872
873        rule parameter_replacement_str() -> String =
874            "/" s:$(word(<['}']>)) { s.to_owned() }
875
876        rule parameter_search_pattern() -> String =
877            s:$(word(<['}' | '/']>)) { s.to_owned() }
878
879        rule parameter_expression_word() -> String =
880            s:$(word(<['}']>)) { s.to_owned() }
881
882        rule extglob_enabled() -> () =
883            &[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } }
884
885        rule non_posix_extensions_enabled() -> () =
886            &[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
887
888        rule tilde_parsing_enabled() -> () =
889            &[_] {? if parser_options.tilde_expansion { Ok(()) } else { Err("no tilde expansion") } }
890    }
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896    use anyhow::Result;
897    use assert_matches::assert_matches;
898
899    #[test]
900    fn parse_command_substitution() -> Result<()> {
901        super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
902        super::expansion_parser::command_piece("hi", &ParserOptions::default())?;
903        super::expansion_parser::command("echo hi", &ParserOptions::default())?;
904        super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?;
905
906        let parsed = super::parse("$(echo hi)", &ParserOptions::default())?;
907        assert_matches!(
908            &parsed[..],
909            [WordPieceWithSource { piece: WordPiece::CommandSubstitution(s), .. }] if s.as_str() == "echo hi"
910        );
911
912        Ok(())
913    }
914
915    #[test]
916    fn parse_command_substitution_with_embedded_quotes() -> Result<()> {
917        super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
918        super::expansion_parser::command_piece(r#""hi""#, &ParserOptions::default())?;
919        super::expansion_parser::command(r#"echo "hi""#, &ParserOptions::default())?;
920        super::expansion_parser::command_substitution(
921            r#"$(echo "hi")"#,
922            &ParserOptions::default(),
923        )?;
924
925        let parsed = super::parse(r#"$(echo "hi")"#, &ParserOptions::default())?;
926        assert_matches!(
927            &parsed[..],
928            [WordPieceWithSource { piece: WordPiece::CommandSubstitution(s), .. }] if s.as_str() == r#"echo "hi""#
929        );
930
931        Ok(())
932    }
933
934    #[test]
935    fn parse_command_substitution_with_embedded_extglob() -> Result<()> {
936        let parsed = super::parse("$(echo !(x))", &ParserOptions::default())?;
937        assert_matches!(
938            &parsed[..],
939            [WordPieceWithSource { piece: WordPiece::CommandSubstitution(s), .. }] if s.as_str() == "echo !(x)"
940        );
941
942        Ok(())
943    }
944
945    #[test]
946    fn parse_extglob_with_embedded_parameter() -> Result<()> {
947        let parsed = super::parse("+([$var])", &ParserOptions::default())?;
948        assert_matches!(
949            &parsed[..],
950            [WordPieceWithSource { piece: WordPiece::Text(s1), .. },
951             WordPieceWithSource { piece: WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter: Parameter::Named(s2), .. }), ..},
952             WordPieceWithSource { piece: WordPiece::Text(s3), .. }] if s1 == "+([" && s2 == "var" && s3 == "])"
953        );
954
955        Ok(())
956    }
957
958    #[test]
959    fn parse_arithmetic_expansion() -> Result<()> {
960        let parsed = super::parse("$((0))", &ParserOptions::default())?;
961        const EXPECTED_RESULT: &str = "0";
962
963        assert_matches!(
964            &parsed[..],
965            [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value }), .. }] if value == EXPECTED_RESULT
966        );
967
968        Ok(())
969    }
970
971    #[test]
972    fn parse_arithmetic_expansion_with_parens() -> Result<()> {
973        let parsed = super::parse("$((((1+2)*3)))", &ParserOptions::default())?;
974        const EXPECTED_RESULT: &str = "((1+2)*3)";
975
976        assert_matches!(
977            &parsed[..],
978            [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value }), .. }] if value == EXPECTED_RESULT
979        );
980
981        Ok(())
982    }
983
984    #[test]
985    fn test_arithmetic_word_parsing() {
986        let options = ParserOptions::default();
987
988        assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok());
989        assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok());
990        assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok());
991        assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok());
992        assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok());
993        assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok());
994        assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok());
995        assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok());
996        assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok());
997    }
998
999    #[test]
1000    fn test_arithmetic_word_piece_parsing() {
1001        let options = ParserOptions::default();
1002
1003        assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok());
1004        assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok());
1005        assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok());
1006        assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok());
1007        assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok());
1008        assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok());
1009        assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok());
1010        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok());
1011        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok());
1012        assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err());
1013        assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err());
1014        assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err());
1015    }
1016}