Skip to content

Commit fe78353

Browse files
authored
feat: support backticks (#147)
1 parent 0d11591 commit fe78353

File tree

2 files changed

+183
-27
lines changed

2 files changed

+183
-27
lines changed

src/parser.rs

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

3+
use std::borrow::Cow;
4+
35
use anyhow::Result;
46
use anyhow::bail;
57
use monch::*;
@@ -693,15 +695,82 @@ fn parse_single_quoted_string(input: &str) -> ParseResult<&str> {
693695

694696
fn parse_double_quoted_string(input: &str) -> ParseResult<Vec<WordPart>> {
695697
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
696-
// Double quotes may have escaped
697-
delimited(
698-
ch('"'),
698+
parse_surrounded_expression(
699+
input,
700+
'"',
701+
"Expected closing double quote.",
699702
parse_word_parts(ParseWordPartsMode::DoubleQuotes),
700-
with_failure_input(
701-
input,
702-
assert_exists(ch('"'), "Expected closing double quote."),
703-
),
704-
)(input)
703+
)
704+
}
705+
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> {
712+
let start_input = input;
713+
let (input, _) = ch(surrounded_char)(input)?;
714+
let mut was_escape = false;
715+
for (index, c) in input.char_indices() {
716+
match c {
717+
c if c == surrounded_char && !was_escape => {
718+
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+
}
741+
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));
763+
}
764+
'\\' => {
765+
was_escape = true;
766+
}
767+
_ => {
768+
was_escape = false;
769+
}
770+
}
771+
}
772+
773+
ParseError::fail(start_input, fail_message)
705774
}
706775

707776
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -790,15 +859,9 @@ fn parse_word_parts(
790859
map(first_escaped_char(mode), PendingPart::Char),
791860
map(parse_command_substitution, PendingPart::Command),
792861
),
862+
map(parse_backticks_command_substitution, PendingPart::Command),
793863
map(ch('~'), |_| PendingPart::Tilde),
794864
map(preceded(ch('$'), parse_env_var_name), PendingPart::Variable),
795-
|input| {
796-
let (_, _) = ch('`')(input)?;
797-
ParseError::fail(
798-
input,
799-
"Back ticks in strings is currently not supported.",
800-
)
801-
},
802865
// words can have escaped spaces
803866
map(
804867
if_true(preceded(ch('\\'), ch(' ')), |_| {
@@ -862,7 +925,28 @@ fn parse_word_parts(
862925
}
863926

864927
fn parse_command_substitution(input: &str) -> ParseResult<SequentialList> {
865-
delimited(tag("$("), parse_sequential_list, ch(')'))(input)
928+
delimited(
929+
tag("$("),
930+
parse_sequential_list,
931+
with_failure_input(
932+
input,
933+
assert_exists(
934+
ch(')'),
935+
"Expected closing parenthesis for command substitution.",
936+
),
937+
),
938+
)(input)
939+
}
940+
941+
fn parse_backticks_command_substitution(
942+
input: &str,
943+
) -> ParseResult<SequentialList> {
944+
parse_surrounded_expression(
945+
input,
946+
'`',
947+
"Expected closing backtick.",
948+
parse_sequential_list,
949+
)
866950
}
867951

868952
fn parse_subshell(input: &str) -> ParseResult<SequentialList> {
@@ -976,19 +1060,21 @@ mod test {
9761060
parse("cmd 'test").err().unwrap().to_string(),
9771061
concat!("Expected closing single quote.\n", " 'test\n", " ~"),
9781062
);
979-
980-
assert!(parse("( test ||other&&test;test);(t&est );").is_ok());
981-
assert!(parse("command --arg='value'").is_ok());
982-
assert!(parse("command --arg=\"value\"").is_ok());
983-
9841063
assert_eq!(
985-
parse("echo `echo 1`").err().unwrap().to_string(),
1064+
parse("cmd \"test$(echo testing\"")
1065+
.err()
1066+
.unwrap()
1067+
.to_string(),
9861068
concat!(
987-
"Back ticks in strings is currently not supported.\n",
988-
" `echo 1`\n",
989-
" ~",
1069+
"Failed parsing within double quotes. Expected closing parenthesis for command substitution.\n",
1070+
" test$(echo testing\"\n",
1071+
" ~"
9901072
),
9911073
);
1074+
1075+
assert!(parse("( test ||other&&test;test);(t&est );").is_ok());
1076+
assert!(parse("command --arg='value'").is_ok());
1077+
assert!(parse("command --arg=\"value\"").is_ok());
9921078
assert!(
9931079
parse("deno run --allow-read=. --allow-write=./testing main.ts").is_ok(),
9941080
);
@@ -1442,7 +1528,7 @@ mod test {
14421528
run_test(
14431529
parse_quoted_string,
14441530
r#""asdf`""#,
1445-
Err("Back ticks in strings is currently not supported."),
1531+
Err("Failed parsing within double quotes. Expected closing backtick."),
14461532
);
14471533

14481534
run_test_with_end(

tests/integration_test.rs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ async fn exit() {
207207
.await;
208208
}
209209

210+
#[tokio::test]
211+
async fn double_quotes() {
212+
// escaped
213+
TestBuilder::new()
214+
.command("echo \"testing\\\" this out\"")
215+
.assert_stdout("testing\" this out\n")
216+
.run()
217+
.await;
218+
}
219+
210220
#[tokio::test]
211221
async fn async_commands() {
212222
TestBuilder::new()
@@ -281,7 +291,7 @@ async fn async_commands() {
281291
}
282292

283293
#[tokio::test]
284-
async fn command_substition() {
294+
async fn command_substitution() {
285295
TestBuilder::new()
286296
.command("echo $(echo 1)")
287297
.assert_stdout("1\n")
@@ -294,6 +304,19 @@ async fn command_substition() {
294304
.run()
295305
.await;
296306

307+
TestBuilder::new()
308+
.command("echo \"hi $(echo 1)\"")
309+
.assert_stdout("hi 1\n")
310+
.run()
311+
.await;
312+
313+
// nested
314+
TestBuilder::new()
315+
.command("echo $(echo $(echo 1))")
316+
.assert_stdout("1\n")
317+
.run()
318+
.await;
319+
297320
// async inside subshell should wait
298321
TestBuilder::new()
299322
.command("$(sleep 0.1 && echo 1 & echo echo) 2")
@@ -308,6 +331,53 @@ async fn command_substition() {
308331
.await;
309332
}
310333

334+
#[tokio::test]
335+
async fn backticks() {
336+
TestBuilder::new()
337+
.command("echo ``")
338+
.assert_stdout("\n")
339+
.run()
340+
.await;
341+
342+
TestBuilder::new()
343+
.command("echo `echo 1`")
344+
.assert_stdout("1\n")
345+
.run()
346+
.await;
347+
348+
TestBuilder::new()
349+
.command("echo `echo 1 && echo 2`")
350+
.assert_stdout("1 2\n")
351+
.run()
352+
.await;
353+
354+
TestBuilder::new()
355+
.command("echo \"hi `echo 1`\"")
356+
.assert_stdout("hi 1\n")
357+
.run()
358+
.await;
359+
360+
// nested
361+
TestBuilder::new()
362+
.command("echo `echo \\`echo 1\\``")
363+
.assert_stdout("1\n")
364+
.run()
365+
.await;
366+
367+
// async inside subshell should wait
368+
TestBuilder::new()
369+
.command("`sleep 0.1 && echo 1 & echo echo` 2")
370+
.assert_stdout("1 2\n")
371+
.run()
372+
.await;
373+
TestBuilder::new()
374+
.command("`sleep 0.1 && echo 1 && exit 5 &` ; echo 2")
375+
.assert_stdout("2\n")
376+
.assert_stderr("1: command not found\n")
377+
.run()
378+
.await;
379+
}
380+
311381
#[tokio::test]
312382
async fn shell_variables() {
313383
TestBuilder::new()

0 commit comments

Comments
 (0)