Bash命令行参数通过ssh传递给sed

时间:2017-01-03 22:44:09

标签: linux bash shell ssh sed

我希望编写一个简单的脚本来同时在许多主机上执行SSH命令,而哪些主机完全是从另一个脚本生成的。问题是,当我使用像sed这样的sometihng运行脚本时,它无法正常工作。

它应该像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. 有没有更好的方法来实现这个目标?

5 个答案:

答案 0 :(得分:6)

eval - 安全引用的命令版本替换为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将解析来自printf '%q'的输出bash等;因此,您无法在远程参数向量上安全地传递此字符串,因为它在/bin/sh - 而不是bash - 在那里运行。 (如果您知道您的远程/bin/sh是由bash提供的,则然后可以安全地运行ssh "$hostname" "$cmd_q",但仅限于此条件下。)
  • bash -s读取脚本从stdin运行,这意味着将命令传递给那里 - 而不是在参数向量上 - 确保它被同一个shell解析为参数逃脱它是壳安全的

答案 1 :(得分:3)

您希望将整个命令(包含其所有参数,空格和引号)传递给ssh,以便它可以不加改变地传递给远程shell进行解析。

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

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

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

另一个有助于排除故障的技巧是添加-v标志做ssh标志,这将标出大量文本,但最重要的是它会向你显示它传递的字符串到底是什么到远程shell进行解析和执行。

-

所有这些都在你的参数中的空间相当脆弱,你需要避免,因为你依赖于对面的shell解析。

答案 2 :(得分:1)

在盒子外面思考:你可以尝试a)在本地构建脚本(也许使用here-document?),b){{而不是处理所有引用问题和在错误的地方分词。 1}}脚本到远程端,然后c)在那里调用它。这很容易允许更复杂的命令序列,具有shell控件结构的所有功能。通过简单地查看生成的脚本,调试(检查正确的引用)将是轻而易举的。

答案 3 :(得分:1)

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

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 - 分隔的“单词”,并且通常不是遍历数组的首选方法。通常最好在调整后的read -r while循环中调用$IFS。为简单起见,我使用了for循环,因为问题实际上是关于调用ssh命令。

答案 4 :(得分:0)

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

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