先来看一段shell脚本:
theIp="1.1.1.1"
echo $theIp | while read ip; do
ssh -t root@$ip
done
#
while read ip; do
ssh -tt root@$ip
done < <(echo $theIp)
#
while true; do
ssh root@$theIp
exit
done
经测试发现,只有第三种写法才可以使用SSH连接,另外两种都会自动退出
- 会报错:tcgetattr: Invalid argument,可以登入远程机器,但是无法操作,只能通过ctrl+c退出并提示:Killed by signal 2
- 如果不加参数“-t”,则会提示:Pseudo-terminal will not be allocated because stdin is not a terminal. 然后退出登录
- 第二种方式与第一种表现一样,但是必须加参数“-tt”,用“-t”会自动退出
综上所述,以上前两种方法完全不科学,用while true只可以登录一台,不能连续登录多台(虽然几乎没有这种需求…)
于是只好在SF上向高人提问:Why can't ssh connect to any host inside a while loop?
ssh was eating up your loop's input. Probably in this case your ssh session exits when it gets EOF from it. That's the likely reason but some input may also cause it to exit. You have to redirect its input by specifying < /dev/null or use -n
@konsolebox回答说,ssh会把while循环里的输入给吃掉!
解决方法是把输入重定向,查看manual:
-n Redirects stdin from /dev/null (actually, prevents reading from stdin). This must be used when ssh is run in the background. A common trick is to use this to run X11 programs on a remote machine. For example, ssh -n shadows.cs.hut.fi emacs & will start an emacs on shadows.cs.hut.fi, and the X11 connection will be automatically forwarded over an encrypted channel. The ssh program will be put in the background. (This does not work if ssh needs to ask for a password or passphrase; see also the -f option.)
将输入重定向到/dev/null,防止从标准输入流中读取数据。ssh在后台执行的时候必须使用该参数,亦或远程执行X11程序.
例如使用-n执行远程emacs的时候,X11连接会自动转向一个加密的通道里,并且ssh会被保持在后台。
当然,不使用-n参数,而直接将输入重定向至null也是可以的,如:ssh ip "cmd" < /dev/null
然后他还提出了一个使用文件描述符的解决方案来避免ssh读取标准输入:
while read -u 4 ip; do
ssh root@$ip
exit
done 4< <(echo $theIp)
从BASH_BUILTINS中查到,read -u的参数意为:Read lines from file descriptor fd instead of the standard input.
上面代码中while read -u 4表示从文件描述符4读取数据到while循环的ip变量中,然后执行ssh操作,这样就避免了ssh直接读取标准输入,从而可以正常连接远程主机。
再来看看“4< <(echo ...)”的写法(注意中间有空格)。从文件描述符中读取数据有两种办法:
echo "a b" > test
#创建文件描述符3指向文件test
exec 3< test
#查看fd3的内容
cat <&3
#从fd3中读取数据到变量a和b
read -u 3 a b
echo $a,$b
#使用过程替换将标准输入转化为fd4
exec 4< <(echo "e f")
#一步到位
read -u5 a b 5< <(echo "ss zz")
#关闭fd3-5
eval "exec "{3..5}"<&-;"
- 以上脚本的前面部分创建了一个一次性的只读文件描述符:
使用 " exec descriptor<file Name " 的模式创建只读模式的用户自定义文件描述符; 使用 "&descriptor" 的模式引用文件描述符; 只读方式的文件描述符只能读取一次,要再次读取,需要对文件描述符重新打开赋值
- 而后面部分则用到了shell中的过程替换(Process Substitution)查看manual可知:
The process list is run with its input or output connected to a FIFO or some file in /dev/fd. The name of this file is passed as an argument to the current command as the result of the expansion. If the >(list) form is used, writing to the file will provide input for list. If the <(list) form is used, the file passed as an argument should be read to obtain the output of list. Note that no space may appear between the < or > and the left parenthesis, otherwise the construct would be interpreted as a redirection.
包括两种用法: <(list) 或 >(list)
进程列表执行时会将输入或输出连接到一个FIFO命名管道或一个在“/dev/fd”中的文件。该文件的名称作为参数传递到当前命令作为扩展的结果。
如果是“ >(list)”方式则写入该文件时将给进程列表提供输入流。若“ <(list)“方式被使用时,作为参数传递的文件应被理解为获取进程列表的输出。
注意,<、>都不应该出现空格和左括号,否则该结构将被解释为重定向。下面有个简单的用法示例:
查看下表可知“/dev/fd/63”就是63文件描述符的复制品。每次创建fd都会在这个目录下面产生一个对应的文件,被读取一次后内容就消失了…
文件
说明
/dev/stdin
文件描述符 0 的复制品
/dev/stdout
文件描述符 1 的复制品
/dev/stderr
文件描述符 2 的复制品
/dev/fd/n
文件描述符 n 的复制品
/dev/tcp/host/port
Bash 在 port 打开到 host 的 TCP 连接
/dev/udp/host/port
Bash 在 port 打开到 host 的 UDP 连接
附录:关于bash中一次性给多个变量赋值--命名管道的使用:
用while read的方式可以同时赋值多个管道内的变量,但是这些变量只存在于子shell,无法改变父进程中的变量值,
因此还可以使用文件描述符的方式(原理同命名管道)给多个外部变量赋值:read -u5 a b c d 5< <(echo "a b c d")
参考:关于 bash shell 重定向的几个特殊文件、Reads from the file descriptor (fd)、