一、嵌套循环
循环语句可以在循环内使用任意类型的命令,包括其他循环命令。这种循环叫作嵌套循环(nested loop)。注意,在使用嵌套循环时,你是在迭代中使用迭代,与命令运行的次数是乘积关系。不注意这点的话,有可能会在脚本中造成问题。
这里有个在for循环中嵌套for循环的简单例子。
1 $ cat test14
2 #!/bin/bash
3 # nesting for loops
4 for (( a = 1; a <= 3; a++ ))
5 do
6 echo "Starting loop $a:"
7 for (( b = 1; b <= 3; b++ ))
8 do
9 echo " Inside loop: $b"
10 done
11 done
12 $ ./test14
13 Starting loop 1:
14 Inside loop: 1
15 Inside loop: 2
16 Inside loop: 3
17 Starting loop 2:
18 Inside loop: 1
19 Inside loop: 2
20 Inside loop: 3
21 Starting loop 3:
22 Inside loop: 1
23 Inside loop: 2
24 Inside loop: 3
25 $
这个被嵌套的循环(也称为内部循环,inner loop)会在外部循环的每次迭代中遍历一次它所有的值。注意,两个循环的do和done命令没有任何差别。bash shell知道当第一个done命令执行时是指内部循环而非外部循环。
在混用循环命令时也一样,比如在while循环内部放置一个for循环。
1 $ cat test15
2 #!/bin/bash
3 # placing a for loop inside a while loop
4 var1=5
5 while [ $var1 -ge 0 ]
6 do
7 echo "Outer loop: $var1"
8 for (( var2 = 1; $var2 < 3; var2++ ))
9 do
10 var3=$[ $var1 * $var2 ]
11 echo " Inner loop: $var1 * $var2 = $var3"
12 done
13 var1=$[ $var1 - 1 ]
14 done
15 $ ./test15
16 Outer loop: 5
17 Inner loop: 5 * 1 = 5
18 Inner loop: 5 * 2 = 10
19 Outer loop: 4
20 Inner loop: 4 * 1 = 4
21 Inner loop: 4 * 2 = 8
22 Outer loop: 3
23 Inner loop: 3 * 1 = 3
24 Inner loop: 3 * 2 = 6
25 Outer loop: 2
26 Inner loop: 2 * 1 = 2
27 Inner loop: 2 * 2 = 4
28 Outer loop: 1
29 Inner loop: 1 * 1 = 1
30 Inner loop: 1 * 2 = 2
31 Outer loop: 0
32 Inner loop: 0 * 1 = 0
33 Inner loop: 0 * 2 = 0
34 $
同样,shell能够区分开内部for循环和外部while循环各自的do和done命令。
如果真的想挑战脑力,可以混用until和while循环。
1 $ cat test16
2 #!/bin/bash
3 # using until and while loops
4 var1=3
5 until [ $var1 -eq 0 ]
6 do
7 echo "Outer loop: $var1"
8 var2=1
9 while [ $var2 -lt 5 ]
10 do
11 var3=$(echo "scale=4; $var1 / $var2" | bc)
12 echo " Inner loop: $var1 / $var2 = $var3"
13 var2=$[ $var2 + 1 ]
14 done
15 var1=$[ $var1 - 1 ]
16 done
17 $ ./test16
18 Outer loop: 3
19 Inner loop: 3 / 1 = 3.0000
20 Inner loop: 3 / 2 = 1.5000
21 Inner loop: 3 / 3 = 1.0000
22 Inner loop: 3 / 4 = .7500
23 Outer loop: 2
24 Inner loop: 2 / 1 = 2.0000
25 Inner loop: 2 / 2 = 1.0000
26 Inner loop: 2 / 3 = .6666
27 Inner loop: 2 / 4 = .5000
28 Outer loop: 1
29 Inner loop: 1 / 1 = 1.0000
30 Inner loop: 1 / 2 = .5000
31 Inner loop: 1 / 3 = .3333
32 Inner loop: 1 / 4 = .2500
33 $
外部的until循环以值3开始,并继续执行到值等于0。内部while循环以值1开始并一直执行,只要值小于5。每个循环都必须改变在测试条件中用到的值,否则循环就会无止尽进行下去。
二、循环处理文件数据
通常必须遍历存储在文件中的数据。这要求结合已经讲过的两种技术:
- 使用嵌套循环
- 修改IFS环境变量
通过修改IFS环境变量,就能强制for命令将文件中的每行都当成单独的一个条目来处理,即便数据中有空格也是如此。一旦从文件中提取出了单独的行,可能需要再次利用循环来提取行中的数据。
典型的例子是处理/etc/passwd文件中的数据。这要求你逐行遍历/etc/passwd文件,并将IFS变量的值改成冒号,这样就能分隔开每行中的各个数据段了。
1 #!/bin/bash
2 # changing the IFS value
3 IFS.OLD=$IFS
4 IFS=$‘\n‘
5 for entry in $(cat /etc/passwd)
6 do
7 echo "Values in $entry –"
8 IFS=:
9 for value in $entry
10 do
11 echo " $value"
12 done
13 done
14 $
这个脚本使用了两个不同的IFS值来解析数据。第一个IFS值解析出/etc/passwd文件中的单独的行。内部for循环接着将IFS的值修改为冒号,允许你从/etc/passwd的行中解析出单独的值。在运行这个脚本时,你会得到如下输出。
1 Values in rich:x:501:501:Rich Blum:/home/rich:/bin/bash -
2 rich
3 x
4 501
5 501
6 Rich Blum
7 /home/rich
8 /bin/bash
9 Values in katie:x:502:502:Katie Blum:/home/katie:/bin/bash -
10 katie
11 x
12 506
13 509
14 Katie Blum
15 /home/katie
16 /bin/bash
内部循环会解析出/etc/passwd每行中的各个值。这种方法在处理外部导入电子表格所采用的逗号分隔的数据时也很方便。
三、控制循环
你可能会想,一旦启动了循环,就必须苦等到循环完成所有的迭代。并不是这样的。有两个命令能帮我们控制循环内部的情况:
- break命令
- continue命令
每个命令在如何控制循环的执行方面有不同的用法。下面几节将介绍如何使用这些命令来控制循环。
3.1、break 命令
break命令是退出循环的一个简单方法。可以用break命令来退出任意类型的循环,包括while和until循环。
有几种情况可以使用break命令,本节将介绍这些方法。
3.1.1、跳出单个循环
在shell执行break命令时,它会尝试跳出当前正在执行的循环。
1 $ cat test17
2 #!/bin/bash
3 # breaking out of a for loop
4 for var1 in 1 2 3 4 5 6 7 8 9 10
5 do
6 if [ $var1 -eq 5 ]
7 then
8 break
9 fi
10 echo "Iteration number: $var1"
11 done
12 echo "The for loop is completed"
13 $ ./test17
14 Iteration number: 1
15 Iteration number: 2
16 Iteration number: 3
17 Iteration number: 4
18 The for loop is completed
19 $
for循环通常都会遍历列表中指定的所有值。但当满足if-then的条件时,shell会执行break命令,停止for循环。
这种方法同样适用于while和until循环。
1 $ cat test18
2 #!/bin/bash
3 # breaking out of a while loop
4 var1=1
5 while [ $var1 -lt 10 ]
6 do
7 if [ $var1 -eq 5 ]
8 then
9 break
10 fi
11 echo "Iteration: $var1"
12 var1=$[ $var1 + 1 ]
13 done
14 echo "The while loop is completed"
15 $ ./test18
16 Iteration: 1
17 Iteration: 2
18 Iteration: 3
19 Iteration: 4
20 The while loop is completed
21 $
while循环会在if-then的条件满足时执行break命令,终止。
3.1.2、跳出内部循环
在处理多个循环时,break命令会自动终止你所在的最内层的循环。
1 $ cat test19
2 #!/bin/bash
3 # breaking out of an inner loop
4 for (( a = 1; a < 4; a++ ))
5 do
6 echo "Outer loop: $a"
7 for (( b = 1; b < 100; b++ ))
8 do
9 if [ $b -eq 5 ]
10 then
11 break
12 fi
13 echo " Inner loop: $b"
14 done
15 done
16 $ ./test19
17 Outer loop: 1
18 Inner loop: 1
19 Inner loop: 2
20 Inner loop: 3
21 Inner loop: 4
22 Outer loop: 2
23 Inner loop: 1
24 Inner loop: 2
25 Inner loop: 3
26 Inner loop: 4
27 Outer loop: 3
28 Inner loop: 1
29 Inner loop: 2
30 Inner loop: 3
31 Inner loop: 4
32 $
内部循环里的for语句指明当变量b等于100时停止迭代。但内部循环的if-then语句指明当变量b的值等于5时执行break命令。注意,即使内部循环通过break命令终止了,外部循环依然继续执行。
3.1.3、跳出外部循环
有时你在内部循环,但需要停止外部循环。break命令接受单个命令行参数值:
1 break n
其中n指定了要跳出的循环层级。默认情况下,n为1,表明跳出的是当前的循环。如果你将n设为2,break命令就会停止下一级的外部循环。
1 $ cat test20
2 #!/bin/bash
3 # breaking out of an outer loop
4 for (( a = 1; a < 4; a++ ))
5 do
6 echo "Outer loop: $a"
7 for (( b = 1; b < 100; b++ ))
8 do
9 if [ $b -gt 4 ]
10 then
11 break 2
12 fi
13 echo " Inner loop: $b"
14 done
15 done
16 $ ./test20
17 Outer loop: 1
18 Inner loop: 1
19 Inner loop: 2
20 Inner loop: 3
21 Inner loop: 4
22 $
注意,当shell执行了break命令后,外部循环就停止了。
3.2、continue 命令
continue命令可以提前中止某次循环中的命令,但并不会完全终止整个循环。可以在循环内部设置shell不执行命令的条件。这里有个在for循环中使用continue命令的简单例子。
1 $ cat test21
2 #!/bin/bash
3 # using the continue command
4 for (( var1 = 1; var1 < 15; var1++ ))
5 do
6 if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
7 then
8 continue
9 fi
10 echo "Iteration number: $var1"
11 done
12 $ ./test21
13 Iteration number: 1
14 Iteration number: 2
15 Iteration number: 3
16 Iteration number: 4
17 Iteration number: 5
18 Iteration number: 10
19 Iteration number: 11
20 Iteration number: 12
21 Iteration number: 13
22 Iteration number: 14
23 $
当if-then语句的条件被满足时(值大于5且小于10),shell会执行continue命令,跳过此次循环中剩余的命令,但整个循环还会继续。当if-then的条件不再被满足时,一切又回到正轨。
也可以在while和until循环中使用continue命令,但要特别小心。记住,当shell执行continue命令时,它会跳过剩余的命令。如果你在其中某个条件里对测试条件变量进行增值,问题就会出现。
1 $ cat badtest3
2 #!/bin/bash
3 # improperly using the continue command in a while loop
4 var1=0
5 while echo "while iteration: $var1"
6 [ $var1 -lt 15 ]
7 do
8 if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
9 then
10 continue
11 fi
12 echo " Inside iteration number: $var1"
13 var1=$[ $var1 + 1 ]
14 done
15 $ ./badtest3 | more
16 while iteration: 0
17 Inside iteration number: 0
18 while iteration: 1
19 Inside iteration number: 1
20 while iteration: 2
21 Inside iteration number: 2
22 while iteration: 3
23 Inside iteration number: 3
24 while iteration: 4
25 Inside iteration number: 4
26 while iteration: 5
27 Inside iteration number: 5
28 while iteration: 6
29 while iteration: 6
30 while iteration: 6
31 while iteration: 6
32 while iteration: 6
33 while iteration: 6
34 while iteration: 6
35 while iteration: 6
36 while iteration: 6
37 while iteration: 6
38 while iteration: 6
39 $
你得确保将脚本的输出重定向到了more命令,这样才能停止输出。在if-then的条件成立之前,所有一切看起来都很正常,然后shell执行了continue命令。当shell执行continue命令时,它跳过了while循环中余下的命令。不幸的是,被跳过的部分正是$var1计数变量增值的地方,而这个变量又被用于while测试命令中。这意味着这个变量的值不会再变化了,从前面连续的输出显示中你也可以看出来。
和break命令一样,continue命令也允许通过命令行参数指定要继续执行哪一级循环:
1 continue n
其中n定义了要继续的循环层级。下面是继续外部for循环的一个例子。
1 $ cat test22
2 #!/bin/bash
3 # continuing an outer loop
4 for (( a = 1; a <= 5; a++ ))
5 do
6 echo "Iteration $a:"
7 for (( b = 1; b < 3; b++ ))
8 do
9 if [ $a -gt 2 ] && [ $a -lt 4 ]
10 then
11 continue 2
12 fi
13 var3=$[ $a * $b ]
14 echo " The result of $a * $b is $var3"
15 done
16 done
17 $ ./test22
18 Iteration 1:
19 The result of 1 * 1 is 1
20 The result of 1 * 2 is 2
21 Iteration 2:
22 The result of 2 * 1 is 2
23 The result of 2 * 2 is 4
24 Iteration 3:
25 Iteration 4:
26 The result of 4 * 1 is 4
27 The result of 4 * 2 is 8
28 Iteration 5:
29 The result of 5 * 1 is 5
30 The result of 5 * 2 is 10
31 $
其中的if-then语句:
1 if [ $a -gt 2 ] && [ $a -lt 4 ]
2 then
3 continue 2
4 fi
此处用continue命令来停止处理循环内的命令,但会继续处理外部循环。注意,值为3的那次迭代并没有处理任何内部循环语句,因为尽管continue命令停止了处理过程,但外部循环依然会继续。
四、处理循环的输出
最后,在shell脚本中,你可以对循环的输出使用管道或进行重定向。这可以通过在done命令之后添加一个处理命令来实现。
1 for file in /home/rich/*
2 do
3 if [ -d "$file" ]
4 then
5 echo "$file is a directory"
6 elif
7 echo "$file is a file"
8 fi
9 done > output.txt
shell会将for命令的结果重定向到文件output.txt中,而不是显示在屏幕上。考虑下面将for命令的输出重定向到文件的例子。
1 $ cat test23
2 #!/bin/bash
3 # redirecting the for output to a file
4 for (( a = 1; a < 10; a++ ))
5 do
6 echo "The number is $a"
7 done > test23.txt
8 echo "The command is finished."
9 $ ./test23
10 The command is finished.
11 $ cat test23.txt
12 The number is 1
13 The number is 2
14 The number is 3
15 The number is 4
16 The number is 5
17 The number is 6
18 The number is 7
19 The number is 8
20 The number is 9
21 $
shell创建了文件test23.txt并将for命令的输出重定向到这个文件。shell在for命令之后正常显示了echo语句。
这种方法同样适用于将循环的结果管接给另一个命令。
1 $ cat test24
2 #!/bin/bash
3 # piping a loop to another command
4 for state in "North Dakota" Connecticut Illinois Alabama Tennessee
5 do
6 echo "$state is the next place to go"
7 done | sort
8 echo "This completes our travels"
9 $ ./test24
10 Alabama is the next place to go
11 Connecticut is the next place to go
12 Illinois is the next place to go
13 North Dakota is the next place to go
14 Tennessee is the next place to go
15 This completes our travels
16 $
state值并没有在for命令列表中以特定次序列出。for命令的输出传给了sort命令,该命令会改变for命令输出结果的顺序。运行这个脚本实际上说明了结果已经在脚本内部排好序了。
五、实例
现在你已经看到了shell脚本中各种循环的使用方法,来看一些实际应用的例子吧。循环是对系统数据进行迭代的常用方法,无论是目录中的文件还是文件中的数据。下面的一些例子演示了如何使用简单的循环来处理数据。
5.1、查找可执行文件
当你从命令行中运行一个程序的时候,Linux系统会搜索一系列目录来查找对应的文件。这些目录被定义在环境变量PATH中。如果你想找出系统中有哪些可执行文件可供使用,只需要扫描PATH环境变量中所有的目录就行了。如果要徒手查找的话,就得花点时间了。不过我们可以编写一个小小的脚本,轻而易举地搞定这件事。
首先是创建一个for循环,对环境变量PATH中的目录进行迭代。处理的时候别忘了设置IFS分隔符。
1 IFS=:
2 for folder in $PATH
3 do
现在你已经将各个目录存放在了变量$folder中,可以使用另一个for循环来迭代特定目录中的所有文件。
1 for file in $folder/*
2 do
最后一步是检查各个文件是否具有可执行权限,你可以使用if-then测试功能来实现。
1 if [ -x $file ]
2 then
3 echo " $file"
4 fi
好了,搞定了!将这些代码片段组合成脚本就行了。
1 $ cat test25
2 #!/bin/bash
3 # finding files in the PATH
4 IFS=:
5 for folder in $PATH
6 do
7 echo "$folder:"
8 for file in $folder/*
9 do
10 if [ -x $file ]
11 then
12 echo " $file"
13 fi
14 done
15 done
16 $
运行这段代码时,你会得到一个可以在命令行中使用的可执行文件的列表。
1 $ ./test25 | more
2 /usr/local/bin:
3 /usr/bin:
4 /usr/bin/Mail
5 /usr/bin/Thunar
6 /usr/bin/X
7 /usr/bin/Xorg
8 /usr/bin/[
9 /usr/bin/a2p
10 /usr/bin/abiword
11 /usr/bin/ac
12 /usr/bin/activation-client
13 /usr/bin/addr2line
14 ...
输出显示了在环境变量PATH所包含的所有目录中找到的全部可执行文件,数量真是不少!
5.2、创建多个用户账户
shell脚本的目标是让系统管理员过得更轻松。如果你碰巧工作在一个拥有大量用户的环境中,最烦人的工作之一就是创建新用户账户。好在可以使用while循环来降低工作的难度。
你不用为每个需要创建的新用户账户手动输入useradd命令,而是可以将需要添加的新用户账户放在一个文本文件中,然后创建一个简单的脚本进行处理。这个文本文件的格式如下:
1 userid,user name
第一个条目是你为新用户账户所选用的用户ID。第二个条目是用户的全名。两个值之间使用逗号分隔,这样就形成了一种名为逗号分隔值的文件格式(或者是.csv)。这种文件格式在电子表格中极其常见,所以你可以轻松地在电子表格程序中创建用户账户列表,然后将其保存成.csv格
式,以备shell脚本读取及处理。
要读取文件中的数据,得用上一点shell脚本编程技巧。我们将IFS分隔符设置成逗号,并将其放入while语句的条件测试部分。然后使用read命令读取文件中的各行。实现代码如下:
1 while IFS=’,’ read –r userid name
read命令会自动读取.csv文本文件的下一行内容,所以不需要专门再写一个循环来处理。当read命令返回FALSE时(也就是读取完整个文件时),while命令就会退出。妙极了!
要想把数据从文件中送入while命令,只需在while命令尾部使用一个重定向符就可以了。
将各部分处理过程写成脚本如下。
1 $ cat test26
2 #!/bin/bash
3 # process new user accounts
4 input="users.csv"
5 while IFS=‘,‘ read -r userid name
6 do
7 echo "adding $userid"
8 useradd -c "$name" -m $userid
9 done < "$input"
10 $
$input变量指向数据文件,并且该变量被作为while命令的重定向数据。users.csv文件内容如下。
1 $ cat users.csv
2 rich,Richard Blum
3 christine,Christine Bresnahan
4 barbara,Barbara Blum
5 tim,Timothy Bresnahan
6 $
必须作为root用户才能运行这个脚本,因为useradd命令需要root权限。
1 # ./test26
2 adding rich
3 adding christine
4 adding barbara
5 adding tim
6 #
来看一眼/etc/passwd文件,你会发现账户已经创建好了。
1 # tail /etc/passwd
2 rich:x:1001:1001:Richard Blum:/home/rich:/bin/bash
3 christine:x:1002:1002:Christine Bresnahan:/home/christine:/bin/bash
4 barbara:x:1003:1003:Barbara Blum:/home/barbara:/bin/bash
5 tim:x:1004:1004:Timothy Bresnahan:/home/tim:/bin/bash
6 #
恭喜,你已经在添加用户账户这项任务上给自己省出了大量时间!
六、小结
循环是编程的一部分。bash shell提供了三种可用于脚本中的循环命令。
for命令允许你遍历一系列的值,不管是在命令行里提供好的、包含在变量中的还是通过文件扩展匹配获得的文件名和目录名。
while命令使用普通命令或测试命令提供了基于命令条件的循环。只有在命令(或条件)产生退出状态码0时,while循环才会继续迭代指定的一组命令。
until命令也提供了迭代命令的一种方法,但它的迭代是建立在命令(或条件)产生非零退出状态码的基础上。这个特性允许你设置一个迭代结束前都必须满足的条件。
可以在shell脚本中对循环进行组合,生成多层循环。bash shell提供了continue和break命令,允许你根据循环内的不同值改变循环的正常流程。
bash shell还允许使用标准的命令重定向和管道来改变循环的输出。你可以使用重定向来将循环的输出重定向到一个文件或是另一个命令。这就为控制shell脚本执行提供了丰富的功能。