|
1 | 1 | // Copyright 2018-2024 the Deno authors. MIT license. |
2 | 2 |
|
| 3 | +use std::borrow::Cow; |
| 4 | + |
3 | 5 | use anyhow::Result; |
4 | 6 | use anyhow::bail; |
5 | 7 | use monch::*; |
@@ -693,15 +695,82 @@ fn parse_single_quoted_string(input: &str) -> ParseResult<&str> { |
693 | 695 |
|
694 | 696 | fn parse_double_quoted_string(input: &str) -> ParseResult<Vec<WordPart>> { |
695 | 697 | // 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.", |
699 | 702 | 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) |
705 | 774 | } |
706 | 775 |
|
707 | 776 | #[derive(Clone, Copy, PartialEq, Eq)] |
@@ -790,15 +859,9 @@ fn parse_word_parts( |
790 | 859 | map(first_escaped_char(mode), PendingPart::Char), |
791 | 860 | map(parse_command_substitution, PendingPart::Command), |
792 | 861 | ), |
| 862 | + map(parse_backticks_command_substitution, PendingPart::Command), |
793 | 863 | map(ch('~'), |_| PendingPart::Tilde), |
794 | 864 | 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 | | - }, |
802 | 865 | // words can have escaped spaces |
803 | 866 | map( |
804 | 867 | if_true(preceded(ch('\\'), ch(' ')), |_| { |
@@ -862,7 +925,28 @@ fn parse_word_parts( |
862 | 925 | } |
863 | 926 |
|
864 | 927 | 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 | + ) |
866 | 950 | } |
867 | 951 |
|
868 | 952 | fn parse_subshell(input: &str) -> ParseResult<SequentialList> { |
@@ -976,19 +1060,21 @@ mod test { |
976 | 1060 | parse("cmd 'test").err().unwrap().to_string(), |
977 | 1061 | concat!("Expected closing single quote.\n", " 'test\n", " ~"), |
978 | 1062 | ); |
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 | | - |
984 | 1063 | assert_eq!( |
985 | | - parse("echo `echo 1`").err().unwrap().to_string(), |
| 1064 | + parse("cmd \"test$(echo testing\"") |
| 1065 | + .err() |
| 1066 | + .unwrap() |
| 1067 | + .to_string(), |
986 | 1068 | 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 | + " ~" |
990 | 1072 | ), |
991 | 1073 | ); |
| 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()); |
992 | 1078 | assert!( |
993 | 1079 | parse("deno run --allow-read=. --allow-write=./testing main.ts").is_ok(), |
994 | 1080 | ); |
@@ -1442,7 +1528,7 @@ mod test { |
1442 | 1528 | run_test( |
1443 | 1529 | parse_quoted_string, |
1444 | 1530 | r#""asdf`""#, |
1445 | | - Err("Back ticks in strings is currently not supported."), |
| 1531 | + Err("Failed parsing within double quotes. Expected closing backtick."), |
1446 | 1532 | ); |
1447 | 1533 |
|
1448 | 1534 | run_test_with_end( |
|
0 commit comments