如何让ruby ShellWords.shellescape使用多字节字符?

时间:2015-11-24 22:31:02

标签: ruby regex windows character-encoding escaping

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

为了简单起见,假设我有一个名为“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之前将其转义。

2 个答案:

答案 0 :(得分:1)

TL; DR

转义字符

Metacharacters

<强>代码

String.class_eval do
    def escapeshell()
        # Escape shell special characters
        self.gsub!(/[#-&(-*;<>?\[-^`{-~\u00FF]/, '\\\\\0')
        # Escape unbalanced quotes (single and double quotes)
        self.gsub!(/(["'])(?:([^"']*(?:(?!\1)["'][^"']*)*)\1)?/) do
            if $2.nil? 
                '\\' + $1
            else
                # and escape quotes inside (e.g. "x'x" or 'y"y')
                qt = $1
                qt + $2.gsub(/["']/, '\\\\\0') + 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!()在类中的任何字符之前添加反斜杠:

str.gsub!(/[#-&(-*;<>?\[-^`{-~\u00FF]/, '\\\\\0')


<强>行情

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

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

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

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

但我们还需要在另一对中避开引号。这是双引号内的单引号,反之亦然。因此,我们将嵌套另一个gsub()以替换匹配在引号内的文本($2):

str.gsub!(/(["'])(?:([^"']*(?:(?!\1)["'][^"']*)*)\1)?/) do
    if $2.nil? 
        '\\' + $1
    else
        qt = $1
        qt + $2.gsub(/["']/, '\\\\\0') + qt
    end
end

答案 1 :(得分:1)

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

您需要添加速记类以排除所有Unicode字母和数字。为了使它更安全,我们可以添加一个变音符号类,以便保持变音符号:

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

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

请注意,当放置在字符类的开头/结尾(或在有效范围或速记字符类之后)时,不需要转义连字符。