Skip to content

Commit dcb59db

Browse files
authored
fix: improve parsing of quotes in command substitution (#155)
1 parent d0008f4 commit dcb59db

File tree

1 file changed

+176
-72
lines changed

1 file changed

+176
-72
lines changed

src/parser.rs

Lines changed: 176 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

3-
use std::borrow::Cow;
4-
53
use anyhow::Result;
64
use anyhow::bail;
75
use monch::*;
@@ -694,72 +692,81 @@ fn parse_single_quoted_string(input: &str) -> ParseResult<&str> {
694692
}
695693

696694
fn parse_double_quoted_string(input: &str) -> ParseResult<Vec<WordPart>> {
697-
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
698-
parse_surrounded_expression(
699-
input,
700-
'"',
701-
"Expected closing double quote.",
702-
parse_word_parts(ParseWordPartsMode::DoubleQuotes),
703-
)
704-
}
695+
fn parse_words_within(input: &str) -> ParseResult<Vec<WordPart>> {
696+
match parse_word_parts(ParseWordPartsMode::DoubleQuotes)(input) {
697+
Ok((result_input, parts)) => {
698+
if !result_input.is_empty() {
699+
return ParseError::fail(
700+
input,
701+
format!(
702+
"Failed parsing within double quotes. Unexpected character: {}",
703+
result_input
704+
),
705+
);
706+
}
707+
Ok((result_input, parts))
708+
}
709+
Err(err) => ParseError::fail(
710+
input,
711+
format!(
712+
"Failed parsing within double quotes. {}",
713+
match &err {
714+
ParseError::Backtrace => "Could not determine expression.",
715+
ParseError::Failure(parse_error_failure) =>
716+
parse_error_failure.message.as_str(),
717+
}
718+
),
719+
),
720+
}
721+
}
705722

706-
fn parse_surrounded_expression<'a, TResult>(
707-
input: &'a str,
708-
surrounded_char: char,
709-
fail_message: &str,
710-
parse: impl Fn(&str) -> ParseResult<TResult>,
711-
) -> ParseResult<'a, TResult> {
723+
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
712724
let start_input = input;
713-
let (input, _) = ch(surrounded_char)(input)?;
725+
let (mut input, _) = ch('"')(input)?;
714726
let mut was_escape = false;
715-
for (index, c) in input.char_indices() {
727+
let mut pending_parts = Vec::new();
728+
let mut iter = input.char_indices().peekable();
729+
while let Some((index, c)) = iter.next() {
716730
match c {
717-
c if c == surrounded_char && !was_escape => {
731+
c if c == '$'
732+
&& !was_escape
733+
&& iter.peek().map(|(_, c)| *c) == Some('(') =>
734+
{
735+
let previous_input = &input[..index];
736+
pending_parts.extend(parse_words_within(previous_input)?.1);
737+
let next_input = &input[index..];
738+
let (next_input, sequence) = with_error_context(
739+
parse_command_substitution,
740+
"Failed parsing command substitution in double quoted string.",
741+
)(next_input)?;
742+
pending_parts.push(WordPart::Command(sequence));
743+
iter = next_input.char_indices().peekable();
744+
input = next_input;
745+
}
746+
c if c == '`' && !was_escape => {
747+
let previous_input = &input[..index];
748+
pending_parts.extend(parse_words_within(previous_input)?.1);
749+
let next_input = &input[index..];
750+
let (next_input, sequence) = with_error_context(
751+
parse_backticks_command_substitution,
752+
"Failed parsing backticks in double quoted string.",
753+
)(next_input)?;
754+
pending_parts.push(WordPart::Command(sequence));
755+
iter = next_input.char_indices().peekable();
756+
input = next_input;
757+
}
758+
c if c == '"' && !was_escape => {
718759
let inner_input = &input[..index];
719-
let inner_input =
720-
if surrounded_char == '`' && inner_input.contains("\\`") {
721-
Cow::Owned(inner_input.replace("\\`", "`"))
722-
} else {
723-
Cow::Borrowed(inner_input)
724-
};
725-
let parts = match parse(&inner_input) {
726-
Ok((result_input, parts)) => {
727-
if !result_input.is_empty() {
728-
return ParseError::fail(
729-
input,
730-
format!(
731-
"Failed parsing within {}. Unexpected character: {}",
732-
if c == '`' {
733-
"backticks"
734-
} else {
735-
"double quotes"
736-
},
737-
result_input
738-
),
739-
);
740-
}
760+
let (_, parts) = parse_words_within(inner_input)?;
761+
return Ok((
762+
&input[index + 1..],
763+
if pending_parts.is_empty() {
741764
parts
742-
}
743-
Err(err) => {
744-
return ParseError::fail(
745-
input,
746-
format!(
747-
"Failed parsing within {}. {}",
748-
if c == '`' {
749-
"backticks"
750-
} else {
751-
"double quotes"
752-
},
753-
match &err {
754-
ParseError::Backtrace => "Could not determine expression.",
755-
ParseError::Failure(parse_error_failure) =>
756-
parse_error_failure.message.as_str(),
757-
}
758-
),
759-
);
760-
}
761-
};
762-
return Ok((&input[index + 1..], parts));
765+
} else {
766+
pending_parts.extend(parts);
767+
pending_parts
768+
},
769+
));
763770
}
764771
'\\' => {
765772
was_escape = true;
@@ -770,7 +777,7 @@ fn parse_surrounded_expression<'a, TResult>(
770777
}
771778
}
772779

773-
ParseError::fail(start_input, fail_message)
780+
ParseError::fail(start_input, "Expected closing double quote.")
774781
}
775782

776783
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -941,12 +948,58 @@ fn parse_command_substitution(input: &str) -> ParseResult<SequentialList> {
941948
fn parse_backticks_command_substitution(
942949
input: &str,
943950
) -> ParseResult<SequentialList> {
944-
parse_surrounded_expression(
945-
input,
946-
'`',
947-
"Expected closing backtick.",
948-
parse_sequential_list,
949-
)
951+
let start_input = input;
952+
let (input, _) = ch('`')(input)?;
953+
let mut was_escape = false;
954+
for (index, c) in input.char_indices() {
955+
match c {
956+
c if c == '`' && !was_escape => {
957+
let inner_input = &input[..index];
958+
let inner_input = inner_input.replace("\\`", "`");
959+
let parts = match parse_sequential_list(&inner_input) {
960+
Ok((result_input, parts)) => {
961+
if !result_input.is_empty() {
962+
return ParseError::fail(
963+
input,
964+
format!(
965+
"Failed parsing within backticks. Unexpected character: {}",
966+
result_input
967+
),
968+
);
969+
}
970+
parts
971+
}
972+
Err(err) => {
973+
return ParseError::fail(
974+
input,
975+
format!(
976+
"Failed parsing within {}. {}",
977+
if c == '`' {
978+
"backticks"
979+
} else {
980+
"double quotes"
981+
},
982+
match &err {
983+
ParseError::Backtrace => "Could not determine expression.",
984+
ParseError::Failure(parse_error_failure) =>
985+
parse_error_failure.message.as_str(),
986+
}
987+
),
988+
);
989+
}
990+
};
991+
return Ok((&input[index + 1..], parts));
992+
}
993+
'\\' => {
994+
was_escape = true;
995+
}
996+
_ => {
997+
was_escape = false;
998+
}
999+
}
1000+
}
1001+
1002+
ParseError::fail(start_input, "Expected closing backtick.")
9501003
}
9511004

9521005
fn parse_subshell(input: &str) -> ParseResult<SequentialList> {
@@ -1066,8 +1119,10 @@ mod test {
10661119
.unwrap()
10671120
.to_string(),
10681121
concat!(
1069-
"Failed parsing within double quotes. Expected closing parenthesis for command substitution.\n",
1070-
" test$(echo testing\"\n",
1122+
"Failed parsing command substitution in double quoted string.\n",
1123+
"\n",
1124+
"Expected closing double quote.\n",
1125+
" \"\n",
10711126
" ~"
10721127
),
10731128
);
@@ -1528,7 +1583,9 @@ mod test {
15281583
run_test(
15291584
parse_quoted_string,
15301585
r#""asdf`""#,
1531-
Err("Failed parsing within double quotes. Expected closing backtick."),
1586+
Err(
1587+
"Failed parsing backticks in double quoted string.\n\nExpected closing backtick.",
1588+
),
15321589
);
15331590

15341591
run_test_with_end(
@@ -1537,6 +1594,53 @@ mod test {
15371594
Ok(vec![WordPart::Text("test".to_string())]),
15381595
" asdf",
15391596
);
1597+
1598+
run_test(
1599+
parse_quoted_string,
1600+
r#""test $(deno eval 'console.info("test")') test `backticks "test"` test""#,
1601+
Ok(vec![
1602+
WordPart::Text("test ".to_string()),
1603+
WordPart::Command(SequentialList {
1604+
items: Vec::from([SequentialListItem {
1605+
is_async: false,
1606+
sequence: Sequence::Pipeline(Pipeline {
1607+
negated: false,
1608+
inner: PipelineInner::Command(Command {
1609+
redirect: None,
1610+
inner: CommandInner::Simple(SimpleCommand {
1611+
env_vars: vec![],
1612+
args: Vec::from([
1613+
Word::new_word("deno"),
1614+
Word::new_word("eval"),
1615+
Word::new_string("console.info(\"test\")"),
1616+
]),
1617+
}),
1618+
}),
1619+
}),
1620+
}]),
1621+
}),
1622+
WordPart::Text(" test ".to_string()),
1623+
WordPart::Command(SequentialList {
1624+
items: Vec::from([SequentialListItem {
1625+
is_async: false,
1626+
sequence: Sequence::Pipeline(Pipeline {
1627+
negated: false,
1628+
inner: PipelineInner::Command(Command {
1629+
redirect: None,
1630+
inner: CommandInner::Simple(SimpleCommand {
1631+
env_vars: vec![],
1632+
args: Vec::from([
1633+
Word::new_word("backticks"),
1634+
Word::new_string("test"),
1635+
]),
1636+
}),
1637+
}),
1638+
}),
1639+
}]),
1640+
}),
1641+
WordPart::Text(" test".to_string()),
1642+
]),
1643+
);
15401644
}
15411645

15421646
#[test]

0 commit comments

Comments
 (0)