4

有没有办法在 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 转义?如果有一个“标准”的方法,我会很高兴,但我也很高兴有一个非标准的 Tcl 库,甚至是从 C 中做到这一点的方法,所以我可以从 Tcl 调用它.

4

3 回答 3

2

这样做的关键是使用string mapor regsub

用于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 {\\&}]

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


讨论/替代方法

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

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

于 2012-06-21T08:22:12.880 回答
1

您可以'将所有非'字符-quote一起而不是单独引用,并且您只需要结束和恢复'-quoting 中间字符串即可 -\转义任何'字符。

所以你在'-quote spamming 上走在了正确的轨道上,因为你已经意识到

  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 '
''\'''
于 2021-06-15T17:58:56.740 回答
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
  }

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

于 2012-06-21T04:26:27.060 回答