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.txt

echo "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.java

SortMeta:
/xx/path/src/com/poly/merge/test/TestMergeSort.java
/xx/path/src/com/poly/merge/test/TestMergeSort16.java

DataTube:
/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 ];
do     echo -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

通过添加执行权限和手动指定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的区别及用法详解

你可能感兴趣的:(Shell脚本中的while和for循环)