1use std::fmt::{Debug, Display};
13
14use crate::{ParserOptions, SourceSpan, ast, error};
15
16#[derive(Clone, Debug)]
19#[cfg_attr(
20 any(test, feature = "serde"),
21 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
22)]
23pub struct WordPieceWithSource {
24 pub piece: WordPiece,
26 pub start_index: usize,
28 pub end_index: usize,
30}
31
32#[derive(Clone, Debug)]
34#[cfg_attr(
35 any(test, feature = "serde"),
36 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
37)]
38pub enum WordPiece {
39 Text(String),
41 SingleQuotedText(String),
43 AnsiCQuotedText(String),
45 DoubleQuotedSequence(Vec<WordPieceWithSource>),
47 GettextDoubleQuotedSequence(Vec<WordPieceWithSource>),
49 TildeExpansion(TildeExpr),
51 ParameterExpansion(ParameterExpr),
53 CommandSubstitution(String),
55 BackquotedCommandSubstitution(String),
57 EscapeSequence(String),
59 ArithmeticExpression(ast::UnexpandedArithmeticExpr),
61}
62
63#[derive(Clone, Debug)]
65#[cfg_attr(
66 any(test, feature = "serde"),
67 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
68)]
69pub enum TildeExpr {
70 Home,
72 UserHome(String),
74 WorkingDir,
76 OldWorkingDir,
78 NthDirFromTopOfDirStack {
83 n: usize,
86 plus_used: bool,
88 },
89 NthDirFromBottomOfDirStack {
93 n: usize,
96 },
97}
98
99#[derive(Clone, Debug)]
101#[cfg_attr(
102 any(test, feature = "serde"),
103 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
104)]
105pub enum ParameterTestType {
106 UnsetOrNull,
108 Unset,
110}
111
112#[derive(Clone, Debug)]
114#[cfg_attr(
115 any(test, feature = "serde"),
116 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
117)]
118pub enum Parameter {
119 Positional(u32),
121 Special(SpecialParameter),
123 Named(String),
125 NamedWithIndex {
127 name: String,
129 index: String,
131 },
132 NamedWithAllIndices {
134 name: String,
136 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#[derive(Clone, Debug)]
163#[cfg_attr(
164 any(test, feature = "serde"),
165 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
166)]
167pub enum SpecialParameter {
168 AllPositionalParameters {
170 concatenate: bool,
172 },
173 PositionalParameterCount,
175 LastExitStatus,
177 CurrentOptionFlags,
179 ProcessId,
181 LastBackgroundProcessId,
183 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#[derive(Clone, Debug)]
209#[cfg_attr(
210 any(test, feature = "serde"),
211 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
212)]
213pub enum ParameterExpr {
214 Parameter {
216 parameter: Parameter,
218 indirect: bool,
222 },
223 UseDefaultValues {
225 parameter: Parameter,
227 indirect: bool,
231 test_type: ParameterTestType,
233 default_value: Option<String>,
235 },
236 AssignDefaultValues {
238 parameter: Parameter,
240 indirect: bool,
244 test_type: ParameterTestType,
246 default_value: Option<String>,
248 },
249 IndicateErrorIfNullOrUnset {
251 parameter: Parameter,
253 indirect: bool,
257 test_type: ParameterTestType,
259 error_message: Option<String>,
261 },
262 UseAlternativeValue {
264 parameter: Parameter,
266 indirect: bool,
270 test_type: ParameterTestType,
272 alternative_value: Option<String>,
274 },
275 ParameterLength {
277 parameter: Parameter,
279 indirect: bool,
283 },
284 RemoveSmallestSuffixPattern {
287 parameter: Parameter,
289 indirect: bool,
293 pattern: Option<String>,
295 },
296 RemoveLargestSuffixPattern {
299 parameter: Parameter,
301 indirect: bool,
305 pattern: Option<String>,
307 },
308 RemoveSmallestPrefixPattern {
311 parameter: Parameter,
313 indirect: bool,
317 pattern: Option<String>,
319 },
320 RemoveLargestPrefixPattern {
323 parameter: Parameter,
325 indirect: bool,
329 pattern: Option<String>,
331 },
332 Substring {
334 parameter: Parameter,
336 indirect: bool,
340 offset: ast::UnexpandedArithmeticExpr,
343 length: Option<ast::UnexpandedArithmeticExpr>,
347 },
348 Transform {
350 parameter: Parameter,
352 indirect: bool,
356 op: ParameterTransformOp,
358 },
359 UppercaseFirstChar {
361 parameter: Parameter,
363 indirect: bool,
367 pattern: Option<String>,
369 },
370 UppercasePattern {
372 parameter: Parameter,
374 indirect: bool,
378 pattern: Option<String>,
380 },
381 LowercaseFirstChar {
383 parameter: Parameter,
385 indirect: bool,
389 pattern: Option<String>,
391 },
392 LowercasePattern {
394 parameter: Parameter,
396 indirect: bool,
400 pattern: Option<String>,
402 },
403 ReplaceSubstring {
405 parameter: Parameter,
407 indirect: bool,
411 pattern: String,
413 replacement: Option<String>,
415 match_kind: SubstringMatchKind,
417 },
418 VariableNames {
420 prefix: String,
422 concatenate: bool,
424 },
425 MemberKeys {
427 variable_name: String,
429 concatenate: bool,
431 },
432}
433
434#[derive(Clone, Debug)]
436#[cfg_attr(
437 any(test, feature = "serde"),
438 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
439)]
440pub enum SubstringMatchKind {
441 Prefix,
443 Suffix,
445 FirstOccurrence,
447 Anywhere,
449}
450
451#[derive(Clone, Debug)]
453#[cfg_attr(
454 any(test, feature = "serde"),
455 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
456)]
457pub enum ParameterTransformOp {
458 CapitalizeInitial,
460 ExpandEscapeSequences,
462 PossiblyQuoteWithArraysExpanded {
464 separate_words: bool,
466 },
467 PromptExpand,
469 Quoted,
471 ToAssignmentLogic,
473 ToAttributeFlags,
475 ToLowerCase,
477 ToUpperCase,
479}
480
481#[derive(Clone, Debug)]
484#[cfg_attr(
485 any(test, feature = "serde"),
486 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
487)]
488pub enum BraceExpressionOrText {
489 Expr(BraceExpression),
491 Text(String),
493}
494
495pub type BraceExpression = Vec<BraceExpressionMember>;
497
498#[derive(Clone, Debug)]
500#[cfg_attr(
501 any(test, feature = "serde"),
502 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
503)]
504pub enum BraceExpressionMember {
505 NumberSequence {
507 start: i64,
509 end: i64,
511 increment: i64,
513 zero_padded_width: Option<usize>,
516 },
517 CharSequence {
519 start: char,
521 end: char,
523 increment: i64,
525 },
526 Child(Vec<BraceExpressionOrText>),
528}
529
530pub 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
567pub 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
586pub 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
605pub 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
630pub 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#[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
675pub(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
701fn parse_sequence_bound(text: &str) -> Option<i64> {
714 text.parse().ok()
715}
716
717fn 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
737fn 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 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 pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
788 pieces:(brace_expansion_piece(<![_]>)+) { Some(pieces) } /
789 [_]* { None }
790
791 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 rule non_brace_expr_text<T>(stop_condition: rule<T>) -> () =
801 !"{" word_piece(<['{'] {} / stop_condition() {}>) {} /
802 !brace_expr() !stop_condition() "{" {}
803
804 pub(crate) rule brace_expr() -> BraceExpression =
806 "{" inner:brace_expr_inner() "}" { inner }
807
808 pub(crate) rule brace_expr_inner() -> BraceExpression =
811 brace_text_list_expr() /
812 seq:brace_sequence_expr() { vec![seq] }
813
814 pub(crate) rule brace_text_list_expr() -> BraceExpression =
817 brace_text_list_member() **<2,> ","
818
819 pub(crate) rule brace_text_list_member() -> BraceExpressionMember =
822 &[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } /
824 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 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 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 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 rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
878 "(" arithmetic_word_plus_right_paren() {} /
883 array_element_name() {} /
886 !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>) {}
890
891 rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
893 stop_condition() {} /
894 "(" {}
895
896 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 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 dollar_sign_word_piece() /
913 normal_escape_sequence() /
915 enabled_tilde_expr_after_colon() /
917 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 peg::RuleResult::Failed
968 } else {
969 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 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 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 #[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 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 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 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 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 if command.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 {
1233 command.push('\\');
1234 }
1235 command
1236 }
1237
1238 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 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 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 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 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 (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 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 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 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 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 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 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 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 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 assert_eq!(width("1", "5"), None);
1827 assert_eq!(width("0", "10"), None);
1828 assert_eq!(width("-0", "3"), None);
1829
1830 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 assert_eq!(width("-01", "01"), Some(3));
1838 assert_eq!(width("00", "-3"), Some(2));
1839
1840 assert_eq!(width("+01", "3"), None);
1843 assert_eq!(width("+01", "05"), Some(3));
1844 }
1845}