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.
536pub fn parse(
537    word: &str,
538    options: &ParserOptions,
539) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
540    cacheable_parse(word, options)
541}
542
543#[cached::macros::cached(
544    max_size = 64,
545    key = "(String, ParserOptions)",
546    convert = r#"{ (word.to_owned(), options.to_owned()) }"#
547)]
548fn cacheable_parse(
549    word: &str,
550    options: &ParserOptions,
551) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
552    tracing::debug!(target: "expansion", "Parsing word '{}'", word);
553
554    let pieces = expansion_parser::unexpanded_word(word, options)
555        .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))?;
556
557    tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces);
558
559    Ok(pieces)
560}
561
562/// Parse a heredoc body, treating `"` and `'` as literal characters.
563///
564/// # Arguments
565///
566/// * `word` - The heredoc body to parse.
567/// * `options` - The parser options to use.
568pub fn parse_heredoc(
569    word: &str,
570    options: &ParserOptions,
571) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
572    expansion_parser::unexpanded_heredoc_word(word, options)
573        .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))
574}
575
576/// Parse the given word into a parameter expression.
577///
578/// # Arguments
579///
580/// * `word` - The word to parse.
581/// * `options` - The parser options to use.
582pub fn parse_parameter(
583    word: &str,
584    options: &ParserOptions,
585) -> Result<Parameter, error::WordParseError> {
586    expansion_parser::parameter(word, options)
587        .map_err(|err| error::WordParseError::Parameter(word.to_owned(), err.into()))
588}
589
590/// Parse brace expansion from a given word .
591///
592/// # Arguments
593///
594/// * `word` - The word to parse.
595/// * `options` - The parser options to use.
596pub fn parse_brace_expansions(
597    word: &str,
598    options: &ParserOptions,
599) -> Result<Option<Vec<BraceExpressionOrText>>, error::WordParseError> {
600    expansion_parser::brace_expansions(word, options)
601        .map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err.into()))
602}
603
604/// Parses a scalar assignment from a given word.
605///
606/// A parenthesized value is treated as scalar text; see
607/// [`parse_compound_assignment_value`] for reinterpreting such a value as a
608/// compound one.
609///
610/// # Arguments
611///
612/// * `word` - The word to parse.
613/// * `options` - The parser options to use.
614pub fn parse_scalar_assignment(
615    word: &str,
616    options: &ParserOptions,
617) -> Result<ast::Assignment, error::WordParseError> {
618    expansion_parser::name_equals_scalar_value(word, options)
619        .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))
620}
621
622/// Parses text as a compound assignment value, returning its optionally-keyed
623/// element words. Returns `None` if the text is not a well-formed compound
624/// value.
625///
626/// This exists so that text which only becomes recognizable as a compound value
627/// *after* expansion can be reinterpreted without reconstructing and re-parsing
628/// a whole assignment word.
629///
630/// # Arguments
631///
632/// * `value` - The candidate compound value text, including its enclosing
633///   parentheses.
634/// * `options` - The parser options to use.
635#[must_use]
636pub fn parse_compound_assignment_value(
637    value: &str,
638    options: &ParserOptions,
639) -> Option<Vec<(Option<ast::Word>, ast::Word)>> {
640    let elements = crate::parser::parse_compound_assignment_value(value, options)?;
641    parse_array_elements(elements.iter(), options).ok()
642}
643
644/// Parses an array assignment from a given word and its array elements.
645///
646/// # Arguments
647///
648/// * `word` - The assignment name and equals sign to parse.
649/// * `elements` - The array element words to parse.
650/// * `options` - The parser options to use.
651pub(crate) fn parse_array_assignment(
652    word: &str,
653    elements: &[&String],
654    options: &ParserOptions,
655) -> Result<ast::Assignment, &'static str> {
656    let (assignment_name, append) =
657        expansion_parser::name_equals(word, options).map_err(|_| "not array assignment word")?;
658
659    Ok(ast::Assignment {
660        name: assignment_name,
661        value: ast::AssignmentValue::Array(parse_array_elements(
662            elements.iter().copied(),
663            options,
664        )?),
665        append,
666        loc: SourceSpan::default(),
667    })
668}
669
670/// Parses one bound of a numeric brace sequence, e.g. `-007`.
671///
672/// Returns `None` when the bound does not fit in an `i64`, which lets the
673/// sequence rule decline rather than panic on something like
674/// `{99999999999999999999..1}`.
675///
676/// The sign goes to the parser along with the digits, so `i64::MIN` is accepted
677/// even though its digits alone are one past `i64::MAX`.
678///
679/// # Arguments
680///
681/// * `text` - The bound as it was written, sign included.
682fn parse_sequence_bound(text: &str) -> Option<i64> {
683    text.parse().ok()
684}
685
686/// Returns the width the members of a numeric brace sequence should be zero
687/// padded out to, or `None` if they should be written as plain numbers.
688///
689/// A leading zero in either bound asks for padding, and the width is then the
690/// longer of the two bounds as they were written, sign included. A lone `0` is
691/// not a leading zero, and neither is a zero that follows a `+`.
692///
693/// # Arguments
694///
695/// * `start` - The start bound as it was written.
696/// * `end` - The end bound as it was written.
697fn zero_padded_width(start: &str, end: &str) -> Option<usize> {
698    fn has_leading_zero(bound: &str) -> bool {
699        let digits = bound.strip_prefix('-').unwrap_or(bound);
700        digits.len() > 1 && digits.starts_with('0')
701    }
702
703    (has_leading_zero(start) || has_leading_zero(end)).then(|| start.len().max(end.len()))
704}
705
706/// Parses literal array element text into optionally-keyed element words.
707///
708/// # Arguments
709///
710/// * `elements` - The array element texts to parse.
711/// * `options` - The parser options to use.
712fn parse_array_elements<'a>(
713    elements: impl IntoIterator<Item = &'a String>,
714    options: &ParserOptions,
715) -> Result<Vec<(Option<ast::Word>, ast::Word)>, &'static str> {
716    elements
717        .into_iter()
718        .map(|element| {
719            let (key, value) = expansion_parser::literal_array_element(element, options)
720                .map_err(|_| "invalid array element in literal")?;
721            Ok((
722                key.map(|key| ast::Word::new(key.as_str())),
723                ast::Word::new(value.as_str()),
724            ))
725        })
726        .collect()
727}
728
729peg::parser! {
730    grammar expansion_parser(parser_options: &ParserOptions) for str {
731        // Helper rule that enables pegviz to be used to visualize debug peg traces.
732        rule traced<T>(e: rule<T>) -> T =
733            &(input:$([_]*) {
734                #[cfg(feature = "debug-tracing")]
735                println!("[PEG_INPUT_START]\n{input}\n[PEG_TRACE_START]");
736            })
737            e:e()? {?
738                #[cfg(feature = "debug-tracing")]
739                println!("[PEG_TRACE_STOP]");
740                e.ok_or("")
741            }
742
743        pub(crate) rule unexpanded_word() -> Vec<WordPieceWithSource> = traced(<word(<![_]>)>)
744
745        rule word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
746            tilde:tilde_expr_prefix_with_source()? pieces:word_piece_with_source(<stop_condition()>)* {
747                let mut all_pieces = Vec::new();
748                if let Some(tilde) = tilde {
749                    all_pieces.push(tilde);
750                }
751                all_pieces.extend(pieces);
752                all_pieces
753            }
754
755        // Takes a word as input.
756        pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
757            pieces:(brace_expansion_piece(<![_]>)+) { Some(pieces) } /
758            [_]* { None }
759
760        // Returns either a complete brace expression (without any prefix or suffix), or a
761        // non-brace-expression string.
762        rule brace_expansion_piece<T>(stop_condition: rule<T>) -> BraceExpressionOrText =
763            expr:brace_expr() {
764                BraceExpressionOrText::Expr(expr)
765            } /
766            text:$(non_brace_expr_text(<stop_condition()>)+) { BraceExpressionOrText::Text(text.to_owned()) }
767
768        // Parses text that is not considered to contain a brace expression.
769        rule non_brace_expr_text<T>(stop_condition: rule<T>) -> () =
770            !"{" word_piece(<['{'] {} / stop_condition() {}>) {} /
771            !brace_expr() !stop_condition() "{" {}
772
773        // Parses a complete brace expression, with no prefix or suffix.
774        pub(crate) rule brace_expr() -> BraceExpression =
775            "{" inner:brace_expr_inner() "}" { inner }
776
777        // Parses the text inside a complete brace expression; basically the complete brace
778        // expression without the opening and closing brace characters.
779        pub(crate) rule brace_expr_inner() -> BraceExpression =
780            brace_text_list_expr() /
781            seq:brace_sequence_expr() { vec![seq] }
782
783        // Parses a list of brace expression members, including the separating commas; does
784        // not include the opening and closing braces.
785        pub(crate) rule brace_text_list_expr() -> BraceExpression =
786            brace_text_list_member() **<2,> ","
787
788        // Parses an element that can occur in a brace expression member list, not including the
789        // terminating comma or closing brace.
790        pub(crate) rule brace_text_list_member() -> BraceExpressionMember =
791            // Matches an empty-string member, without consuming the comma or closing brace that terminates it.
792            &[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } /
793            // Matches a nested string that may include some combination of concatenated textual strings
794            // and brace expressions.
795            child_pieces:(brace_expansion_piece(<[',' | '}']>)+) {
796                BraceExpressionMember::Child(child_pieces)
797            }
798
799        pub(crate) rule brace_sequence_expr() -> BraceExpressionMember =
800            start:sequence_bound() ".." end:sequence_bound() increment:(".." n:number() { n })? {
801                BraceExpressionMember::NumberSequence {
802                    start: start.0,
803                    end: end.0,
804                    increment: increment.unwrap_or(1),
805                    zero_padded_width: zero_padded_width(start.1, end.1),
806                }
807            } /
808            start:character() ".." end:character() increment:(".." n:number() { n })? {
809                BraceExpressionMember::CharSequence { start, end, increment: increment.unwrap_or(1) }
810            }
811
812        // A bound of a numeric sequence, kept alongside the text it was written as.
813        // Whether the members come out zero padded is decided by that text and not
814        // by the value, so `{01..3}` and `{1..3}` have to stay distinguishable.
815        rule sequence_bound() -> (i64, &'input str) = text:$(number_sign()? ['0'..='9']+) {?
816            parse_sequence_bound(text).map(|n| (n, text)).ok_or("number out of range")
817        }
818
819        rule number() -> i64 = sign:number_sign()? n:$(['0'..='9']+) {
820            let sign = sign.unwrap_or(1);
821            let num: i64 = n.parse().unwrap();
822            num * sign
823        }
824
825        rule number_sign() -> i64 =
826            ['-'] { -1 } /
827            ['+'] { 1 }
828
829        rule character() -> char = ['a'..='z' | 'A'..='Z']
830
831        pub(crate) rule is_arithmetic_word() =
832            arithmetic_word(<![_]>)
833
834        // N.B. We don't bother returning the word pieces, as all users of this rule
835        // only try to extract the consumed input string and not the parse result.
836        rule arithmetic_word<T>(stop_condition: rule<T>) =
837            arithmetic_word_piece(<stop_condition()>)* {}
838
839        pub(crate) rule is_arithmetic_word_piece() =
840            arithmetic_word_piece(<![_]>)
841
842        // This rule matches an individual "piece" of an arithmetic expression. It needs to handle
843        // matching nested parenthesized expressions as well. We stop consuming the input when
844        // we reach the provided stop condition, which typically denotes the end of the containing
845        // arithmetic expression.
846        rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
847            // This branch matches a parenthesized piece; we consume the opening parenthesis and
848            // delegate the rest to a helper rule. We don't worry about the stop condition passed
849            // into us, because if we see an opening parenthesis then we *must* find its closing
850            // partner.
851            "(" arithmetic_word_plus_right_paren() {} /
852            // An array subscript may contain the stop condition. Other brackets are left
853            // as text, so malformed arithmetic doesn't fall back to command substitution.
854            array_element_name() {} /
855            // This branch matches any standard piece of a word, stopping as soon as we reach
856            // either the overall stop condition *OR* an opening parenthesis. We add this latter
857            // condition to ensure that *we* handle matching parentheses.
858            !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>) {}
859
860        // This is a helper rule that matches either the provided stop condition or an opening parenthesis.
861        rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
862            stop_condition() {} /
863            "(" {}
864
865        // This rule matches an arithmetic word followed by a right parenthesis. It must consume the right parenthesis.
866        rule arithmetic_word_plus_right_paren() =
867            arithmetic_word(<[')']>) ")"
868
869        rule word_piece_with_source<T>(stop_condition: rule<T>) -> WordPieceWithSource =
870            start_index:position!() piece:word_piece(<stop_condition()>) end_index:position!() {
871                WordPieceWithSource { piece, start_index, end_index }
872            }
873
874        rule word_piece<T>(stop_condition: rule<T>) -> WordPiece =
875            // Rules that match quoted text.
876            s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
877            s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
878            s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
879            s:gettext_double_quoted_sequence() { WordPiece::GettextDoubleQuotedSequence(s) } /
880            // Rules that match pieces starting with a dollar sign ('$').
881            dollar_sign_word_piece() /
882            // Rules that match unquoted text that doesn't start with an unescaped dollar sign.
883            normal_escape_sequence() /
884            // Allow tilde expression to be matched as a word piece (for tilde-after-colon expansion)
885            enabled_tilde_expr_after_colon() /
886            // Finally, match unquoted literal text.
887            unquoted_literal_text(<stop_condition()>)
888
889        rule dollar_sign_word_piece() -> WordPiece =
890            arithmetic_expansion() /
891            legacy_arithmetic_expansion() /
892            command_substitution() /
893            parameter_expansion()
894
895        rule double_quoted_word_piece() -> WordPiece =
896            arithmetic_expansion() /
897            legacy_arithmetic_expansion() /
898            command_substitution() /
899            parameter_expansion() /
900            double_quoted_escape_sequence() /
901            double_quoted_text()
902
903        rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
904            "\"" i:double_quoted_sequence_inner()* "\"" { i }
905
906        rule gettext_double_quoted_sequence() -> Vec<WordPieceWithSource> =
907            "$\"" i:double_quoted_sequence_inner()* "\"" { i }
908
909        rule double_quoted_sequence_inner() -> WordPieceWithSource =
910            start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
911                WordPieceWithSource {
912                    piece,
913                    start_index,
914                    end_index
915                }
916            }
917
918        rule single_quoted_literal_text() -> &'input str =
919            "\'" inner:$([^'\'']*) "\'" { inner }
920
921        rule ansi_c_quoted_text() -> &'input str =
922            r"$'" inner:$((r"\\" / r"\'" / [^'\''])*) r"'" { inner }
923
924        rule unquoted_literal_text<T>(stop_condition: rule<T>) -> WordPiece =
925            s:$(unquoted_literal_text_piece(<stop_condition()>)+) { WordPiece::Text(s.to_owned()) }
926
927        rule unquoted_literal_text_piece<T>(stop_condition: rule<T>) =
928            !stop_condition() !normal_escape_sequence() !enabled_tilde_expr_after_colon() [^'\'' | '\"' | '$' | '`'] {}
929
930        rule enabled_tilde_expr_after_colon() -> WordPiece =
931            tilde_exprs_after_colon_enabled() last_char_is_colon() piece:tilde_expression_piece() { piece }
932
933        rule last_char_is_colon() = #{|input, pos| {
934            if pos == 0 {
935                // No preceding character - can't be preceded by ':'
936                peg::RuleResult::Failed
937            } else {
938                // Check the byte directly (`:` is ASCII, single byte)
939                if input.as_bytes()[pos - 1] == b':' {
940                    peg::RuleResult::Matched(pos, ())
941                } else {
942                    peg::RuleResult::Failed
943                }
944            }
945        }}
946
947        rule double_quoted_text() -> WordPiece =
948            s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
949
950        rule double_quote_body_text() -> &'input str =
951            $((!double_quoted_escape_sequence() !dollar_sign_word_piece() !expansion_opener() [^'\"'])+)
952
953        // Heredoc body parsing: like double-quoted content, but " and ' are literal characters.
954        pub(crate) rule unexpanded_heredoc_word() -> Vec<WordPieceWithSource> =
955            traced(<heredoc_word(<![_]>)>)
956
957        rule heredoc_word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
958            pieces:heredoc_word_piece_with_source(<stop_condition()>)* { pieces }
959
960        rule heredoc_word_piece_with_source<T>(stop_condition: rule<T>) -> WordPieceWithSource =
961            !stop_condition() start_index:position!() piece:heredoc_word_piece() end_index:position!() {
962                WordPieceWithSource { piece, start_index, end_index }
963            }
964
965        rule heredoc_word_piece() -> WordPiece =
966            arithmetic_expansion() /
967            legacy_arithmetic_expansion() /
968            command_substitution() /
969            parameter_expansion() /
970            heredoc_escape_sequence() /
971            heredoc_literal_text()
972
973        rule heredoc_escape_sequence() -> WordPiece =
974            s:$("\\" ['$' | '`' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
975
976        rule heredoc_literal_text() -> WordPiece =
977            s:$((!heredoc_escape_sequence() !dollar_sign_word_piece() !expansion_opener() [^'`'])+) {
978                WordPiece::Text(s.to_owned())
979            }
980
981        rule normal_escape_sequence() -> WordPiece =
982            s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
983
984        rule double_quoted_escape_sequence() -> WordPiece =
985            s:$("\\" ['$' | '`' | '\"' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
986
987        rule tilde_expr_prefix_with_source() -> WordPieceWithSource =
988            start_index:position!() piece:tilde_expr_prefix() end_index:position!() {
989                WordPieceWithSource {
990                    piece,
991                    start_index,
992                    end_index
993                }
994            }
995
996        rule tilde_expr_prefix() -> WordPiece =
997            tilde_exprs_at_word_start_enabled() piece:tilde_expression_piece() { piece }
998
999        rule tilde_expr_after_colon() -> WordPiece =
1000            tilde_exprs_after_colon_enabled() piece:tilde_expression_piece() { piece }
1001
1002        rule tilde_expression_piece() -> WordPiece =
1003            "~" expr:tilde_expression() { WordPiece::TildeExpansion(expr) }
1004
1005        rule tilde_expression() -> TildeExpr =
1006            &tilde_terminator() { TildeExpr::Home } /
1007            "+" &tilde_terminator() { TildeExpr::WorkingDir } /
1008            // N.B. A run of digits need not fit in a `usize`; decline the rule
1009            // when it does not, so the word falls through to being treated as a
1010            // (non-existent) user name instead of panicking the parser.
1011            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() })) } /
1012            "-" &tilde_terminator() { TildeExpr::OldWorkingDir } /
1013            "-" n:$(['0'..='9']*) &tilde_terminator() {? n.parse().map_or_else(|_| Err("dir stack index out of range"), |n| Ok(TildeExpr::NthDirFromBottomOfDirStack { n })) } /
1014            user:$(portable_filename_char()*) &tilde_terminator() { TildeExpr::UserHome(user.to_owned()) }
1015
1016        rule tilde_terminator() = ['/' | ':' | ';' | '}'] / ![_]
1017
1018        rule portable_filename_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '-']
1019
1020        // TODO(parser): Deal with fact that there may be a quoted word or escaped closing brace chars.
1021        // TODO(parser): Improve on how we handle a '$' not followed by a valid variable name or parameter.
1022        // Cached: each `${...}` can match any of ~20 parameter expression forms, and a failure
1023        // within one (e.g., an unterminated `$(`) would otherwise be re-discovered by every form
1024        // at every enclosing level, taking time exponential in the nesting depth.
1025        #[cache]
1026        rule parameter_expansion() -> WordPiece =
1027            "${" e:parameter_expression() "}" {
1028                WordPiece::ParameterExpansion(e)
1029            } /
1030            "$" parameter:unbraced_parameter() {
1031                WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false })
1032            } /
1033            !expansion_opener() "$" !['\''] {
1034                WordPiece::Text("$".to_owned())
1035            }
1036
1037        // As in bash, these always start an expansion, whose end the tokenizer finds (so it
1038        // always does, in a word the tokenizer accepted). If none parses, the text is malformed
1039        // (e.g., unterminated, or nested too deeply), so its `$` mustn't be taken as literal text.
1040        // TODO(expansion): #540: the same holds for `${`, which is still taken as text.
1041        rule expansion_opener() = "$" ['(' | '[']
1042
1043        rule parameter_expression() -> ParameterExpr =
1044            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? {
1045                ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value }
1046            } /
1047            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? {
1048                ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value }
1049            } /
1050            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? {
1051                ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message }
1052            } /
1053            indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? {
1054                ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value }
1055            } /
1056            "#" parameter:parameter() {
1057                ParameterExpr::ParameterLength { parameter, indirect: false }
1058            } /
1059            indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? {
1060                ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern }
1061            } /
1062            indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? {
1063                ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern }
1064            } /
1065            indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? {
1066                ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern }
1067            } /
1068            indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? {
1069                ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern }
1070            } /
1071            // N.B. The following case is for non-sh extensions.
1072            non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } /
1073            indirect:parameter_indirection() parameter:parameter() {
1074                ParameterExpr::Parameter { parameter, indirect }
1075            }
1076
1077        rule parameter_test_type() -> ParameterTestType =
1078            colon:":"? {
1079                if colon.is_some() {
1080                    ParameterTestType::UnsetOrNull
1081                } else {
1082                    ParameterTestType::Unset
1083                }
1084            }
1085
1086        rule non_posix_parameter_expression() -> ParameterExpr =
1087            "!" variable_name:variable_name() "[*]" {
1088                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true }
1089            } /
1090            "!" variable_name:variable_name() "[@]" {
1091                ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false }
1092            } /
1093            indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? {
1094                ParameterExpr::Substring { parameter, indirect, offset, length }
1095            } /
1096            indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() {
1097                ParameterExpr::Transform { parameter, indirect, op }
1098            } /
1099            "!" prefix:variable_name() "*" {
1100                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true }
1101            } /
1102            "!" prefix:variable_name() "@" {
1103                ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false }
1104            } /
1105            indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1106                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix }
1107            } /
1108            indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1109                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix }
1110            } /
1111            indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1112                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere }
1113            } /
1114            indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1115                ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence }
1116            } /
1117            indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? {
1118                ParameterExpr::UppercasePattern { parameter, indirect, pattern }
1119            } /
1120            indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? {
1121                ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern }
1122            } /
1123            indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? {
1124                ParameterExpr::LowercasePattern { parameter, indirect, pattern }
1125            } /
1126            indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? {
1127                ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern }
1128            }
1129
1130        rule parameter_indirection() -> bool =
1131            non_posix_extensions_enabled() "!" { true } /
1132            { false }
1133
1134        rule non_posix_parameter_transformation_op() -> ParameterTransformOp =
1135            "U" { ParameterTransformOp::ToUpperCase } /
1136            "u" { ParameterTransformOp::CapitalizeInitial } /
1137            "L" { ParameterTransformOp::ToLowerCase } /
1138            "Q" { ParameterTransformOp::Quoted } /
1139            "E" { ParameterTransformOp::ExpandEscapeSequences } /
1140            "P" { ParameterTransformOp::PromptExpand } /
1141            "A" { ParameterTransformOp::ToAssignmentLogic } /
1142            "K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } /
1143            "a" { ParameterTransformOp::ToAttributeFlags } /
1144            "k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } }
1145
1146
1147        rule unbraced_parameter() -> Parameter =
1148            p:unbraced_positional_parameter() { Parameter::Positional(p) } /
1149            p:special_parameter() { Parameter::Special(p) } /
1150            p:variable_name() { Parameter::Named(p.to_owned()) }
1151
1152        // N.B. The indexing syntax is not a standard sh-ism.
1153        pub(crate) rule parameter() -> Parameter =
1154            p:positional_parameter() { Parameter::Positional(p) } /
1155            p:special_parameter() { Parameter::Special(p) } /
1156            non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } /
1157            non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } /
1158            non_posix_extensions_enabled() p:variable_name() "[" index:array_index() "]" {?
1159                Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() })
1160            } /
1161            p:variable_name() { Parameter::Named(p.to_owned()) }
1162
1163        rule positional_parameter() -> u32 =
1164            n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) }
1165        rule unbraced_positional_parameter() -> u32 =
1166            n:$(['1'..='9']) {? n.parse().or(Err("u32")) }
1167
1168        rule special_parameter() -> SpecialParameter =
1169            "@" { SpecialParameter::AllPositionalParameters { concatenate: false } } /
1170            "*" { SpecialParameter::AllPositionalParameters { concatenate: true } } /
1171            "#" { SpecialParameter::PositionalParameterCount } /
1172            "?" { SpecialParameter::LastExitStatus } /
1173            "-" { SpecialParameter::CurrentOptionFlags } /
1174            "$" { SpecialParameter::ProcessId } /
1175            "!" { SpecialParameter::LastBackgroundProcessId } /
1176            "0" { SpecialParameter::ShellName }
1177
1178        rule variable_name() -> &'input str =
1179            $(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+)
1180
1181        pub(crate) rule command_substitution() -> WordPiece =
1182            "$(" c:command_substitution_body() ")" { WordPiece::CommandSubstitution(c.to_owned()) } /
1183            "`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) }
1184
1185        // The tokenizer already has the logic to find where the command ends (e.g., skipping
1186        // over here-doc bodies), so we leverage it here. `pos` and `body.len()` are both
1187        // byte offsets.
1188        rule command_substitution_body() -> &'input str = #{|input, pos| {
1189            let rest = input.split_at(pos).1;
1190            match crate::tokenizer::command_substitution_body(rest, &parser_options.tokenizer_options()) {
1191                Ok(body) => peg::RuleResult::Matched(pos + body.len(), body),
1192                Err(_) => peg::RuleResult::Failed,
1193            }
1194        }}
1195
1196        rule backquoted_command() -> String =
1197            chars:(backquoted_char()*) {
1198                let mut command: String = chars.into_iter().collect();
1199                // TODO(tokenizer): A backslash ending the input is reported as an unterminated
1200                // escape, where bash takes it literally; escape it so it stays literal.
1201                if command.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 {
1202                    command.push('\\');
1203                }
1204                command
1205            }
1206
1207        // As in bash, a backslash is removed only before `$`, `` ` ``, or `\`. (The tokenizer
1208        // already removed line continuations.)
1209        rule backquoted_char() -> &'input str =
1210            "\\" s:$(['$' | '`' | '\\']) { s } /
1211            s:$([^'`']) { s }
1212
1213        rule arithmetic_expansion() -> WordPiece =
1214            "$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1215
1216        rule legacy_arithmetic_expansion() -> WordPiece =
1217            "$[" e:legacy_arithmetic_expansion_body() "]" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1218
1219        // Reuse the tokenizer's iterative bracket counting rather than recursively parsing
1220        // each bracket in the expression. As with command substitutions, preserve the source.
1221        rule legacy_arithmetic_expansion_body() -> &'input str = #{|input, pos| {
1222            let rest = input.split_at(pos).1;
1223            match crate::tokenizer::legacy_arithmetic_expansion_body(rest, &parser_options.tokenizer_options()) {
1224                Ok(body) => peg::RuleResult::Matched(pos + body.len(), body),
1225                Err(_) => peg::RuleResult::Failed,
1226            }
1227        }}
1228
1229        rule substring_offset() -> ast::UnexpandedArithmeticExpr =
1230            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1231
1232        rule substring_length() -> ast::UnexpandedArithmeticExpr =
1233            s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1234
1235        rule parameter_replacement_str() -> String =
1236            "/" s:$(word(<['}']>)) { s.to_owned() }
1237
1238        rule parameter_search_pattern() -> String =
1239            s:$(word(<['}' | '/']>)) { s.to_owned() }
1240
1241        rule parameter_expression_word() -> String =
1242            s:$(word(<['}']>)) { s.to_owned() }
1243
1244        rule extglob_enabled() -> () =
1245            &[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } }
1246
1247        rule non_posix_extensions_enabled() -> () =
1248            &[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
1249
1250        rule tilde_exprs_at_word_start_enabled() -> () =
1251            &[_] {? if parser_options.tilde_expansion_at_word_start { Ok(()) } else { Err("no tilde expansion at word start") } }
1252
1253        rule tilde_exprs_after_colon_enabled() -> () =
1254            &[_] {? if parser_options.tilde_expansion_after_colon { Ok(()) } else { Err("no tilde expansion after colon") } }
1255
1256        // Assignment rules.
1257
1258        pub(crate) rule name_equals_scalar_value() -> ast::Assignment =
1259            nae:name_equals() value:assigned_scalar_value() {
1260                let (name, append) = nae;
1261                ast::Assignment { name, value, append, loc: SourceSpan::default() }
1262            }
1263
1264        pub(crate) rule name_equals() -> (ast::AssignmentName, bool) =
1265            name:assignment_name() append:("+"?) "=" {
1266                (name, append.is_some())
1267            }
1268
1269        pub(crate) rule literal_array_element() -> (Option<String>, String) =
1270            "[" inner:$((!"]" [_])*) "]=" value:$([_]*) {
1271                (Some(inner.to_owned()), value.to_owned())
1272            } /
1273            value:$([_]+) {
1274                (None, value.to_owned())
1275            }
1276
1277        rule assignment_name() -> ast::AssignmentName =
1278            aen:array_element_name() {
1279                let (name, index) = aen;
1280                ast::AssignmentName::ArrayElementName(name.to_owned(), index.to_owned())
1281            } /
1282            name:assigned_scalar_name() {
1283                ast::AssignmentName::VariableName(name.to_owned())
1284            }
1285
1286        rule array_element_name() -> (&'input str, &'input str) =
1287            name:assigned_scalar_name() "[" ai:array_index() "]" { (name, ai) }
1288
1289        rule array_index() -> &'input str =
1290            $(arithmetic_word(<"]">))
1291
1292        rule assigned_scalar_name() -> &'input str =
1293            $(alpha_or_underscore() non_first_variable_char()*)
1294
1295        rule non_first_variable_char() -> () =
1296            ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z'] {}
1297
1298        rule alpha_or_underscore() -> () =
1299            ['_' | 'a'..='z' | 'A'..='Z'] {}
1300
1301        rule assigned_scalar_value() -> ast::AssignmentValue =
1302            v:$([_]*) { ast::AssignmentValue::Scalar(ast::Word::from(v.to_owned())) }
1303    }
1304}
1305
1306#[cfg(test)]
1307#[allow(clippy::panic_in_result_fn)]
1308mod tests {
1309    use anyhow::Result;
1310    use insta::assert_ron_snapshot;
1311    use pretty_assertions::assert_matches;
1312
1313    use super::*;
1314
1315    #[derive(serde::Serialize, serde::Deserialize)]
1316    struct ParseTestResults<'a> {
1317        input: &'a str,
1318        result: Vec<WordPieceWithSource>,
1319    }
1320
1321    fn test_parse(word: &str) -> Result<ParseTestResults<'_>> {
1322        let parsed = super::parse(word, &ParserOptions::default())?;
1323        Ok(ParseTestResults {
1324            input: word,
1325            result: parsed,
1326        })
1327    }
1328
1329    #[test]
1330    fn parse_ansi_c_quoted_text() -> Result<()> {
1331        assert_ron_snapshot!(test_parse(r"$'hi\nthere\t'")?);
1332        Ok(())
1333    }
1334
1335    #[test]
1336    fn parse_ansi_c_quoted_escape_seq() -> Result<()> {
1337        assert_ron_snapshot!(test_parse(r"$'\\'")?);
1338        Ok(())
1339    }
1340
1341    #[test]
1342    fn parse_tilde_with_out_of_range_dir_stack_index() -> Result<()> {
1343        // Regression: the dir-stack index was unwrapped, so a run of digits too
1344        // large for a `usize` panicked the parser. It should fall through to
1345        // being treated as a user name, which is what bash does with it.
1346        // `~N` and `~-N` fall through to a user name; `~+N` cannot, because '+'
1347        // is not a valid user-name character, so it stays literal text. Both
1348        // match what bash does with an out-of-range index.
1349        let parsed = super::parse("~99999999999999999999", &ParserOptions::default())?;
1350        assert_eq!(parsed.len(), 1);
1351        assert_matches!(
1352            parsed[0].piece,
1353            WordPiece::TildeExpansion(TildeExpr::UserHome(_))
1354        );
1355
1356        let parsed = super::parse("~-99999999999999999999", &ParserOptions::default())?;
1357        assert_eq!(parsed.len(), 1);
1358        assert_matches!(
1359            parsed[0].piece,
1360            WordPiece::TildeExpansion(TildeExpr::UserHome(_))
1361        );
1362
1363        let parsed = super::parse("~+99999999999999999999", &ParserOptions::default())?;
1364        assert_eq!(parsed.len(), 1);
1365        assert_matches!(parsed[0].piece, WordPiece::Text(_));
1366
1367        Ok(())
1368    }
1369
1370    #[test]
1371    fn parse_tilde_with_in_range_dir_stack_index() -> Result<()> {
1372        let parsed = super::parse("~+2", &ParserOptions::default())?;
1373        assert_eq!(parsed.len(), 1);
1374        assert_matches!(
1375            parsed[0].piece,
1376            WordPiece::TildeExpansion(TildeExpr::NthDirFromTopOfDirStack { n: 2, .. })
1377        );
1378
1379        Ok(())
1380    }
1381
1382    #[test]
1383    fn parse_tilde_after_colon() -> Result<()> {
1384        let opts = ParserOptions {
1385            tilde_expansion_after_colon: true,
1386            ..ParserOptions::default()
1387        };
1388
1389        let parsed = super::parse("a:~", &opts)?;
1390
1391        // Should have: Text("a:"), TildeExpansion("")
1392        assert_eq!(parsed.len(), 2);
1393        assert_matches!(parsed[0].piece, WordPiece::Text(_));
1394        assert_matches!(parsed[1].piece, WordPiece::TildeExpansion(_));
1395
1396        Ok(())
1397    }
1398
1399    #[test]
1400    fn parse_double_quoted_text() -> Result<()> {
1401        assert_ron_snapshot!(test_parse(r#""a ${b} c""#)?);
1402        Ok(())
1403    }
1404
1405    #[test]
1406    fn parse_gettext_double_quoted_text() -> Result<()> {
1407        assert_ron_snapshot!(test_parse(r#"$"a ${b} c""#)?);
1408        Ok(())
1409    }
1410
1411    #[test]
1412    fn parse_command_substitution() -> Result<()> {
1413        super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?;
1414
1415        assert_ron_snapshot!(test_parse("$(echo hi)")?);
1416
1417        Ok(())
1418    }
1419
1420    #[test]
1421    fn parse_command_substitution_with_embedded_quotes() -> Result<()> {
1422        super::expansion_parser::command_substitution(
1423            r#"$(echo "hi")"#,
1424            &ParserOptions::default(),
1425        )?;
1426
1427        assert_ron_snapshot!(test_parse(r#"$(echo "hi")"#)?);
1428        Ok(())
1429    }
1430
1431    #[test]
1432    fn parse_command_substitution_with_multibyte_chars() -> Result<()> {
1433        assert_ron_snapshot!(test_parse("é$(echo “ü”)ñ")?);
1434        Ok(())
1435    }
1436
1437    #[test]
1438    fn parse_command_substitution_with_embedded_extglob() -> Result<()> {
1439        assert_ron_snapshot!(test_parse("$(echo !(x))")?);
1440        Ok(())
1441    }
1442
1443    #[test]
1444    fn parse_command_sub_with_unbalanced_single_quote() -> Result<()> {
1445        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\nit's here\nEOF\n)\"")?);
1446        Ok(())
1447    }
1448
1449    #[test]
1450    fn parse_command_sub_with_unbalanced_backtick() -> Result<()> {
1451        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\na ` b\nEOF\n)\"")?);
1452        Ok(())
1453    }
1454
1455    #[test]
1456    fn parse_command_sub_with_balanced_double_quotes() -> Result<()> {
1457        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n\"hello\"\nEOF\n)\"")?);
1458        Ok(())
1459    }
1460
1461    #[test]
1462    fn parse_command_sub_with_balanced_single_quotes() -> Result<()> {
1463        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n'hello'\nEOF\n)\"")?);
1464        Ok(())
1465    }
1466
1467    #[test]
1468    fn parse_command_sub_with_balanced_backticks() -> Result<()> {
1469        assert_ron_snapshot!(test_parse("\"$(cat <<'EOF'\n`hello`\nEOF\n)\"")?);
1470        Ok(())
1471    }
1472
1473    #[test]
1474    fn parse_backquoted_command() -> Result<()> {
1475        assert_ron_snapshot!(test_parse("`echo hi`")?);
1476        Ok(())
1477    }
1478
1479    #[test]
1480    fn parse_backquoted_command_in_double_quotes() -> Result<()> {
1481        assert_ron_snapshot!(test_parse(r#""`echo hi`""#)?);
1482        Ok(())
1483    }
1484
1485    #[test]
1486    fn parse_backquoted_command_escapes() -> Result<()> {
1487        for (input, expected) in [
1488            (r"`printf \\2`", r"printf \2"),
1489            (r"`printf \\\\2`", r"printf \\2"),
1490            (r#"`echo \$x \` \" \a`"#, r#"echo $x ` \" \a"#),
1491            // A backslash left ending the command is escaped to keep it literal.
1492            (r"`printf \\`", r"printf \\"),
1493            (r"`printf \\\\`", r"printf \\"),
1494            (r"`printf \\\\\\`", r"printf \\\\"),
1495        ] {
1496            let parsed = super::parse(input, &ParserOptions::default())?;
1497            assert_matches!(
1498                &parsed[..],
1499                [WordPieceWithSource { piece: WordPiece::BackquotedCommandSubstitution(command), .. }]
1500                    if command == expected,
1501                "parsing {input:?}"
1502            );
1503        }
1504        Ok(())
1505    }
1506
1507    #[test]
1508    fn parse_extglob_with_embedded_parameter() -> Result<()> {
1509        assert_ron_snapshot!(test_parse("+([$var])")?);
1510        Ok(())
1511    }
1512
1513    #[test]
1514    fn parse_arithmetic_expansion() -> Result<()> {
1515        assert_ron_snapshot!(test_parse("$((0))")?);
1516        Ok(())
1517    }
1518
1519    #[test]
1520    fn parse_arithmetic_expansion_with_parens() -> Result<()> {
1521        assert_ron_snapshot!(test_parse("$((((1+2)*3)))")?);
1522        Ok(())
1523    }
1524
1525    #[test]
1526    fn parse_arithmetic_expansion_with_unmatched_brackets() -> Result<()> {
1527        // Invalid arithmetic must stay arithmetic, rather than fall back to
1528        // executing the expression as a command substitution.
1529        for expr in ["echo unexpected [", "1[", "a[1", "[ ["] {
1530            let word = std::format!("$(({expr}))");
1531            let parsed = super::parse(&word, &ParserOptions::default())?;
1532            assert_matches!(
1533                parsed.as_slice(),
1534                [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(e), .. }] if e.value == expr,
1535                "for {word:?}"
1536            );
1537        }
1538        Ok(())
1539    }
1540
1541    #[test]
1542    fn parse_arithmetic_expansion_with_deep_brackets() -> Result<()> {
1543        // Brackets in the expression must not grow the word parser's call
1544        // stack. Arithmetic evaluation can reject this expression after
1545        // its boundaries are found.
1546        let expr = std::format!("{}1{}", "[".repeat(15_000), "]".repeat(15_000));
1547        for (open, close) in [("$((", "))"), ("$[", "]")] {
1548            let word = std::format!("{open}{expr}{close}");
1549            let parsed = super::parse(&word, &ParserOptions::default())?;
1550            assert_matches!(
1551                parsed.as_slice(),
1552                [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(e), .. }] if e.value == expr
1553            );
1554        }
1555        Ok(())
1556    }
1557
1558    #[test]
1559    fn parse_legacy_arithmetic_expansion_with_array_subscripts() -> Result<()> {
1560        for (word, expr) in [
1561            ("$[a[1]]", "a[1]"),
1562            ("$[ a[1] + 1 ]", " a[1] + 1 "),
1563            ("$[ a[ a[0] ] ]", " a[ a[0] ] "),
1564            ("$[ a[$[1]] + $(printf 2) ]", " a[$[1]] + $(printf 2) "),
1565            ("$[ 'é]' ]", " 'é]' "),
1566            ("$[ \\] ]", " \\] "),
1567        ] {
1568            let parsed = super::parse(word, &ParserOptions::default())?;
1569            assert_matches!(
1570                parsed.as_slice(),
1571                [WordPieceWithSource { piece: WordPiece::ArithmeticExpression(e), .. }] if e.value == expr,
1572                "for {word:?}"
1573            );
1574        }
1575        Ok(())
1576    }
1577
1578    #[test]
1579    fn parse_malformed_expansions_as_errors() {
1580        // As in bash, a `$` before `(` or `[` always starts an expansion; if
1581        // none parses, the text is malformed, not literal text.
1582        let options = ParserOptions::default();
1583        for word in ["a$(echo", "\"$(echo\"", "$((1 +", "$[1 +", "\"a $[1 + b\""] {
1584            assert!(super::parse(word, &options).is_err(), "for {word:?}");
1585        }
1586        for body in [
1587            "a $(echo hi\n",
1588            "a $(echo 'b) c\n",
1589            "a $((1 + b\n",
1590            "a $[1 + b\n",
1591        ] {
1592            assert!(
1593                super::parse_heredoc(body, &options).is_err(),
1594                "for {body:?}"
1595            );
1596        }
1597    }
1598
1599    #[test]
1600    fn parse_malformed_expansions_nested_in_parameter_expansions_quickly() {
1601        // Each `${...}` can match any of ~20 parameter expression forms; unless
1602        // its parse is memoized, a failure within (e.g., an
1603        // unterminated `$(`) is re-discovered by each form, taking time
1604        // exponential in the nesting depth.
1605        let options = ParserOptions::default();
1606        let nested =
1607            |inner: &str| std::format!("{}{inner}{}\n", "${x:-".repeat(30), "}".repeat(30));
1608        assert!(super::parse_heredoc(&nested("$(echo"), &options).is_err());
1609        assert!(super::parse_heredoc(&nested("$((1 +"), &options).is_err());
1610        assert!(super::parse(&std::format!("\"{}\"", nested("$[1").trim_end()), &options).is_err());
1611    }
1612
1613    #[test]
1614    fn parse_lone_dollar_signs_as_text() -> Result<()> {
1615        let options = ParserOptions::default();
1616        for word in [
1617            "$", "a$", "$ b", "$%", "\"$\"", "\"a$ b\"", "\"$'x'\"", "${x}$",
1618        ] {
1619            super::parse(word, &options)?;
1620        }
1621        super::parse_heredoc("cost: $5 $ $% \"$'x'\" a$\n", &options)?;
1622        Ok(())
1623    }
1624
1625    #[test]
1626    fn parse_heredoc_with_command_substitutions_nested_too_deeply() -> Result<()> {
1627        // Here-doc bodies aren't tokenized along with the script, so the word
1628        // parser is the first to find nesting past the tokenizer's
1629        // limit; that's an error, not literal text.
1630        let nested = |depth: u32| {
1631            let depth = depth as usize;
1632            std::format!("{}:{}\n", "$(".repeat(depth), ")".repeat(depth))
1633        };
1634        let limit = crate::tokenizer::MAX_EXPANSION_NESTING;
1635        let options = ParserOptions::default();
1636        super::parse_heredoc(&nested(limit), &options)?;
1637        assert!(super::parse_heredoc(&nested(limit + 1), &options).is_err());
1638        Ok(())
1639    }
1640
1641    #[test]
1642    fn test_arithmetic_word_parsing() {
1643        let options = ParserOptions::default();
1644
1645        assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok());
1646        assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok());
1647        assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok());
1648        assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok());
1649        assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok());
1650        assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok());
1651        assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok());
1652        assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok());
1653        assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok());
1654    }
1655
1656    #[test]
1657    fn test_arithmetic_word_piece_parsing() {
1658        let options = ParserOptions::default();
1659
1660        assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok());
1661        assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok());
1662        assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok());
1663        assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok());
1664        assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok());
1665        assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok());
1666        assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok());
1667        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok());
1668        assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok());
1669        assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err());
1670        assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err());
1671        assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err());
1672    }
1673
1674    #[test]
1675    fn test_brace_expansion_parsing() -> Result<()> {
1676        let options = ParserOptions::default();
1677
1678        let inputs = ["x{a,b}y", "{a,b{1,2}}"];
1679
1680        for input in inputs {
1681            assert_ron_snapshot!(super::parse_brace_expansions(input, &options)?.ok_or_else(
1682                || anyhow::anyhow!("Expected brace expansion to be parsed successfully")
1683            )?);
1684        }
1685
1686        Ok(())
1687    }
1688
1689    #[test]
1690    fn test_scalar_assignment_parsing() -> Result<()> {
1691        let options = ParserOptions::default();
1692
1693        super::parse_scalar_assignment("x=3", &options)?;
1694        super::parse_scalar_assignment("x=", &options)?;
1695        super::parse_scalar_assignment("x[3]=a", &options)?;
1696        super::parse_scalar_assignment("x[${y[3]}]=a", &options)?;
1697        super::parse_scalar_assignment("x[y[3]]=a", &options)?;
1698        Ok(())
1699    }
1700
1701    #[test]
1702    fn parse_compound_assignment_value() {
1703        let options = ParserOptions::default();
1704        let parse = |value| super::parse_compound_assignment_value(value, &options);
1705
1706        assert_matches!(parse("()").as_deref(), Some([]));
1707
1708        assert_matches!(parse("(1 2)").as_deref(), Some([(None, first), (None, second)])
1709            if first.value == "1" && second.value == "2");
1710
1711        assert_matches!(
1712            parse(r#"([key]=value "other one")"#).as_deref(),
1713            Some([(Some(key), value), (None, other)])
1714                if key.value == "key"
1715                    && value.value == "value"
1716                    && other.value == r#""other one""#);
1717
1718        // Text that is not exclusively a compound value must not be accepted;
1719        // otherwise trailing shell syntax hidden in an expanded value
1720        // would be silently dropped.
1721        assert!(parse(r#"(x); printf "INJECTED\n"; #"#).is_none());
1722        assert!(parse("(unterminated").is_none());
1723        assert!(parse("plain text").is_none());
1724        assert!(parse("").is_none());
1725    }
1726
1727    #[test]
1728    fn parse_sequence_bound() {
1729        assert_eq!(super::parse_sequence_bound("3"), Some(3));
1730        assert_eq!(super::parse_sequence_bound("007"), Some(7));
1731        assert_eq!(super::parse_sequence_bound("+7"), Some(7));
1732        assert_eq!(super::parse_sequence_bound("-007"), Some(-7));
1733        assert_eq!(super::parse_sequence_bound("0"), Some(0));
1734        assert_eq!(super::parse_sequence_bound("-0"), Some(0));
1735        assert_eq!(super::parse_sequence_bound("99999999999999999999"), None);
1736
1737        // The digits of `i64::MIN` are one past `i64::MAX`, so the sign has to
1738        // be parsed with them.
1739        assert_eq!(
1740            super::parse_sequence_bound("-9223372036854775808"),
1741            Some(i64::MIN)
1742        );
1743        assert_eq!(
1744            super::parse_sequence_bound("9223372036854775807"),
1745            Some(i64::MAX)
1746        );
1747        assert_eq!(super::parse_sequence_bound("-9223372036854775809"), None);
1748    }
1749
1750    #[test]
1751    fn zero_padded_width() {
1752        let width = super::zero_padded_width;
1753
1754        // Neither bound was written with a leading zero.
1755        assert_eq!(width("1", "5"), None);
1756        assert_eq!(width("0", "10"), None);
1757        assert_eq!(width("-0", "3"), None);
1758
1759        // The width is the longer bound as written, whichever one asked for
1760        // padding.
1761        assert_eq!(width("01", "5"), Some(2));
1762        assert_eq!(width("1", "005"), Some(3));
1763        assert_eq!(width("0009", "11"), Some(4));
1764
1765        // A sign takes one of the columns.
1766        assert_eq!(width("-01", "01"), Some(3));
1767        assert_eq!(width("00", "-3"), Some(2));
1768
1769        // A zero after a plus does not ask for padding, though the text still
1770        // counts toward the width once the other bound has asked.
1771        assert_eq!(width("+01", "3"), None);
1772        assert_eq!(width("+01", "05"), Some(3));
1773    }
1774}