Shell脚本中的while和for循环

在日常工作中,学会使用shell编程,可以在很大程度上替代手工重复性质的工作,提高工作效率。从这点上来说,了解shell中循环的写法非常关键。下面介绍shell中的while循环和for循环。
1、两种循环基本写法 常见的while和for循环的写法,大概有如下几种:
(1) 通过输入重定向到while循环

while read line do echo $line done < file(待读取的文件)

(2) 通过cat命令输出重定向到while循环
cat file(待读取的文件) | while read line do echo $line done

(3) for循环读取命令输出
for line in `cat file(待读取的文件)` do echo $line done

2、两种循环的区别 按照我的理解,准确的说,上面例子中while和for循环的区别在于:while循环会将每行的内容读入到line变量;for循环中,将读入的内容以IFS(shell中的环境变量,Internal Field Seperator,字段分隔符)为界分隔,然后将各个分隔开的内容,逐一读入变量line。本质上说,for循环读取的是字段,只不过可以设置IFS为\n这样能够逐行读取。
为了方便测试,我们用echo命令来实现多行文字的输出。其中,echo命令的-e选项,意思就是可以识别转义字符能够输出行分隔符。如下例:
$ echo -e "a 12\nb 10" a 12 b 10 $

(1) while逐行读文件
$ echo -e "a 12\nb 10" | while read line > do > echo $line > done a 12 b 10 $

(2) for循环的默认行为
$ for line in `echo -e "a 12\nb 10"` > do > echo $line > done a 12 b 10 $

(3) 通过改变IFS实现for循环按行读入
$ IFS=$'\n' $ for line in `echo -e "a 12\nb 10"` > do > echo $line > done a 12 b 10 $

除了上面常见循环的写法,while循环在逐行读入的同时,还能够根据IFS将整行的内容分隔成多个字段,依次赋值给read后跟的变量名。如果变量数目多余字段的实际个数,少的那些变量值为空;如果变量的数目少于字段实际个数,最后一个变量对所有后面的字段照单全收。下面是一个例子:
$ echo -e "Tom 13\nLily 10 120cm\nJohn" | while read name age > do > echo "${name}: ${age}" > done Tom: 13 Lily: 10 120cm John: $

3、一个简单的shell循环应用 假定有这样一个场景,需要在一个目录中,查找好多关键词。如果用shell搞定,我们就需要先将待搜索的关键词写入一个文件,比如keyword.txt,每行一个关键词。然后,写一个脚本读这个文件,取出每个关键词,然后用grep命令查找。下面是一个参考脚本的例子:
keyword_file='keyword.txt' search_dir='/xx/path/' result_file=result.txtecho "Results:" | tee $result_file cat $keyword_file | while read keyword do echo "${keyword}:" | tee -a $result_file #word match, recursively search in directory and sub directory. only .java file will be searched. case insensitive. -l means only list file name grep -irw --include="*.java" "$keyword" "$search_dir" -l | tee -a $result_file echo "" | tee -a $result_file done

运行结果如下:
$ cat keyword.txt Polymerize SortMeta DataTube $ sh search.sh Results: Polymerize: /xx/path/src/com/poly/merge/test/TestMergeSortDesc.java /xx/path/src/com/poly/merge/test/TestMergeSortDescMultiSort.java /xx/path/src/com/poly/merge/basic/Polymerize.javaSortMeta: /xx/path/src/com/poly/merge/test/TestMergeSort.java /xx/path/src/com/poly/merge/test/TestMergeSort16.javaDataTube: /xx/path/UnitTest/com/poly/merge/basic/PolymerizeTest.java /xx/path/src/com/poly/merge/test/TestMergeSort.java /xx/path/src/com/poly/merge/test/DataTubeImp16.java /xx/path/src/com/poly/merge/basic/Polymerize.java /xx/path/src/com/poly/merge/basic/DataTube.java$

注意:这里的keyword文件涉及到按行读文件,所以这里要注意行分隔符必须是Unix/OS X风格的LF, 也就是\n。如果文件的行分隔符是Windows风格的CRLF,也就是\r\n,会什么都找不到。(我在windows电脑上,将keyword文件分隔符设置为CRLF时,用git bash运行搜索脚本,然后发现什么都搜不到,最终发现是行分隔符的问题。将行分隔符换成LF之后,搜索就一切正常了。)
4、Shell脚本按行读文件是行分隔符的坑 在用Shell编写按行读文件的脚本时,经常会遇到行分隔符的坑,现象看起来十分诡异。这里做下解释。
说明:
在vim中可以通过命令:set ff=xxx来改变文本文件的行分隔符(fffileformat的缩写)。其中xxx可以是dos(代表windows风格的行分隔符,即CRLF,\r\n)、unix(代表Unix和OS X风格的行分隔符,即LF,\n)或者mac(代表Mac风格的行分隔符,即CR,\r)。
假设有一个文件有如下内容,行分隔符是windows格式的\r\n
$ cat text.txt in the house, there is a little horse. finally, it won over a long race near the small inn. all above is just a makeup story. vaginally

写如下的shell脚本test.sh,按行读该文件,然后输出:
cat text.txt | while read line do echo AAAA${line}BB; done

输出如下:
$ sh test.sh BBAAin the house, there is a little horse. BBAAfinally, it won over a long race near the small inn. BBAAall above is just a makeup story. BBAAvaginally

这个输出十分诡异,AAAABB并没有按照预期的显示在每行内容的两侧,BB反而覆盖掉了AAAA中的前两个AA。原因就是因为这里按行读的时候,分隔符是\n,但是文本文件的行分隔符是\r\n。这样,就导致每行读出的内容最后,有一个\r。而这个字符的意思是回车,也就是将光标移动到行开头。这样,以第一行为例,输出BBBBin the house, there is a little horse.之后,输出\r将光标移动到行开头,然后输出AA就盖掉了最前面的BB,出现了前面的效果。
\r字符也不是所有时候都是坑,比如我们想在命令行显示一个跳动的时间,就要用到这个字符,让后面的输出盖掉前面的。下面是一个例子time.sh
i=0 while [ $i -lt 30 ]; doecho -ne "\r"`date` #you should remove new line too; sleep 1; i=$(($i + 1)); done

我在MacOS自带的命令行上执行上面的脚本,如果直接将脚本内容复制到命令行执行,这时候,会得到一个跳动的时间,不会输出多行。
如果通过命令sh time.sh来执行,输出效果是逐行输出。这里怀疑是sh指定的shell不是Bash,可能echo命令对于选项ne的支持有问题(这里可以看出echo的可移植性不好,在脚本编写的时候,尽量使用printf)。,看了下,果然是:
$ which sh /bin/sh

【Shell脚本中的while和for循环】通过添加执行权限和手动指定Bash执行,都可以达到跳动的效果,如下:
$ chmod a+x time.sh $ ./time.sh 2019年 8月14日 星期三 09时03分26秒 $ /bin/bash time.sh 2019年 8月14日 星期三 09时03分35秒 $

参考资料
  • Shell编程中while与for的区别及用法详解

    推荐阅读