如何在Tcl中执行POSIX shell转义

4

有没有一种方法可以在Tcl中对字符串执行POSIX shell转义?

背景:

我有一个包含任意文件名的Tcl列表。 我需要扩展该列表以粘贴到后面将通过execing“sh -c”执行的任意POSIX shell(bash,dash,posh等)的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 中有一个 %q 的格式化程序可以满足我的需求。我可以针对每个文件名调用一次 Bash 来利用这个功能,但这会使脚本变得很臃肿,并且还会引入对 Bash 的依赖,而我并不希望这样做。

  • 按照 POSIX 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转义字符串?如果有标准的方法,我会非常高兴,但是如果只有非标准的Tcl库或甚至可以从C中调用的方法也可以让我满意。请注意保留HTML标记。

1
如果$name以重定向字符如>开头,那么posix_escape_via_bash将会遇到问题。 exec充满了陷阱... - Donal Fellows
@Donal 提到了依赖于 Bash 版本的好处;这是我不能使用它的另一个原因! - wjl
3个回答

2
使用 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转换一组字符

另一种方法是使用带有-all选项的regsub。只有当在所有替换情况下都进行完全相同类型的转义时,这种方法才能很好地工作。

# 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具有自己奇怪的问题,其根源是试图过多地像shell)。是否适合您取决于其他正在进行的事情,这是您在问题中没有告诉我们的。


感谢这些绝妙的技巧!我仍然惊讶于没有更标准的方法来做类似的事情,但至少现在我有了几种不错的方法来处理自己的实现。 - wjl
反引号运算符(``)如果您想要转义字符串并防止安全问题,也可能会出现问题。 - Dereckson
我认为这个答案总体上是好的和有信息量的。但是我想强烈警告,详尽地涵盖所有可能是特殊字符的方法总是存在遗漏一个或多个特殊字符的风险。这就像打“打地鼠”的游戏,你永远不知道自己是否已经完成了游戏。例如,即使我们只正式支持POSIX,在现实生活中,我们代码的用户可能会使用bashzsh或一些非常接近POSIX的/bin/sh,他们甚至没有意识到像!^这样的偏差是特殊的。 - mtraceur
此外,我可以理解为什么你要推迟对 [~{} 等字符的讲解,直到最后(因为原问题没有在示例中转义或提及它们),但我觉得这是覆盖每个可能特殊字符的方法的缺点的一个很好的例子(而不是识别涵盖所有字符的最小规则/情况集)。我敢打赌问问题的人 不知道 他们想要从 shell 转义 [!因为除非你的工作目录中有一个名为 e 的文件,否则你不会知道这是一个转义问题。 - mtraceur
所以,虽然您可能知道在那种情况下详尽地覆盖 [,但遵循“我将处理每个特殊字符”的总体方法的典型结果(我几乎普遍看到这导致转义错误进入生产代码!)是人们转义他们知道是特殊的或可以快速发现是特殊的东西,即使在某些情况下(包括 Bourne/POSIX shell!)也有一种极其简单的方法可以将任何和所有字符逐字传递给 shell 的命令评估,无论 shell 将其传递给什么。 - mtraceur

1
你可以将所有非'字符一起'引起来,而不是单独引起来,只需要在字符串中间结束和恢复'引号以转义任何'字符即可。
所以你的'引用垃圾邮件是正确的,因为你已经意识到:
  1. 单引号转义除了'之外的所有内容,这将特殊情况减少到只有一个;
  2. 你可以在shell中连接引用字符串,它会将它们解释为一个字符串('a''b'解析为与'ab'相同的原始字符串)。
最后缺失的一块是第二个点让我们优化掉几乎所有的结束和立即恢复'引用,当单独引用每个字符时,这种情况发生。
所以你需要的逻辑就是:
  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 '
''\'''

0
最终我采用了一种“引号轰炸”方法的变体,但特殊处理了各种类别的字符,其中一些字符不需要引号,而另一些字符则可以用简单的反斜杠进行引用。这个方法仍然有点过于迫切,但比最初的天真方法好得多了。在大多数情况下,这与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)反斜杠密集的方法对于更多可能的输入而言,其人类可读性和输入大小与输出大小比“标准”更糟糕。 - mtraceur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接