所有文章

逐行读取文件的几种方法

shell编程也是一门学问,虽然功能有限,但要完全掌握它并不容易,甚至比JAVA这样的编程语言更难掌握,因为它考虑了太多的特殊情况,在编写的时候有更多的不确定性。

有一次同事问我,shell里怎样逐行读取一个文件,以处理它,没想到他会被这样的问题给难住。原来他写了一个while语句(从网上搜的)来逐行处理文件,发现这个语句没有像预期的那样执行,写法如下:

[user@node1 ~]$ vim test.sh
#!/bin/bash
 
index=1
while read line
do
	echo "$((index++)): $line"
	ssh localhost "echo $line >> b.txt"
done < a.txt

执行后,只打印出了a.txt中的第一行,当时以为写错了,结果在这个小问题上折腾了很久,再次上网搜索发现原来已很多人也遇到了同样的问题,而且有人已经给出来解释与解决方法。

分析原因

首先是跟ssh这个命令有关系,因为把ssh那一个行去掉以后就一切正常了。

后来看到有网友说为ssh加上-n选项就可以了,所以特地看了下ssh命令的文档,找到了如下描述:

[user@node1 ~]$ man ssh
...
-n      Redirects stdin from /dev/null (actually, prevents reading from stdin).  This must be used when ssh is run in the background.  A com-
             mon 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.)
...

看到这里应该明白了,主要是因为ssh会主动从输入流中读取数据。

现在我们来完整地分析一下这个语句执行的过程,首先a.txt的内容会全部重定向到stdin中等待其它程序读取,这时所有内容中的\n还是存在的,在调用read命令时会以\n为结束符,每次读取一行,这时echo命令把这一行内容给打印出来,而到了ssh这一行的时候,ssh命令会主动从stdin中读出所有内容(就像read命令一样),但它不把\n当做结束符,所以就读取了剩下的所有数据,所以到while第二次循环时,stdin中已经没有数据了,这时跳出循环,执行结束。

为ssh加上-n选项并不是一个完美的解决方法,因为假如while中还有其它类似的命令呢?又要被坑一次,那用for语句行不行呢?就像下面这样:

[user@node1 ~]$ vi test.sh
#!/bin/bash
 
index=1
for line in `cat a.txt`
do
	echo "$((index++)): $line"
	ssh -n localhost "cat"
done

这样还是不行的,因为for line in语句每次读取数据是以系统IFS为结束符的,而IFS包括空格,这意味着如果a.txt中有空格,for语句也就不能逐行读取了。

几种可行的方法

如果只是对文件做一些简单的操作,为ssh加上-n选项就是一个很好的解决方法,因为大多数人对while的用法足够了解:

[user@node1 ~]$ vi test.sh
#!/bin/bash
 
index=1
while read line
do
	echo "$((index++)): $line"
	ssh -n localhost "echo $line >> b.txt"
done < a.txt

如果要处理的文件不是很大,也可以用sed命令来处理,看起来更简单,如果文件太大,性能当然会有损耗了:

[user@node1 ~]$ vi test.sh
#!/bin/bash
 
count=`cat a.txt | wc -l`
for i in `seq 1 $count`
do
    line=`sed -n "${i}p" a.txt`
    echo $line
done

还可以head命令加tail命令,如果文件太大,这种方式的性能损耗会更明显,不过几百行的小文件还是看不出损耗的啦:

[user@node1 ~]$ vi test.sh
#!/bin/bash
 
count=`cat a.txt | wc -l`
for i in `seq 1 $count`
do
    line=`head -n $i | tail -n 1`
    echo $line
done

还可以用强大的awk命令,好像有点大材小用了,=_=!

[user@node1 ~]$ vi test.sh
#!/bin/bash
 
count=`cat a.txt | wc -l`
for i in `seq 1 $count`
do
    line=`awk "NR==$i" a.txt`
    echo $line
done

当然还有xargs命令啦,性能会比sed和awk强些,不过它后面一般只接一个命令,如果操作比较复杂的话,可能满足不了你:

cat a.txt | xargs -I _LINE ssh localhost "echo _LINE > b.txt"

对上面的方法稍加修改,可以在xargs后面接多个命令,不过依然不太适合复杂的操作:

cat a.txt | xargs -I _LINE bash -c 'line="_LINE" ; ssh hostname1 "echo $line > b.txt" ; ssh hostname2 "echo $line > b.txt" '

好吧,先就写这么多,如果以后发现其它更好的方法再写上来。


编写日期:2017-04-09