Skip to main content

shrub_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, Display};
13
14use crate::{ParserOptions, SourceSpan, ast, error};
15
16/// Encapsulates a `WordPiece` together with its position in the string it came
17/// from.
18#[derive(Clone, Debug)]
19#[cfg_attr(
20    any(test, feature = "serde"),
21    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
22)]
23pub struct WordPieceWithSource {
24    /// The word piece.
25    pub piece: WordPiece,
26    /// The start index of the piece in the source string.
27    pub start_index: usize,
28    /// The end index of the piece in the source string.
29    pub end_index: usize,
30}
31
32/// Represents a piece of a word.
33#[derive(Clone, Debug)]
34#[cfg_attr(
35    any(test, feature = "serde"),
36    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
37)]
38pub enum WordPiece {
39    /// A simple unquoted, unescaped string.
40    Text(String),
41    /// A string that is single-quoted.
42    SingleQuotedText(String),
43    /// A string that is ANSI-C quoted.
44    AnsiCQuotedText(String),
45    /// A sequence of pieces that are embedded in double quotes.
46    DoubleQuotedSequence(Vec<WordPieceWithSource>),
47    /// Gettext enabled variant of [`WordPiece::DoubleQuotedSequence`].
48    GettextDoubleQuotedSequence(Vec<WordPieceWithSource>),
49    /// A tilde expansion.
50    TildeExpansion(TildeExpr),
51    /// A parameter expansion.
52    ParameterExpansion(ParameterExpr),
53    /// A command substitution.
54    CommandSubstitution(String),
55    /// A backquoted command substitution.
56    BackquotedCommandSubstitution(String),
57    /// An escape sequence.
58    EscapeSequence(String),
59    /// An arithmetic expression.
60    ArithmeticExpression(ast::UnexpandedArithmeticExpr),
61}
62
63/// Represents an expandable tilde expression (e.g., ~).
64#[derive(Clone, Debug)]
65#[cfg_attr(
66    any(test, feature = "serde"),
67    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
68)]
69pub enum TildeExpr {
70    /// `~`
71    Home,
72    /// `~<user>`
73    UserHome(String),
74    /// `~+`
75    WorkingDir,
76    /// `~-`
77    OldWorkingDir,
78    /// Represents a tilde expansion of the form `~+N`, referring to the Nth
79    /// directory in the shell's directory stack, starting at the top of the
80    /// stack. Note that the directory stack is expected to contains the
81    /// current working directory as its topmost entry.
82    NthDirFromTopOfDirStack {
83        /// Index into the directory stack (zero-based: 0 is the top of the
84        /// stack).
85        n: usize,
86        /// Whether the '+' prefix was explicitly used.
87        plus_used: bool,
88    },
89    /// Represents a tilde expansion of the form `~-N`, referring to the Nth
90    /// directory in the shell's directory stack, starting at the bottom of
91    /// the stack.
92    NthDirFromBottomOfDirStack {
93        /// Index into the directory stack (zero-based: 0 is the bottom of the
94        /// stack).
95        n: usize,
96    },
97}
98
99/// Type of a parameter test.
100#[derive(Clone, Debug)]
101#[cfg_attr(
102    any(test, feature = "serde"),
103    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
104)]
105pub enum ParameterTestType {
106    /// Check for unset or null.
107    UnsetOrNull,
108    /// Check for unset.
109    Unset,
110}
111
112/// A parameter, used in a parameter expansion.
113#[derive(Clone, Debug)]
114#[cfg_attr(
115    any(test, feature = "serde"),
116    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
117)]
118pub enum Parameter {
119    /// A 0-indexed positional parameter.
120    Positional(u32),
121    /// A special parameter.
122    Special(SpecialParameter),
123    /// A named variable.
124    Named(String),
125    /// An index into a named variable.
126    NamedWithIndex {
127        /// Variable name.
128        name: String,
129        /// Index.
130        index: String,
131    },
132    /// A named array variable with all indices.
133    NamedWithAllIndices {
134        /// Variable name.
135        name: String,
136        /// Whether to concatenate the values.
137        concatenate: bool,
138    },
139}
140
141impl Display for Parameter {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::Positional(n) => write!(f, "${n}"),
145            Self::Special(s) => write!(f, "${s}"),
146            Self::Named(name) => write!(f, "${{{name}}}"),
147            Self::NamedWithIndex { name, index } => {
148                write!(f, "${{{name}[{index}]}}")
149            }
150            Self::NamedWithAllIndices { name, concatenate } => {
151                if *concatenate {
152                    write!(f, "${{{name}[*]}}")
153                } else {
154                    write!(f, "${{{name}[@]}}")
155                }
156            }
157        }
158    }
159}
160
161/// A special parameter, used in a parameter expansion.
162#[derive(Clone, Debug)]
163#[cfg_attr(
164    any(test, feature = "serde"),
165    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
166)]
167pub enum SpecialParameter {
168    /// All positional parameters.
169    AllPositionalParameters {
170        /// Whether to concatenate the values.
171        concatenate: bool,
172    },
173    /// The count of positional parameters.
174    PositionalParameterCount,
175    /// The last exit status in the shell.
176    LastExitStatus,
177    /// The current shell option flags.
178    CurrentOptionFlags,
179    /// The current shell process ID.
180    ProcessId,
181    /// The last background process ID managed by the shell.
182    LastBackgroundProcessId,
183    /// The name of the shell.
184    ShellName,
185}
186
187impl Display for SpecialParameter {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            Self::AllPositionalParameters { concatenate } => {
191                if *concatenate {
192                    write!(f, "*")
193                } else {
194                    write!(f, "@")
195                }
196            }
197            Self::PositionalParameterCount => write!(f, "#"),
198            Self::LastExitStatus => write!(f, "?"),
199            Self::CurrentOptionFlags => write!(f, "-"),
200            Self::ProcessId => write!(f, "$"),
201            Self::LastBackgroundProcessId => write!(f, "!"),
202            Self::ShellName => write!(f, "0"),
203        }
204    }
205}
206
207/// A parameter expression, used in a parameter expansion.
208#[derive(Clone, Debug)]
209#[cfg_attr(
210    any(test, feature = "serde"),
211    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
212)]
213pub enum ParameterExpr {
214    /// A parameter, with optional indirection.
215    Parameter {
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    },
223    /// Conditionally use default values.
224    UseDefaultValues {
225        /// The parameter.
226        parameter: Parameter,
227        /// Whether to treat the expanded parameter as an indirect
228        /// reference, which should be subsequently dereferenced
229        /// for the expansion.
230        indirect: bool,
231        /// The type of test to perform.
232        test_type: ParameterTestType,
233        /// Default value to conditionally use.
234        default_value: Option<String>,
235    },
236    /// Conditionally assign default values.
237    AssignDefaultValues {
238        /// The parameter.
239        parameter: Parameter,
240        /// Whether to treat the expanded parameter as an indirect
241        /// reference, which should be subsequently dereferenced
242        /// for the expansion.
243        indirect: bool,
244        /// The type of test to perform.
245        test_type: ParameterTestType,
246        /// Default value to conditionally assign.
247        default_value: Option<String>,
248    },
249    /// Indicate error if null or unset.
250    IndicateErrorIfNullOrUnset {
251        /// The parameter.
252        parameter: Parameter,
253        /// Whether to treat the expanded parameter as an indirect
254        /// reference, which should be subsequently dereferenced
255        /// for the expansion.
256        indirect: bool,
257        /// The type of test to perform.
258        test_type: ParameterTestType,
259        /// Error message to conditionally yield.
260        error_message: Option<String>,
261    },
262    /// Conditionally use an alternative value.
263    UseAlternativeValue {
264        /// The parameter.
265        parameter: Parameter,
266        /// Whether to treat the expanded parameter as an indirect
267        /// reference, which should be subsequently dereferenced
268        /// for the expansion.
269        indirect: bool,
270        /// The type of test to perform.
271        test_type: ParameterTestType,
272        /// Alternative value to conditionally use.
273        alternative_value: Option<String>,
274    },
275    /// Compute the length of the given parameter.
276    ParameterLength {
277        /// The parameter.
278        parameter: Parameter,
279        /// Whether to treat the expanded parameter as an indirect
280        /// reference, which should be subsequently dereferenced
281        /// for the expansion.
282        indirect: bool,
283    },
284    /// Remove the smallest suffix from the given string matching the given
285    /// pattern.
286    RemoveSmallestSuffixPattern {
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    /// Remove the largest suffix from the given string matching the given
297    /// pattern.
298    RemoveLargestSuffixPattern {
299        /// The parameter.
300        parameter: Parameter,
301        /// Whether to treat the expanded parameter as an indirect
302        /// reference, which should be subsequently dereferenced
303        /// for the expansion.
304        indirect: bool,
305        /// Optionally provides a pattern to match.
306        pattern: Option<String>,
307    },
308    /// Remove the smallest prefix from the given string matching the given
309    /// pattern.
310    RemoveSmallestPrefixPattern {
311        /// The parameter.
312        parameter: Parameter,
313        /// Whether to treat the expanded parameter as an indirect
314        /// reference, which should be subsequently dereferenced
315        /// for the expansion.
316        indirect: bool,
317        /// Optionally provides a pattern to match.
318        pattern: Option<String>,
319    },
320    /// Remove the largest prefix from the given string matching the given
321    /// pattern.
322    RemoveLargestPrefixPattern {
323        /// The parameter.
324        parameter: Parameter,
325        /// Whether to treat the expanded parameter as an indirect
326        /// reference, which should be subsequently dereferenced
327        /// for the expansion.
328        indirect: bool,
329        /// Optionally provides a pattern to match.
330        pattern: Option<String>,
331    },
332    /// Extract a substring from the given parameter.
333    Substring {
334        /// The parameter.
335        parameter: Parameter,
336        /// Whether to treat the expanded parameter as an indirect
337        /// reference, which should be subsequently dereferenced
338        /// for the expansion.
339        indirect: bool,
340        /// Arithmetic expression that will be expanded to compute the offset
341        /// at which the substring should be extracted.
342        offset: ast::UnexpandedArithmeticExpr,
343        /// Optionally provides an arithmetic expression that will be expanded
344        /// to compute the length of substring to be extracted; if left
345        /// unspecified, the remainder of the string will be extracted.
346        length: Option<ast::UnexpandedArithmeticExpr>,
347    },
348    /// Transform the given parameter.
349    Transform {
350        /// The parameter.
351        parameter: Parameter,
352        /// Whether to treat the expanded parameter as an indirect
353        /// reference, which should be subsequently dereferenced
354        /// for the expansion.
355        indirect: bool,
356        /// Type of transformation to apply.
357        op: ParameterTransformOp,
358    },
359    /// Uppercase the first character of the given parameter.
360    UppercaseFirstChar {
361        /// The parameter.
362        parameter: Parameter,
363        /// Whether to treat the expanded parameter as an indirect
364        /// reference, which should be subsequently dereferenced
365        /// for the expansion.
366        indirect: bool,
367        /// Optionally provides a pattern to match.
368        pattern: Option<String>,
369    },
370    /// Uppercase the portion of the given parameter matching the given pattern.
371    UppercasePattern {
372        /// The parameter.
373        parameter: Parameter,
374        /// Whether to treat the expanded parameter as an indirect
375        /// reference, which should be subsequently dereferenced
376        /// for the expansion.
377        indirect: bool,
378        /// Optionally provides a pattern to match.
379        pattern: Option<String>,
380    },
381    /// Lowercase the first character of the given parameter.
382    LowercaseFirstChar {
383        /// The parameter.
384        parameter: Parameter,
385        /// Whether to treat the expanded parameter as an indirect
386        /// reference, which should be subsequently dereferenced
387        /// for the expansion.
388        indirect: bool,
389        /// Optionally provides a pattern to match.
390        pattern: Option<String>,
391    },
392    /// Lowercase the portion of the given parameter matching the given pattern.
393    LowercasePattern {
394        /// The parameter.
395        parameter: Parameter,
396        /// Whether to treat the expanded parameter as an indirect
397        /// reference, which should be subsequently dereferenced
398        /// for the expansion.
399        indirect: bool,
400        /// Optionally provides a pattern to match.
401        pattern: Option<String>,
402    },
403    /// Replace occurrences of the given pattern in the given parameter.
404    ReplaceSubstring {
405        /// The parameter.
406        parameter: Parameter,
407        /// Whether to treat the expanded parameter as an indirect
408        /// reference, which should be subsequently dereferenced
409        /// for the expansion.
410        indirect: bool,
411        /// Pattern to match.
412        pattern: String,
413        /// Replacement string.
414        replacement: Option<String>,
415        /// Kind of match to perform.
416        match_kind: SubstringMatchKind,
417    },
418    /// Select variable names from the environment with a given prefix.
419    VariableNames {
420        /// The prefix to match.
421        prefix: String,
422        /// Whether to concatenate the results.
423        concatenate: bool,
424    },
425    /// Select member keys from the named array.
426    MemberKeys {
427        /// Name of the array variable.
428        variable_name: String,
429        /// Whether to concatenate the results.
430        concatenate: bool,
431    },
432}
433
434/// Kind of substring match.
435#[derive(Clone, Debug)]
436#[cfg_attr(
437    any(test, feature = "serde"),
438    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
439)]
440pub enum SubstringMatchKind {
441    /// Match the prefix of the string.
442    Prefix,
443    /// Match the suffix of the string.
444    Suffix,
445    /// Match the first occurrence in the string.
446    FirstOccurrence,
447    /// Match all instances in the string.
448    Anywhere,
449}
450
451/// Kind of operation to apply to a parameter.
452#[derive(Clone, Debug)]
453#[cfg_attr(
454    any(test, feature = "serde"),
455    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
456)]
457pub enum ParameterTransformOp {
458    /// Capitalizate initials.
459    CapitalizeInitial,
460    /// Expand escape sequences.
461    ExpandEscapeSequences,
462    /// Possibly quote with arrays expanded.
463    PossiblyQuoteWithArraysExpanded {
464        /// Whether or not to yield separate words.
465        separate_words: bool,
466    },
467    /// Apply prompt expansion.
468    PromptExpand,
469    /// Quote the parameter.
470    Quoted,
471    /// Translate to a format usable in an assignment/declaration.
472    ToAssignmentLogic,
473    /// Translate to the parameter's attribute flags.
474    ToAttributeFlags,
475    /// Translate to lowercase.
476    ToLowerCase,
477    /// Translate to uppercase.
478    ToUpperCase,
479}
480
481/// Represents a sub-word that is either a brace expression or some other word
482/// text.
483#[derive(Clone, Debug)]
484#[cfg_attr(
485    any(test, feature = "serde"),
486    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
487)]
488pub enum BraceExpressionOrText {
489    /// A brace expression.
490    Expr(BraceExpression),
491    /// Other word text.
492    Text(String),
493}
494
495/// Represents a brace expression to be expanded.
496pub type BraceExpression = Vec<BraceExpressionMember>;
497
498/// Member of a brace expression.
499#[derive(Clone, Debug)]
500#[cfg_attr(
501    any(test, feature = "serde"),
502    derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
503)]
504pub enum BraceExpressionMember {
505    /// An inclusive numerical sequence.
506    NumberSequence {
507        /// Start of the sequence.
508        start: i64,
509        /// Inclusive end of the sequence.
510        end: i64,
511        /// Increment value.
512        increment: i64,
513        /// Width to zero pad each member out to, set when either bound was
514        /// written with a leading zero.
515        zero_padded_width: Option<usize>,
516    },
517    /// An inclusive character sequence.
518    CharSequence {
519        /// Start of the sequence.
520        start: char,
521        /// Inclusive end of the sequence.
522        end: char,
523        /// Increment value.
524        increment: i64,
525    },
526    /// Child text or expressions.
527    Child(Vec<BraceExpressionOrText>),
528}
529
530/// Parse a word into its constituent pieces.
531///
532/// # Arguments
533///
534/// * `word` - The word to parse.
535/// * `options` - The parser options to use.
536///
537/// # Errors
538///
539/// Returns [`crate::WordParseError::Word`] if `word` is not a valid word, such
540/// as one with a malformed expansion.
541pub fn parse(
542    word: &str,
543    options: &ParserOptions,
544) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
545    cacheable_parse(word, options)
546}
547
548#[cached::macros::cached(
549    max_size = 64,
550    key = "(String, ParserOptions)",
551    convert = r#"{ (word.to_owned(), options.to_owned()) }"#
552)]
553fn cacheable_parse(
554    word: &str,
555    options: &ParserOptions,
556) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
557    tracing::debug!(target: "expansion", "Parsing word '{}'", word);
558
559    let pieces = expansion_parser::unexpanded_word(word, options)
560        .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))?;
561
562    tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces);
563
564    Ok(pieces)
565}
566
567/// Parse a heredoc body, treating `"` and `'` as literal characters.
568///
569/// # Arguments
570///
571/// * `word` - The heredoc body to parse.
572/// * `options` - The parser options to use.
573///
574/// # Errors
575///
576/// Returns [`crate::WordParseError::Word`] if `word` is not a valid heredoc
577/// body, such as one with a malformed expansion.
578pub fn parse_heredoc(
579    word: &str,
580    options: &ParserOptions,
581) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
582    expansion_parser::unexpanded_heredoc_word(word, options)
583        .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))
584}
585
586/// Parse the given word into a parameter expression.
587///
588/// # Arguments
589///
590/// * `word` - The word to parse.
591/// * `options` - The parser options to use.
592///
593/// # Errors
594///
595/// Returns [`crate::WordParseError::Parameter`] if `word` is not a valid
596/// parameter expression.
597pub fn parse_parameter(
598    word: &str,
599    options: &ParserOptions,
600) -> Result<Parameter, error::WordParseError> {
601    expansion_parser::parameter(word, options)
602        .map_err(|err| error::WordParseError::Parameter(word.to_owned(), err.into()))
603}
604
605/// Parse brace expansion from a given word .
606///
607/// # Arguments
608///
609/// * `word` - The word to parse.
610/// * `options` - The parser options to use.
611///
612/// # Errors
613///
614/// Returns [`crate::WordParseError::BraceExpansion`] if `word` cannot be parsed
615/// for brace expansion.
616///
617/// # Panics
618///
619/// Panics if a sequence increment (the `n` in `{a..b..n}`) is a digit string
620/// that does not fit in an `i64`, because the increment is parsed with
621/// `unwrap`.
622pub fn parse_brace_expansions(
623    word: &str,
624    options: &ParserOptions,
625) -> Result<Option<Vec<BraceExpressionOrText>>, error::WordParseError> {
626    expansion_parser::brace_expansions(word, options)
627        .map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err.into()))
628}
629
630/// Parses a scalar assignment from a given word.
631///
632/// A parenthesized value is treated as scalar text; see
633/// [`parse_compound_assignment_value`] for reinterpreting such a value as a
634/// compound one.
635///
636/// # Arguments
637///
638/// * `word` - The word to parse.
639/// * `options` - The parser options to use.
640///
641/// # Errors
642///
643/// Returns [`crate::WordParseError::Word`] if `word` is not a valid
644/// `name=value` assignment.
645pub fn parse_scalar_assignment(
646    word: &str,
647    options: &ParserOptions,
648) -> Result<ast::Assignment, error::WordParseError> {
649    expansion_parser::name_equals_scalar_value(word, options)
650        .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))
651}
652
653/// Parses text as a compound assignment value, returning its optionally-keyed
654/// element words. Returns `None` if the text is not a well-formed compound
655/// value.
656///
657/// This exists so that text which only becomes recognizable as a compound value
658/// *after* expansion can be reinterpreted without reconstructing and re-parsing
659/// a whole assignment word.
660///
661/// # Arguments
662///
663/// * `value` - The candidate compound value text, including its enclosing
664///   parentheses.
665/// * `options` - The parser options to use.
666#[must_use]
667pub fn parse_compound_assignment_value(
668    value: &str,
669    options: &ParserOptions,
670) -> Option<Vec<(Option<ast::Word>, ast::Word)>> {
671    let elements = crate::parser::parse_compound_assignment_value(value, options)?;
672    parse_array_elements(elements.iter(), options).ok()
673}
674
675/// Parses an array assignment from a given word and its array elements.
676///
677/// # Arguments
678///
679/// * `word` - The assignment name and equals sign to parse.
680/// * `elements` - The array element words to parse.
681/// * `options` - The parser options to use.
682pub(crate) fn parse_array_assignment(
683    word: &str,
684    elements: &[&String],
685    options: &ParserOptions,
686) -> Result<ast::Assignment, &'static str> {
687    let (assignment_name, append) =
688        expansion_parser::name_equals(word, options).map_err(|_| "not array assignment word")?;
689
690    Ok(ast::Assignment {
691        name: assignment_name,
692        value: ast::AssignmentValue::Array(parse_array_elements(
693            elements.iter().copied(),
694            options,
695        )?),
696        append,
697        loc: SourceSpan::default(),
698    })
699}
700
701/// Parses one bound of a numeric brace sequence, e.g. `-007`.
702///
703/// Returns `None` when the bound does not fit in an `i64`, which lets the
704/// sequence rule decline rather than panic on something like
705/// `{99999999999999999999..1}`.
706///
707/// The sign goes to the parser along with the digits, so `i64::MIN` is accepted
708/// even though its digits alone are one past `i64::MAX`.
709///
710/// # Arguments
711///
712/// * `text` - The bound as it was written, sign included.
713fn parse_sequence_bound(text: &str) -> Option<i64> {
714    text.parse().ok()
715}
716
717/// Returns the width the members of a numeric brace sequence should be zero
718/// padded out to, or `None` if they should be written as plain numbers.
719///
720/// A leading zero in either bound asks for padding, and the width is then the
721/// longer of the two bounds as they were written, sign included. A lone `0` is
722/// not a leading zero, and neither is a zero that follows a `+`.
723///
724/// # Arguments
725///
726/// * `start` - The start bound as it was written.
727/// * `end` - The end bound as it was written.
728fn zero_padded_width(start: &str, end: &str) -> Option<usize> {
729    fn has_leading_zero(bound: &str) -> bool {
730        let digits = bound.strip_prefix('-').unwrap_or(bound);
731        digits.len() > 1 && digits.starts_with('0')
732    }
733
734    (has_leading_zero(start) || has_leading_zero(end)).then(|| start.len().max(end.len()))
735}
736
737/// Parses literal array element text into optionally-keyed element words.
738///
739/// # Arguments
740///
741/// * `elements` - The array element texts to parse.
742/// * `options` - The parser options to use.
743fn parse_array_elements<'a>(
744    elements: impl IntoIterator<Item = &'a String>,
745    options: &ParserOptions,
746) -> Result<Vec<(Option<ast::Word>, ast::Word)>, &'static str> {
747    elements
748        .into_iter()
749        .map(|element| {
750            let (key, value) = expansion_parser::literal_array_element(element, options)
751                .map_err(|_| "invalid array element in literal")?;
752            Ok((
753                key.map(|key| ast::Word::new(key.as_str())),
754                ast::Word::new(value.as_str()),
755            ))
756        })
757        .collect()
758}
759
760peg::parser! {
761    grammar expansion_parser(parser_options: &ParserOptions) for str {
762        // Helper rule that enables pegviz to be used to visualize debug peg traces.
763        rule traced<T>(e: rule<T>) -> T =
764            &(input:$([_]*) {
765                #[cfg(feature = "debug-tracing")]
766                println!("[PEG_INPUT_START]\n{input}\n[PEG_TRACE_START]");
767            })
768            e:e()? {?
769                #[cfg(feature = "debug-tracing")]
770                println!("[PEG_TRACE_STOP]");
771                e.ok_or("")
772            }
773
774        pub(crate) rule unexpanded_word() -> Vec<WordPieceWithSource> = traced(<word(<![_]>)>)
775
776        rule word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
777            tilde:tilde_expr_prefix_with_source()? pieces:word_piece_with_source(<stop_condition()>)* {
778                let mut all_pieces = Vec::new();
779                if let Some(tilde) = tilde {
780                    all_pieces.push(tilde);
781                }
782                all_pieces.extend(pieces);
783                all_pieces
784            }
785
786        // Takes a word as input.
787        pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
788            pieces:(brace_expansion_piece(<![_]>)+) { Some(pieces) } /
789            [_]* { None }
790
791        // Returns either a complete brace expression (without any prefix or suffix), or a
792        // non-brace-expression string.
793        rule brace_expansion_piece<T>(stop_condition: rule<T>) -> BraceExpressionOrText =
794            expr:brace_expr() {
795                BraceExpressionOrText::Expr(expr)
796            } /
797            text:$(non_brace_expr_text(<stop_condition()>)+) { BraceExpressionOrText::Text(text.to_owned()) }
798
799        // Parses text that is not considered to contain a brace expression.
800        rule non_brace_expr_text<T>(stop_condition: rule<T>) -> () =
801            !"{" word_piece(<['{'] {} / stop_condition() {}>) {} /
802            !brace_expr() !stop_condition() "{" {}
803
804        // Parses a complete brace expression, with no prefix or suffix.
805        pub(crate) rule brace_expr() -> BraceExpression =
806            "{" inner:brace_expr_inner() "}" { inner }
807
808        // Parses the text inside a complete brace expression; basically the complete brace
809        // expression without the opening and closing brace characters.
810        pub(crate) rule brace_expr_inner() -> BraceExpression =
811            brace_text_list_expr() /
812            seq:brace_sequence_expr() { vec![seq] }
813
814        // Parses a list of brace expression members, including the separating commas; does
815        // not include the opening and closing braces.
816        pub(crate) rule brace_text_list_expr() -> BraceExpression =
817            brace_text_list_member() **<2,> ","
818
819        // Parses an element that can occur in a brace expression member list, not including the
820        // terminating comma or closing brace.
821        pub(crate) rule brace_text_list_member() -> BraceExpressionMember =
822            // Matches an empty-string member, without consuming the comma or closing brace that terminates it.
823            &[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } /
824            // Matches a nested string that may include some combination of concatenated textual strings
825            // and brace expressions.
826            child_pieces:(brace_expansion_piece(<[',' | '}']>)+) {
827                BraceExpressionMember::Child(child_pieces)
828            }
829
830        pub(crate) rule brace_sequence_expr() -> BraceExpressionMember =
831            start:sequence_bound() ".." end:sequence_bound() increment:(".." n:number() { n })? {
832                BraceExpressionMember::NumberSequence {
833                    start: start.0,
834                    end: end.0,
835                    increment: increment.unwrap_or(1),
836                    zero_padded_width: zero_padded_width(start.1, end.1),
837                }
838            } /
839            start:character() ".." end:character() increment:(".." n:number() { n })? {
840                BraceExpressionMember::CharSequence { start, end, increment: increment.unwrap_or(1) }
841            }
842
843        // A bound of a numeric sequence, kept alongside the text it was written as.
844        // Whether the members come out zero padded is decided by that text and not
845        // by the value, so `{01..3}` and `{1..3}` have to stay distinguishable.
846        rule sequence_bound() -> (i64, &'input str) = text:$(number_sign()? ['0'..='9']+) {?
847            parse_sequence_bound(text).map(|n| (n, text)).ok_or("number out of range")
848        }
849
850        // The increment of a sequence. Like a bound, it falls out of the rule, and the
851        // brace expression with it, when it doesn't fit.
852        rule number() -> i64 = text:$(number_sign()? ['0'..='9']+) {?
853            parse_sequence_bound(text).ok_or("number out of range")
854        }
855
856        rule number_sign() -> i64 =
857            ['-'] { -1 } /
858            ['+'] { 1 }
859
860        rule character() -> char = ['a'..='z' | 'A'..='Z']
861
862        pub(crate) rule is_arithmetic_word() =
863            arithmetic_word(<![_]>)
864
865        // N.B. We don't bother returning the word pieces, as all users of this rule
866        // only try to extract the consumed input string and not the parse result.
867        rule arithmetic_word<T>(stop_condition: rule<T>) =
868            arithmetic_word_piece(<stop_condition()>)* {}
869
870        pub(crate) rule is_arithmetic_word_piece() =
871            arithmetic_word_piece(<![_]>)
872
873        // This rule matches an individual "piece" of an arithmetic expression. It needs to handle
874        // matching nested parenthesized expressions as well. We stop consuming the input when
875        // we reach the provided stop condition, which typically denotes the end of the containing
876        // arithmetic expression.
877        rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
878            // This branch matches a parenthesized piece; we consume the opening parenthesis and
879            // delegate the rest to a helper rule. We don't worry about the stop condition passed
880            // into us, because if we see an opening parenthesis then we *must* find its closing
881            // partner.
882            "(" arithmetic_word_plus_right_paren() {} /
883            // An array subscript may contain the stop condition. Other brackets are left
884            // as text, so malformed arithmetic doesn't fall back to command substitution.
885            array_element_name() {} /
886            // This branch matches any standard piece of a word, stopping as soon as we reach
887            // either the overall stop condition *OR* an opening parenthesis. We add this latter
888            // condition to ensure that *we* handle matching parentheses.
889            !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>) {}
890
891        // This is a helper rule that matches either the provided stop condition or an opening parenthesis.
892        rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
893            stop_condition() {} /
894            "(" {}
895
896        // This rule matches an arithmetic word followed by a right parenthesis. It must consume the right parenthesis.
897        rule arithmetic_word_plus_right_paren() =
898            arithmetic_word(<[')']>) ")"
899
900        rule word_piece_with_source<T>(stop_condition: rule<T>) -> WordPieceWithSource =
901            start_index:position!() piece:word_piece(<stop_condition()>) end_index:position!() {
902                WordPieceWithSource { piece, start_index, end_index }
903            }
904
905        rule word_piece<T>(stop_condition: rule<T>) -> WordPiece =
906            // Rules that match quoted text.
907            s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
908            s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
909            s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
910            s:gettext_double_quoted_sequence() { WordPiece::GettextDoubleQuotedSequence(s) } /
911            // Rules that match pieces starting with a dollar sign ('$').
912            dollar_sign_word_piece() /
913            // Rules that match unquoted text that doesn't start with an unescaped dollar sign.
914            normal_escape_sequence() /
915            // Allow tilde expression to be matched as a word piece (for tilde-after-colon expansion)
916            enabled_tilde_expr_after_colon() /
917            // Finally, match unquoted literal text.
918            unquoted_literal_text(<stop_condition()>)
919
920        rule dollar_sign_word_piece() -> WordPiece =
921            arithmetic_expansion() /
922            legacy_arithmetic_expansion() /
923            command_substitution() /
924            parameter_expansion()
925
926        rule double_quoted_word_piece() -> WordPiece =
927            arithmetic_expansion() /
928            legacy_arithmetic_expansion() /
929            command_substitution() /
930            parameter_expansion() /
931            double_quoted_escape_sequence() /
932            double_quoted_text()
933
934        rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
935            "\"" i:double_quoted_sequence_inner()* "\"" { i }
936
937        rule gettext_double_quoted_sequence() -> Vec<WordPieceWithSource> =
938            "$\"" i:double_quoted_sequence_inner()* "\"" { i }
939
940        rule double_quoted_sequence_inner() -> WordPieceWithSource =
941            start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
942                WordPieceWithSource {
943                    piece,
944                    start_index,
945                    end_index
946                }
947            }
948
949        rule single_quoted_literal_text() -> &'input str =
950            "\'" inner:$([^'\'']*) "\'" { inner }
951
952        rule ansi_c_quoted_text() -> &'input str =
953            r"$'" inner:$((r"\\" / r"\'" / [^'\''])*) r"'" { inner }
954
955        rule unquoted_literal_text<T>(stop_condition: rule<T>) -> WordPiece =
956            s:$(unquoted_literal_text_piece(<stop_condition()>)+) { WordPiece::Text(s.to_owned()) }
957
958        rule unquoted_literal_text_piece<T>(stop_condition: rule<T>) =
959            !stop_condition() !normal_escape_sequence() !enabled_tilde_expr_after_colon() [^'\'' | '\"' | '$' | '`'] {}
960
961        rule enabled_tilde_expr_after_colon() -> WordPiece =
962            tilde_exprs_after_colon_enabled() last_char_is_colon() piece:tilde_expression_piece() { piece }
963
964        rule last_char_is_colon() = #{|input, pos| {
965            if pos == 0 {
966                // No preceding character - can't be preceded by ':'
967                peg::RuleResult::Failed
968            } else {
969                // Check the byte directly (`:` is ASCII, single byte)
970                if input.as_bytes()[pos - 1] == b':' {
971                    peg::RuleResult::Matched(pos, ())
972                } else {
973                    peg::RuleResult::Failed
974                }
975            }
976        }}
977
978        rule double_quoted_text() -> WordPiece =
979            s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
980
981        rule double_quote_body_text() -> &'input str =
982            $((!double_quoted_escape_sequence() !dollar_sign_word_piece() !expansion_opener() [^'\"'])+)
983
984        // Heredoc body parsing: like double-quoted content, but " and ' are literal characters.
985        pub(crate) rule unexpanded_heredoc_word() -> Vec<WordPieceWithSource> =
986            traced(<heredoc_word(<![_]>)>)
987
988        rule heredoc_word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
989            pieces:heredoc_word_piece_with_source(<stop_condition()>)* { pieces }
990
991        rule heredoc_word_piece_with_source<T>(stop_condition: rule<T>) -> WordPieceWithSource =
992            !stop_condition() start_index:position!() piece:heredoc_word_piece() end_index:position!() {
993                WordPieceWithSource { piece, start_index, end_index }
994            }
995
996        rule heredoc_word_piece() -> WordPiece =
997            arithmetic_expansion() /
998            legacy_arithmetic_expansion() /
999            command_substitution() /
1000            parameter_expansion() /
1001            heredoc_escape_sequence() /
1002            heredoc_literal_text()
1003
1004        rule heredoc_escape_sequence() -> WordPiece =
1005            s:$("\\" ['$' | '`' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
1006
1007        rule heredoc_literal_text() -> WordPiece =
1008            s:$((!heredoc_escape_sequence() !dollar_sign_word_piece() !expansion_opener() [^'`'])+) {
1009                WordPiece::Text(s.to_owned())
1010            }
1011
1012        rule normal_escape_sequence() -> WordPiece =
1013            s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
1014
1015        rule double_quoted_escape_sequence() -> WordPiece =
1016            s:$("\\" ['$' | '`' | '\"' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
1017
1018        rule tilde_expr_prefix_with_source() -> WordPieceWithSource =
1019            start_index:position!() piece:tilde_expr_prefix() end_index:position!() {
1020                WordPieceWithSource {
1021                    piece,
1022                    start_index,
1023                    end_index
1024                }
1025            }
1026
1027        rule tilde_expr_prefix() -> WordPiece =
1028            tilde_exprs_at_word_start_enabled() piece:tilde_expression_piece() { piece }
1029
1030        rule tilde_expr_after_colon() -> WordPiece =
1031            tilde_exprs_after_colon_enabled() piece:tilde_expression_piece() { piece }
1032
1033        rule tilde_expression_piece() -> WordPiece =
1034            "~" expr:tilde_expression() { WordPiece::TildeExpansion(expr) }
1035
1036        rule tilde_expression() -> TildeExpr =
1037            &tilde_terminator() { TildeExpr::Home } /
1038            "+" &tilde_terminator() { TildeExpr::WorkingDir } /
1039            // N.B. A run of digits need not fit in a `usize`; decline the rule
1040            // when it does not, so the word falls through to being treated as a
1041            // (non-existent) user name instead of panicking the parser.
1042            plus:("+"?) n:$(['0'..='9']*) &tilde_terminator() {? n.parse().map_or_else(|_| Err("dir stack index out of range"), |n| Ok(TildeExpr::NthDirFromTopOfDirStack { n, plus_used: plus.is_some() })) } /
1043            "-" &tilde_terminator() { TildeExpr::OldWorkingDir } /
1044            "-" n:$(['0'..='9']*) &tilde_terminator() {? n.parse().map_or_else(|_| Err("dir stack index out of range"), |n| Ok(TildeExpr::NthDirFromBottomOfDirStack { n })) } /
1045            user:$(portable_filename_char()*) &tilde_terminator() { TildeExpr::UserHome(user.to_owned()) }
1046
1047        rule tilde_terminator() = ['/' | ':' | ';' | '}'] / ![_]
1048
1049        rule portable_filename_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '-']
1050
1051        // TODO(parser): Deal with fact that there may be a quoted word or escaped closing brace chars.
1052        // TODO(parser): Improve on how we handle a '$' not followed by a valid variable name or parameter.
1053        // Cached: each `${...}` can match any of ~20 parameter expression forms, and a failure
1054        // within one (e.g., an unterminated `$(`) would otherwise be re-discovered by every form
1055        // at every enclosing level, taking time exponential in the nesting depth.
1056        #[cache]
1057        rule parameter_expansion() -> WordPiece =
1058            "${" e:parameter_expression() "}" {
1059                WordPiece::ParameterExpansion(e)
1060            } /
1061            "$" parameter:unbraced_parameter() {
1062                WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false })
1063            } /
1064            !expansion_opener() "$" !['\''] {
1065                WordPiece::Text("$".to_owned())
1066            }
1067
1068        // As in bash, these always start an expansion, whose end the tokenizer finds (so it
1069        // always does, in a word the tokenizer accepted). If none parses, the text is malformed
1070        // (e.g., unterminated, or nested too deeply), so its `$` mustn't be taken as literal text.
1071        // TODO(expansion): #540: the same holds for `${`, which is still taken as text.
1072        rule expansion_opener() = "$" ['(' | '[']
1073
1074        rule parameter_expression() -> ParameterExpr =
1075            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? {
1076                ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value }
1077            } /
1078            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? {
1079                ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value }
1080            } /
1081            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? {
1082                ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message }
1083            } /
1084            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? {
1085                ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value }
1086            } /
1087            "#" parameter:parameter() {
1088                ParameterExpr::ParameterLength { parameter, indirect: false }
1089            } /
1090            indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? {
1091                ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern }
1092            } /
1093            indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? {
1094                ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern }
1095            } /
1096            indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? {
1097                ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern }
1098            } /
1099            indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? {
1100                ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern }
1101            } /
1102            // N.B. The following case is for non-sh extensions.
1103            non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } /
1104            indirect:parameter_indirection() parameter:parameter() {
1105                ParameterExpr::Parameter { parameter, indirect }
1106            }
1107
1108        rule parameter_test_type() -> ParameterTestType =
1109            colon:":"? {
1110                if colon.is_some() {
1111                    ParameterTestType::UnsetOrNull
1112                } else {
1113                    ParameterTestType::Unset
1114                }
1115            }
1116
1117        rule non_posix_parameter_expression() -> ParameterExpr =
1118            "!" variable_name:variable_name() "[*]" {
1119                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true }
1120            } /
1121            "!" variable_name:variable_name() "[@]" {
1122                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false }
1123            } /
1124            indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? {
1125                ParameterExpr::Substring { parameter, indirect, offset, length }
1126            } /
1127            indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() {
1128                ParameterExpr::Transform { parameter, indirect, op }
1129            } /
1130            "!" prefix:variable_name() "*" {
1131                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true }
1132            } /
1133            "!" prefix:variable_name() "@" {
1134                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false }
1135            } /
1136            indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1137                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix }
1138            } /
1139            indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1140                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix }
1141            } /
1142            indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1143                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere }
1144            } /
1145            indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1146                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence }
1147            } /
1148            indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? {
1149                ParameterExpr::UppercasePattern { parameter, indirect, pattern }
1150            } /
1151            indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? {
1152                ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern }
1153            } /
1154            indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? {
1155                ParameterExpr::LowercasePattern { parameter, indirect, pattern }
1156            } /
1157            indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? {
1158                ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern }
1159            }
1160
1161        rule parameter_indirection() -> bool =
1162            non_posix_extensions_enabled() "!" { true } /
1163            { false }
1164
1165        rule non_posix_parameter_transformation_op() -> ParameterTransformOp =
1166            "U" { ParameterTransformOp::ToUpperCase } /
1167            "u" { ParameterTransformOp::CapitalizeInitial } /
1168            "L" { ParameterTransformOp::ToLowerCase } /
1169            "Q" { ParameterTransformOp::Quoted } /
1170            "E" { ParameterTransformOp::ExpandEscapeSequences } /
1171            "P" { ParameterTransformOp::PromptExpand } /
1172            "A" { ParameterTransformOp::ToAssignmentLogic } /
1173            "K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } /
1174            "a" { ParameterTransformOp::ToAttributeFlags } /
1175            "k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } }
1176
1177
1178        rule unbraced_parameter() -> Parameter =
1179            p:unbraced_positional_parameter() { Parameter::Positional(p) } /
1180            p:special_parameter() { Parameter::Special(p) } /
1181            p:variable_name() { Parameter::Named(p.to_owned()) }
1182
1183        // N.B. The indexing syntax is not a standard sh-ism.
1184        pub(crate) rule parameter() -> Parameter =
1185            p:positional_parameter() { Parameter::Positional(p) } /
1186            p:special_parameter() { Parameter::Special(p) } /
1187            non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } /
1188            non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } /
1189            non_posix_extensions_enabled() p:variable_name() "[" index:array_index() "]" {?
1190                Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() })
1191            } /
1192            p:variable_name() { Parameter::Named(p.to_owned()) }
1193
1194        rule positional_parameter() -> u32 =
1195            n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) }
1196        rule unbraced_positional_parameter() -> u32 =
1197            n:$(['1'..='9']) {? n.parse().or(Err("u32")) }
1198
1199        rule special_parameter() -> SpecialParameter =
1200            "@" { SpecialParameter::AllPositionalParameters { concatenate: false } } /
1201            "*" { SpecialParameter::AllPositionalParameters { concatenate: true } } /
1202            "#" { SpecialParameter::PositionalParameterCount } /
1203            "?" { SpecialParameter::LastExitStatus } /
1204            "-" { SpecialParameter::CurrentOptionFlags } /
1205            "$" { SpecialParameter::ProcessId } /
1206            "!" { SpecialParameter::LastBackgroundProcessId } /
1207            "0" { SpecialParameter::ShellName }
1208
1209        rule variable_name() -> &'input str =
1210            $(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+)
1211
1212        pub(crate) rule command_substitution() -> WordPiece =
1213            "$(" c:command_substitution_body() ")" { WordPiece::CommandSubstitution(c.to_owned()) } /
1214            "`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) }
1215
1216        // The tokenizer already has the logic to find where the command ends (e.g., skipping
1217        // over here-doc bodies), so we leverage it here. `pos` and `body.len()` are both
1218        // byte offsets.
1219        rule command_substitution_body() -> &'input str = #{|input, pos| {
1220            let rest = input.split_at(pos).1;
1221            match crate::tokenizer::command_substitution_body(rest, &parser_options.tokenizer_options()) {
1222                Ok(body) => peg::RuleResult::Matched(pos + body.len(), body),
1223                Err(_) => peg::RuleResult::Failed,
1224            }
1225        }}
1226
1227        rule backquoted_command() -> String =
1228            chars:(backquoted_char()*) {
1229                let mut command: String = chars.into_iter().collect();
1230                // TODO(tokenizer): A backslash ending the input is reported as an unterminated
1231                // escape, where bash takes it literally; escape it so it stays literal.
1232                if command.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 {
1233                    command.push('\\');
1234                }
1235                command
1236            }
1237
1238        // As in bash, a backslash is removed only before `$`, `` ` ``, or `\`. (The tokenizer
1239        // already removed line continuations.)
1240        rule backquoted_char() -> &'input str =
1241            "\\" s:$(['$' | '`' | '\\']) { s } /
1242            s:$([^'`']) { s }
1243
1244        rule arithmetic_expansion() -> WordPiece =
1245            "$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1246
1247        rule legacy_arithmetic_expansion() -> WordPiece =
1248            "$[" e:legacy_arithmetic_expansion_body() "]" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1249
1250        // Reuse the tokenizer's iterative bracket counting rather than recursively parsing
1251        // each bracket in the expression. As with command substitutions, preserve the source.
1252        rule legacy_arithmetic_expansion_body() -> &'input str = #{|input, pos| {
1253            let rest = input.split_at(pos).1;
1254            match crate::tokenizer::legacy_arithmetic_expansion_body(rest, &parser_options.tokenizer_options()) {
1255                Ok(body) => peg::RuleResult::Matched(pos + body.len(), body),
1256                Err(_) => peg::RuleResult::Failed,
1257            }
1258        }}
1259
1260        rule substring_offset() -> ast::UnexpandedArithmeticExpr =
1261            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1262
1263        rule substring_length() -> ast::UnexpandedArithmeticExpr =
1264            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1265
1266        rule parameter_replacement_str() -> String =
1267            "/" s:$(word(<['}']>)) { s.to_owned() }
1268
1269        rule parameter_search_pattern() -> String =
1270            s:$(word(<['}' | '/']>)) { s.to_owned() }
1271
1272        rule parameter_expression_word() -> String =
1273            s:$(word(<['}']>)) { s.to_owned() }
1274
1275        rule extglob_enabled() -> () =
1276            &[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } }
1277
1278        rule non_posix_extensions_enabled() -> () =
1279            &[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
1280
1281        rule tilde_exprs_at_word_start_enabled() -> () =
1282            &[_] {? if parser_options.tilde_expansion_at_word_start { Ok(()) } else { Err("no tilde expansion at word start") } }
1283
1284        rule tilde_exprs_after_colon_enabled() -> () =
1285            &[_] {? if parser_options.tilde_expansion_after_colon { Ok(()) } else { Err("no tilde expansion after colon") } }
1286
1287        // Assignment rules.
1288
1289        pub(crate) rule name_equals_scalar_value() -> ast::Assignment =
1290            nae:name_equals() value:assigned_scalar_value() {
1291                let (name, append) = nae;
1292                ast::Assignment { name, value, append, loc: SourceSpan::default() }
1293            }
1294
1295        pub(crate) rule name_equals() -> (ast::AssignmentName, bool) =
1296            name:assignment_name() append:("+"?) "=" {
1297                (name, append.is_some())
1298            }
1299
1300        pub(crate) rule literal_array_element() -> (Option<String>, String) =
1301            "[" inner:$((!"]" [_])*) "]=" value:$([_]*) {
1302                (Some(inner.to_owned()), value.to_owned())
1303            } /
1304            value:$([_]+) {
1305                (None, value.to_owned())
1306            }
1307
1308        rule assignment_name() -> ast::AssignmentName =
1309            aen:array_element_name() {
1310                let (name, index) = aen;
1311                ast::AssignmentName::ArrayElementName(name.to_owned(), index.to_owned())
1312            } /
1313            name:assigned_scalar_name() {
1314                ast::AssignmentName::VariableName(name.to_owned())
1315            }
1316
1317        rule array_element_name() -> (&'input str, &'input str) =
1318            name:assigned_scalar_name() "[" ai:array_index() "]" { (name, ai) }
1319
1320        rule array_index() -> &'input str =
1321            $(arithmetic_word(<"]">))
1322
1323        rule assigned_scalar_name() -> &'input str =
1324            $(alpha_or_underscore() non_first_variable_char()*)
1325
1326        rule non_first_variable_char() -> () =
1327            ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z'] {}
1328
1329        rule alpha_or_underscore() -> () =
1330            ['_' | 'a'..='z' | 'A'..='Z'] {}
1331
1332        rule assigned_scalar_value() -> ast::AssignmentValue =
1333            v:$([_]*) { ast::AssignmentValue::Scalar(ast::Word::from(v.to_owned())) }
1334    }
1335}
1336
1337#[cfg(test)]
1338#[allow(clippy::panic_in_result_fn)]
1339mod tests {
1340    use anyhow::Result;
1341    use insta::assert_ron_snapshot;
1342    use pretty_assertions::assert_matches;
1343
1344    use super::*;
1345
1346    #[derive(serde::Serialize, serde::Deserialize)]
1347    struct ParseTestResults<'a> {
1348        input: &'a str,
1349        result: Vec<WordPieceWithSource>,
1350    }
1351
1352    fn test_parse(word: &str) -> Result<ParseTestResults<'_>> {
1353        let parsed = super::parse(word, &ParserOptions::default())?;
1354        Ok(ParseTestResults {
1355            input: word,
1356            result: parsed,
1357        })
1358    }
1359
1360    #[test]
1361    fn parse_ansi_c_quoted_text() -> Result<()> {
1362        assert_ron_snapshot!(test_parse(r"$'hi\nthere\t'")?);
1363        Ok(())
1364    }
1365
1366    #[test]
1367    fn parse_ansi_c_quoted_escape_seq() -> Result<()> {
1368        assert_ron_snapshot!(test_parse(r"$'\\'")?);
1369        Ok(())
1370    }
1371
1372    #[test]
1373    fn parse_tilde_with_out_of_range_dir_stack_index() -> Result<()> {
1374        // Regression: the dir-stack index was unwrapped, so a run of digits too
1375        // large for a `usize` panicked the parser. It should fall through to
1376        // being treated as a user name, which is what bash does with it.
1377        // `~N` and `~-N` fall through to a user name; `~+N` cannot, because '+'
1378        // is not a valid user-name character, so it stays literal text. Both
1379        // match what bash does with an out-of-range index.
1380        let parsed = super::parse("~99999999999999999999", &ParserOptions::default())?;
1381        assert_eq!(parsed.len(), 1);
1382        assert_matches!(
1383            parsed[0].piece,
1384            WordPiece::TildeExpansion(TildeExpr::UserHome(_))
1385        );
1386
1387        let parsed = super::parse("~-99999999999999999999", &ParserOptions::default())?;
1388        assert_eq!(parsed.len(), 1);
1389        assert_matches!(
1390            parsed[0].piece,
1391            WordPiece::TildeExpansion(TildeExpr::UserHome(_))
1392        );
1393
1394        let parsed = super::parse("~+99999999999999999999", &ParserOptions::default())?;
1395        assert_eq!(parsed.len(), 1);
1396        assert_matches!(parsed[0].piece, WordPiece::Text(_));
1397
1398        Ok(())
1399    }
1400
1401    #[test]
1402    fn parse_tilde_with_in_range_dir_stack_index() -> Result<()> {
1403        let parsed = super::parse("~+2", &ParserOptions::default())?;
1404        assert_eq!(parsed.len(), 1);
1405        assert_matches!(
1406            parsed[0].piece,
1407            WordPiece::TildeExpansion(TildeExpr::NthDirFromTopOfDirStack { n: 2, .. })
1408        );
1409
1410        Ok(())
1411    }
1412
1413    #[test]
1414    fn parse_tilde_after_colon() -> Result<()> {
1415        let opts = ParserOptions {
1416            tilde_expansion_after_colon: true,
1417            ..ParserOptions::default()
1418        };
1419
1420        let parsed = super::parse("a:~", &opts)?;
1421
1422        // Should have: Text("a:"), TildeExpansion("")
1423        assert_eq!(parsed.len(), 2);
1424        assert_matches!(parsed[0].piece, WordPiece::Text(_));
1425        assert_matches!(parsed[1].piece, WordPiece::TildeExpansion(_));
1426
1427        Ok(())
1428    }
1429
1430    #[test]
1431    fn parse_double_quoted_text() -> Result<()> {
1432        assert_ron_snapshot!(test_parse(r#""a ${b} c""#)?);
1433        Ok(())
1434    }
1435
1436    #[test]
1437    fn parse_gettext_double_quoted_text() -> Result<()> {
1438        assert_ron_snapshot!(test_parse(r#"$"a ${b} c""#)?);
1439        Ok(())
1440    }
1441
1442    #[test]
1443    fn parse_command_substitution() -> Result<()> {
1444        super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?;
1445
1446        assert_ron_snapshot!(test_parse("$(echo hi)")?);
1447
1448        Ok(())
1449    }
1450
1451    #[test]
1452    fn parse_command_substitution_with_embedded_quotes() -> Result<()> {
1453        super::expansion_parser::command_substitution(
1454            r#"$(echo "hi")"#,
1455            &ParserOptions::default(),
1456        )?;
1457
1458        assert_ron_snapshot!(test_parse(r#"$(echo "hi")"#)?);
1459        Ok(())
1460    }
1461
1462    #[test]
1463    fn parse_command_substitution_with_multibyte_chars() -> Result<()> {
1464        assert_ron_snapshot!(test_parse("é$(echo “ü”)ñ")?);
1465        Ok(())
1466    }
1467
1468    #[test]
1469    fn parse_command_substitution_with_embedded_extglob() -> Result<()> {
1470        assert_ron_snapshot!(test_parse("$(echo !(x))")?);
1471        Ok(())
1472    }
1473
1474    #[test]
1475    fn parse_command_sub_with_unbalanced_single_quote() -> Result<()> {
1476        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\nit's here\nEOF\n)\"")?);
1477        Ok(())
1478    }
1479
1480    #[test]
1481    fn parse_command_sub_with_unbalanced_backtick() -> Result<()> {
1482        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\na ` b\nEOF\n)\"")?);
1483        Ok(())
1484    }
1485
1486    #[test]
1487    fn parse_command_sub_with_balanced_double_quotes() -> Result<()> {
1488        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n\"hello\"\nEOF\n)\"")?);
1489        Ok(())
1490    }
1491
1492    #[test]
1493    fn parse_command_sub_with_balanced_single_quotes() -> Result<()> {
1494        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n'hello'\nEOF\n)\"")?);
1495        Ok(())
1496    }
1497
1498    #[test]
1499    fn parse_command_sub_with_balanced_backticks() -> Result<()> {
1500        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n`hello`\nEOF\n)\"")?);
1501        Ok(())
1502    }
1503
1504    #[test]
1505    fn parse_backquoted_command() -> Result<()> {
1506        assert_ron_snapshot!(test_parse("`echo hi`")?);
1507        Ok(())
1508    }
1509
1510    #[test]
1511    fn parse_backquoted_command_in_double_quotes() -> Result<()> {
1512        assert_ron_snapshot!(test_parse(r#""`echo hi`""#)?);
1513        Ok(())
1514    }
1515
1516    #[test]
1517    fn parse_backquoted_command_escapes() -> Result<()> {
1518        for (input, expected) in [
1519            (r"`printf \\2`", r"printf \2"),
1520            (r"`printf \\\\2`", r"printf \\2"),
1521            (r#"`echo \$x \` \" \a`"#, r#"echo $x ` \" \a"#),
1522            // A backslash left ending the command is escaped to keep it literal.
1523            (r"`printf \\`", r"printf \\"),
1524            (r"`printf \\\\`", r"printf \\"),
1525            (r"`printf \\\\\\`", r"printf \\\\"),
1526        ] {
1527            let parsed = super::parse(input, &ParserOptions::default())?;
1528            assert_matches!(
1529                &parsed[..],
1530                [WordPieceWithSource { piece: WordPiece::BackquotedCommandSubstitution(command), .. }]
1531                    if command == expected,
1532                "parsing {input:?}"
1533            );
1534        }
1535        Ok(())
1536    }
1537
1538    #[test]
1539    fn parse_extglob_with_embedded_parameter() -> Result<()> {
1540        assert_ron_snapshot!(test_parse("+([$var])")?);
1541        Ok(())
1542    }
1543
1544    #[test]
1545    fn parse_arithmetic_expansion() -> Result<()> {
1546        assert_ron_snapshot!(test_parse("$((0))")?);
1547        Ok(())
1548    }
1549
1550    #[test]
1551    fn parse_arithmetic_expansion_with_parens() -> Result<()> {
1552        assert_ron_snapshot!(test_parse("$((((1+2)*3)))")?);
1553        Ok(())
1554    }
1555
1556    #[test]
1557    fn parse_arithmetic_expansion_with_unmatched_brackets() -> Result<()> {
1558        // Invalid arithmetic must stay arithmetic, rather than fall back to
1559        // executing the expression as a command substitution.
1560        for expr in ["echo unexpected [", "1[", "a[1", "[ ["] {
1561            let word = std::format!("$(({expr}))");
1562            let parsed = super::parse(&word, &ParserOptions::default())?;
1563            assert_matches!(
1564                parsed.as_slice(),
1565                [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(e), .. }] if e.value == expr,
1566                "for {word:?}"
1567            );
1568        }
1569        Ok(())
1570    }
1571
1572    #[test]
1573    fn parse_arithmetic_expansion_with_deep_brackets() -> Result<()> {
1574        // Brackets in the expression must not grow the word parser's call
1575        // stack. Arithmetic evaluation can reject this expression after
1576        // its boundaries are found.
1577        let expr = std::format!("{}1{}", "[".repeat(15_000), "]".repeat(15_000));
1578        for (open, close) in [("$((", "))"), ("$[", "]")] {
1579            let word = std::format!("{open}{expr}{close}");
1580            let parsed = super::parse(&word, &ParserOptions::default())?;
1581            assert_matches!(
1582                parsed.as_slice(),
1583                [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(e), .. }] if e.value == expr
1584            );
1585        }
1586        Ok(())
1587    }
1588
1589    #[test]
1590    fn parse_legacy_arithmetic_expansion_with_array_subscripts() -> Result<()> {
1591        for (word, expr) in [
1592            ("$[a[1]]", "a[1]"),
1593            ("$[ a[1] + 1 ]", " a[1] + 1 "),
1594            ("$[ a[ a[0] ] ]", " a[ a[0] ] "),
1595            ("$[ a[$[1]] + $(printf 2) ]", " a[$[1]] + $(printf 2) "),
1596            ("$[ 'é]' ]", " 'é]' "),
1597            ("$[ \\] ]", " \\] "),
1598        ] {
1599            let parsed = super::parse(word, &ParserOptions::default())?;
1600            assert_matches!(
1601                parsed.as_slice(),
1602                [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(e), .. }] if e.value == expr,
1603                "for {word:?}"
1604            );
1605        }
1606        Ok(())
1607    }
1608
1609    #[test]
1610    fn parse_malformed_expansions_as_errors() {
1611        // As in bash, a `$` before `(` or `[` always starts an expansion; if
1612        // none parses, the text is malformed, not literal text.
1613        let options = ParserOptions::default();
1614        for word in ["a$(echo", "\"$(echo\"", "$((1 +", "$[1 +", "\"a $[1 + b\""] {
1615            assert!(super::parse(word, &options).is_err(), "for {word:?}");
1616        }
1617        for body in [
1618            "a $(echo hi\n",
1619            "a $(echo 'b) c\n",
1620            "a $((1 + b\n",
1621            "a $[1 + b\n",
1622        ] {
1623            assert!(
1624                super::parse_heredoc(body, &options).is_err(),
1625                "for {body:?}"
1626            );
1627        }
1628    }
1629
1630    #[test]
1631    fn parse_malformed_expansions_nested_in_parameter_expansions_quickly() {
1632        // Each `${...}` can match any of ~20 parameter expression forms; unless
1633        // its parse is memoized, a failure within (e.g., an
1634        // unterminated `$(`) is re-discovered by each form, taking time
1635        // exponential in the nesting depth.
1636        let options = ParserOptions::default();
1637        let nested =
1638            |inner: &str| std::format!("{}{inner}{}\n", "${x:-".repeat(30), "}".repeat(30));
1639        assert!(super::parse_heredoc(&nested("$(echo"), &options).is_err());
1640        assert!(super::parse_heredoc(&nested("$((1 +"), &options).is_err());
1641        assert!(super::parse(&std::format!("\"{}\"", nested("$[1").trim_end()), &options).is_err());
1642    }
1643
1644    #[test]
1645    fn parse_lone_dollar_signs_as_text() -> Result<()> {
1646        let options = ParserOptions::default();
1647        for word in [
1648            "$", "a$", "$ b", "$%", "\"$\"", "\"a$ b\"", "\"$'x'\"", "${x}$",
1649        ] {
1650            super::parse(word, &options)?;
1651        }
1652        super::parse_heredoc("cost: $5 $ $% \"$'x'\" a$\n", &options)?;
1653        Ok(())
1654    }
1655
1656    #[test]
1657    fn parse_heredoc_with_command_substitutions_nested_too_deeply() -> Result<()> {
1658        // Here-doc bodies aren't tokenized along with the script, so the word
1659        // parser is the first to find nesting past the tokenizer's
1660        // limit; that's an error, not literal text.
1661        let nested = |depth: u32| {
1662            let depth = depth as usize;
1663            std::format!("{}:{}\n", "$(".repeat(depth), ")".repeat(depth))
1664        };
1665        let limit = crate::tokenizer::MAX_EXPANSION_NESTING;
1666        let options = ParserOptions::default();
1667        super::parse_heredoc(&nested(limit), &options)?;
1668        assert!(super::parse_heredoc(&nested(limit + 1), &options).is_err());
1669        Ok(())
1670    }
1671
1672    #[test]
1673    fn test_arithmetic_word_parsing() {
1674        let options = ParserOptions::default();
1675
1676        assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok());
1677        assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok());
1678        assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok());
1679        assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok());
1680        assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok());
1681        assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok());
1682        assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok());
1683        assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok());
1684        assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok());
1685    }
1686
1687    #[test]
1688    fn test_arithmetic_word_piece_parsing() {
1689        let options = ParserOptions::default();
1690
1691        assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok());
1692        assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok());
1693        assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok());
1694        assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok());
1695        assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok());
1696        assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok());
1697        assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok());
1698        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok());
1699        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok());
1700        assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err());
1701        assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err());
1702        assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err());
1703    }
1704
1705    #[test]
1706    fn test_brace_expansion_parsing() -> Result<()> {
1707        let options = ParserOptions::default();
1708
1709        let inputs = ["x{a,b}y", "{a,b{1,2}}"];
1710
1711        for input in inputs {
1712            assert_ron_snapshot!(super::parse_brace_expansions(input, &options)?.ok_or_else(
1713                || anyhow::anyhow!("Expected brace expansion to be parsed successfully")
1714            )?);
1715        }
1716
1717        Ok(())
1718    }
1719
1720    #[test]
1721    fn test_scalar_assignment_parsing() -> Result<()> {
1722        let options = ParserOptions::default();
1723
1724        super::parse_scalar_assignment("x=3", &options)?;
1725        super::parse_scalar_assignment("x=", &options)?;
1726        super::parse_scalar_assignment("x[3]=a", &options)?;
1727        super::parse_scalar_assignment("x[${y[3]}]=a", &options)?;
1728        super::parse_scalar_assignment("x[y[3]]=a", &options)?;
1729        Ok(())
1730    }
1731
1732    #[test]
1733    fn parse_compound_assignment_value() {
1734        let options = ParserOptions::default();
1735        let parse = |value| super::parse_compound_assignment_value(value, &options);
1736
1737        assert_matches!(parse("()").as_deref(), Some([]));
1738
1739        assert_matches!(parse("(1 2)").as_deref(), Some([(None, first), (None, second)])
1740            if first.value == "1" && second.value == "2");
1741
1742        assert_matches!(
1743            parse(r#"([key]=value "other one")"#).as_deref(),
1744            Some([(Some(key), value), (None, other)])
1745                if key.value == "key"
1746                    && value.value == "value"
1747                    && other.value == r#""other one""#);
1748
1749        // Text that is not exclusively a compound value must not be accepted;
1750        // otherwise trailing shell syntax hidden in an expanded value
1751        // would be silently dropped.
1752        assert!(parse(r#"(x); printf "INJECTED\n"; #"#).is_none());
1753        assert!(parse("(unterminated").is_none());
1754        assert!(parse("plain text").is_none());
1755        assert!(parse("").is_none());
1756    }
1757
1758    #[test]
1759    fn parse_sequence_bound() {
1760        assert_eq!(super::parse_sequence_bound("3"), Some(3));
1761        assert_eq!(super::parse_sequence_bound("007"), Some(7));
1762        assert_eq!(super::parse_sequence_bound("+7"), Some(7));
1763        assert_eq!(super::parse_sequence_bound("-007"), Some(-7));
1764        assert_eq!(super::parse_sequence_bound("0"), Some(0));
1765        assert_eq!(super::parse_sequence_bound("-0"), Some(0));
1766        assert_eq!(super::parse_sequence_bound("99999999999999999999"), None);
1767
1768        // The digits of `i64::MIN` are one past `i64::MAX`, so the sign has to
1769        // be parsed with them.
1770        assert_eq!(
1771            super::parse_sequence_bound("-9223372036854775808"),
1772            Some(i64::MIN)
1773        );
1774        assert_eq!(
1775            super::parse_sequence_bound("9223372036854775807"),
1776            Some(i64::MAX)
1777        );
1778        assert_eq!(super::parse_sequence_bound("-9223372036854775809"), None);
1779    }
1780
1781    #[test]
1782    fn test_brace_sequence_increment_out_of_range() -> Result<()> {
1783        let options = ParserOptions::default();
1784        let in_range = |input| {
1785            super::expansion_parser::brace_expr(input, &options)
1786                .map(|_| ())
1787                .map_err(anyhow::Error::from)
1788        };
1789
1790        in_range("{1..3..2}")?;
1791        in_range("{1..3..-9223372036854775808}")?;
1792        in_range("{a..e..9223372036854775807}")?;
1793
1794        // An increment that doesn't fit leaves no brace expression, so the
1795        // word stays literal text, whichever sign or form it has.
1796        for input in [
1797            "{1..3..99999999999999999999}",
1798            "{1..3..-99999999999999999999}",
1799            "{1..3..+99999999999999999999}",
1800            "{1..3..9223372036854775808}",
1801            "{1..3..-9223372036854775809}",
1802            "{a..e..99999999999999999999}",
1803            "{a..e..-99999999999999999999}",
1804        ] {
1805            assert!(
1806                in_range(input).is_err(),
1807                "{input} parsed as a brace expression"
1808            );
1809            assert_eq!(
1810                super::parse_brace_expansions(input, &options)?
1811                    .ok_or_else(|| anyhow::anyhow!("Expected {input} to parse as text"))?
1812                    .len(),
1813                1,
1814                "{input} should be a single text piece"
1815            );
1816        }
1817
1818        Ok(())
1819    }
1820
1821    #[test]
1822    fn zero_padded_width() {
1823        let width = super::zero_padded_width;
1824
1825        // Neither bound was written with a leading zero.
1826        assert_eq!(width("1", "5"), None);
1827        assert_eq!(width("0", "10"), None);
1828        assert_eq!(width("-0", "3"), None);
1829
1830        // The width is the longer bound as written, whichever one asked for
1831        // padding.
1832        assert_eq!(width("01", "5"), Some(2));
1833        assert_eq!(width("1", "005"), Some(3));
1834        assert_eq!(width("0009", "11"), Some(4));
1835
1836        // A sign takes one of the columns.
1837        assert_eq!(width("-01", "01"), Some(3));
1838        assert_eq!(width("00", "-3"), Some(2));
1839
1840        // A zero after a plus does not ask for padding, though the text still
1841        // counts toward the width once the other bound has asked.
1842        assert_eq!(width("+01", "3"), None);
1843        assert_eq!(width("+01", "05"), Some(3));
1844    }
1845}