跳转至

shell脚本执行冲突事件-ssh&while

今天发现一个问题

先看下脚本内容:

Bash
# 首先目录下有两个.cfg文件
root@pts/0 # ls *.cfg
anaconda-ks.cfg  original-ks.cfg

# 看脚本
root@pts/0 # cat test.sh 
#!/bin/bash
ls *.cfg | tr ' ' '\n' | while read line
do
  echo $line 
  if [ 'yes' == 'yes' ]
  then
    ssh 172.16.1.27 "echo 'yes'"
  else
    ssh 172.16.1.27 "echo 'no'"
  fi
done

上面脚本很简单,读取当前目录的所有以cfg结尾的文件(实际上我只是取得循环次数,2次),但我们 运行脚本会发现脚本只循环一次就退出了。效果如下:

Bash
root@pts/0 # bash -x test.sh 
+ tr ' ' '\n'
+ ls anaconda-ks.cfg original-ks.cfg
+ read line
+ echo anaconda-ks.cfg
anaconda-ks.cfg
+ '[' yes == yes ']'
+ ssh 172.16.1.27 'echo '\''yes'\'''
yes
+ read line

此时恭喜你,你如愿接触到sshwhile 冲突事件了!

产生的原因

ssh 命令将while未执行的循环给读取了,下面内容可以帮助你理解。

Bash
root@pts/0 # bash -x /tmp/test_nginx 
+ ls 1.txt 2.txt ip.txt png.txt
+ tr ' ' '\n'
+ read line
+ echo 1.txt
1.txt
+ '[' yes == yes ']'
+ ssh 192.168.9.10 cat     //我们将命令改成cat,就会发现了。
2.txt
ip.txt
png.txt
+ read line

ssh命令会从标准输入读取数据,而while循环中read line也是从标准输入读取,这样ssh会抢走while循环的输入,导致循环只执行一次 在脚本中:

Bash
ls *.cfg | tr ' ' '\n' | while read line

这个管道会将两个文件名传递给while循环,但ssh命令默认会读取标准输入,当执行第一个ssh命令时,它会消耗掉管道中剩余的所有输入,导致 read 命令没有数据可读,循环提前结束

解决办法很简单

方法1:重定向 ssh 的输入

Bash
ssh 172.16.1.27 "echo 'yes'" < /dev/null

方法2:使用 -n 选项禁用 ssh 的 stdin

Bash
ssh -n 172.16.1.27 "echo 'yes'"

方法3:使用 for 循环替代 while 循环(推荐)

Bash
#!/bin/bash
for line in *.cfg
do
  echo "$line"   # 防止文件名称有空格,使用引号
  if [ 'yes' == 'yes' ]
  then
    ssh 172.16.1.27 "echo 'yes'"
  else
    ssh 172.16.1.27 "echo 'no'"
  fi
done
  • For循环不会有管道,避免管道和子shell的问题
  • 不需要特殊处理标准输入

方案4:使用 process substitution

Bash
#!/bin/bash
while read line
do
  echo $line 
  if [ 'yes' == 'yes' ]
  then
    ssh 172.16.1.27 "echo 'yes'"
  else
    ssh 172.16.1.27 "echo 'no'"
  fi
done < <(ls *.cfg)