Shell学习笔记(3)——for、while和until语句

前言

本篇笔记的参考资料是(《Linux命令行与shell脚本编程大全》(第3版),外加百度辅助,本篇笔记的主要内容是循环语句,即for、while和until语句。

for命令

如果遇到这样的场景:重复一组命令直到某个特定条件,例如处理某个目录下的所有文件、系统上所有用户或是某个文本文件中的所有行。此时就会用到for命令,for命令可以创建一个遍历一系列值的的循环,每次迭代就会使用其中一个值来执行已定义好的一组命令,for命令的格式如下所示:

1
2
3
4
for var in list
do
commands
done

格式解释:

第一,在list参数中,用户需要提供推荐中要用到的一系列值;

第二,在每次迭代时,变量var会包含列表中的当前值,第一次迭代使用列表中的第1个值,第2次使用第2个值,以此类推;

第三,在dodone语句之间输入的命令可以是一条或多条标准的bash sehll命令,在这些命令中,$var变量包含着这次迭代对应的当前列表项的中值。

第1案例:读取列表中的值

for命令最基本的用法就是遍历for命令自身所定义的一系列值,如下所示:

1
2
3
4
5
6
#!/bin/bash
# basic for command
for test in Apple Pear Banana Orange Onion
do
echo The next fruit is $test
done

运行结果如下所示:

1
2
3
4
5
The next fruit is Apple
The next fruit is Pear
The next fruit is Banana
The next fruit is Orange
The next fruit is Onion

每次for命令遍历值列表,它都会将列表中的下个值赋给$test变量,$test变量可以像for命令语句中的其他脚本变量一样使用,在最后一次迭代后,$test变量的值会在shell脚本的剩余部分一直保持有效,它会一直保持最后一次迭代的值,除非再次为变量赋值,看下面的案例:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# testing the for variable after the looping
for test in Apple Pear Banana Orange Onion
do
echo The next fruit is $test
done
echo "The last fruit we visited was $test"
test=Watermelon
echo "Wait, now we're visiting $test"

运行结果如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/loop$ bash test1b.sh
The next fruit is Apple
The next fruit is Pear
The next fruit is Banana
The next fruit is Orange
The next fruit is Onion
The last fruit we visited was Onion
Wait, now we're visiting Watermelon

转义字符的使用

for中有可能会遇到比较复杂的值,如下所示:

1
2
3
4
5
6
7
#!/bin/bash
# another example of how not to use the for command
for test in I don't know if this'll work
do
echo "word:$test"
done

结果运行如下:

1
2
3
4
biotest@ubuntu:~/loop$ bash test1c.sh
word:I
word:dont know if thisll
word:work

从结果中可以看出,如果for的列表中存在单引号,则在迭代中会出错误,它会把单引号去掉,并把单引号中间的字符串当作是一个值,如果要解决这个问题,可以使用两种方法:第一,使用转义字符来将单引号转义;第二,使用双引号来定义用到的单引号的值。

现在用这两种方法来解决这个问题,第一个引用用第一种方法,第二个引号用第二种方法,如下所示:

1
2
3
4
5
6
7
#!/bin/bash
# another example of how not to use the for command
for test in I don\'t know if this\'ll work
do
echo "word:$test"
done

运行结果如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/loop$ bash test1c1.sh
word:I
word:don't
word:know
word:if
word:this'll
word:work

从结果可以看出两个单引号都能正常工作了。

多个词的分割

在for循环中,每个值都是用空格进行分割的,如果某个值包含空格,就会比较麻烦,如下面的案例所示:

1
2
3
4
5
6
#!/bin/bash
# another example of how not to user the for command
for test in Nevada New Hampshire New Mexico New York North Carolina
do
echo "Now going to $test"
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/loop$ bash bedtest2
Now going to Nevada
Now going to New
Now going to Hampshire
Now going to New
Now going to Mexico
Now going to New
Now going to York
Now going to North
Now going to Carolina

从结果来看有问题,如果想要把含有空格的数值正确地迭代出来,必须使用双引号将这些值给圈起来,如下所示:

1
2
3
4
5
6
#!/bin/bash
# another example of how not to user the for command
for test in Nevada "New Hampshire" " New Mexico" "New York" "North Carolina"
do
echo "Now going to $test"
done

运行结果如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/loop$ bash bedtest2
Now going to Nevada
Now going to New Hampshire
Now going to New Mexico
Now going to New York
Now going to North Carolina

从变量读取列表

在shell脚本中会遇到这样的情况:将一系列的值都储存在了一个变量中,然后需要遍历变量中的整个列表。这种情况下,可以使用for命令,如下所示:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# using a variable to hold the list
#
list="Henan Chongqing Hebei Jiangsu"
list=$list" Heilongjiang"
for province in $list
do
echo "Have you ever visited $province?"
done

运行结果如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/loop$ bash test4.sh
Have you ever visited Henan?
Have you ever visited Chongqing?
Have you ever visited Hebei?
Have you ever visited Jiangsu?
Have you ever visited Heilongjiang?

代码解释,在代码中有一行这样的代码,即list=$list" Heilongjiang",它表示在原来的列表中添加一个元素。

从命令读取值

生成列表中所需要值的另外一个途径就是使用命令的输出,可以用命令替换来执行任何能产生输出的 命令,然后在for命令中使用该命令的输出,如下所示:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# reading values from a file
file="province"
for province in $(cat $file)
do
echo "Visit beautifull $province"
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/loop$ cat province
Henan
Chongqing
Hubei
Jiansu
Guangdong
Guangxi
biotest@ubuntu:~/loop$ bash test5.sh
Visit beautifull Henan
Visit beautifull Chongqing
Visit beautifull Hubei
Visit beautifull Jiansu
Visit beautifull Guangdong
Visit beautifull Guangxi

在这个案例中,使用了cat命令来输出文件province的内容,在province文件中,每一行有一个省,其中最后一行是China HongKong,中间有一个空格。然后在输出时,for命令把China HongKong分开了。这是因为,在bash shell中,空格,换行符,制表符都是分割符,因此会被分开。

更改字段分割符

for在迭代变量时,使用空格作为分割符,这是因为存在一个特殊的环境就是IFS,它的全称是internal field separator,中文名是内部字段分割符。IFS环境变量定义了bash shell用作字段分割符的一系列字符。默认情况下,bash shell会将以下字符当作是字段分割符,其中包括①空格;②制表符;③换行符。bash shell会将数据中的这些字符都当成分割符,因此在处理含有空格的数据(例如某些文件名)时非常麻烦。如果要解决这个问题,需要修改IFS的值,例如,想让bash shell只识别换行符,就当IFS更改为IFS=$'/n'。将这个语句加入到脚本中,就会告诉bash shell在数据中忽略空格和制表符,还以上面的案例为基础,看一下这段代码。

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# reading values from a file
file="province"
IFS=$'\n'
for province in $(cat $file)
do
echo "Visit beautifull $province"
done

运行后,结果如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/loop$ bash test5a.sh
Visit beautifull Henan
Visit beautifull Chongqing
Visit beautifull Hubei
Visit beautifull Jiansu
Visit beautifull Guangdong
Visit beautifull Guangxi
Visit beautifull China HongKong

现在结果一切正常。如果处理的代码量比较大时,可能在一个地方需要修改IFS的值,然后忽略这次悠,在脚本的其他地方继续没用IFS的默认值,这个时候需要在改变IFS之前保存原来的IFS值,之后再恢复它,这种做法习惯是这样的:

1
2
3
4
IFS.OLD=$IFS
IFS=$'\n'
<在代码中使用新的IFS值>
IFS=$IFS.OLD

此外,还有一种情况,如果某个文件中的分割隔都是冒号(:),例如etc/passwd文件中就是这样,此时也可以将IFS的值更改炒冒号,就像这样IFS=:,如果需要指定多个IFS字符,只要将它们在赋值行中一并输入就行,例如IFS=$'\n':;",这个赋值就是将换行符、冒号、分号和双引号作为字段分隔符。

使用通配符读取目录

使用for命令可以自动遍历目录中的文件,进行操作时,必须要在文件名或路径名中使用通配符,它会强制shell使用文件扩展匹配,文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程,如果不清楚所有的文件名,这个特性在处理上当中的文件时很好用,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
# iterate throught all the files in a directory
for file in /home/biotest/test/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done

运行后结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/loop$ bash test6.sh|head
/home/biotest/test/badtest2.sh is a file
/home/biotest/test/badtest.sh is a file
/home/biotest/test/cheat is a directory
/home/biotest/test/contrast_file.sh is a file
/home/biotest/test/directest.sh is a file
/home/biotest/test/group.sh is a file
/home/biotest/test/hockey is a file
/home/biotest/test/object_exist.sh is a file
/home/biotest/test/owner.sh is a file
/home/biotest/test/sentinel is a file

代码及结果解释:for命令遍历/home/biotest/test/*输出的结果,此代码使用了test命令测试了每个目录,以查看它是目录(-d参数)还是文件(-f参数)。在上面的代码中,if [ -d "$file" ]这里比较特殊,因此它加了引号,因为目录名和文件中包含空格非常常见,因此为了适应这种情况,需要将$file变量用双引号圈起来,如果不圈起来,就会输出错误,如下所示:

现在在/home/biotest/test目录中添加一个目录,命令为a b(中间有空格),再把上面的代码中的if [ -d "$file" ]的引号去掉,运行,如下所示:

1
2
biotest@ubuntu:~/loop$ bash test6a.sh
test6a.sh: line 7: [: /home/biotest/test/a: binary operator expected

这里就显示出错。因为在test命令方面,bash shell会将额外的单词当作参数,从而造成错误。

在for命令中可以列出多个目录通配符,将目录查找和列表合并到同一个for语句中,看下面的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# iterating through multiple directories
for file in /home/biotest/.b* /home/biotest/testing
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
else
echo "$file doesn't exist"
fi
done

运行结果如下所示:

1
2
3
4
5
biotest@ubuntu:~/loop$ bash test7.sh
/home/biotest/.bash_history is a file
/home/biotest/.bash_logout is a file
/home/biotest/.bashrc is a file
/home/biotest/testing is a directory

for语句使用了文件扩展匹配来遍历通配符生成的文件列表,然后它会遍历列表中的下一个文件。

C语言风格的for命令

简单的C风格for命令

在C语言中,它的for命令有一个用来指明变量的特定方法,一个必须保持成立才能继续迭代的条件。以及另一个在每个迭代中改变变量的方法,当指定的条件不成立时,for循环就会停止,条件等式通过标准的数学符号定义,例如下面的C语言代码:

1
2
3
4
for (i = 0; i < 10; i++)
{
printf("The next number is %d\n",i);
}

这段代码会产生一个简单的迭代循环,其中变量i作为计数器,第一部分将一个默认值赋给该变量,中间的部分定义了循环重复的条件。当定义的条件不成立时,for循环就停止迭代。最后一部分定义了迭代的过程,在每次迭代之后,最后一部分占定义的表达式会被执行,在此案例中,每迭代一次,i变量就会增加1。

bash shell也支持一种for循环,与C语言的for循环类似,它的格式如下所示:

1
for (( variable assignment ; condition ; iteration process ))

在bash shell的C语言风格的for命令中,它有一些特殊的地方:第一,变量赋值可以有空格;第二,条件中的变量不以美元符号开头;第三,迭代过程中的算式并使用expr命令格式。看下面的案例:

1
2
3
4
5
6
7
#!/bin/bash
# testing the C-sytle for loop
for (( i=1; i<=10;i++))
do
echo "The next number is $i"
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/loop$ bash test8.sh
The next number is 1
The next number is 2
The next number is 3
The next number is 4
The next number is 5
The next number is 6
The next number is 7
The next number is 8
The next number is 9
The next number is 10

 使用多个变量

C风格的for命令也可以迭代多个变量,循环会单独处理每个变量,用户可以为每个变量定义不同的迭代过程,虽然可以使用多个变量,但在for循环中只能定义一种条件,如下所示:

1
2

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/loop$ bash test9.sh
1-10
2-9
3-8
4-7
5-6
6-5
7-4
8-3
9-2
10-1

代码及结果解释:变量a和b分别用不同的值来初始化并定义了不同的迭代过程,循环的每次迭代在增加变量a的同时,减小了变量b。

while命令

while命令在某种程度上是if-thenfor循环的混合体。while命令允许定义一个要测试的命令,然后循环执行一组命令,只要定义的测试命令返回的是退出状态码0。它会在每次迭代的一开始测试test命令。在test命令返回非零退出状态码时,while命令会停止执行那组命令。

while命令的基本使用

while命令的基本格式如下所示:

1
2
3
4
while test command
do
other commands
done

while命令中定义的test command和if-then语句中的格式一样。可以使用任何普通bash shell命令,或者用test命令进行条件测试,比如测试变量值。while命令的关键在于所指定的test command的退出状态码必须随着循环中运行的命令而改变,如果退出状态码不发生变化,while循环就将一直不停地进行下去。常用的test command的用法是用方括号来检查循环命令中用到的shell变量的值,如下所示:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# while command test
var1=10
while [ $var1 -gt 0 ]
do
echo $var1
var1=$[ $var1 -1 ]
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/loop$ bash test10.sh
10
9
8
7
6
5
4
3
2
1

在这个例子中,while命令定义了每次迭代时检查的测试条件,即whle [ $var1 -gt 0 ],只要测试条件成立,while命令就会不停地循环执行定义好的命令,在这些命令中,测试条件中用到的变量必须修改,否则就会降入无限循环,在这个案例中,每次循环,变量值就会减一,即var1=[ $var1 -1 ]。while循环会在测试条件不再成立时停止。

使用多个测试命令

while命令允许用户在while语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。如果在输入命令时不小心,会导致一些意外,如下面的案例所示:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# testing a multicommand while loop
var1=10
while echo $var1
[ $var1 -ge 0 ]
do
echo "This is inside the loop"
var1=$[ $var1 - 1 ]
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
biotest@ubuntu:~/loop$ bash test11.sh
10
This is inside the loop
9
This is inside the loop
8
This is inside the loop
7
This is inside the loop
6
This is inside the loop
5
This is inside the loop
4
This is inside the loop
3
This is inside the loop
2
This is inside the loop
1
This is inside the loop
0
This is inside the loop
-1

代码及结果解释:第一个测试显示了var1变量的当前值,即while echo $var1,第二个测试用方括号来判断var1变量的值,即[ $var1 -ge 0 ],在循环内部,echo语句会显示一条信息,说明循环被执行了,在最后,变量成为-1,这是因为while循环会在var1变量等于0时执行echo语句,然后将var1送去1,接下来再执行测试命令,用于下一次迭代,echo不测试命令被执行并显示了var变量的值,直到shell执行test测试命令,while循环才停止。这就说明在含有多个命令的while语句中,在每次迭代中所有的测试命令都会被执行,包括测试命令失败的最后一交迭代,需要注意这种用法。

until命令

until命令和while命令的工作方式完全相反,until命令要求你指定一个通常返回非零退出状态码的测试命令。只有油门工命令的退出状态码不为0,bash shell才会执行循环中列出的命令。一旦测试命令返回了退出状态码0,循环就结果了,until命令的基本格式如下所示:

until命令的基本格式

1
2
3
4
until test commands
do
other commands
done

与while命令类似,在until命令中也可以放入多个测试命令,只有最后个命令的退出状态码决定了bash shell是否执行已经定义的other commands,如下所示:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# using the until command
var1=100
until [ $var1 -eq 0 ]
do
echo $var1
var1=$[ $var1 - 25 ]
done

运行结果如下所示:

1
2
3
4
5
biotest@ubuntu:~/loop$ bash test12.sh
100
75
50
25

代码及结果解释:由于变量var1来决定until循环什么时候停止,只要该变量的的值等于0,until命令就会停止循环。until与while类似,在使用多个测试命令时要注意,如下所示:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# using the until command
var1=100
until echo $var1
[ $var1 -eq 0 ]
do
echo Inside the loop: $var1
var1=$[ var1 - 25 ]
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/loop$ bash test13.sh
100
Inside the loop: 100
75
Inside the loop: 75
50
Inside the loop: 50
25
Inside the loop: 25
0

直到var1=0,程序运行结束。

嵌套循环

循环语句可以在循环内使用任意类型的命令,包括其他循环命令。这种循环叫嵌套循环(nested loop)。需要注意的是,在使用嵌套循环时,是在迭代中使用迭代,与命令运行次数的关系是乘积的关系,在写代码时尤其要注意这一点,看下面的一个案例。

for循环与for循环的嵌套

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# nesting for loops
for (( a=1;a<=3;a++))
do
echo "Starting loop $a: "
for ((b=1;b<=3;b++))
do
echo " Inside loop: $b"
done
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
biotest@ubuntu:~/loop$ bash test14.sh
Starting loop 1:
Inside loop: 1
Inside loop: 2
Inside loop: 3
Starting loop 2:
Inside loop: 1
Inside loop: 2
Inside loop: 3
Starting loop 3:
Inside loop: 1
Inside loop: 2
Inside loop: 3

代码及结果解释:在嵌套循环中,在外部循环的每次迭代中遍历一次它所有的值,两个循环的do和done命令没有任何差别,bash shell知道当第一个node命令执行的是内部循环,而非外部循环。

while循环与for循环的嵌套

在while循环内也可以放置一个for循环,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# palcing a for loop inside a while loop
var1=5
while [ $var1 -ge 0 ]
do
echo "Outer loop: $var1"
for ((var2=1;$var2<3;var2++))
do
var3=$[ $var1*$var2 ]
echo " Inner loop: $var1*$var2=$var3"
done
var1=$[ $var1 - 1 ]
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Outer loop: 5
Inner loop: 5*1=5
Inner loop: 5*2=10
Outer loop: 4
Inner loop: 4*1=4
Inner loop: 4*2=8
Outer loop: 3
Inner loop: 3*1=3
Inner loop: 3*2=6
Outer loop: 2
Inner loop: 2*1=2
Inner loop: 2*2=4
Outer loop: 1
Inner loop: 1*1=1
Inner loop: 1*2=2
Outer loop: 0
Inner loop: 0*1=0
Inner loop: 0*2=0

until循环与for循环的嵌套

案例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# using until and while loops
var1=3
# 变量以3开始
until [ $var1 -eq 0 ] # 除非var1等于0,否则继续运行do...done之间的语句
do
echo "Outer loop: $vaar1"
var2=1
while [ $var2 -lt 5 ] # 当var2小于5时,一直运行
do
var3=$(echo "scale=4;$var1/$var2" |bc)
echo " Inner loop: $var1/$var2=$var3"
var2=$[ $var2+1 ]
done
var1=$[ $var1-1 ]
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
biotest@ubuntu:~/loop$ bash test16.sh
Outer loop:
Inner loop: 3/1=3.0000
Inner loop: 3/2=1.5000
Inner loop: 3/3=1.0000
Inner loop: 3/4=.7500
Outer loop:
Inner loop: 2/1=2.0000
Inner loop: 2/2=1.0000
Inner loop: 2/3=.6666
Inner loop: 2/4=.5000
Outer loop:
Inner loop: 1/1=1.0000
Inner loop: 1/2=.5000
Inner loop: 1/3=.3333
Inner loop: 1/4=.2500

循环处理文件数据

当我们遇到一种情况,即必须要遍历储存在文件中的数据时,需要两种技术。第一,使用嵌套循环;第二,修改IFS环境变量。通过修改IFS环境变量,就能强制for命令将文件中的每行都当成单独的一个条目来处理,即使数据中有空格也是如此,一旦从文件中提取出了单独的行,可能需要再次利用循环来提取行中的数据,典型的案例就是处理/etc/passwd文件中的数据,这要求你逐行遍历/etc/passwd文件,并将IFS变量的值改为冒号,这样就能分隔开每行中的各个数据段了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# changing the IFS value
IFS=$'\n'
for entry in $(cat /etc/passwd)
do
echo "Values in $entry -"
IFS=:
for value in $entry
do
echo " $value"
done
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/loop$ bash test17.sh|head
Values in root:x:0:0:root:/root:/bin/bash -
root
x
0
0
root
/root
/bin/bash
Values in daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin -
daemon

这个脚本使用了两个不同的FIS值来解析数据,第一个IFS值解析出/etc/passwd文件中的单独的行,内部for循环接着将FIS的值修改为冒号,然后从单独的行中解析出单独的值,因此在第一次运行时,会提取出一行数据,例如root​:x:​0:0:root:/root:/bin/bash ,接头使用for循环,提取出这一行中以冒号为分割符的元素,即root x 0 0 root /root /bin/bash信息。

控制循环

如果遇到这样的情况:一旦开启了循环,必须要等到所有的循环完成迭代时才结束,如果我想提前想结束循环的话,此时就要用到break命令和continue命令。

break命令

break命令可以退出任意类型的循环,包括untilwhile循环。

break跳出单个循环

在shell中执行break命令,它会深度跳出当前正在执行的循环,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# breaking out of a for loop
for var1 in 1 2 3 4 5 6 7 8 9 10
do
if [ $var1 -eq 5 ]
then
break
fi
echo "Iteration number: $var1"
done
echo "The for loop is completed"

结果如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/loop$ bash test18.sh
Iteration number: 1
Iteration number: 2
Iteration number: 3
Iteration number: 4
The for loop is completed

for循环通常都会遍历列表中指定的所有值。但当满足if-then的条件时,shell会执行break命令,停止for循环。同样的方法也适用于while和until循环,如下所示::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# breaking oiut of a while loop
var1=1
while [ $var1 -lt 10 ]
do
if [ $var1 -eq 5 ]
then
break
fi
echo "Iteration: $var1"
var1=$[ $var1+1 ]
done
echo "The while is loop is completed"

运行结果如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/loop$ bash test19.sh
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
The while is loop is completed

跳出内部循环

在处理多个循环时,break命令会自动终止你所在的最内层的循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# breaking out of an inner loop
for ((a=1;a<4;a++))
do
echo "Outer loop: $a"
for ((b=1;b<100;b++)) # 内部循环指定当变量b等于100时停止迭代;
do
if [ $b -eq 5 ] # 但是当内部循环的if-then语句指明当变量b的值等于5时就执行break命令,跳出循环
then
break
fi
echo " Inner loop: $b" # 但是外部的循环仍然继续执行
done
done

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
biotest@ubuntu:~/loop$ bash test20.sh
Outer loop: 1
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4
Outer loop: 2
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4
Outer loop: 3
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4

跳出外部循环

有时候需要在内部循环,但外部循环停止,break命令可以接受单个命令行参数值,break -n,其中n指定了要跳出的循环层级,默认情况下,n为1,表明跳出的是当前的循环,即最内层的是第1层循环,如果设为2,break命令就会停止下一级的外部循环,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# breaking out of an outer loop
for ((a=1;a<4;a++))
do
echo "Outer loop: $a"
for ((b=1;b<100;b++))
do
if [ $b -gt 4 ]
then
break 2
fi
echo " Inner loop: $b"
done
done

运行结果如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/loop$ bash test21.sh
Outer loop: 1
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4

当shell执行了break命令后,外部循环就停止了。

continue命令

continue命令可以提前中止某次循环中的命令,但并不会完全终止整个循环,可以在循环内部设置shell不执行命令的条件,这里有个for循环中使用continue命令的简单例子,如下所示:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# testing continue command
for ((var1=1;var1<15;var1++))
do
if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
then
continue
fi
echo "Iteration number: $var1"
done

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/loop$ vim continue_test.sh
biotest@ubuntu:~/loop$ bash continue_test.sh
Iteration number: 1
Iteration number: 2
Iteration number: 3
Iteration number: 4
Iteration number: 5
Iteration number: 10
Iteration number: 11
Iteration number: 12
Iteration number: 13
Iteration number: 14

代码解释:当if-then语句的条件满足时,即值大于5小于10,shell会 执行continue命令,跳过此次循环中的剩余命令,但整个循环还会继续,当if-then的条件不再满足后,又继续循环。在while和until循环中也可以使用continue命令,但要谨慎,因为当shell执行continue命令时,它会跳过剩余的部分,如果在其中的条件具条件里对测试条件变量进行增值,问题就会出现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# improperly using the continue command in a while loop
var1=0
while echo "while iteration; $var1"
[ $var1 -lt 15 ]
do
if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
then
continue
fi
echo " Inside iteration number: $var1"
var1=$[ $var1+1 ]
done

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
biotest@ubuntu:~/loop$ bash badtest3.sh|head -20
while iteration; 0
Inside iteration number: 0
while iteration; 1
Inside iteration number: 1
while iteration; 2
Inside iteration number: 2
while iteration; 3
Inside iteration number: 3
while iteration; 4
Inside iteration number: 4
while iteration; 5
Inside iteration number: 5
while iteration; 6
while iteration; 6
while iteration; 6
while iteration; 6
while iteration; 6
while iteration; 6
while iteration; 6
while iteration; 6

从结果可以看了同,在if-then的条件成立之后,一切都正常,但是当执行了continue命令后,它就跳过了while循环中的剩余部分,则跳过的部分正好是$var1计数变量增值的部分,而这个变量又被用于个渭测试命令中,因此这个值就不再变化,从而连续输出。

continue也可以与break一样,使用n来指定要继续执行哪一级循环,格式为continue n,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# continuing an outer loop
for ((a=1;a<=5;a++))
do
echo "Iteration $a: "
for((b=1;b<3;b++))
do
if [ $a -gt 2 ] && [ $a -lt 4 ]
then
continue 2
fi
var3=$[ $a*$b ]
echo " The result of $a*$b is $var3"
done
done

结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/loop$ bash test22.sh
Iteration 1:
The result of 1*1 is 1
The result of 1*2 is 2
Iteration 2:
The result of 2*1 is 2
The result of 2*2 is 4
Iteration 3:
Iteration 4:
The result of 4*1 is 4
The result of 4*2 is 8
Iteration 5:
The result of 5*1 is 5
The result of 5*2 is 10

代码解释,其中的if-then部分为:

1
2
3
4
if [ $a -gt 2 ] && [ $a -lt 4 ]
then
continue 2
fi

在此处用continue命令来停止处理循环内的命令,但会继续处理外部循环,值为3的那次迭代并没有处理任何内部循环语句,因为尽管continue命令命令会停止了处理过程,但外部循环会继续。(有关break ncontinue n的这个原理,再写一篇笔记,我是没弄清楚原理)。

处理循环输出

在shell脚本中,可以对循环的输出使用管道或重定向,这可以通过在done命令之后添加一个处理命令来实现。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
for file in /home/biotest/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done > output.tx

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/loop$ cat output.txt|head
/home/biotest/Desktop is a directory
/home/biotest/Documents is a directory
/home/biotest/Downloads is a directory
/home/biotest/examples.desktop is a file
/home/biotest/filename is a file
/home/biotest/log. is a file
/home/biotest/log.180427 is a file
/home/biotest/loop is a directory
/home/biotest/miniconda2 is a directory
/home/biotest/Miniconda2-latest-Linux-x86_64.sh is a file

第2个案例,如下所示:

1
2
3
4
5
6
7
8
#!/bin/bash
# redirecting the for output to a file
for ((a=1;a<10;a++))
do
echo "The number is $a"
done > test23.txt
echo "The commmand is finished."

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/loop$ cat test23.txt
The number is 1
The number is 2
The number is 3
The number is 4
The number is 5
The number is 6
The number is 7
The number is 8
The number is 9

代码及结果解释:shell创建了文件test23.txt并将for命令的输出重定向到这个文件,shell在for命令之后正常显示了echo语句。这种方法也同样适用于将循环的结果用通道导入另一个命令。

1
2
3
4
5
6
7
8
#!/bin/bash
# piping a loop to another command
for province in "Chine HK" Henan Guangdong
do
echo "$province is the next place to go"
done | sort
echo "This completes out travels"

运行结果如下所示:

1
2
3
4
5
biotest@ubuntu:~/loop$ bash test24.sh
Chine HK is the next place to go
Guangdong is the next place to go
Henan is the next place to go
This completes out travels

代码及结果解释:province的值并没有在for命令列表中以特定次序列出,for命令的输出结果导入到了sort命令中,该命令会改变for命令输出结果的顺序。运行这个脚本实际上说明了结果已经在脚本内部排好序了。

最终案例

第1案例:查找可执行文件

如果要找出系统中有哪些可执行文件,只需要扫描PATH环境变量中的所有目录即可,下面是这样的一个脚本,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# finding files in the PATH
IFS=:
for folder in $PATH
do
echo "$folder:"
for file in $folder/*
do
if [ -x $file ]
then
echo " $file"
fi
done
done

运行,结果如下所示:

1
2
3
4
5
6
7
biotest@ubuntu:~/loop$ bash test25.sh|head -5
/home/biotest/miniconda2/bin:
/home/biotest/miniconda2/bin/2to3
/home/biotest/miniconda2/bin/activate
/home/biotest/miniconda2/bin/appletviewer
/home/biotest/miniconda2/bin/c2ph
biotest@ubuntu:~/loop$ cat test25.sh

第2案例:创建多个用户账户

如果要创建大量的用户,可以使用while循环来实现,将需要添加的新用户账户放在一个文本文件中,然后创建一个简单的脚本处理,文件文件的格式是这样的userid, user name,第一个条目是新用户的ID,第二个条目是用户的全名。两个值之间使用逗号分隔,这样就形成卫 种名为逗号分隔值的文本文件(或是csv格式文件)。如果要读取这里面的数据,需要将IFS分隔符设置为逗号,并将其放入while语句的条件测试部分。然后使用read命令读取文件中的各行,这一部分 的代码就是while IFS=',',read -r userid name,read命令会自动读取.csv文件的下一行内容,因此不需要创建一个循环,当read命令返回FALSE时(也就是读取完整个文件时),while命令就会退出,完整的脚本如下所示:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# process new user accounts
input="user.csv"
while IFS=',' read -r userid name # -r这个参数表示允许输入的值中包含反斜杠“\”,反斜杠也作为值输出
do
echo "adding $userid"
useradd -c "$name" -m $userid #-c表示:加上备注文字。备注文字会保存在passwd的备注栏位中;
# -m表示自动建立用户的登入目录
done < "$input" #将input变量中的csv文件输导入到while循环中

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
biotest@ubuntu:~/loop$ cat user.csv
userid,user name
501,test01
502,test02
503,test03
biotest@ubuntu:~/loop$ sudo bash test26.sh
[sudo] password for biotest:
adding userid
adding 501
adding 502
adding 503
biotest@ubuntu:~/loop$ tail /etc/passwd
kernoops:x:116:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false
pulse:x:117:124:PulseAudio daemon,,,:/var/run/pulse:/bin/false
rtkit:x:118:126:RealtimeKit,,,:/proc:/bin/false
saned:x:119:127::/var/lib/saned:/bin/false
usbmux:x:120:46:usbmux daemon,,,:/var/lib/usbmux:/bin/false
biotest:x:1000:1000:UBUNTU,,,:/home/biotest:/bin/bash
:/home/userid:1002:user name
:/home/501:1003:test01
:/home/502:1004:test02
:/home/503:1005:test03

从结果可以看出,这3个账户已经生成。