我希望编写一个简单的脚本来同时在许多主机上执行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命令上消失。
答案 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有一个'原始'模块,它甚至不需要对目标主机有任何依赖,你可能会发现一种非常简单的方法来实现这种功能,使你摆脱循环的考虑主机,处理错误,编组命令等,让您专注于您实际想要实现的目标。