如何使 ruby ShellWords.shellescape 与多字节字符一起使用?

How can I make ruby ShellWords.shellescape work with multibyte characters?

我一直在尝试使用包含来自 Windows 环境变量的多字节字符的参数来调用 exec,但尚未找到可行的解决方案。到目前为止,这是我能够调试的内容。

为简单起见,假设我有一个名为 "Seán" 的目录,我试图将其用作 exec 的参数。如果我只是打电话

exec 'script', "Se\u00E1n".encode("IBM437") 

执行的脚本找不到文件,因为 arg 被调整为重音字符丢失。如果我执行以下操作,它会起作用,但这是不好的做法,因为 arg 应该在进入 shell.

之前进行转义
exec "script #{"Se\u00E1n".encode("IBM437")}"

所以我的想法是我会使用 shellescape 来保护 exec 的使用。

require 'shellwords'
exec "script #{"Se\u00E1n".encode("IBM437").shellescape}"

但问题是它对特殊字符进行了转义,因此它看起来像下面这样 - "Se\án"。我弄清楚这是哪里发生的,它来自这个 regular expression.

str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\1")

乍一看似乎转义了不在已知良好 shell 字符集中的字符。不幸的是,这个集合不包含特殊字符,所以我 运行 遇到了问题。

我正在寻找的是一个可以进行 shell 转义且不会弄乱特殊字符的正则表达式,这样我就可以在将这些 args 传递给 exec 之前对其进行转义。

TL;DR

转义字符


代码

String.class_eval do
    def escapeshell()
        # Escape shell special characters
        self.gsub!(/[#-&(-*;<>?\[-^`{-~\u00FF]/, '\\[=10=]')
        # Escape unbalanced quotes (single and double quotes)
        self.gsub!(/(["'])(?:([^"']*(?:(?!)["'][^"']*)*))?/) do
            if .nil? 
                '\' + 
            else
                # and escape quotes inside (e.g. "x'x" or 'y"y')
                qt = 
                qt + .gsub(/["']/, '\\[=10=]') + qt
            end
        end
        self
    end
end


# Test it
str = "(dir *.txt & dir \"\some dir\Sè\u00E1ñ*.rb\") | sort /R >Filé.txt 2>&1"
puts 'String:'
puts str

puts "\nEscaped:"
puts str.escapeshell

输出

String:
(dir *.txt & dir "\some dir\Sèáñ*.rb") | sort /R >Filé.txt 2>&1

Escaped:
\(dir \*.txt \& dir "\some dir\Sèáñ\*.rb"\) \| sort /R \>Filé.txt 2\>\&1

ideone demo


描述

元字符

考虑应转义的 shell 个元字符:

# & % ; ` | * ? ~ < > ^ ( ) [ ] { } $ \ \u00FF

我们可以在 character class:

中包含每个字符
[#&%;`|*?~<>^()\[\]{}$\\u00FF]

与以下内容完全相同:

/[#-&(-*;<>?\[-^`{-~\u00FF]/

然后,我们使用 gsub!() 在 class:

中的任何字符前添加一个反斜杠
str.gsub!(/[#-&(-*;<>?\[-^`{-~\u00FF]/, '\\[=15=]')

行情

只有不平衡的引号需要转义。这对于保留命令的参数很重要。使用以下表达式,我们匹配平衡引号:

/(["'])[^"']*(?:(?!)["'][^"']*)*)/

以及不平衡,使最后一部分成为可选的

/(["'])(?:[^"']*(?:(?!)["'][^"']*)*))?/

但我们还需要在另一对中转义引号。这是双引号内的单引号,反之亦然。因此,我们将嵌套另一个 gsub() 以替换引号内匹配的文本 (</code>):</p> <pre><code>str.gsub!(/(["'])(?:([^"']*(?:(?!)["'][^"']*)*))?/) do if .nil? '\' + else qt = qt + .gsub(/["']/, '\\[=18=]') + qt end end

正则表达式 /([^A-Za-z0-9_\-.,:\/@\n])/ 只处理 ASCII 字母和数字,而不是所有 Unicode 字母。 [^...] 是一个 negated character class,匹配除 class 中指定的所有字符 。因此,所有 ЯЦĄ 都与该表达式一起删除,因为它们与 [A-Za-z].

不匹配

你需要的是添加shorthand classes来排除所有Unicode字母和数字。为了使它更安全,我们可以添加变音符号 class 以保留变音符号:

str.gsub(/([^\p{L}\p{M}\p{N}_.,:\/@\n-])/, "\\\1")

此处,\p{L} 匹配所有 Unicode 基本字母,\p{M} 匹配所有变音符号,\p{N} 匹配任何 Unicode 数字。

请注意,当连字符位于字符 class 的 start/end 处(或在有效范围或 shorthand 字符后 class).