引用ssh $ host $ FOO和ssh $ host“ sudo su user -c $ FOO”类型的构造


30

我经常最终会通过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'"呢?在这种情况下是否有管理报价的一般方法?

Answers:


35

处理多个级别的引用(实际上是多个级别的解析/解释)可能会变得很复杂。请记住以下几点:

  • 每个“报价水平”都可能涉及不同的语言。
  • 报价规则因语言而异。
  • 当处理一个或两个以上的嵌套层时,通常最容易“从下到上”(即,从最内到最外)进行工作。

报价水平

让我们看看您的示例命令。

pgrep -fl java | grep -i datanode | awk '{print $1}'

您的第一个示例命令(上述)使用四种语言:您的外壳,pgrep中的正则表达式,grep中的正则表达式(可能与pgrep中的正则表达式语言不同)和awk。涉及两个级别的解释:外壳程序和每个相关命令在外壳程序之后的一级。报价只有一个明确的级别(将shell报价为awk)。

ssh host 

接下来,您在顶部添加了一个ssh级别。这实际上是另一个shell级别:ssh不会解释命令本身,而是将其交给远程端的shell(通过(例如)sh -c …),并且该shell解释字符串。

ssh host "sudo su user -c …"

然后,您询问了如何使用su(通过sudo在中间添加另一个shell级别,它不会解释其命令参数,因此我们可以忽略它)。至此,您将进行三个级别的嵌套(awk →shell,shell→shell(ssh),shell→shell(su用户-c),所以我建议使用“自下而上”的方法。您的外壳是Bourne兼容的(例如shashdashkshbashzsh等)。其他某种外壳(fishrc等等)可能需要使用不同的语法,但是该方法仍然适用。

自下而上

  1. 制定要在最内层表示的字符串。
  2. 从第二高语言的报价单中选择一种报价机制。
  3. 根据您选择的报价机制为所需的字符串报价。
    • 如何应用哪种报价机制通常会有很多变体。手工操作通常是实践和经验的问题。以编程方式进行操作时,通常最好选择最容易正确的方法(通常是“最文字”(最少的转义符))。
  4. (可选)将结果带引号的字符串与其他代码一起使用。
  5. 如果尚未达到所需的引用/解释水平,请使用生成的带引号的字符串(加上任何添加的代码)并将其用作步骤2中的起始字符串。

报价语义变化

这里要记住的是,每种语言(引用级别)可能给相同的引用字符赋予稍微不同的语义(甚至是完全不同的语义)。

大多数语言都有“文字”报价机制,但是它们的字面意义完全不同。类似于Bourne的shell的单引号实际上是文字(这意味着您不能使用它来引述单引号字符本身)。其他语言(Perl,Ruby)的字面性较差,因为它们非直译地(特别是在单引号区域内)解释了一些反斜杠序列,\\\'导致\',但其他反斜杠序列实际上是文字)。

您将必须阅读每种语言的文档,以了解其引用规则和整体语法。

你的例子

您的示例的最内层是awk程序。

{print $1}

您将把它嵌入到shell命令行中:

pgrep -fl java | grep -i datanode | awk 

我们需要保护(至少)的空间和$AWK程序。显而易见的选择是在整个程序的外壳程序中使用单引号。

  • '{print $1}'

但是,还有其他选择:

  • {print\ \$1} 直接逃脱空间, $
  • {print' $'1} 单引号仅空格和 $
  • "{print \$1}" 请双引号整体并转义 $
  • {print" $"1}仅对空格使用双引号,并且$
    这可能会使规则有些弯曲($在双引号字符串的末尾未转义的是文字),但这似乎在大多数shell中都有效。

如果程序在左花括号和右花括号之间使用逗号,则我们还需要引用或转义逗号或花括号,以避免在某些shell中出现“花括号扩展”。

我们选择'{print $1}'它并将其嵌入外壳的其余“代码”中:

pgrep -fl java | grep -i datanode | awk '{print $1}'

接下来,您想通过susudo运行它。

sudo su user -c 

su user -c …就像some-shell -c …(除了在其他UID下运行)一样,所以su只是添加了另一个shell级别。sudo不会解释其参数,因此不会添加任何引用级别。

我们需要另一个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}'

sudo / * su *命令行中嵌入第一个版本可以得到以下效果:

sudo su user -c 'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'

您可以在其他任何单个Shell级别上下文中使用相同的字符串(例如ssh host …)。

接下来,您在顶部添加了一个ssh级别。这实际上是另一个shell级别:ssh不会解释命令本身,而是将其交给远程端的shell(通过(例如)sh -c …),并且该shell解释字符串。

ssh host 

过程是相同的:接收字符串,选择一种引用方法,使用它,然后将其嵌入。

再次使用单引号:

'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

现在有11个字符串被解释和连接起来:'sudo su user -c ',转义的单引号'pgrep … awk ',,转义的单引号,转义的反斜杠,两个转义的单引号,单引号awk程序,一个转义的单引号,一个转义的反斜杠以及最后一个转义的单引号。

最终形式如下:

ssh host 'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

手工键入有点笨拙,但是外壳的单引号的字面性质使自动进行轻微的变化变得容易:

#!/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")"


7

有关常规解决方案的清晰,深入的解释,请参见Chris Johnsen的答案。我将提供一些额外的技巧,这些技巧在某些常见情况下会有所帮助。

单引号会转义除单引号之外的所有内容。因此,如果您知道变量的值不包含任何单引号,则可以在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看到了"$LC_CMD",最里面的shell看到了命令。

相似的方法对于将数据传递到文本处理实用程序很有用。如果您使用外壳插值,该实用程序会将变量的值视为命令,例如,sed "s/$pattern/$replacement/"如果变量包含,将无法使用/。因此,请使用awk(而不是sed)及其-v选项或ENVIRON数组从外壳传递数据(如果经过ENVIRON,请记住导出变量)。

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'

2

正如克里斯·约翰逊(Chris Johnson)所描述的那样,您在这里有一些引述的间接引用。您指示本地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>

更多:

请查看我对管道路径的回答(也许太冗长),用不同类型的引号代替斜杠,在其中我讨论了为什么行之有效的一些理论。

-麦克风


这看起来很有趣,但是我无法使其正常工作。您能发表一个简单的例子吗?
约翰·劳伦斯·阿斯普登

我相信这个示例需要zsh,因为它使用多个重定向到stdin。在其他类似Bourne的贝壳中,第二个<<仅替换了第一个。它应该在某处说“仅zsh”还是我错过了什么?(不过,巧妙的技巧是,使


0

如何使用更多的双引号?

然后,您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)\""

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.