【问题标题】:Bash command line arguments passed to sed via ssh通过 ssh 传递给 sed 的 Bash 命令行参数
【发布时间】:2017-05-18 03:17:42
【问题描述】:

我希望编写一个简单的脚本来同时在许多主机上执行 SSH 命令,并且哪些主机是从另一个脚本生成的。问题是当我使用 sed 之类的东西运行脚本时,它不能正常工作。

它应该像sshall.sh {anything here} 一样运行,它将在列表中的所有节点上运行{anything here} 部分。

sshall.sh

#!/bin/bash

NODES=`listNodes | grep "node-[0-9*]" -o`

echo "Connecting to all nodes and running: ${@:1}"

for i in $NODES
do
   :
        echo "$i : Begin"
        echo "----------------------------------------"

        ssh -q -o "StrictHostKeyChecking no"  $i "${@:1}"

        echo "----------------------------------------"
        echo "$i : Complete";
        echo ""
done

当它使用whoami 之类的东西运行时,它可以工作,但是当我运行时:

[root@myhost bin]# sshall.sh sed -i '/^somebeginning/ s/$/,appendme/' /etc/myconfig.conf
Connecting to all nodes and running: sed -i /^somebeginning/ s/$/,appendme/ /etc/myconfig.conf

node-1 : Begin
----------------------------------------
sed: -e expression #1, char 18: missing command
----------------------------------------
node-1 : Complete

node-2 : Begin
----------------------------------------
sed: -e expression #1, char 18: missing command
----------------------------------------
node-2 : Complete

…

请注意,当发送到远程客户端时,sed 命令上的引号会消失。

  1. 如何修复我的 bash 命令?
  2. 有没有更好的方法来实现这一点?

【问题讨论】:

  • 你可能想说sshall.sh $(sed -i '/^somebeginning/ s/$/,appendme/' /etc/myconfig.conf)。也就是说,使用命令sed ... 的输出来馈送sshall.sh。
  • 我不这么认为,我实际上想在每个节点(节点 1-n)上运行 sed 命令,而不是我从中运行命令的节点;除非它实际上会在每个节点上运行它?
  • 嗯,我明白了。但是,这很奇怪:您将完整的 sed 命令作为一组参数传递给脚本。它看起来非常脆弱。另外,listNodes 是什么?
  • 当您开始转义空格以便可以在远程计算机上运行命令时,它变得太复杂了,创建一个脚本文件几乎肯定会更好,将它复制到目标主机,然后运行它,而不是花时间计算你的命令行被评估了多少次,这样你就可以得到正确的转义量。如果这过于激进,请确保您不需要空格:sshall.sh sed -i '/^somebeginning/s/$/,appendme/' /etc/myconfig.conf。如果你需要处理空格,那就很难了。
  • 有(可能)有办法做到这一点;它们并不容易——根据我的经验,它们也不是万无一失的,因为一些该死的傻瓜(通常是我)找到了一种新的方法来破坏事物。 OTOH,复制脚本并执行它——无需过多努力就可以做到近乎万无一失。 (请注意,whoami(和 who am i)之所以有效,是因为您不必担心在参数中保留空格并且不处理元字符。)

标签: linux bash shell ssh sed


【解决方案1】:

您希望将整个命令(及其所有参数、空格和引号)传递给 ssh,以便它可以将其原封不动地传递给远程 shell 进行解析。

一种方法是将其全部放在单引号内。但是您还需要确保命令中的单引号保留在参数中,以便远程 shell 为 sed 构建正确的参数。

sshall.sh 'sed -i '"'"'/^somebeginning/ s/$/,appendme/'"'"' /etc/myconfig.conf'

它看起来是多余的,但'"'"' 是一个常见的 Bourne 技巧,可以将单引号转换为单引号字符串。第一个引号暂时结束单引号,double-quote-single-quote-double-quote 构造附加一个单引号,然后单引号恢复您的单引号部分。可以这么说。

另一个有助于故障排除的技巧是添加-v 标志做你的ssh 标志,这会吐出大量文本,但最重要的是它会准确地告诉你它传递给远程shell 的字符串用于解析和执行。

--

所有这些在参数中的空格周围都是相当脆弱的,你需要避免这种情况,因为你依赖于对端的 shell 解析。

【讨论】:

  • 你也可以,我通常这样做,使用'\'',虽然反斜杠可能会导致多层次解释的问题,但是你可以用你的有趣来遇到这些问题(因为我'之前没注意到)'"'"' 表示法。
  • 是的,这可以说更具可读性,而且基本上是一样的:告诉本地 shell 在字符串中包含单引号。
  • 只要你实际使用bash,$'sed -i \'/^somebeginning/ s/$/,appendme/\' /etc/myconfig.conf' 比POSIX shell 所需的引号爆炸更容易阅读。
【解决方案2】:

跳出框框思考:您可以尝试 a) 在本地构建脚本(也许使用 here-document?),b) @987654321,而不是处理所有引用问题和错误位置的单词拆分@ 脚本到远程端,然后 c) 在那里调用它。这很容易允许更复杂的命令序列,以及 shell 控制结构等的所有功能。只需查看生成的脚本,调试(检查正确的引用)将变得轻而易举。

【讨论】:

    【解决方案3】:

    将您的命令的eval-safe 引用版本替换为heredoc:

    #!/bin/bash
    #      ^^^^- not /bin/sh; printf %q is an extension
    
    # Put your command into a single string, with each argument quoted to be eval-safe
    printf -v cmd_q '%q ' "$@"
    
    while IFS= read -r hostname; do
      # run bash -s remotely, with that string passed on stdin
      ssh -q -o 'StrictHostKeyChecking no' "$hostname" "bash -s" <<EOF
    $cmd_q
    EOF
    done < <(listNodes | grep -o -e "node-[0-9*]")
    

    为什么这能可靠地工作(而其他方法不能):

    • printf %q 知道如何引用要由同一个 shell 评估的内容(因此将始终支持空格、通配符、各种本地引用方法等)。
    • 给ssh 的参数不会单独传递给远程命令! 相反,它们被连接成一个字符串传递给sh -c。
    • 但是:printf %q 的输出不能移植到所有 POSIX 派生的 shell!它保证与本地使用的同一个 shell 兼容——ksh 将始终在 ksh 中解析来自 printf '%q' 的输出,bash 将在 bash 中解析来自 printf '%q' 的输出等;因此,您不能安全地将这个字符串传递给远程参数向量,因为它是/bin/sh——而不是bash——在那里运行。 (如果您知道您的远程 /bin/sh 是由 bash 提供的,那么您可以安全地运行 ssh "$hostname" "$cmd_q",但只能在这种情况下)。
    • bash -s 读取脚本以从标准输入运行,这意味着将您的命令传递到那里 - 而不是在参数向量上 - 确保它将被相同的 shell 解析为参数 将它转义为 shell 安全。

    【讨论】:

    • 哇,这个答案真的很有帮助!关于幕后工作情况的非常好的信息。
    • bash -s 不允许脚本从标准输入读取数据。 producer | script args | consumer 应该看起来像 producer | sshwrap [sshopts] remote -- script args | consumer。假设远程有/bin/bash,但/bin/sh 是一些非bash-escape-groking shell。 args 可能包含所有邪恶的东西,如空格或尾随换行符。 script 当然不能更改。如果脚本支持sshwrap -t,则奖励积分,这要求 stderr 是 TTY 而 stdin/out 保持透明管道。当然\n~. 和 TTY 控制序列必须到达脚本。生命是致命的! ;)
    【解决方案4】:

    我建议从标准输入而不是命令行参数中读取命令:

    cmd.sh

    #!/bin/bash -
    # Load server_list with user@host "words" here.
    
    cmd=$(</dev/stdin)
    
    for h in ${server_list[*]}; do
        ssh "$h" "$cmd"
    done
    

    用法:

    ./cmd.sh <<'CMD'
    sed -i '/^somebeginning/ s/$/,appendme/' /path/to/file1
    # other commands
    # here...
    CMD
    

    或者,运行./cmd.sh,输入命令,然后按Ctrl-D。

    我发现后一种变体最方便,因为您甚至不需要这里的文档,也不需要额外的转义。只需调用您的脚本,键入命令,然后按快捷方式。还有什么比这更容易的?

    说明

    你的方法的问题是引号被shell从参数中删除了。例如,参数'/^somebeginning/ s/$/,appendme/' 将被解释为/^somebeginning/ s/$/,appendme/ 字符串(不带单引号),这是sed 的无效参数。

    当然,您可以按照此处其他答案中的建议使用内置的 printf 转义命令。但是转义后命令变得不太可读。例如

    printf %q 'sed -i /^somebeginning/ s/$/,appendme/ /home/ruslan/tmp/file1.txt'
    

    产生

    sed\ -i\ /\^somebeginning/\ s/\$/\,appendme/\ /home/ruslan/tmp/file1.txt
    

    如果您将其打印到屏幕上以显示进度,则可读性不强,并且看起来很难看。

    这就是为什么我更喜欢从标准输入中读取并保持命令不变的原因。我的脚本将命令字符串打印到屏幕上,我看到的只是我编写的形式。


    注意,for .. in 循环迭代 $IFS 分隔的“单词”,通常不是遍历数组的首选方式。通常最好在 while 循环中调用 read -r 并调整 $IFS。为了简单起见,我使用了for 循环,因为问题实际上是关于调用ssh 命令。

    【讨论】:

      【解决方案5】:

      通过 SSH 登录多个系统并使用相同(或相同的变体)命令是 ansible 背后的基本用例。该系统并非没有重大缺陷,但对于简单的用例来说非常棒。如果您想要一个更可靠的解决方案,而不需要过多地在主机上转义和循环,请看一下。

      Ansible 有一个“原始”模块,它甚至不需要对目标主机的任何依赖,您可能会发现一种非常简单的方法来实现此类功能,从而使您无需考虑循环主机,处理错误,编组命令等,让您专注于您实际尝试实现的目标。

      【讨论】:

        猜你喜欢
        • 2016-07-25
        • 2014-10-17
        • 2014-01-13
        • 1970-01-01
        • 1970-01-01
        • 2021-11-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多