1use std::fmt::Debug;
13use std::fmt::Display;
14
15use crate::ParserOptions;
16use crate::SourceSpan;
17use crate::ast;
18use crate::error;
19
20#[derive(Clone, Debug)]
22#[cfg_attr(
23 any(test, feature = "serde"),
24 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
25)]
26pub struct WordPieceWithSource {
27 pub piece: WordPiece,
29 pub start_index: usize,
31 pub end_index: usize,
33}
34
35#[derive(Clone, Debug)]
37#[cfg_attr(
38 any(test, feature = "serde"),
39 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
40)]
41pub enum WordPiece {
42 Text(String),
44 SingleQuotedText(String),
46 AnsiCQuotedText(String),
48 DoubleQuotedSequence(Vec<WordPieceWithSource>),
50 GettextDoubleQuotedSequence(Vec<WordPieceWithSource>),
52 TildeExpansion(TildeExpr),
54 ParameterExpansion(ParameterExpr),
56 CommandSubstitution(String),
58 BackquotedCommandSubstitution(String),
60 EscapeSequence(String),
62 ArithmeticExpression(ast::UnexpandedArithmeticExpr),
64}
65
66#[derive(Clone, Debug)]
68#[cfg_attr(
69 any(test, feature = "serde"),
70 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
71)]
72pub enum TildeExpr {
73 Home,
75 UserHome(String),
77 WorkingDir,
79 OldWorkingDir,
81 NthDirFromTopOfDirStack {
85 n: usize,
87 plus_used: bool,
89 },
90 NthDirFromBottomOfDirStack {
93 n: usize,
95 },
96}
97
98#[derive(Clone, Debug)]
100#[cfg_attr(
101 any(test, feature = "serde"),
102 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
103)]
104pub enum ParameterTestType {
105 UnsetOrNull,
107 Unset,
109}
110
111#[derive(Clone, Debug)]
113#[cfg_attr(
114 any(test, feature = "serde"),
115 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
116)]
117pub enum Parameter {
118 Positional(u32),
120 Special(SpecialParameter),
122 Named(String),
124 NamedWithIndex {
126 name: String,
128 index: String,
130 },
131 NamedWithAllIndices {
133 name: String,
135 concatenate: bool,
137 },
138}
139
140impl Display for Parameter {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 match self {
143 Self::Positional(n) => write!(f, "${n}"),
144 Self::Special(s) => write!(f, "${s}"),
145 Self::Named(name) => write!(f, "${{{name}}}"),
146 Self::NamedWithIndex { name, index } => {
147 write!(f, "${{{name}[{index}]}}")
148 }
149 Self::NamedWithAllIndices { name, concatenate } => {
150 if *concatenate {
151 write!(f, "${{{name}[*]}}")
152 } else {
153 write!(f, "${{{name}[@]}}")
154 }
155 }
156 }
157 }
158}
159
160#[derive(Clone, Debug)]
162#[cfg_attr(
163 any(test, feature = "serde"),
164 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
165)]
166pub enum SpecialParameter {
167 AllPositionalParameters {
169 concatenate: bool,
171 },
172 PositionalParameterCount,
174 LastExitStatus,
176 CurrentOptionFlags,
178 ProcessId,
180 LastBackgroundProcessId,
182 ShellName,
184}
185
186impl Display for SpecialParameter {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::AllPositionalParameters { concatenate } => {
190 if *concatenate {
191 write!(f, "*")
192 } else {
193 write!(f, "@")
194 }
195 }
196 Self::PositionalParameterCount => write!(f, "#"),
197 Self::LastExitStatus => write!(f, "?"),
198 Self::CurrentOptionFlags => write!(f, "-"),
199 Self::ProcessId => write!(f, "$"),
200 Self::LastBackgroundProcessId => write!(f, "!"),
201 Self::ShellName => write!(f, "0"),
202 }
203 }
204}
205
206#[derive(Clone, Debug)]
208#[cfg_attr(
209 any(test, feature = "serde"),
210 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
211)]
212pub enum ParameterExpr {
213 Parameter {
215 parameter: Parameter,
217 indirect: bool,
221 },
222 UseDefaultValues {
224 parameter: Parameter,
226 indirect: bool,
230 test_type: ParameterTestType,
232 default_value: Option<String>,
234 },
235 AssignDefaultValues {
237 parameter: Parameter,
239 indirect: bool,
243 test_type: ParameterTestType,
245 default_value: Option<String>,
247 },
248 IndicateErrorIfNullOrUnset {
250 parameter: Parameter,
252 indirect: bool,
256 test_type: ParameterTestType,
258 error_message: Option<String>,
260 },
261 UseAlternativeValue {
263 parameter: Parameter,
265 indirect: bool,
269 test_type: ParameterTestType,
271 alternative_value: Option<String>,
273 },
274 ParameterLength {
276 parameter: Parameter,
278 indirect: bool,
282 },
283 RemoveSmallestSuffixPattern {
285 parameter: Parameter,
287 indirect: bool,
291 pattern: Option<String>,
293 },
294 RemoveLargestSuffixPattern {
296 parameter: Parameter,
298 indirect: bool,
302 pattern: Option<String>,
304 },
305 RemoveSmallestPrefixPattern {
307 parameter: Parameter,
309 indirect: bool,
313 pattern: Option<String>,
315 },
316 RemoveLargestPrefixPattern {
318 parameter: Parameter,
320 indirect: bool,
324 pattern: Option<String>,
326 },
327 Substring {
329 parameter: Parameter,
331 indirect: bool,
335 offset: ast::UnexpandedArithmeticExpr,
338 length: Option<ast::UnexpandedArithmeticExpr>,
342 },
343 Transform {
345 parameter: Parameter,
347 indirect: bool,
351 op: ParameterTransformOp,
353 },
354 UppercaseFirstChar {
356 parameter: Parameter,
358 indirect: bool,
362 pattern: Option<String>,
364 },
365 UppercasePattern {
367 parameter: Parameter,
369 indirect: bool,
373 pattern: Option<String>,
375 },
376 LowercaseFirstChar {
378 parameter: Parameter,
380 indirect: bool,
384 pattern: Option<String>,
386 },
387 LowercasePattern {
389 parameter: Parameter,
391 indirect: bool,
395 pattern: Option<String>,
397 },
398 ReplaceSubstring {
400 parameter: Parameter,
402 indirect: bool,
406 pattern: String,
408 replacement: Option<String>,
410 match_kind: SubstringMatchKind,
412 },
413 VariableNames {
415 prefix: String,
417 concatenate: bool,
419 },
420 MemberKeys {
422 variable_name: String,
424 concatenate: bool,
426 },
427}
428
429#[derive(Clone, Debug)]
431#[cfg_attr(
432 any(test, feature = "serde"),
433 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
434)]
435pub enum SubstringMatchKind {
436 Prefix,
438 Suffix,
440 FirstOccurrence,
442 Anywhere,
444}
445
446#[derive(Clone, Debug)]
448#[cfg_attr(
449 any(test, feature = "serde"),
450 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
451)]
452pub enum ParameterTransformOp {
453 CapitalizeInitial,
455 ExpandEscapeSequences,
457 PossiblyQuoteWithArraysExpanded {
459 separate_words: bool,
461 },
462 PromptExpand,
464 Quoted,
466 ToAssignmentLogic,
468 ToAttributeFlags,
470 ToLowerCase,
472 ToUpperCase,
474}
475
476#[derive(Clone, Debug)]
478#[cfg_attr(
479 any(test, feature = "serde"),
480 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
481)]
482pub enum BraceExpressionOrText {
483 Expr(BraceExpression),
485 Text(String),
487}
488
489pub type BraceExpression = Vec<BraceExpressionMember>;
491
492#[derive(Clone, Debug)]
494#[cfg_attr(
495 any(test, feature = "serde"),
496 derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
497)]
498pub enum BraceExpressionMember {
499 NumberSequence {
501 start: i64,
503 end: i64,
505 increment: i64,
507 },
508 CharSequence {
510 start: char,
512 end: char,
514 increment: i64,
516 },
517 Child(Vec<BraceExpressionOrText>),
519}
520
521pub fn parse(
528 word: &str,
529 options: &ParserOptions,
530) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
531 cacheable_parse(word.to_owned(), options.to_owned())
532}
533
534#[cached::proc_macro::cached(size = 64, result = true)]
535fn cacheable_parse(
536 word: String,
537 options: ParserOptions,
538) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
539 tracing::debug!(target: "expansion", "Parsing word '{}'", word);
540
541 let pieces = expansion_parser::unexpanded_word(word.as_str(), &options)
542 .map_err(|err| error::WordParseError::Word(word.clone(), err.into()))?;
543
544 tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces);
545
546 Ok(pieces)
547}
548
549pub fn parse_heredoc(
556 word: &str,
557 options: &ParserOptions,
558) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
559 expansion_parser::unexpanded_heredoc_word(word, options)
560 .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))
561}
562
563pub fn parse_parameter(
570 word: &str,
571 options: &ParserOptions,
572) -> Result<Parameter, error::WordParseError> {
573 expansion_parser::parameter(word, options)
574 .map_err(|err| error::WordParseError::Parameter(word.to_owned(), err.into()))
575}
576
577pub fn parse_brace_expansions(
584 word: &str,
585 options: &ParserOptions,
586) -> Result<Option<Vec<BraceExpressionOrText>>, error::WordParseError> {
587 expansion_parser::brace_expansions(word, options)
588 .map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err.into()))
589}
590
591pub(crate) fn parse_assignment_word(
592 word: &str,
593) -> Result<ast::Assignment, peg::error::ParseError<peg::str::LineCol>> {
594 expansion_parser::name_equals_scalar_value(word, &ParserOptions::default())
595}
596
597pub(crate) fn parse_array_assignment(
598 word: &str,
599 elements: &[&String],
600) -> Result<ast::Assignment, &'static str> {
601 let (assignment_name, append) = expansion_parser::name_equals(word, &ParserOptions::default())
602 .map_err(|_| "not array assignment word")?;
603
604 let elements = elements
605 .iter()
606 .map(|element| expansion_parser::literal_array_element(element, &ParserOptions::default()))
607 .collect::<Result<Vec<_>, _>>()
608 .map_err(|_| "invalid array element in literal")?;
609
610 let elements_as_words = elements
611 .into_iter()
612 .map(|(key, value)| {
613 (
614 key.map(|k| ast::Word::new(k.as_str())),
615 ast::Word::new(value.as_str()),
616 )
617 })
618 .collect();
619
620 Ok(ast::Assignment {
621 name: assignment_name,
622 value: ast::AssignmentValue::Array(elements_as_words),
623 append,
624 loc: SourceSpan::default(),
625 })
626}
627
628peg::parser! {
629 grammar expansion_parser(parser_options: &ParserOptions) for str {
630 rule traced<T>(e: rule<T>) -> T =
632 &(input:$([_]*) {
633 #[cfg(feature = "debug-tracing")]
634 println!("[PEG_INPUT_START]\n{input}\n[PEG_TRACE_START]");
635 })
636 e:e()? {?
637 #[cfg(feature = "debug-tracing")]
638 println!("[PEG_TRACE_STOP]");
639 e.ok_or("")
640 }
641
642 pub(crate) rule unexpanded_word() -> Vec<WordPieceWithSource> = traced(<word(<![_]>)>)
643
644 rule word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
645 tilde:tilde_expr_prefix_with_source()? pieces:word_piece_with_source(<stop_condition()>, false )* {
646 let mut all_pieces = Vec::new();
647 if let Some(tilde) = tilde {
648 all_pieces.push(tilde);
649 }
650 all_pieces.extend(pieces);
651 all_pieces
652 }
653
654 pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
656 pieces:(brace_expansion_piece(<![_]>)+) { Some(pieces) } /
657 [_]* { None }
658
659 rule brace_expansion_piece<T>(stop_condition: rule<T>) -> BraceExpressionOrText =
662 expr:brace_expr() {
663 BraceExpressionOrText::Expr(expr)
664 } /
665 text:$(non_brace_expr_text(<stop_condition()>)+) { BraceExpressionOrText::Text(text.to_owned()) }
666
667 rule non_brace_expr_text<T>(stop_condition: rule<T>) -> () =
669 !"{" word_piece(<['{'] {} / stop_condition() {}>, false) {} /
670 !brace_expr() !stop_condition() "{" {}
671
672 pub(crate) rule brace_expr() -> BraceExpression =
674 "{" inner:brace_expr_inner() "}" { inner }
675
676 pub(crate) rule brace_expr_inner() -> BraceExpression =
679 brace_text_list_expr() /
680 seq:brace_sequence_expr() { vec![seq] }
681
682 pub(crate) rule brace_text_list_expr() -> BraceExpression =
685 brace_text_list_member() **<2,> ","
686
687 pub(crate) rule brace_text_list_member() -> BraceExpressionMember =
690 &[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } /
692 child_pieces:(brace_expansion_piece(<[',' | '}']>)+) {
695 BraceExpressionMember::Child(child_pieces)
696 }
697
698 pub(crate) rule brace_sequence_expr() -> BraceExpressionMember =
699 start:number() ".." end:number() increment:(".." n:number() { n })? {
700 BraceExpressionMember::NumberSequence { start, end, increment: increment.unwrap_or(1) }
701 } /
702 start:character() ".." end:character() increment:(".." n:number() { n })? {
703 BraceExpressionMember::CharSequence { start, end, increment: increment.unwrap_or(1) }
704 }
705
706 rule number() -> i64 = sign:number_sign()? n:$(['0'..='9']+) {
707 let sign = sign.unwrap_or(1);
708 let num: i64 = n.parse().unwrap();
709 num * sign
710 }
711
712 rule number_sign() -> i64 =
713 ['-'] { -1 } /
714 ['+'] { 1 }
715
716 rule character() -> char = ['a'..='z' | 'A'..='Z']
717
718 pub(crate) rule is_arithmetic_word() =
719 arithmetic_word(<![_]>)
720
721 rule arithmetic_word<T>(stop_condition: rule<T>) =
724 arithmetic_word_piece(<stop_condition()>)* {}
725
726 pub(crate) rule is_arithmetic_word_piece() =
727 arithmetic_word_piece(<![_]>)
728
729 rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
734 "(" arithmetic_word_plus_right_paren() {} /
739 array_element_name() {} /
742 !"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>, false ) {}
746
747 rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
749 stop_condition() {} /
750 "(" {}
751
752 rule arithmetic_word_plus_right_paren() =
754 arithmetic_word(<[')']>) ")"
755
756 rule word_piece_with_source<T>(stop_condition: rule<T>, in_command: bool) -> WordPieceWithSource =
757 start_index:position!() piece:word_piece(<stop_condition()>, in_command) end_index:position!() {
758 WordPieceWithSource { piece, start_index, end_index }
759 }
760
761 rule word_piece<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
762 s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
764 s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
765 s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
766 s:gettext_double_quoted_sequence() { WordPiece::GettextDoubleQuotedSequence(s) } /
767 dollar_sign_word_piece() /
769 normal_escape_sequence() /
771 enabled_tilde_expr_after_colon() /
773 unquoted_literal_text(<stop_condition()>, in_command)
775
776 rule dollar_sign_word_piece() -> WordPiece =
777 arithmetic_expansion() /
778 legacy_arithmetic_expansion() /
779 command_substitution() /
780 parameter_expansion()
781
782 rule double_quoted_word_piece() -> WordPiece =
783 arithmetic_expansion() /
784 legacy_arithmetic_expansion() /
785 command_substitution() /
786 parameter_expansion() /
787 double_quoted_escape_sequence() /
788 double_quoted_text()
789
790 rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
791 "\"" i:double_quoted_sequence_inner()* "\"" { i }
792
793 rule gettext_double_quoted_sequence() -> Vec<WordPieceWithSource> =
794 "$\"" i:double_quoted_sequence_inner()* "\"" { i }
795
796 rule double_quoted_sequence_inner() -> WordPieceWithSource =
797 start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
798 WordPieceWithSource {
799 piece,
800 start_index,
801 end_index
802 }
803 }
804
805 rule single_quoted_literal_text() -> &'input str =
806 "\'" inner:$([^'\'']*) "\'" { inner }
807
808 rule ansi_c_quoted_text() -> &'input str =
809 r"$'" inner:$((r"\\" / r"\'" / [^'\''])*) r"'" { inner }
810
811 rule unquoted_literal_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
812 s:$(unquoted_literal_text_piece(<stop_condition()>, in_command)+) { WordPiece::Text(s.to_owned()) }
813
814 rule unquoted_literal_text_piece<T>(stop_condition: rule<T>, in_command: bool) =
816 is_true(in_command) extglob_pattern() /
817 is_true(in_command) subshell_command() /
818 !stop_condition() !normal_escape_sequence() !enabled_tilde_expr_after_colon() [^'\'' | '\"' | '$' | '`'] {}
819
820 rule enabled_tilde_expr_after_colon() -> WordPiece =
821 tilde_exprs_after_colon_enabled() last_char_is_colon() piece:tilde_expression_piece() { piece }
822
823 rule last_char_is_colon() = #{|input, pos| {
824 if pos == 0 {
825 peg::RuleResult::Failed
827 } else {
828 if input.as_bytes()[pos - 1] == b':' {
830 peg::RuleResult::Matched(pos, ())
831 } else {
832 peg::RuleResult::Failed
833 }
834 }
835 }}
836
837 rule is_true(value: bool) = &[_] {? if value { Ok(()) } else { Err("not true") } }
838
839 rule extglob_pattern() =
840 ("@" / "!" / "?" / "+" / "*") "(" extglob_body_piece()* ")" {}
841
842 rule extglob_body_piece() =
843 word_piece(<[')']>, true ) {}
844
845 rule subshell_command() =
846 "(" command() ")" {}
847
848 rule double_quoted_text() -> WordPiece =
849 s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
850
851 rule double_quote_body_text() -> &'input str =
852 $((!double_quoted_escape_sequence() !dollar_sign_word_piece() [^'\"'])+)
853
854 pub(crate) rule unexpanded_heredoc_word() -> Vec<WordPieceWithSource> =
856 traced(<heredoc_word(<![_]>)>)
857
858 rule heredoc_word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
859 pieces:heredoc_word_piece_with_source(<stop_condition()>)* { pieces }
860
861 rule heredoc_word_piece_with_source<T>(stop_condition: rule<T>) -> WordPieceWithSource =
862 !stop_condition() start_index:position!() piece:heredoc_word_piece() end_index:position!() {
863 WordPieceWithSource { piece, start_index, end_index }
864 }
865
866 rule heredoc_word_piece() -> WordPiece =
867 arithmetic_expansion() /
868 legacy_arithmetic_expansion() /
869 command_substitution() /
870 parameter_expansion() /
871 heredoc_escape_sequence() /
872 heredoc_literal_text()
873
874 rule heredoc_escape_sequence() -> WordPiece =
875 s:$("\\" ['$' | '`' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
876
877 rule heredoc_literal_text() -> WordPiece =
878 s:$((!heredoc_escape_sequence() !dollar_sign_word_piece() [^'`'])+) {
879 WordPiece::Text(s.to_owned())
880 }
881
882 rule normal_escape_sequence() -> WordPiece =
883 s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
884
885 rule double_quoted_escape_sequence() -> WordPiece =
886 s:$("\\" ['$' | '`' | '\"' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
887
888 rule tilde_expr_prefix_with_source() -> WordPieceWithSource =
889 start_index:position!() piece:tilde_expr_prefix() end_index:position!() {
890 WordPieceWithSource {
891 piece,
892 start_index,
893 end_index
894 }
895 }
896
897 rule tilde_expr_prefix() -> WordPiece =
898 tilde_exprs_at_word_start_enabled() piece:tilde_expression_piece() { piece }
899
900 rule tilde_expr_after_colon() -> WordPiece =
901 tilde_exprs_after_colon_enabled() piece:tilde_expression_piece() { piece }
902
903 rule tilde_expression_piece() -> WordPiece =
904 "~" expr:tilde_expression() { WordPiece::TildeExpansion(expr) }
905
906 rule tilde_expression() -> TildeExpr =
907 &tilde_terminator() { TildeExpr::Home } /
908 "+" &tilde_terminator() { TildeExpr::WorkingDir } /
909 plus:("+"?) n:$(['0'..='9']*) &tilde_terminator() { TildeExpr::NthDirFromTopOfDirStack { n: n.parse().unwrap(), plus_used: plus.is_some() } } /
910 "-" &tilde_terminator() { TildeExpr::OldWorkingDir } /
911 "-" n:$(['0'..='9']*) &tilde_terminator() { TildeExpr::NthDirFromBottomOfDirStack { n: n.parse().unwrap() } } /
912 user:$(portable_filename_char()*) &tilde_terminator() { TildeExpr::UserHome(user.to_owned()) }
913
914 rule tilde_terminator() = ['/' | ':' | ';' | '}'] / ![_]
915
916 rule portable_filename_char() = ['A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '-']
917
918 rule parameter_expansion() -> WordPiece =
921 "${" e:parameter_expression() "}" {
922 WordPiece::ParameterExpansion(e)
923 } /
924 "$" parameter:unbraced_parameter() {
925 WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false })
926 } /
927 "$" !['\''] {
928 WordPiece::Text("$".to_owned())
929 }
930
931 rule parameter_expression() -> ParameterExpr =
932 indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? {
933 ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value }
934 } /
935 indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? {
936 ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value }
937 } /
938 indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? {
939 ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message }
940 } /
941 indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? {
942 ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value }
943 } /
944 "#" parameter:parameter() {
945 ParameterExpr::ParameterLength { parameter, indirect: false }
946 } /
947 indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? {
948 ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern }
949 } /
950 indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? {
951 ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern }
952 } /
953 indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? {
954 ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern }
955 } /
956 indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? {
957 ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern }
958 } /
959 non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } /
961 indirect:parameter_indirection() parameter:parameter() {
962 ParameterExpr::Parameter { parameter, indirect }
963 }
964
965 rule parameter_test_type() -> ParameterTestType =
966 colon:":"? {
967 if colon.is_some() {
968 ParameterTestType::UnsetOrNull
969 } else {
970 ParameterTestType::Unset
971 }
972 }
973
974 rule non_posix_parameter_expression() -> ParameterExpr =
975 "!" variable_name:variable_name() "[*]" {
976 ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true }
977 } /
978 "!" variable_name:variable_name() "[@]" {
979 ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false }
980 } /
981 indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? {
982 ParameterExpr::Substring { parameter, indirect, offset, length }
983 } /
984 indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() {
985 ParameterExpr::Transform { parameter, indirect, op }
986 } /
987 "!" prefix:variable_name() "*" {
988 ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true }
989 } /
990 "!" prefix:variable_name() "@" {
991 ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false }
992 } /
993 indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
994 ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix }
995 } /
996 indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
997 ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix }
998 } /
999 indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1000 ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere }
1001 } /
1002 indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
1003 ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence }
1004 } /
1005 indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? {
1006 ParameterExpr::UppercasePattern { parameter, indirect, pattern }
1007 } /
1008 indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? {
1009 ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern }
1010 } /
1011 indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? {
1012 ParameterExpr::LowercasePattern { parameter, indirect, pattern }
1013 } /
1014 indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? {
1015 ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern }
1016 }
1017
1018 rule parameter_indirection() -> bool =
1019 non_posix_extensions_enabled() "!" { true } /
1020 { false }
1021
1022 rule non_posix_parameter_transformation_op() -> ParameterTransformOp =
1023 "U" { ParameterTransformOp::ToUpperCase } /
1024 "u" { ParameterTransformOp::CapitalizeInitial } /
1025 "L" { ParameterTransformOp::ToLowerCase } /
1026 "Q" { ParameterTransformOp::Quoted } /
1027 "E" { ParameterTransformOp::ExpandEscapeSequences } /
1028 "P" { ParameterTransformOp::PromptExpand } /
1029 "A" { ParameterTransformOp::ToAssignmentLogic } /
1030 "K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } /
1031 "a" { ParameterTransformOp::ToAttributeFlags } /
1032 "k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } }
1033
1034
1035 rule unbraced_parameter() -> Parameter =
1036 p:unbraced_positional_parameter() { Parameter::Positional(p) } /
1037 p:special_parameter() { Parameter::Special(p) } /
1038 p:variable_name() { Parameter::Named(p.to_owned()) }
1039
1040 pub(crate) rule parameter() -> Parameter =
1042 p:positional_parameter() { Parameter::Positional(p) } /
1043 p:special_parameter() { Parameter::Special(p) } /
1044 non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } /
1045 non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } /
1046 non_posix_extensions_enabled() p:variable_name() "[" index:array_index() "]" {?
1047 Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() })
1048 } /
1049 p:variable_name() { Parameter::Named(p.to_owned()) }
1050
1051 rule positional_parameter() -> u32 =
1052 n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) }
1053 rule unbraced_positional_parameter() -> u32 =
1054 n:$(['1'..='9']) {? n.parse().or(Err("u32")) }
1055
1056 rule special_parameter() -> SpecialParameter =
1057 "@" { SpecialParameter::AllPositionalParameters { concatenate: false } } /
1058 "*" { SpecialParameter::AllPositionalParameters { concatenate: true } } /
1059 "#" { SpecialParameter::PositionalParameterCount } /
1060 "?" { SpecialParameter::LastExitStatus } /
1061 "-" { SpecialParameter::CurrentOptionFlags } /
1062 "$" { SpecialParameter::ProcessId } /
1063 "!" { SpecialParameter::LastBackgroundProcessId } /
1064 "0" { SpecialParameter::ShellName }
1065
1066 rule variable_name() -> &'input str =
1067 $(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+)
1068
1069 pub(crate) rule command_substitution() -> WordPiece =
1070 "$(" c:command() ")" { WordPiece::CommandSubstitution(c.to_owned()) } /
1071 "`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) }
1072
1073 pub(crate) rule command() -> &'input str =
1074 $(command_piece()*)
1075
1076 pub(crate) rule command_piece() -> () =
1077 word_piece(<[')']>, true ) {} /
1078 ([' ' | '\t'])+ {}
1079
1080 rule backquoted_command() -> String =
1081 chars:(backquoted_char()*) { chars.into_iter().collect() }
1082
1083 rule backquoted_char() -> &'input str =
1084 "\\`" { "`" } /
1085 "\\\\" { "\\\\" } /
1086 s:$([^'`']) { s }
1087
1088 rule arithmetic_expansion() -> WordPiece =
1089 "$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1090
1091 rule legacy_arithmetic_expansion() -> WordPiece =
1092 "$[" e:$(arithmetic_word(<"]">)) "]" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
1093
1094 rule substring_offset() -> ast::UnexpandedArithmeticExpr =
1095 s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1096
1097 rule substring_length() -> ast::UnexpandedArithmeticExpr =
1098 s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
1099
1100 rule parameter_replacement_str() -> String =
1101 "/" s:$(word(<['}']>)) { s.to_owned() }
1102
1103 rule parameter_search_pattern() -> String =
1104 s:$(word(<['}' | '/']>)) { s.to_owned() }
1105
1106 rule parameter_expression_word() -> String =
1107 s:$(word(<['}']>)) { s.to_owned() }
1108
1109 rule extglob_enabled() -> () =
1110 &[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } }
1111
1112 rule non_posix_extensions_enabled() -> () =
1113 &[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
1114
1115 rule tilde_exprs_at_word_start_enabled() -> () =
1116 &[_] {? if parser_options.tilde_expansion_at_word_start { Ok(()) } else { Err("no tilde expansion at word start") } }
1117
1118 rule tilde_exprs_after_colon_enabled() -> () =
1119 &[_] {? if parser_options.tilde_expansion_after_colon { Ok(()) } else { Err("no tilde expansion after colon") } }
1120
1121 pub(crate) rule name_equals_scalar_value() -> ast::Assignment =
1124 nae:name_equals() value:assigned_scalar_value() {
1125 let (name, append) = nae;
1126 ast::Assignment { name, value, append, loc: SourceSpan::default() }
1127 }
1128
1129 pub(crate) rule name_equals() -> (ast::AssignmentName, bool) =
1130 name:assignment_name() append:("+"?) "=" {
1131 (name, append.is_some())
1132 }
1133
1134 pub(crate) rule literal_array_element() -> (Option<String>, String) =
1135 "[" inner:$((!"]" [_])*) "]=" value:$([_]*) {
1136 (Some(inner.to_owned()), value.to_owned())
1137 } /
1138 value:$([_]+) {
1139 (None, value.to_owned())
1140 }
1141
1142 rule assignment_name() -> ast::AssignmentName =
1143 aen:array_element_name() {
1144 let (name, index) = aen;
1145 ast::AssignmentName::ArrayElementName(name.to_owned(), index.to_owned())
1146 } /
1147 name:assigned_scalar_name() {
1148 ast::AssignmentName::VariableName(name.to_owned())
1149 }
1150
1151 rule array_element_name() -> (&'input str, &'input str) =
1152 name:assigned_scalar_name() "[" ai:array_index() "]" { (name, ai) }
1153
1154 rule array_index() -> &'input str =
1155 $(arithmetic_word(<"]">))
1156
1157 rule assigned_scalar_name() -> &'input str =
1158 $(alpha_or_underscore() non_first_variable_char()*)
1159
1160 rule non_first_variable_char() -> () =
1161 ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z'] {}
1162
1163 rule alpha_or_underscore() -> () =
1164 ['_' | 'a'..='z' | 'A'..='Z'] {}
1165
1166 rule assigned_scalar_value() -> ast::AssignmentValue =
1167 v:$([_]*) { ast::AssignmentValue::Scalar(ast::Word::from(v.to_owned())) }
1168 }
1169}
1170
1171#[cfg(test)]
1172#[allow(clippy::panic_in_result_fn)]
1173mod tests {
1174 use super::*;
1175 use anyhow::Result;
1176 use insta::assert_ron_snapshot;
1177 use pretty_assertions::assert_matches;
1178
1179 #[derive(serde::Serialize, serde::Deserialize)]
1180 struct ParseTestResults<'a> {
1181 input: &'a str,
1182 result: Vec<WordPieceWithSource>,
1183 }
1184
1185 fn test_parse(word: &str) -> Result<ParseTestResults<'_>> {
1186 let parsed = super::parse(word, &ParserOptions::default())?;
1187 Ok(ParseTestResults {
1188 input: word,
1189 result: parsed,
1190 })
1191 }
1192
1193 #[test]
1194 fn parse_ansi_c_quoted_text() -> Result<()> {
1195 assert_ron_snapshot!(test_parse(r"$'hi\nthere\t'")?);
1196 Ok(())
1197 }
1198
1199 #[test]
1200 fn parse_ansi_c_quoted_escape_seq() -> Result<()> {
1201 assert_ron_snapshot!(test_parse(r"$'\\'")?);
1202 Ok(())
1203 }
1204
1205 #[test]
1206 fn parse_tilde_after_colon() -> Result<()> {
1207 let opts = ParserOptions {
1208 tilde_expansion_after_colon: true,
1209 ..ParserOptions::default()
1210 };
1211
1212 let parsed = super::parse("a:~", &opts)?;
1213
1214 assert_eq!(parsed.len(), 2);
1216 assert_matches!(parsed[0].piece, WordPiece::Text(_));
1217 assert_matches!(parsed[1].piece, WordPiece::TildeExpansion(_));
1218
1219 Ok(())
1220 }
1221
1222 #[test]
1223 fn parse_double_quoted_text() -> Result<()> {
1224 assert_ron_snapshot!(test_parse(r#""a ${b} c""#)?);
1225 Ok(())
1226 }
1227
1228 #[test]
1229 fn parse_gettext_double_quoted_text() -> Result<()> {
1230 assert_ron_snapshot!(test_parse(r#"$"a ${b} c""#)?);
1231 Ok(())
1232 }
1233
1234 #[test]
1235 fn parse_command_substitution() -> Result<()> {
1236 super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
1237 super::expansion_parser::command_piece("hi", &ParserOptions::default())?;
1238 super::expansion_parser::command("echo hi", &ParserOptions::default())?;
1239 super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?;
1240
1241 assert_ron_snapshot!(test_parse("$(echo hi)")?);
1242
1243 Ok(())
1244 }
1245
1246 #[test]
1247 fn parse_command_substitution_with_embedded_quotes() -> Result<()> {
1248 super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
1249 super::expansion_parser::command_piece(r#""hi""#, &ParserOptions::default())?;
1250 super::expansion_parser::command(r#"echo "hi""#, &ParserOptions::default())?;
1251 super::expansion_parser::command_substitution(
1252 r#"$(echo "hi")"#,
1253 &ParserOptions::default(),
1254 )?;
1255
1256 assert_ron_snapshot!(test_parse(r#"$(echo "hi")"#)?);
1257 Ok(())
1258 }
1259
1260 #[test]
1261 fn parse_command_substitution_with_embedded_extglob() -> Result<()> {
1262 assert_ron_snapshot!(test_parse("$(echo !(x))")?);
1263 Ok(())
1264 }
1265
1266 #[test]
1267 fn parse_backquoted_command() -> Result<()> {
1268 assert_ron_snapshot!(test_parse("`echo hi`")?);
1269 Ok(())
1270 }
1271
1272 #[test]
1273 fn parse_backquoted_command_in_double_quotes() -> Result<()> {
1274 assert_ron_snapshot!(test_parse(r#""`echo hi`""#)?);
1275 Ok(())
1276 }
1277
1278 #[test]
1279 fn parse_extglob_with_embedded_parameter() -> Result<()> {
1280 assert_ron_snapshot!(test_parse("+([$var])")?);
1281 Ok(())
1282 }
1283
1284 #[test]
1285 fn parse_arithmetic_expansion() -> Result<()> {
1286 assert_ron_snapshot!(test_parse("$((0))")?);
1287 Ok(())
1288 }
1289
1290 #[test]
1291 fn parse_arithmetic_expansion_with_parens() -> Result<()> {
1292 assert_ron_snapshot!(test_parse("$((((1+2)*3)))")?);
1293 Ok(())
1294 }
1295
1296 #[test]
1297 fn test_arithmetic_word_parsing() {
1298 let options = ParserOptions::default();
1299
1300 assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok());
1301 assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok());
1302 assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok());
1303 assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok());
1304 assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok());
1305 assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok());
1306 assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok());
1307 assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok());
1308 assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok());
1309 }
1310
1311 #[test]
1312 fn test_arithmetic_word_piece_parsing() {
1313 let options = ParserOptions::default();
1314
1315 assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok());
1316 assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok());
1317 assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok());
1318 assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok());
1319 assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok());
1320 assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok());
1321 assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok());
1322 assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok());
1323 assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok());
1324 assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err());
1325 assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err());
1326 assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err());
1327 }
1328
1329 #[test]
1330 fn test_brace_expansion_parsing() -> Result<()> {
1331 let options = ParserOptions::default();
1332
1333 let inputs = ["x{a,b}y", "{a,b{1,2}}"];
1334
1335 for input in inputs {
1336 assert_ron_snapshot!(super::parse_brace_expansions(input, &options)?.ok_or_else(
1337 || anyhow::anyhow!("Expected brace expansion to be parsed successfully")
1338 )?);
1339 }
1340
1341 Ok(())
1342 }
1343
1344 #[test]
1345 fn parse_assignment_word() -> Result<()> {
1346 super::parse_assignment_word("x=3")?;
1347 super::parse_assignment_word("x=")?;
1348 super::parse_assignment_word("x[3]=a")?;
1349 super::parse_assignment_word("x[${y[3]}]=a")?;
1350 super::parse_assignment_word("x[y[3]]=a")?;
1351 Ok(())
1352 }
1353}