有没有办法在Tcl中对字符串执行POSIX shell转义?
背景:
我在Tcl列表中有一个任意文件名列表。我需要扩展列表以粘贴到一个shell片段中,稍后将通过执行“sh -c”由任意POSIX shell(bash,dash,posh等)执行。
以下是一个说明问题的示例:
#!/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格式化程序来执行我想要的printf。我可以执行每个文件名执行一次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调用它。
答案 0 :(得分:3)
执行此操作的关键是使用string map
或regsub
。
string map
转换一组字符您所要做的就是为要转义的内容提供正确的映射。
对于您的具体案例,您似乎只想引用的字符为'
,"
,$
,(
,)
,{ {1}},<
和>
。我们也添加|
,;
和*
(我猜你不需要杂散语句分隔符或通配符)。这非常简单,但我们将迭代生成映射而不是使用文字:
?
这是你只需要做一次的事情。完成后,应用地图很容易:
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 {\\&}]
复杂性在于为所有问题情况确定正确的特征化正则表达式,这就是为什么经常声明使用正则表达式将一个问题变成两个......
上面的地图并未涵盖所有POSIX shell元字符 - 特别是它不处理反斜杠本身或空格(这样做会导致问题,因为你似乎想要获得多个单词)并且它也应该处理这些:# This _particular_ case has an almost-equivalent-good-enough that's shorter
set escapedTargets [regsub -all {\W} $targets {\\&}]
- 正则表达式可能有点太敏锐,在完全无辜的事情面前放反斜杠。实际上,一些用途(例如,变量名称)需要比上述任何一种方法更多的关注,因为它们具有根本无法使用的东西。
根本问题是shell实际上有一个非常复杂的语法,有很多交互规则。如果您可以编写代码以便不需要运行shell,那么您可能会发现更可靠的事情(模数是Tcl的{}[]~
和管道exec
有自己的奇怪问题源于尝试太多,不像壳)。这是否适合你取决于你在问题中没有告诉我们的其他事情。
答案 1 :(得分:1)
您可以'
-引用所有非'
字符一起而不是单独引用,并且您只需要结束和恢复'
-引用中间-string to \
-escape 任何 '
字符。
所以您在使用 '
-quote 垃圾邮件时走在正确的轨道上,因为您已经意识到
'
),这将特殊情况减少到只有一种,并且'a''b'
解析为与 'ab'
相同的原始字符串)。最后一个缺失的部分是第二点让我们优化了几乎所有的结尾并立即恢复 '
-quoting 发生在 '
单独引用每个字符时。
所以你需要的逻辑就是
'
替换为 '\''
,并且'
: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 '
''\'''
答案 2 :(得分: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
}
如果有更标准的方法,我仍然非常感兴趣。如果没有人遇到过这个问题,我会感到非常惊讶! =)