From 7cddb9393de74e945756d7ff3c8fc99cd6c8b3ab Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Mon, 2 Feb 2026 21:08:18 -0800 Subject: [PATCH] Respect escaped command substitution --- ruby/shell/repl.rb | 7 +------ ruby/shell/word_expander.rb | 37 ++++++++++++++++++++++++++++++++++++- ruby/test/shell_test.rb | 6 ++---- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/ruby/shell/repl.rb b/ruby/shell/repl.rb index 2a95fe3..a0c0b40 100644 --- a/ruby/shell/repl.rb +++ b/ruby/shell/repl.rb @@ -113,12 +113,7 @@ module Shell state = :unquoted if c == "\"" when :double_quoted_escape - case c - when "\"", "\\", "$", "`" - # no-op - else - command << "\\" # POSIX behaviour, backslash remains - end + command << "\\" command << c state = :double_quoted diff --git a/ruby/shell/word_expander.rb b/ruby/shell/word_expander.rb index 029aa86..b97de11 100644 --- a/ruby/shell/word_expander.rb +++ b/ruby/shell/word_expander.rb @@ -6,6 +6,7 @@ module Shell ENV_VAR_REGEX = /\$(?:\{([^}]+)\}|(\w+)\b)/ DEFAULT_VAR_REGEX = /\A(\w+):-([\s\S]*)\z/ ESCAPED_DOLLAR = "\u0001" + ESCAPED_BACKTICK = "\u0002" # Splits the given line into multiple words, performing the following transformations: # @@ -18,7 +19,9 @@ module Shell substituted_line = expand_command_substitution(protected_line) shellsplit(substituted_line) .map do |word| - expand_variables(word).tr(ESCAPED_DOLLAR, "$") + expand_variables(word) + .tr(ESCAPED_DOLLAR, "$") + .tr(ESCAPED_BACKTICK, "`") # TODO: expand globs end end @@ -138,6 +141,23 @@ module Shell output << c i += 1 end + when "\\" + if i + 1 < line.length + escaped = line[i + 1] + if escaped == "$" + output << ESCAPED_DOLLAR + i += 2 + elsif escaped == "`" + output << ESCAPED_BACKTICK + i += 2 + else + output << c + i += 1 + end + else + output << c + i += 1 + end else output << c i += 1 @@ -154,6 +174,20 @@ module Shell output << c state = :unquoted i += 1 + when "\\" + if i + 1 < line.length + escaped = line[i + 1] + if escaped == "$" || escaped == "`" || escaped == "\\" || escaped == "\"" + output << (escaped == "$" ? ESCAPED_DOLLAR : (escaped == "`" ? ESCAPED_BACKTICK : escaped)) + else + output << "\\" + output << escaped + end + i += 2 + else + output << c + i += 1 + end when "`" cmd, i = read_backtick(line, i + 1) output << run_command_substitution(cmd) @@ -263,5 +297,6 @@ module Shell end output end + end end diff --git a/ruby/test/shell_test.rb b/ruby/test/shell_test.rb index f0a1802..3c747a8 100644 --- a/ruby/test/shell_test.rb +++ b/ruby/test/shell_test.rb @@ -99,13 +99,11 @@ class ShellTest < Minitest::Test end def test_does_not_expand_escaped_command_substitution_dollar_paren_in_double_quotes - skip "escape handling for command substitution in double quotes is limited" - assert_equal "$(echo hi)", `#{A1_PATH} -c 'echo "\\$(echo hi)"'`.chomp + assert_equal "$(echo hi)", %x(#{A1_PATH} -c 'echo "\\$(echo hi)"').chomp end def test_does_not_expand_escaped_command_substitution_backticks_in_double_quotes - skip "escape handling for command substitution in double quotes is limited" - assert_equal "`echo hi`", %x(#{A1_PATH} -c "echo \"\\`echo hi\\`\"").chomp + assert_equal "`echo hi`", %x(#{A1_PATH} -c 'echo "\\`echo hi\\`"').chomp end #################################