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(
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
562pub 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
576pub 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
590pub 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
604pub 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#[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
644pub(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
670fn parse_sequence_bound(text: &str) -> Option<i64> {
683 text.parse().ok()
684}
685
686fn 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
706fn 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 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 pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
757 pieces:(brace_expansion_piece(<![_]>)+) { Some(pieces) } /
758 [_]* { None }
759
760 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 rule non_brace_expr_text<T>(stop_condition: rule<T>) -> () =
770 !"{" word_piece(<['{'] {} / stop_condition() {}>) {} /
771 !brace_expr() !stop_condition() "{" {}
772
773 pub(crate) rule brace_expr() -> BraceExpression =
775 "{" inner:brace_expr_inner() "}" { inner }
776
777 pub(crate) rule brace_expr_inner() -> BraceExpression =
780 brace_text_list_expr() /
781 seq:brace_sequence_expr() { vec![seq] }
782
783 pub(crate) rule brace_text_list_expr() -> BraceExpression =
786 brace_text_list_member() **<2,> ","
787
788 pub(crate) rule brace_text_list_member() -> BraceExpressionMember =
791 &[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } /
793 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 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 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 rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
847 "(" arithmetic_word_plus_right_paren() {} /
852 array_element_name() {} /
855 !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>) {}
859
860 rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
862 stop_condition() {} /
863 "(" {}
864
865 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 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 dollar_sign_word_piece() /
882 normal_escape_sequence() /
884 enabled_tilde_expr_after_colon() /
886 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 peg::RuleResult::Failed
937 } else {
938 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 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 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 #[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 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 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 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 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 if command.chars().rev().take_while(|c| *c == '\\').count() % 2 == 1 {
1202 command.push('\\');
1203 }
1204 command
1205 }
1206
1207 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 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 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 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 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 (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 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 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 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 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 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 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 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 assert_eq!(width("1", "5"), None);
1756 assert_eq!(width("0", "10"), None);
1757 assert_eq!(width("-0", "3"), None);
1758
1759 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 assert_eq!(width("-01", "01"), Some(3));
1767 assert_eq!(width("00", "-3"), Some(2));
1768
1769 assert_eq!(width("+01", "3"), None);
1772 assert_eq!(width("+01", "05"), Some(3));
1773 }
1774}