【问题标题】:How to perform POSIX shell escapes from Tcl如何从 Tcl 执行 POSIX shell 转义
【发布时间】:2012-06-21 02:34:30
【问题描述】:

有没有办法在 Tcl 中对字符串执行 POSIX shell 转义?

背景:

我有一个 Tcl 列表中的任意文件名列表。我需要扩展列表以粘贴到稍后将由任意 POSIX shell(bash、dash、posh 等)通过执行“sh -c”执行的 shell 片段中。

这是一个说明问题的例子:

#!/usr/bin/tclsh

set targets {with\ spaces has"stray'quotes has{brackets} $not_a_variable \[escaped_braces\] (not_a_subshell) weird\ \{|#^$(}

set shell_fragment {
  something
  some_command $targets
  something else
}

puts [subst $shell_fragment]

上面的输出是带有 Tcl 转义的名称:

  something
  some_command with\ spaces has"stray'quotes has{brackets} $not_a_variable \[escaped_braces\] (not_a_subshell) weird\ \{|#^$(
  something else

然而,我需要它看起来正常工作是这样的(POSIX shell 转义):

  something
  some_command with\ spaces has\"stray\'quotes has{brackets} \$not_a_variable [escaped_braces] \(not_a_subshell\) weird\ {\|\#^\$\(
  something else

想法:

以下是我可以想象的一些我不想做的解决方法:

  • 在 Bash 中,有一个用于 printf 的 %q 格式化程序,它可以满足我的需求。我可以为每个文件名执行一次 bash 调用以利用此功能,但这 1) 是一个很大的后盾,并且 2) 引入了对 bash 的依赖,我不希望这样做。

    李>
  • 根据 POSIX shell 转义规则实现自己的 shell 转义。这显然可行,但我宁愿不重新发明轮子。我找到了一种“简单”的方法,通过发送垃圾引号来做到这一点,但这会使调试变得很糟糕,并大大减少了可用的命令行长度:

“坏”方法的示例:

proc posix_escape_via_bash {name} {
  return [exec bash -c {printf %q "$0"} $name]
}

proc posix_escape_via_spamming_quotes {name} {
  set escaped {}
  foreach char [split $name {}] {
    switch $char {
      '       {lappend escaped {\'}}
      default {lappend escaped '$char'}
    }
  }
  return [join $escaped {}]
}

再说一遍:有没有办法在 Tcl 中对字符串执行 POSIX shell 转义?如果有的话,我会很高兴有一种“标准”的方法,但我' d 也对非标准的 Tcl 库感到满意,甚至可以从 C 中做到这一点,所以我可以从 Tcl 中调用它。

【问题讨论】:

  • 如果$name> 之类的重定向字符开头,那么posix_escape_via_bash 将遇到问题。 exec布满陷阱......
  • @Donal 关于依赖 bash 的版本的好点;这也是我不能使用它的另一个原因!

标签: escaping tcl filenames sh


【解决方案1】:

这样做的关键是使用string mapregsub

使用string map转换一组字符

您所要做的就是为您想要转义的内容提供正确的映射。

对于您的特定情况,您似乎想要引用的唯一字符是'"$()<>| .让我们添加;*?(我猜你不想要杂散的语句分隔符或通配符)。这很简单,但我们将迭代地生成映射,而不是使用文字:

set mappedChars {'"$()<>|&!;*?}    ;#'# Just to deal with SO's formatting...
set escaping {}
foreach c $mappedChars { lappend escaping $c "\\$c" }

这是您只需要做一次的事情。完成后,应用地图就很容易了:

set escapedTargets [string map $escaping $targets]

我会留给您找出将其与您使用 subst 合并的最佳方法。

使用regsub转换一组字符

另一种方法是将regsub-all 选项一起使用。只有在所有替代情况下都执行完全相同类型的转义时,这才真正有效。

# This puts a backslash in front of all non-alphanumerics
set escapedTargets [regsub -all {[^[:alnum:]]} $targets {\\&}]
# This _particular_ case has an almost-equivalent-good-enough that's shorter
set escapedTargets [regsub -all {\W} $targets {\\&}]

复杂之处在于为所有问题案例确定正确的表征正则表达式,这就是为什么经常说使用正则表达式将一个问题变成两个问题......


讨论/替代方法

上面的映射没有涵盖所有的 POSIX shell 元字符——特别是,它不处理反斜杠本身或空格(这样做会导致你的问题,因为你似乎想要获得多个单词),它也应该处理这些:{}[]~——正则表达式可能有点敏锐了,在完全无辜的东西前面加上反斜杠。事实上,某些用途(例如,变量名)比上述任何一种方法都需要更多的注意,因为它们有一些根本无法使用的东西。

根本问题是shell实际上有一个非常复杂的语法,有很多交互规则。如果您可以编写代码而不需要运行 shell,那么您可能会发现事情更加可靠(以 Tcl 的 exec 和管道 open 的事实为模,它们有自己的奇怪问题,这些问题源于尝试太多像贝壳)。这是否适合您取决于您​​在问题中没有告诉我们的其他事情。

【讨论】:

  • 感谢您的宝贵建议!我仍然对没有更标准的方法来做这样的事情感到惊讶,但至少我现在有几种体面的方法可以在我自己的实现中解决这个问题。
  • 如果您试图转义字符串并防止安全问题,反引号运算符 (``) 也可能有问题。
  • 我认为这个答案总体上很好而且内容丰富。但我要强烈警告说,详尽地覆盖所有可能特殊的字符的方法总是会带来我们忘记另一个特殊字符的风险。这是一个打地鼠游戏,它永远不会真正告诉你你是否玩完了。例如,即使我们只正式支持 POSIX,但在现实生活中,我们代码的用户可能会使用 bashzsh 或一些 /bin/sh,它们与 POSIX 非常接近,以至于他们甚至没有想到有像!^ 这样的偏差是特殊的。
  • 另外,我可以理解你为什么延迟覆盖[~{ 等等直到最后(因为原始问题没有逃脱或在他们的示例中提及它们),但我觉得这是覆盖每个可能特殊字符的方法的缺点的一个很好的例子(而不是识别覆盖所有字符的最小可能的规则/案例集)。我敢打赌,提问者不知道他们希望[ 从外壳中逃脱!因为在您的工作目录中有一个名称类似于 e 的文件之前,您不会知道这是一个转义问题。
  • 所以,虽然 you 可能知道在这种情况下详尽地覆盖[,但遵循“我将处理所有特殊情况”的整体方法的典型结果字符”(我已经看到这几乎普遍导致逃避错误进入生产代码!)是人们逃避他们知道是特殊的或可以很快发现是特殊的东西,即使在某些情况下(包括 Bourne/POSIX shell !)一种非常简单的方法,可以将任何和所有字符逐字逐句通过 shell 的命令评估传递给 shell 传递给它的任何内容。
【解决方案2】:

你可以'-引用所有非'字符一起而不是单独,你只需要结束和恢复'-quoting mid-string to \ - 转义任何' 字符。

所以你在'-quote 垃圾邮件中走在正确的轨道上,因为你已经意识到了

  1. 单引号转义一切(除了'),这将特殊情况减少到只有一种,并且
  2. 您可以在 shell 中连接带引号的字符串,并将它们解释为一个字符串('a''b' 解析为与 'ab' 相同的原始字符串。

最后一个缺失的部分是第二点让我们优化了几乎所有的结尾并立即恢复'-quoting,这是在'-单独引用每个字符时发生的。

所以你需要的逻辑就是

  1. 将所有' 替换为'\'',并且
  2. 在开头和结尾放置一个'
proc posix_escape_via_minimal_quotes {name} {
  set escaped {}
  lappend escaped '
  lappend escaped [string map {' '\\''} $name]
  lappend escaped '
  return [join $escaped {}]
}

示例输出:

% posix_escape_via_minimal_quotes x
'x'
% posix_escape_via_minimal_quotes xxx
'xxx'
% posix_escape_via_minimal_quotes xxx'xxx
'xxx'\''xxx'
% posix_escape_via_minimal_quotes '
''\'''

【讨论】:

    【解决方案3】:

    我最终做了一个我提到的“引用垃圾邮件”方法的变体,但是特殊的各种类型的字符要么永远不需要引用,要么可以用简单的反斜杠引用。这仍然有点过于急切,但比最初的幼稚方法要好得多。在大多数情况下,这与 bash printf 方法的结果相同。

      proc posix_escape {name} {
        foreach char [split $name {}] {
          switch -regexp $char {
            {'}           {append escaped \\'     }
            {[[:alnum:]]} {append escaped $char   }
            {[[:space:]]} {append escaped \\$char }
            {[[:punct:]]} {append escaped \\$char }
            default       {append escaped '$char' }
          }
        }
        return $escaped
      }
    

    如果有更标准的方法可以做到这一点,我仍然非常感兴趣。如果以前没有人遇到过这种情况,我会感到非常惊讶! =)

    【讨论】:

    • 我认为我的回答代表了“一种更标准的方式来做到这一点”。我还要说printf %q 不是一个好的标准,因为 1)如果它是一个标准,它会迫使任何实施它的人进入彻底识别每个特殊字符(shell 本身在为自己正确处理它的特权位置,但其他人必须追赶并理想地覆盖多个 shell),以及 2)反斜杠重的方法具有更差的人类可读性和输入大小-output-size 比我认为的“标准”更多的可能输入。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-21
    相关资源
    最近更新 更多