我经常最终通过 ssh 发出复杂的命令;这些命令涉及到 awk 或 perl 一行的管道,因此包含单引号和 $。我既无法找出一个硬性的规则来正确地进行引用,也没有找到一个很好的参考。例如,考虑以下情况:
# what I'd run locally:
CMD='pgrep -fl java | grep -i datanode | awk '{print $1}'
# this works with ssh $host "$CMD":
CMD='pgrep -fl java | grep -i datanode | awk '"'"'{print $1}'"'"
(请注意 awk 语句中的额外引号。)
但我如何让它工作,例如ssh $host "sudo su user -c '$CMD'"
?在这种情况下有管理报价的通用方法吗?...
答案1
处理多个级别的引用(实际上是多个级别的解析/解释)可能会变得复杂。记住以下几点会有所帮助:
- 每个“引用级别”都可能涉及不同的语言。
- 引用规则因语言而异。
- 当处理超过一两个嵌套级别时,通常最简单的方法是“从下到上”(即从最内层到最外层)。
引用级别
让我们看看您的示例命令。
pgrep -fl java | grep -i datanode | awk '{print $1}'
您的第一个示例命令(上面)使用四种语言:您的 shell、中的正则表达式正则表达式,正则表达式在grep(这可能与正则表达式语言不同正则表达式), 和awk。涉及两个级别的解释:shell 以及 shell 之后针对每个相关命令的解释级别。只有一层显式引用(shell 引用到awk)。
ssh host …
接下来您添加了一个级别SSH在上面。这实际上是另一个 shell 级别:SSH不解释命令本身,它将它传递给远程端的 shell(通过 (eg) sh -c …
),并且该 shell 解释该字符串。
ssh host "sudo su user -c …"
然后您询问如何在中间添加另一个 shell 级别,方法是使用苏(通过须藤,它不解释它的命令参数,所以我们可以忽略它)。此时,您已经进行了三个级别的嵌套(awk→ 壳,壳 → 壳(SSH), 壳 → 壳 (su 用户-c),所以我建议使用“自下而上”的方法。我假设你的 shell 与 Bourne 兼容(例如嘘,灰,短跑,什,巴什,桀骜, ETC。)。一些其他类型的外壳(鱼,RC等)可能需要不同的语法,但该方法仍然适用。
自下而上
- 制定要在最内层表示的字符串。
- 从次高级语言的引用库中选择一种引用机制。
- 根据您选择的引用机制引用所需的字符串。
- 如何应用哪种引用机制通常有很多变化。手工制作通常需要练习和经验。当以编程方式执行此操作时,通常最好选择最容易正确的(通常是“最字面的”(最少转义))。
- (可选)将生成的带引号的字符串与附加代码一起使用。
- 如果您尚未达到所需的引用/解释级别,请获取生成的引用字符串(加上任何添加的代码)并将其用作步骤 2 中的起始字符串。
引用语义各不相同
这里要记住的是,每种语言(引用级别)可能会为相同的引用字符提供稍微不同的语义(甚至完全不同的语义)。
大多数语言都有“字面”引用机制,但它们的字面程度各不相同。类似 Bourne 的 shell 的单引号实际上是字面量的(这意味着您不能使用它来引用单引号字符本身)。其他语言(Perl、Ruby)不那么直白,因为它们解释一些单引号区域内的反斜杠序列非字面意义(具体来说,\\
和\'
导致\
and '
,但其他反斜杠序列实际上是字面意义)。
您必须阅读每种语言的文档,以了解其引用规则和整体语法。
你的例子
你的例子的最内层是awk程序。
{print $1}
您要将其嵌入到 shell 命令行中:
pgrep -fl java | grep -i datanode | awk …
我们需要(至少)保护空间和$
内部awk程序。显而易见的选择是在 shell 中对整个程序使用单引号。
'{print $1}'
不过还有其他选择:
{print\ \$1}
直接逃离空间$
{print' $'1}
单引号仅包含空格和$
"{print \$1}"
双引号整体并转义$
{print" $"1}
双引号仅包含空格和$
这可能会稍微改变规则($
在双引号字符串末尾未转义的是文字),但它似乎在大多数 shell 中都有效。
如果程序在左大括号和右大括号之间使用逗号,我们还需要引用或转义逗号或大括号,以避免在某些 shell 中“大括号扩展”。
我们选择'{print $1}'
它并将其嵌入到 shell“代码”的其余部分中:
pgrep -fl java | grep -i datanode | awk '{print $1}'
接下来,您想通过以下方式运行它苏和须藤。
sudo su user -c …
su user -c …
就像some-shell -c …
(除了在其他 UID 下运行之外),所以苏只是添加了另一个 shell 级别。须藤不解释其参数,因此它不添加任何引用级别。
我们的命令字符串需要另一个 shell 级别。我们可以再次选择单引号,但是我们必须对现有的单引号进行特殊处理。通常的方式是这样的:
'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'
shell 将解释和连接这里的四个字符串:第一个单引号字符串 ( pgrep … awk
)、转义单引号、单引号字符串awk程序中,另一个转义的单引号。
当然,还有很多替代方案:
pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1}
逃避一切重要的事情pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}
相同,但没有多余的空格(即使在awk程序!)"pgrep -fl java | grep -i datanode | awk '{print \$1}'"
双引号整个事情,逃避$
'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"
你的变化;由于使用双引号(两个字符)而不是转义符(一个字符),因此比通常的方式长一点
在第一级中使用不同的引用允许在此级别上进行其他变化:
'pgrep -fl java | grep -i datanode | awk "{print \$1}"'
'pgrep -fl java | grep -i datanode | awk {print\ \$1}'
将第一个变体嵌入须藤/*su* 命令行给出:
sudo su user -c 'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'
您可以在任何其他单 shell 级别上下文中使用相同的字符串(例如ssh host …
)。
接下来,您添加了一个级别SSH在上面。这实际上是另一个 shell 级别:SSH不解释命令本身,而是将其传递给远程端的 shell(通过 (eg) sh -c …
),并且该 shell 解释该字符串。
ssh host …
过程是相同的:获取字符串,选择引用方法,使用它,嵌入它。
再次使用单引号:
'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'
现在有十一个字符串被解释和连接:'sudo su user -c '
,转义单引号,'pgrep … awk '
,转义单引号,转义反斜杠,两个转义单引号,单引号awk程序、转义单引号、转义反斜杠和最终转义单引号。
最终的形式如下所示:
ssh host 'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'
手动输入有点笨拙,但是 shell 单引号的字面性质使得自动执行细微的变化变得很容易:
#!/bin/sh
sq() { # single quote for Bourne shell evaluation
# Change ' to '\'' and wrap in single quotes.
# If original starts/ends with a single quote, creates useless
# (but harmless) '' at beginning/end of result.
printf '%s\n' "$*" | sed -e "s/'/'\\\\''/g" -e 1s/^/\'/ -e \$s/\$/\'/
}
# Some shells (ksh, bash, zsh) can do something similar with %q, but
# the result may not be compatible with other shells (ksh uses $'...',
# but dash does not recognize it).
#
# sq() { printf %q "$*"; }
ap='{print $1}'
s1="pgrep -fl java | grep -i datanode | awk $(sq "$ap")"
s2="sudo su user -c $(sq "$s1")"
ssh host "$(sq "$s2")"
答案2
看克里斯·约翰森的回答以获得清晰、深入的解释和通用解决方案。我将提供一些在某些常见情况下有帮助的额外提示。
单引号转义除了单引号之外的所有内容。因此,如果您知道变量的值不包含任何单引号,则可以在 shell 脚本中的单引号之间安全地插入它。
su -c "grep '$pattern' /root/file" # assuming there is no ' in $pattern
如果您的本地 shell 是 ksh93 或 zsh,您可以通过将变量重写为'\''
. (虽然 bash 也有这个${foo//pattern/replacement}
结构,但它对单引号的处理对我来说没有意义。)
su -c "grep '${pattern//'/'\''}' /root/file" # if the outer shell is zsh
su -c "grep '${pattern//\'/\'\\\'\'}' /root/file" # if the outer shell is ksh93
避免处理嵌套引用的另一个技巧是尽可能通过环境变量传递字符串。 ssh 和 sudo 往往会删除大多数环境变量,但它们通常被配置为允许通过LC_*
,因为这些变量通常对于可用性非常重要(它们包含区域设置信息),并且很少被认为是安全敏感的。
LC_CMD='what you would use locally' ssh $host 'sudo su user -c "$LC_CMD"'
在这里,由于LC_CMD
包含一个 shell 片段,因此必须按字面意思将其提供给最内层的 shell。因此该变量由上面的 shell 扩展。最里面的但只有一个 shell 看到"$LC_CMD"
,最里面的 shell 看到命令。
类似的方法可用于将数据传递到文本处理实用程序。如果您使用 shell 插值,实用程序会将变量的值视为命令,例如,sed "s/$pattern/$replacement/"
如果变量包含/
.因此,请使用 awk (而不是 sed)及其-v
选项或ENVIRON
数组从 shell 传递数据(如果您使用ENVIRON
,请记住导出变量)。
awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'
答案3
作为克里斯·约翰逊描述得很好,这里有几个级别的间接引用;您指示本地通过它应该指示shell
指示远程来指示远程来运行您的管道。这种命令需要很多繁琐的工作。shell
ssh
sudo
su
shell
pgrep -fl java | grep -i datanode | awk '{print $1}'
user
\'"quote quoting"\'
如果您接受我的建议,您将放弃所有废话并执行以下操作:
% ssh $host <<REM=LOC_EXPANSION <<'REMOTE_CMD' |
> localhost_data='$(commands run on localhost at runtime)' #quotes don't affect expansion
> more_localhost_data="$(values set at heredoc expansion)" #remote shell will receive m_h_d="result"
> REM=LOC_EXPANSION
> commands typed exactly as if located at
> the remote terminal including variable
> "${{more_,}localhost_data}" operations
> 'quotes and' \all possibly even
> a\wk <<'REMOTELY_INVOKED_HEREDOC' |
> {as is often useful with $awk
> so long as the terminator for}
> REMOTELY_INVOKED_HEREDOC
> differs from that of REM=LOC_EXPANSION and
> REMOTE_CMD
> and here you can | pipeline operate on |\
> any output | received from | ssh as |\
> run above | in your local | terminal |\
> however | tee > ./you_wish.result
<desired output>
了解更多:
检查我的(也许太啰嗦)答案具有不同类型引号的管道路径用于斜杠替换我在其中讨论了其原理背后的一些理论。
-麦克风
答案4
使用更多双引号怎么样?
那么你ssh $host $CMD
应该可以很好地使用这个:
CMD="pgrep -fl java | grep -i datanode | awk '{print $1}'"
现在来看更复杂的一个,ssh $host "sudo su user -c \"$CMD\""
.我想您所要做的就是转义CMD
:$
和\
中的敏感字符"
。所以我会尝试看看这是否有效:echo $CMD | sed -e 's/[$\\"]/\\\1/g'
。
如果看起来没问题,请将 echo+sed 包装到 shell 函数中,然后就可以使用ssh $host "sudo su user -c \"$(escape_my_var $CMD)\""
.