use crate::ParserOptions;
use crate::ast;
use crate::error;
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub struct WordPieceWithSource {
pub piece: WordPiece,
pub start_index: usize,
pub end_index: usize,
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum WordPiece {
Text(String),
SingleQuotedText(String),
AnsiCQuotedText(String),
DoubleQuotedSequence(Vec<WordPieceWithSource>),
GettextDoubleQuotedSequence(Vec<WordPieceWithSource>),
TildePrefix(String),
ParameterExpansion(ParameterExpr),
CommandSubstitution(String),
BackquotedCommandSubstitution(String),
EscapeSequence(String),
ArithmeticExpression(ast::UnexpandedArithmeticExpr),
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum ParameterTestType {
UnsetOrNull,
Unset,
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum Parameter {
Positional(u32),
Special(SpecialParameter),
Named(String),
NamedWithIndex {
name: String,
index: String,
},
NamedWithAllIndices {
name: String,
concatenate: bool,
},
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum SpecialParameter {
AllPositionalParameters {
concatenate: bool,
},
PositionalParameterCount,
LastExitStatus,
CurrentOptionFlags,
ProcessId,
LastBackgroundProcessId,
ShellName,
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum ParameterExpr {
Parameter {
parameter: Parameter,
indirect: bool,
},
UseDefaultValues {
parameter: Parameter,
indirect: bool,
test_type: ParameterTestType,
default_value: Option<String>,
},
AssignDefaultValues {
parameter: Parameter,
indirect: bool,
test_type: ParameterTestType,
default_value: Option<String>,
},
IndicateErrorIfNullOrUnset {
parameter: Parameter,
indirect: bool,
test_type: ParameterTestType,
error_message: Option<String>,
},
UseAlternativeValue {
parameter: Parameter,
indirect: bool,
test_type: ParameterTestType,
alternative_value: Option<String>,
},
ParameterLength {
parameter: Parameter,
indirect: bool,
},
RemoveSmallestSuffixPattern {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
RemoveLargestSuffixPattern {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
RemoveSmallestPrefixPattern {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
RemoveLargestPrefixPattern {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
Substring {
parameter: Parameter,
indirect: bool,
offset: ast::UnexpandedArithmeticExpr,
length: Option<ast::UnexpandedArithmeticExpr>,
},
Transform {
parameter: Parameter,
indirect: bool,
op: ParameterTransformOp,
},
UppercaseFirstChar {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
UppercasePattern {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
LowercaseFirstChar {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
LowercasePattern {
parameter: Parameter,
indirect: bool,
pattern: Option<String>,
},
ReplaceSubstring {
parameter: Parameter,
indirect: bool,
pattern: String,
replacement: Option<String>,
match_kind: SubstringMatchKind,
},
VariableNames {
prefix: String,
concatenate: bool,
},
MemberKeys {
variable_name: String,
concatenate: bool,
},
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum SubstringMatchKind {
Prefix,
Suffix,
FirstOccurrence,
Anywhere,
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum ParameterTransformOp {
CapitalizeInitial,
ExpandEscapeSequences,
PossiblyQuoteWithArraysExpanded {
separate_words: bool,
},
PromptExpand,
Quoted,
ToAssignmentLogic,
ToAttributeFlags,
ToLowerCase,
ToUpperCase,
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum BraceExpressionOrText {
Expr(BraceExpression),
Text(String),
}
pub type BraceExpression = Vec<BraceExpressionMember>;
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub enum BraceExpressionMember {
NumberSequence {
start: i64,
end: i64,
increment: i64,
},
CharSequence {
start: char,
end: char,
increment: i64,
},
Child(Vec<BraceExpressionOrText>),
}
pub fn parse(
word: &str,
options: &ParserOptions,
) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
cacheable_parse(word.to_owned(), options.to_owned())
}
#[cached::proc_macro::cached(size = 64, result = true)]
fn cacheable_parse(
word: String,
options: ParserOptions,
) -> Result<Vec<WordPieceWithSource>, error::WordParseError> {
tracing::debug!(target: "expansion", "Parsing word '{}'", word);
let pieces = expansion_parser::unexpanded_word(word.as_str(), &options)
.map_err(|err| error::WordParseError::Word(word.clone(), err.into()))?;
tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces);
Ok(pieces)
}
pub fn parse_parameter(
word: &str,
options: &ParserOptions,
) -> Result<Parameter, error::WordParseError> {
expansion_parser::parameter(word, options)
.map_err(|err| error::WordParseError::Parameter(word.to_owned(), err.into()))
}
pub fn parse_brace_expansions(
word: &str,
options: &ParserOptions,
) -> Result<Option<Vec<BraceExpressionOrText>>, error::WordParseError> {
expansion_parser::brace_expansions(word, options)
.map_err(|err| error::WordParseError::BraceExpansion(word.to_owned(), err.into()))
}
peg::parser! {
grammar expansion_parser(parser_options: &ParserOptions) for str {
rule traced<T>(e: rule<T>) -> T =
&(input:$([_]*) {
#[cfg(feature = "debug-tracing")]
println!("[PEG_INPUT_START]\n{input}\n[PEG_TRACE_START]");
})
e:e()? {?
#[cfg(feature = "debug-tracing")]
println!("[PEG_TRACE_STOP]");
e.ok_or("")
}
pub(crate) rule unexpanded_word() -> Vec<WordPieceWithSource> = traced(<word(<![_]>)>)
rule word<T>(stop_condition: rule<T>) -> Vec<WordPieceWithSource> =
tilde:tilde_prefix_with_source()? pieces:word_piece_with_source(<stop_condition()>, false )* {
let mut all_pieces = Vec::new();
if let Some(tilde) = tilde {
all_pieces.push(tilde);
}
all_pieces.extend(pieces);
all_pieces
}
pub(crate) rule brace_expansions() -> Option<Vec<BraceExpressionOrText>> =
pieces:(brace_expansion_piece(<![_]>)+) { Some(pieces) } /
[_]* { None }
rule brace_expansion_piece<T>(stop_condition: rule<T>) -> BraceExpressionOrText =
expr:brace_expr() {
BraceExpressionOrText::Expr(expr)
} /
text:$(non_brace_expr_text(<stop_condition()>)+) { BraceExpressionOrText::Text(text.to_owned()) }
rule non_brace_expr_text<T>(stop_condition: rule<T>) -> () =
!"{" word_piece(<['{'] {} / stop_condition() {}>, false) {} /
!brace_expr() !stop_condition() "{" {}
pub(crate) rule brace_expr() -> BraceExpression =
"{" inner:brace_expr_inner() "}" { inner }
pub(crate) rule brace_expr_inner() -> BraceExpression =
brace_text_list_expr() /
seq:brace_sequence_expr() { vec![seq] }
pub(crate) rule brace_text_list_expr() -> BraceExpression =
brace_text_list_member() **<2,> ","
pub(crate) rule brace_text_list_member() -> BraceExpressionMember =
&[',' | '}'] { BraceExpressionMember::Child(vec![BraceExpressionOrText::Text(String::new())]) } /
child_pieces:(brace_expansion_piece(<[',' | '}']>)+) {
BraceExpressionMember::Child(child_pieces)
}
pub(crate) rule brace_sequence_expr() -> BraceExpressionMember =
start:number() ".." end:number() increment:(".." n:number() { n })? {
BraceExpressionMember::NumberSequence { start, end, increment: increment.unwrap_or(1) }
} /
start:character() ".." end:character() increment:(".." n:number() { n })? {
BraceExpressionMember::CharSequence { start, end, increment: increment.unwrap_or(1) }
}
rule number() -> i64 = sign:number_sign()? n:$(['0'..='9']+) {
let sign = sign.unwrap_or(1);
let num: i64 = n.parse().unwrap();
num * sign
}
rule number_sign() -> i64 =
['-'] { -1 } /
['+'] { 1 }
rule character() -> char = ['a'..='z' | 'A'..='Z']
pub(crate) rule is_arithmetic_word() =
arithmetic_word(<![_]>)
rule arithmetic_word<T>(stop_condition: rule<T>) =
arithmetic_word_piece(<stop_condition()>)* {}
pub(crate) rule is_arithmetic_word_piece() =
arithmetic_word_piece(<![_]>)
rule arithmetic_word_piece<T>(stop_condition: rule<T>) =
"(" arithmetic_word_plus_right_paren() {} /
!"(" word_piece(<param_rule_or_open_paren(<stop_condition()>)>, false ) {}
rule param_rule_or_open_paren<T>(stop_condition: rule<T>) -> () =
stop_condition() {} /
"(" {}
rule arithmetic_word_plus_right_paren() =
arithmetic_word(<[')']>) ")" /
rule word_piece_with_source<T>(stop_condition: rule<T>, in_command: bool) -> WordPieceWithSource =
start_index:position!() piece:word_piece(<stop_condition()>, in_command) end_index:position!() {
WordPieceWithSource { piece, start_index, end_index }
}
rule word_piece<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
s:gettext_double_quoted_sequence() { WordPiece::GettextDoubleQuotedSequence(s) } /
dollar_sign_word_piece() /
normal_escape_sequence() /
unquoted_literal_text(<stop_condition()>, in_command)
rule dollar_sign_word_piece() -> WordPiece =
arithmetic_expansion() /
command_substitution() /
parameter_expansion()
rule double_quoted_word_piece() -> WordPiece =
arithmetic_expansion() /
command_substitution() /
parameter_expansion() /
double_quoted_escape_sequence() /
double_quoted_text()
rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
"\"" i:double_quoted_sequence_inner()* "\"" { i }
rule gettext_double_quoted_sequence() -> Vec<WordPieceWithSource> =
"$\"" i:double_quoted_sequence_inner()* "\"" { i }
rule double_quoted_sequence_inner() -> WordPieceWithSource =
start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
WordPieceWithSource {
piece,
start_index,
end_index
}
}
rule single_quoted_literal_text() -> &'input str =
"\'" inner:$([^'\'']*) "\'" { inner }
rule ansi_c_quoted_text() -> &'input str =
"$\'" inner:$(("\\'" / [^'\''])*) "\'" { inner }
rule unquoted_literal_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
s:$(unquoted_literal_text_piece(<stop_condition()>, in_command)+) { WordPiece::Text(s.to_owned()) }
rule unquoted_literal_text_piece<T>(stop_condition: rule<T>, in_command: bool) =
is_true(in_command) extglob_pattern() /
is_true(in_command) subshell_command() /
!stop_condition() !normal_escape_sequence() [^'\'' | '\"' | '$' | '`'] {}
rule is_true(value: bool) = &[_] {? if value { Ok(()) } else { Err("not true") } }
rule extglob_pattern() =
("@" / "!" / "?" / "+" / "*") "(" extglob_body_piece()* ")" {}
rule extglob_body_piece() =
word_piece(<[')']>, true ) {}
rule subshell_command() =
"(" command() ")" {}
rule double_quoted_text() -> WordPiece =
s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
rule double_quote_body_text() -> &'input str =
$((!double_quoted_escape_sequence() !dollar_sign_word_piece() [^'\"'])+)
rule normal_escape_sequence() -> WordPiece =
s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
rule double_quoted_escape_sequence() -> WordPiece =
s:$("\\" ['$' | '`' | '\"' | '\\']) { WordPiece::EscapeSequence(s.to_owned()) }
rule tilde_prefix_with_source() -> WordPieceWithSource =
start_index:position!() piece:tilde_prefix() end_index:position!() {
WordPieceWithSource {
piece,
start_index,
end_index
}
}
rule tilde_prefix() -> WordPiece =
tilde_parsing_enabled() "~" cs:$((!['/' | ':' | ';'] [c])*) { WordPiece::TildePrefix(cs.to_owned()) }
rule parameter_expansion() -> WordPiece =
"${" e:parameter_expression() "}" {
WordPiece::ParameterExpansion(e)
} /
"$" parameter:unbraced_parameter() {
WordPiece::ParameterExpansion(ParameterExpr::Parameter { parameter, indirect: false })
} /
"$" !['\''] {
WordPiece::Text("$".to_owned())
}
rule parameter_expression() -> ParameterExpr =
indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "-" default_value:parameter_expression_word()? {
ParameterExpr::UseDefaultValues { parameter, indirect, test_type, default_value }
} /
indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "=" default_value:parameter_expression_word()? {
ParameterExpr::AssignDefaultValues { parameter, indirect, test_type, default_value }
} /
indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "?" error_message:parameter_expression_word()? {
ParameterExpr::IndicateErrorIfNullOrUnset { parameter, indirect, test_type, error_message }
} /
indirect:parameter_indirection() parameter:parameter() test_type:parameter_test_type() "+" alternative_value:parameter_expression_word()? {
ParameterExpr::UseAlternativeValue { parameter, indirect, test_type, alternative_value }
} /
"#" parameter:parameter() {
ParameterExpr::ParameterLength { parameter, indirect: false }
} /
indirect:parameter_indirection() parameter:parameter() "%%" pattern:parameter_expression_word()? {
ParameterExpr::RemoveLargestSuffixPattern { parameter, indirect, pattern }
} /
indirect:parameter_indirection() parameter:parameter() "%" pattern:parameter_expression_word()? {
ParameterExpr::RemoveSmallestSuffixPattern { parameter, indirect, pattern }
} /
indirect:parameter_indirection() parameter:parameter() "##" pattern:parameter_expression_word()? {
ParameterExpr::RemoveLargestPrefixPattern { parameter, indirect, pattern }
} /
indirect:parameter_indirection() parameter:parameter() "#" pattern:parameter_expression_word()? {
ParameterExpr::RemoveSmallestPrefixPattern { parameter, indirect, pattern }
} /
non_posix_extensions_enabled() e:non_posix_parameter_expression() { e } /
indirect:parameter_indirection() parameter:parameter() {
ParameterExpr::Parameter { parameter, indirect }
}
rule parameter_test_type() -> ParameterTestType =
colon:":"? {
if colon.is_some() {
ParameterTestType::UnsetOrNull
} else {
ParameterTestType::Unset
}
}
rule non_posix_parameter_expression() -> ParameterExpr =
"!" variable_name:variable_name() "[*]" {
ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: true }
} /
"!" variable_name:variable_name() "[@]" {
ParameterExpr::MemberKeys { variable_name: variable_name.to_owned(), concatenate: false }
} /
indirect:parameter_indirection() parameter:parameter() ":" offset:substring_offset() length:(":" l:substring_length() { l })? {
ParameterExpr::Substring { parameter, indirect, offset, length }
} /
indirect:parameter_indirection() parameter:parameter() "@" op:non_posix_parameter_transformation_op() {
ParameterExpr::Transform { parameter, indirect, op }
} /
"!" prefix:variable_name() "*" {
ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: true }
} /
"!" prefix:variable_name() "@" {
ParameterExpr::VariableNames { prefix: prefix.to_owned(), concatenate: false }
} /
indirect:parameter_indirection() parameter:parameter() "/#" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Prefix }
} /
indirect:parameter_indirection() parameter:parameter() "/%" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Suffix }
} /
indirect:parameter_indirection() parameter:parameter() "//" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::Anywhere }
} /
indirect:parameter_indirection() parameter:parameter() "/" pattern:parameter_search_pattern() replacement:parameter_replacement_str()? {
ParameterExpr::ReplaceSubstring { parameter, indirect, pattern, replacement, match_kind: SubstringMatchKind::FirstOccurrence }
} /
indirect:parameter_indirection() parameter:parameter() "^^" pattern:parameter_expression_word()? {
ParameterExpr::UppercasePattern { parameter, indirect, pattern }
} /
indirect:parameter_indirection() parameter:parameter() "^" pattern:parameter_expression_word()? {
ParameterExpr::UppercaseFirstChar { parameter, indirect, pattern }
} /
indirect:parameter_indirection() parameter:parameter() ",," pattern:parameter_expression_word()? {
ParameterExpr::LowercasePattern { parameter, indirect, pattern }
} /
indirect:parameter_indirection() parameter:parameter() "," pattern:parameter_expression_word()? {
ParameterExpr::LowercaseFirstChar { parameter, indirect, pattern }
}
rule parameter_indirection() -> bool =
non_posix_extensions_enabled() "!" { true } /
{ false }
rule non_posix_parameter_transformation_op() -> ParameterTransformOp =
"U" { ParameterTransformOp::ToUpperCase } /
"u" { ParameterTransformOp::CapitalizeInitial } /
"L" { ParameterTransformOp::ToLowerCase } /
"Q" { ParameterTransformOp::Quoted } /
"E" { ParameterTransformOp::ExpandEscapeSequences } /
"P" { ParameterTransformOp::PromptExpand } /
"A" { ParameterTransformOp::ToAssignmentLogic } /
"K" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: false } } /
"a" { ParameterTransformOp::ToAttributeFlags } /
"k" { ParameterTransformOp::PossiblyQuoteWithArraysExpanded { separate_words: true } }
rule unbraced_parameter() -> Parameter =
p:unbraced_positional_parameter() { Parameter::Positional(p) } /
p:special_parameter() { Parameter::Special(p) } /
p:variable_name() { Parameter::Named(p.to_owned()) }
pub(crate) rule parameter() -> Parameter =
p:positional_parameter() { Parameter::Positional(p) } /
p:special_parameter() { Parameter::Special(p) } /
non_posix_extensions_enabled() p:variable_name() "[@]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: false } } /
non_posix_extensions_enabled() p:variable_name() "[*]" { Parameter::NamedWithAllIndices { name: p.to_owned(), concatenate: true } } /
non_posix_extensions_enabled() p:variable_name() "[" index:$(arithmetic_word(<"]">)) "]" {?
Ok(Parameter::NamedWithIndex { name: p.to_owned(), index: index.to_owned() })
} /
p:variable_name() { Parameter::Named(p.to_owned()) }
rule positional_parameter() -> u32 =
n:$(['1'..='9'](['0'..='9']*)) {? n.parse().or(Err("u32")) }
rule unbraced_positional_parameter() -> u32 =
n:$(['1'..='9']) {? n.parse().or(Err("u32")) }
rule special_parameter() -> SpecialParameter =
"@" { SpecialParameter::AllPositionalParameters { concatenate: false } } /
"*" { SpecialParameter::AllPositionalParameters { concatenate: true } } /
"#" { SpecialParameter::PositionalParameterCount } /
"?" { SpecialParameter::LastExitStatus } /
"-" { SpecialParameter::CurrentOptionFlags } /
"$" { SpecialParameter::ProcessId } /
"!" { SpecialParameter::LastBackgroundProcessId } /
"0" { SpecialParameter::ShellName }
rule variable_name() -> &'input str =
$(!['0'..='9'] ['_' | '0'..='9' | 'a'..='z' | 'A'..='Z']+)
pub(crate) rule command_substitution() -> WordPiece =
"$(" c:command() ")" { WordPiece::CommandSubstitution(c.to_owned()) } /
"`" c:backquoted_command() "`" { WordPiece::BackquotedCommandSubstitution(c) }
pub(crate) rule command() -> &'input str =
$(command_piece()*)
pub(crate) rule command_piece() -> () =
word_piece(<[')']>, true ) {} /
([' ' | '\t'])+ {}
rule backquoted_command() -> String =
chars:(backquoted_char()*) { chars.into_iter().collect() }
rule backquoted_char() -> &'input str =
"\\`" { "`" } /
"\\\\" { "\\\\" } /
s:$([^'`']) { s }
rule arithmetic_expansion() -> WordPiece =
"$((" e:$(arithmetic_word(<"))">)) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) }
rule substring_offset() -> ast::UnexpandedArithmeticExpr =
s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
rule substring_length() -> ast::UnexpandedArithmeticExpr =
s:$(arithmetic_word(<[':' | '}']>)) { ast::UnexpandedArithmeticExpr { value: s.to_owned() } }
rule parameter_replacement_str() -> String =
"/" s:$(word(<['}']>)) { s.to_owned() }
rule parameter_search_pattern() -> String =
s:$(word(<['}' | '/']>)) { s.to_owned() }
rule parameter_expression_word() -> String =
s:$(word(<['}']>)) { s.to_owned() }
rule extglob_enabled() -> () =
&[_] {? if parser_options.enable_extended_globbing { Ok(()) } else { Err("no extglob") } }
rule non_posix_extensions_enabled() -> () =
&[_] {? if !parser_options.sh_mode { Ok(()) } else { Err("posix") } }
rule tilde_parsing_enabled() -> () =
&[_] {? if parser_options.tilde_expansion { Ok(()) } else { Err("no tilde expansion") } }
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use insta::assert_ron_snapshot;
#[derive(serde::Serialize)]
struct ParseTestResults<'a> {
input: &'a str,
result: Vec<WordPieceWithSource>,
}
fn test_parse(word: &str) -> Result<ParseTestResults<'_>> {
let parsed = super::parse(word, &ParserOptions::default())?;
Ok(ParseTestResults {
input: word,
result: parsed,
})
}
#[test]
fn parse_ansi_c_quoted_text() -> Result<()> {
assert_ron_snapshot!(test_parse(r"$'hi\nthere\t'")?);
Ok(())
}
#[test]
fn parse_double_quoted_text() -> Result<()> {
assert_ron_snapshot!(test_parse(r#""a ${b} c""#)?);
Ok(())
}
#[test]
fn parse_gettext_double_quoted_text() -> Result<()> {
assert_ron_snapshot!(test_parse(r#"$"a ${b} c""#)?);
Ok(())
}
#[test]
fn parse_command_substitution() -> Result<()> {
super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
super::expansion_parser::command_piece("hi", &ParserOptions::default())?;
super::expansion_parser::command("echo hi", &ParserOptions::default())?;
super::expansion_parser::command_substitution("$(echo hi)", &ParserOptions::default())?;
assert_ron_snapshot!(test_parse("$(echo hi)")?);
Ok(())
}
#[test]
fn parse_command_substitution_with_embedded_quotes() -> Result<()> {
super::expansion_parser::command_piece("echo", &ParserOptions::default())?;
super::expansion_parser::command_piece(r#""hi""#, &ParserOptions::default())?;
super::expansion_parser::command(r#"echo "hi""#, &ParserOptions::default())?;
super::expansion_parser::command_substitution(
r#"$(echo "hi")"#,
&ParserOptions::default(),
)?;
assert_ron_snapshot!(test_parse(r#"$(echo "hi")"#)?);
Ok(())
}
#[test]
fn parse_command_substitution_with_embedded_extglob() -> Result<()> {
assert_ron_snapshot!(test_parse("$(echo !(x))")?);
Ok(())
}
#[test]
fn parse_backquoted_command() -> Result<()> {
assert_ron_snapshot!(test_parse("`echo hi`")?);
Ok(())
}
#[test]
fn parse_backquoted_command_in_double_quotes() -> Result<()> {
assert_ron_snapshot!(test_parse(r#""`echo hi`""#)?);
Ok(())
}
#[test]
fn parse_extglob_with_embedded_parameter() -> Result<()> {
assert_ron_snapshot!(test_parse("+([$var])")?);
Ok(())
}
#[test]
fn parse_arithmetic_expansion() -> Result<()> {
assert_ron_snapshot!(test_parse("$((0))")?);
Ok(())
}
#[test]
fn parse_arithmetic_expansion_with_parens() -> Result<()> {
assert_ron_snapshot!(test_parse("$((((1+2)*3)))")?);
Ok(())
}
#[test]
fn test_arithmetic_word_parsing() {
let options = ParserOptions::default();
assert!(super::expansion_parser::is_arithmetic_word("a", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word("b", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word(" a + b ", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word("(a)", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word("((a))", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word("(((a)))", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word("(1+2)", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word("(1+2)*3", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word("((1+2)*3)", &options).is_ok());
}
#[test]
fn test_arithmetic_word_piece_parsing() {
let options = ParserOptions::default();
assert!(super::expansion_parser::is_arithmetic_word_piece("a", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("b", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece(" a + b ", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("(a)", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("((a))", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("(((a)))", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("(1+2)", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2))", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("((1+2)*3)", &options).is_ok());
assert!(super::expansion_parser::is_arithmetic_word_piece("(a", &options).is_err());
assert!(super::expansion_parser::is_arithmetic_word_piece("(a))", &options).is_err());
assert!(super::expansion_parser::is_arithmetic_word_piece("((a)", &options).is_err());
}
#[test]
fn test_brace_expansion_parsing() -> Result<()> {
let options = ParserOptions::default();
let inputs = ["x{a,b}y", "{a,b{1,2}}"];
for input in inputs {
assert_ron_snapshot!(super::parse_brace_expansions(input, &options)?.ok_or_else(
|| anyhow::anyhow!("Expected brace expansion to be parsed successfully")
)?);
}
Ok(())
}
}