在日常工作中,学会使用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
来改变文本文件的行分隔符(ff
是fileformat
的缩写)。其中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
这个输出十分诡异,AAAA
和BB
并没有按照预期的显示在每行内容的两侧,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的区别及用法详解