结构化命令

4/1/2022 linuxshell

上一章给出的那些 shell 脚本里,shell 按照命令在脚本中出现的顺序依次进行处理。对顺序操作来说,这已经足够了,因为在这种操作环境下,你想要的就是所有的命令按照正确的顺序执行。然而,并非所有程序都如此操作。

许多程序要求对 shell 脚本中的命令施加一些逻辑流程控制。有一类命令会根据条件使脚本跳过某些命令。这样的命令通常称为结构化命令(structured command)。

结构化命令允许你改变程序执行的顺序。在 bash shell 中有不少结构化命令,我们会逐个研究。

# if 语句

最基本的结构化命令就是 if-then 语句。if-then 语句有如下格式。

if command
then
commands
fi

如果你在用其他编程语言的 if-then 语句,这种形式可能会让你有点困惑。在其他编程语言中,if 语句之后的对象是一个等式,这个等式的求值结果为 TRUE 或 FALSE。但 bash shell 的 if 语句并不是这么做的。

bash shell 的 ==if 语句会运行 if 后面的那个命令。如果该命令的退出状态码是 0(该命令成功运行),位于 then 部分的命令就会被执行。如果该命令的退出状态码是其他值,then 部分的命令就不会被执行,bash shell 会继续执行脚本中的下一个命令==。fi 语句用来表示 if-then 语句到此结束。

这里有个简单的例子可解释这个概念。

$ cat test1.sh
#!/bin/bash
# testing the if statement
if pwd
then
    echo "It worked"
fi
$

这个脚本在 if 行采用了 pwd 命令。如果命令成功结束,echo 语句就会显示该文本字符串。在命令行运行该脚本时,会得到如下结果。

$ ./test1.sh
/home/Christine
It worked
$

shell 执行了 if 行中的 pwd 命令。由于退出状态码是 0,它就又执行了 then 部分的 echo 语句。

你可能在有些脚本中看到过 if-then 语句的另一种形式:

if command; then
    commands
fi

通过把分号放在待求值的命令尾部,就可以将 then 语句放在同一行上了,这样看起来更像其他编程语言中的 if-then 语句

在 then 部分,你可以使用不止一条命令。可以像在脚本中的其他地方一样在这里列出多条命令。bash shell 会将这些命令当成一个块,如果 if 语句行的命令的退出状态值为 0,所有的命令都会被执行;如果 if 语句行的命令的退出状态不为 0,所有的命令都会被跳过。

$ cat test3.sh
#!/bin/bash
# testing multiple commands in the then section
#
testuser=Christine
#
if grep $testuser /etc/passwd; then
    echo "This is my first command"
    echo "This is my second command"
    echo "I can even put in other commands besides echo:"
    ls -a /home/$testuser/.b*
fi
$

if 语句行使用 grep 命令在/etc/passwd 文件中查找某个用户名当前是否在系统上使用。如果有用户使用了那个登录名,脚本会显示一些文本信息并列出该用户 HOME 目录的 bash 文件。

$ ./test3.sh
Christine:x:501:501:Christine B:/home/Christine:/bin/bash
This is my first command
This is my second command
I can even put in other commands besides echo:
/home/Christine/.bash_history  /home/Christine/.bash_profile
/home/Christine/.bash_logout   /home/Christine/.bashrc
$

但是,如果将 testuser 变量设置成一个系统上不存在的用户,则什么都不会显示。看起来也没什么新鲜的。如果在这里显示的一些消息可说明这个用户名在系统中未找到,这样可能就会显得更友好。此时可以用 if-then-else 语句来做到这一点。当 if 语句中的命令返回非零退出状态码时,bash shell 会执行 else 部分中的命令。现在可以复制并修改测试脚本来加入 else 部分。

$ cp test3.sh test4.sh
$
$ vim test4.sh
$
$ cat test4.sh
#!/bin/bash
# testing the else section
#
testuser=NoSuchUser
#
if grep $testuser /etc/passwd
then
    echo "The bash files for user $testuser are:"
    ls -a /home/$testuser/.b*
    echo
else
    echo "The user $testuser does not exist on this system."
    echo
fi
$
$ ./test4.sh
The user NoSuchUser does not exist on this system.
$

这样就更友好了。跟 then 部分一样,else 部分也可以包含多条命令。

# 嵌套 if

有时你需要检查脚本代码中的多种条件。对此,可以使用嵌套的 if-then 语句。

要检查/etc/passwd 文件中是否存在某个用户名以及该用户的目录是否尚在,可以使用嵌套的 if-then 语句。嵌套的 if-then 语句位于主 if-then-else 语句的 else 代码块中。

$ ls -d /home/NoSuchUser/
/home/NoSuchUser/
$
$ cat test5.sh
#!/bin/bash
# Testing nested ifs
#
testuser=NoSuchUser
#
if grep $testuser /etc/passwd
then
echo "The user $testuser exists on this system."
else
    echo "The user $testuser does not exist on this system."
    if ls -d /home/$testuser/
    then
        echo "However, $testuser has a directory."
    fi
fi
$
$ ./test5.sh
The user NoSuchUser does not exist on this system.
/home/NoSuchUser/
However, NoSuchUser has a directory.
$

这个脚本准确无误地发现,尽管登录名已经从/etc/passwd 中删除了,但是该用户的目录仍然存在。在脚本中使用这种嵌套 if-then 语句的问题在于代码不易阅读,很难理清逻辑流程。

可以使用 else 部分的另一种形式:elif。这样就不用再书写多个 if-then 语句了。elif 使用另一个 if-then 语句延续 else 部分。elif 语句行提供了另一个要测试的命令,这类似于原始的 if 语句行。如果 elif 后命令的退出状态码是 0,则 bash 会执行第二个 then 语句部分的命令。使用这种嵌套方法,代码更清晰,逻辑更易懂。甚至可以更进一步,让脚本检查拥有目录的不存在用户以及没有拥有目录的不存在用户。这可以通过在嵌套 elif 中加入一个 else 语句来实现。

$ cat test5.sh
#!/bin/bash
# Testing nested ifs - use elif & else
#
testuser=NoSuchUser
#
if grep $testuser /etc/passwd
then
    echo "The user $testuser exists on this system."
#
elif ls -d /home/$testuser
then
    echo "The user $testuser does not exist on this system."
    echo "However, $testuser has a directory."
#
else
    echo "The user $testuser does not exist on this system."
    echo "And, $testuser does not have a directory."
fi
$
$ ./test5.sh
/home/NoSuchUser
The user NoSuchUser does not exist on this system.
However, NoSuchUser has a directory.
$
$ sudo rmdir /home/NoSuchUser
[sudo] password for Christine:
$
$ ./test5.sh
ls: cannot access /home/NoSuchUser: No such file or directory
The user NoSuchUser does not exist on this system.
And, NoSuchUser does not have a directory.
$

在/home/NoSuchUser 目录被删除之前,这个测试脚本执行的是 elif 语句,返回零值的退出状态。因此 elif 的 then 代码块中的语句得以执行。删除了/home/NoSuchUser 目录之后,elif 语句返回的是非零值的退出状态。这使得 elif 块中的 else 代码块得以执行。

记住,在 elif 语句中,紧跟其后的 else 语句属于 elif 代码块。它们并不属于之前的 if-then 代码块。

可以继续将多个 elif 语句串起来,形成一个大的 if-then-elif 嵌套组合。每块命令都会根据命令是否会返回退出状态码 0 来执行。记住,bash shell 会依次执行 if 语句,只有第一个返回退出状态码 0 的语句中的 then 部分会被执行。

if command1
then
    command set 1
elif command2
then
    command set 2
elif command3
then
    command set 3
elif command4
then
    command set 4
fi

尽管使用了 elif 语句的代码看起来更清晰,但是脚本的逻辑仍然会让人犯晕。在本章稍后你会看到如何使用 case 命令代替 if-then 语句的大量嵌套。

# test 命令

到目前为止,在 if 语句中看到的都是普通 shell 命令。你可能想问,if-then 语句是否能直接测试命令退出状态码之外的条件。

答案是不能。但在 bash shell 中有个好用的工具可以帮你通过 if-then 语句测试其他条件。

test 命令提供了在 if-then 语句中测试不同条件的途径。如果 test 命令中列出的条件成立,test 命令就会退出并返回退出状态码 0。这样 if-then 语句就与其他编程语言中的 if-then 语句以类似的方式工作了。如果条件不成立,test 命令就会退出并返回非零的退出状态码,这使得 if-then 语句不会再被执行。

如果只执行 test 命令本身,不写 test 命令的条件部分,它会以非零的退出状态码退出,并执行 else 语句块。当你加入一个条件时,test 命令会测试该条件。例如,可以使用 test 命令确定变量中是否有内容。这只需要一个简单的条件表达式。

$ cat test6.sh
#!/bin/bash
# Testing the test command
#
my_variable="Full"
#
if test $my_variable
then
    echo "The $my_variable expression returns a True"
#
else
    echo "The $my_variable expression returns a False"
fi
$
$ ./test6.sh
The Full expression returns a True
$

变量 my_variable 中包含有内容(Full),因此当 test 命令测试条件时,返回的退出状态为 0。这使得 then 语句块中的语句得以执行。如你所料,如果该变量中没有包含内容,就会出现相反的情况。

==bash shell 提供了另一种条件测试方法,无需在 if-then 语句中声明 test 命令。==

if [ condition ]
then
    commands
fi

方括号定义了测试条件,是与 test 命令同义的特殊 bash 命令。==注意,第一个方括号之后和第二个方括号之前必须加上一个空格,否则就会报错。==

test 命令可以判断三类条件:

  • 数值比较
  • 字符串比较
  • 文件比较

接下来将会介绍如何在 if-then 语句中使用这些条件测试。

# 数值比较

使用 test 命令最常见的情形是对两个数值进行比较。如下列出了测试两个值时可用的条件参数。

  • n1 -eq n2 检查 n1 是否与 n2 相等
  • n1 -ge n2 检查 n1 是否大于或等于 n2
  • n1 -gt n2 检查 n1 是否大于 n2
  • n1 -le n2 检查 n1 是否小于或等于 n2
  • n1 -lt n2 检查 n1 是否小于 n2
  • n1 -ne n2 检查 n1 是否不等于 n2

数值条件测试可以用在数字和变量上。这里有个例子。

$ cat numeric_test.sh
#!/bin/bash
# Using numeric test evaluations
#
value1=10
value2=11
#
if [ $value1 -gt 5 ]
then
    echo "The test value $value1 is greater than 5"
fi
#
if [ $value1 -eq $value2 ]
then
    echo "The values are equal"
else
    echo "The values are different"
fi
#
$

第一个条件测试测试变量 value1 的值是否大于 5。第二个条件测试测试变量 value1 的值是否和变量 value2 的值相等。两个数值条件测试的结果和预想一致。

但是涉及浮点值时,数值条件测试会有一个限制。bash shell 只能处理整数。如果你只是要通过 echo 语句来显示这个结果,那没问题。但是,在基于数字的函数中就不行了,比如数值测试条件,不能在 test 命令中使用浮点值。

# 字符串比较

条件测试还允许比较字符串值。比较字符串比较烦琐。

  • str1 = str2 检查 str1 是否和 str2 相同
  • str1 != str2 检查 str1 是否和 str2 不同
  • str1 < str2 检查 str1 是否比 str2 小
  • str1 > str2 检查 str1 是否比 str2 大
  • -n str1 检查 str1 的长度是否非 0
  • -z str1 检查 str1 的长度是否为 0

记住,在比较字符串的相等性时,比较测试会将所有的标点和大小写情况都考虑在内。

要测试一个字符串是否比另一个字符串大就是麻烦的开始。当要开始使用测试条件的大于或小于功能时,就会出现两个经常困扰 shell 程序员的问题:

  • 大于号和小于号必须转义,否则 shell 会把它们当作重定向符号,把字符串值当作文件名;
  • 大于和小于顺序和 sort 命令所采用的不同。

在编写脚本时,第一条可能会导致一个不易察觉的严重问题。下面的例子展示了 shell 脚本编程初学者时常碰到的问题。

$ cat badtest.sh
#!/bin/bash
# mis-using string comparisons
#
val1=baseball
val2=hockey
#
if [ $val1 > $val2 ]
then
    echo "$val1 is greater than $val2"
else
    echo "$val1 is less than $val2"
fi
$
$ ./badtest.sh
baseball is greater than hockey
$ ls -l hockey
-rw-r--r-- 1 rich rich 0 Sep 30 19:08 hockey
$

这个脚本中只用了大于号,没有出现错误,但结果是错的。脚本把大于号解释成了输出重定向。因此,它创建了一个名为 hockey 的文件。由于重定向的顺利完成,test 命令返回了退出状态码 0,if 语句便以为所有命令都成功结束了。要解决这个问题,就需要使用反斜杠\>正确转义大于号。

第二个问题更细微,除非你经常处理大小写字母,否则几乎遇不到。sort 命令处理大写字母的方法刚好跟 test 命令相反。比如两个变量val1=Testing val2=testing,**==在 test 命令中,大写字母被认为是小于小写字母的。但 sort 命令恰好相反。当你将同样的字符串放进文件中并用 sort 命令排序时,小写字母会先出现。==**这是由各个命令使用的排序技术不同造成的。

test 命令中使用的是标准的 ASCII 顺序,根据每个字符的 ASCII 数值来决定排序结果。sort 命令使用的是系统的本地化语言设置中定义的排序顺序。对于英语,本地化设置指定了在排序顺序中小写字母出现在大写字母前。

test 命令测试表达式使用标准的数学比较符号来表示字符串比较,而用文本代码来表示数值比较。这个细微的特性被很多程序员理解反了。如果你对数值使用了数学运算符号,shell 会将它们当成字符串值,可能无法得到正确的结果。

最后,-n 和-z 可以检查一个变量是否含有数据。如果一个变量为空字符串,或其从未被定义,那么均会被认为它的字符串长度为 0。

空的和未初始化的变量会对 shell 脚本测试造成灾难性的影响。如果不是很确定一个变量的内容,最好在将其用于数值或字符串比较之前先通过-n 或-z 来测试一下变量是否含有值。

# 文件比较

最后一类比较测试很有可能是 shell 编程中最为强大、也是用得最多的比较形式。它允许你测试 Linux 文件系统上文件和目录的状态。

  • -d file 检查 file 是否存在并是一个目录
  • -e file 检查 file 是否存在(文件或目录)
  • -f file 检查 file 是否存在并是一个文件
  • -r file 检查 file 是否存在并可读
  • -s file 检查 file 是否存在并非空
  • -w file 检查 file 是否存在并可写
  • -x file 检查 file 是否存在并可执行
  • -O file 检查 file 是否存在并属当前用户所有
  • -G file 检查 file 是否存在并且默认组与当前用户相同
  • file1 -nt file2 检查 file1 是否比 file2 新
  • file1 -ot file2 检查 file1 是否比 file2 旧

这些测试条件使你能够在 shell 脚本中检查文件系统中的文件。它们经常出现在需要进行文件访问的脚本中。鉴于其使用广泛,建议熟练掌握。用于比较文件路径是相对你运行该脚本的目录而言的。

需要注意的是,-G 比较会检查文件的默认组,如果它匹配了用户的默认组,则测试成功。由于-G 比较只会检查默认组而非用户所属的所有组,这会叫人有点困惑。如果文件的组被改成了某个组,用户也是其中的一员,但用户并不以其为默认组,此时-G 比较会失败,因为它只比较默认组,不会去比较其他的组。

此外,在比较两个文件的新旧时,这些比较都不会先检查文件是否存在,如果你要检查的文件已经移走,就会出现问题。在你尝试使用-nt 或-ot 比较文件之前,必须先确认文件是存在的。

# 复合条件测试

if-then 语句允许你使用布尔逻辑来组合测试。有两种布尔运算符可用:

  • [ condition1 ] && [ condition2 ]
  • [ condition1 ] || [ condition2 ]

结合方括号测试方式和布尔逻辑组合,可以测试更多条件。

# if 语句的高级特性

bash shell 提供了两项可在 if-then 语句中使用的高级特性:

  • 用于数学表达式的双括号
  • 用于高级字符串处理功能的双方括号

# 使用双括号-常用

双括号命令允许你在比较过程中使用高级数学表达式。test 命令只能在比较中使用简单的算术操作。双括号命令提供了更多的数学符号,这些符号对于用过其他编程语言的程序员而言并不陌生。除了 test 命令使用的标准数学运算符,如下列出了双括号命令中还可以使用的其他运算符。

  • val++ 后增
  • val-- 后减
  • ++val 先增
  • --val 先减
  • ! 逻辑求反
  • ~ 位求反
  • ** 幂运算
  • << 左位移
  • >> 右位移
  • & 位布尔和
  • | 位布尔或
  • && 逻辑和
  • || 逻辑或

可以在 if 语句中用双括号命令,也可以在脚本中的普通命令里使用来赋值。

$ cat test23.sh
#!/bin/bash
# using double parenthesis
#
val1=10
#
if (( $val1 ** 2 > 90 ))
then
    (( val2 = $val1 ** 2 ))
    echo "The square of $val1 is $val2"
fi
$
$ ./test23.sh
The square of 10 is 100
$

==注意,不需要将双括号中表达式里的大于号转义。这是双括号命令提供的另一个高级特性。==

# 使用双方括号

双方括号命令提供了针对字符串比较的高级特性。双方括号使用了 test 命令中采用的标准字符串比较。但它提供了 test 命令未提供的另一个特性——模式匹配(pattern matching)。

双方括号在 bash shell 中工作良好。不过要小心,不是所有的 shell 都支持双方括号。

在模式匹配中,可以定义一个正则表达式(后续将详细讨论)来匹配字符串值。

$ cat test24.sh
#!/bin/bash
# using pattern matching
#
if [[ $USER == r* ]]
then
    echo "Hello $USER"
else
    echo "Sorry, I do not know you"
fi
$
$ ./test24.sh
Hello rich
$

在上面的脚本中,我们使用了双等号(==)。双等号将右边的字符串(r*)视为一个模式,并应用模式匹配规则。双方括号命令$USER 环境变量进行匹配,看它是否以字母 r 开头。如果是的话,比较通过,shell 会执行 then 部分的命令。

# case 命令

你会经常发现自己在尝试计算一个变量的值,在一组可能的值中寻找特定值。在这种情形下,你不得不写出很长的 if-then-else 语句,就像下面这样。

$ cat test25.sh
#!/bin/bash
# looking for a possible value
#
if [ $USER = "rich" ]
then
    echo "Welcome $USER"
    echo "Please enjoy your visit"
elif [ $USER = "barbara" ]
then
    echo "Welcome $USER"
    echo "Please enjoy your visit"
elif [ $USER = "testing" ]
then
    echo "Special testing account"
elif [ $USER = "jessica" ]
then
    echo "Do not forget to logout when you're done"
else
    echo "Sorry, you are not allowed here"
fi
$
$ ./test25.sh
Welcome rich
Please enjoy your visit
$

elif 语句继续 if-then 检查,为比较变量寻找特定的值。有了 case 命令,就不需要再写出所有的 elif 语句来不停地检查同一个变量的值了。case 命令会采用列表格式来检查单个变量的多个值。

case 命令会将指定的变量与不同模式进行比较。如果变量和模式是匹配的,那么 shell 会执行为该模式指定的命令。**可以通过竖线操作符在一行中分隔出多个模式模式。**星号会捕获所有与已知模式不匹配的值。这里有个将 if-then-else 程序转换成用 case 命令的例子。

$ cat test26.sh
#!/bin/bash
# using the case command
#
case $USER in
rich | barbara)
    echo "Welcome, $USER"
    echo "Please enjoy your visit";;
testing)
    echo "Special testing account";;
jessica)
    echo "Do not forget to log off when you're done";;
*)
    echo "Sorry, you are not allowed here";;
esac
$
$ ./test26.sh
Welcome, rich
Please enjoy your visit
$

case 命令提供了一个更清晰的方法来为变量每个可能的值指定不同的选项。

# for 命令

重复执行一系列命令在编程中很常见。通常你需要重复一组命令直至达到某个特定条件,比如处理某个目录下的所有文件、系统上的所有用户或是某个文本文件中的所有行。for 命令有几种不同的方式来读取列表中的值,下面几节将会介绍各种方式。

# 读取列表中的值

for 命令最基本的用法就是遍历 for 命令自身所定义的一系列值。

$ cat test1
 #!/bin/bash
# basic for command
for test in Alabama Alaska Arizona Arkansas California Colorado
do
    echo The next state is $test
done
echo "The last state we visited was $test"
test=Connecticut
echo "Wait, now we're visiting $test"
$ ./test1
The next state is Alabama
The next state is Alaska
The next state is Arizona
The next state is Arkansas
The next state is California
The next state is Colorado
The last state we visited was Colorado
Wait, now we're visiting Connecticut
$

每次 for 命令遍历值列表,它都会将列表中的下个值赋给$test变量。$test 变量可以像 for 命令语句中的其他脚本变量一样使用。在最后一次迭代后,$test 变量的值会在 shell 脚本的剩余部分一直保持有效。它会一直保持最后一次迭代的值(除非你修改了它)。$test 变量保持了其值,也允许我们修改它的值,并在 for 命令循环之外跟其他变量一样使用

事情并不会总像你在 for 循环中看到的那么简单。有时会遇到难处理的数据。有时 for 循环的值列表中可能存在中间有空格的值,此时使用单引号或者双引号将中间存在空格的值括起来即可。有时候,有的值自身中存在单引号或双引号,这时需要用另外一种相反的引号将其括起来,或者使用反斜杠转义即可正常使用。

# 从变量读取列表

通常 shell 脚本遇到的情况是,你将一系列值都集中存储在了一个变量中,然后需要遍历变量中的整个列表。也可以通过 for 命令完成这个任务。

$ cat test4
#!/bin/bash
# using a variable to hold the list
list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut"
for state in $list
do
echo "Have you ever visited $state?"
done
$ ./test4
Have you ever visited Alabama?
Have you ever visited Alaska?
Have you ever visited Arizona?
Have you ever visited Arkansas?
Have you ever visited Colorado?
Have you ever visited Connecticut?

$list变量包含了用于迭代的标准文本值列表。注意,代码还是用了另一个赋值语句向$list 变量包含的已有列表中添加(或者说是拼接)了一个值。这是向变量中存储的已有文本字符串尾部添加文本的一个常用方法。

# 从命令读取值

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

$ cat test5
#!/bin/bash
# reading values from a file
file="states"
for state in $(cat $file)
do
echo "Visit beautiful $state"
done
$ cat states
Alabama
Alaska
Arizona
Arkansas
Colorado
Connecticut
Delaware
Florida
Georgia
$ ./test5
Visit beautiful Alabama
Visit beautiful Alaska
Visit beautiful Arizona
Visit beautiful Arkansas
Visit beautiful Colorado
Visit beautiful Connecticut
Visit beautiful Delaware
Visit beautiful Florida
Visit beautiful Georgia
$

这个例子在命令替换中使用了 cat 命令来输出文件 states 的内容。你会注意到 states 文件中每一行有一个州,而不是通过空格分隔的。for 命令仍然以每次一行的方式遍历了 cat 命令的输出,假定每个州都是在单独的一行上。但这并没有解决数据中有空格的问题。如果你列出了一个名字中有空格的州,for 命令仍然会将每个单词当作单独的值。这是有原因的,下一节我们将会了解。

test5 的代码范例将文件名赋给变量,文件名中没有加入路径。这要求文件和脚本位于同一个目录中。如果不是的话,你需要使用全路径名(不管是绝对路径还是相对路径)来引用文件位置。

# 更改字段分隔符

造成这个问题的原因是特殊的环境变量 IFS,叫作内部字段分隔符(internal field separator)。IFS 环境变量定义了 bash shell 用作字段分隔符的一系列字符。默认情况下,bash shell 会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

如果 bash shell 在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。在处理可能含有空格的数据(比如文件名)时,这会非常麻烦,就像你在上一个脚本示例中看到的。

要解决这个问题,可以在 shell 脚本中临时更改 IFS 环境变量的值来限制被 bash shell 当作字段分隔符的字符。例如,如果你想修改 IFS 的值,使其只能识别换行符,那就必须这么做:

IFS=$'\n'

将这个语句加入到脚本中,告诉 bash shell 在数据值中忽略空格和制表符。对前一个脚本使用这种方法,shell 脚本就能够使用列表中含有空格的值了。

在处理代码量较大的脚本时,可能在一个地方需要修改 IFS 的值,然后忽略这次修改,在脚本的其他地方继续沿用 IFS 的默认值。一个可参考的安全实践是在改变 IFS 之前保存原来的 IFS 值,之后再恢复它。这种技术可以这样实现: IFS.OLD=$IFS IFS=$'\n' <在代码中使用新的 IFS 值> IFS=$IFS.OLD 这就保证了在脚本的后续操作中使用的是 IFS 的默认值。

还有其他一些 IFS 环境变量的绝妙用法。假定你要遍历一个文件中用冒号分隔的值(比如在/etc/passwd 文件中)。你要做的就是将 IFS 的值设为冒号

IFS=:

如果要指定多个 IFS 字符,只要将它们在赋值行串起来就行。

IFS=$'\n':;"

这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。如何使用 IFS 字符解析数据没有任何限制。

# 用通配符读取目录

最后,可以用 for 命令来自动遍历目录中的文件。进行此操作时,必须在文件名或路径名中使用通配符。它会强制 shell 使用文件扩展匹配。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程。

如果不知道所有的文件名,这个特性在处理目录中的文件时就非常好用。

$ cat test6
#!/bin/bash
# iterate through all the files in a directory
for file in /home/rich/test/*
do
    if [ -d "$file" ]
    then
        echo "$file is a directory"
    elif [ -f "$file" ]
    then
    echo "$file is a file"
    fi
done
$ ./test6
/home/rich/test/dir1 is a directory
/home/rich/test/myprog.c is a file
/home/rich/test/myprog is a file
/home/rich/test/myscript is a file
/home/rich/test/newdir is a directory
/home/rich/test/newfile is a file
/home/rich/test/newfile2 is a file
/home/rich/test/testdir is a directory
/home/rich/test/testing is a file
/home/rich/test/testprog is a file
/home/rich/test/testprog.c is a file
$

for 命令会遍历/home/rich/test/*输出的结果。该代码用 test 命令测试了每个条目(使用方括号方法),以查看它是目录(通过-d 参数)还是文件(通过-f 参数)

注意,我们在这个例子的 if 语句中做了一些不同的处理

if [ -d "$file" ]

在 Linux 中,目录名和文件名中包含空格当然是合法的。要适应这种情况,应该将$file 变量用双引号圈起来。如果不这么做,遇到含有空格的目录名或文件名时就会有错误产生。

./test6: line 6: [: too many arguments
./test6: line 9: [: too many arguments

在 test 命令中,bash shell 会将额外的单词当作参数,进而造成错误。

也可以在 for 命令中列出多个目录通配符,将目录查找和列表合并进同一个 for 语句。

$ cat test7
#!/bin/bash
# iterating through multiple directories
for file in /home/rich/.b* /home/rich/badtest
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

$ ./test7
/home/rich/.backup.timestamp is a file
/home/rich/.bash_history is a file
/home/rich/.bash_logout is a file
/home/rich/.bash_profile is a file
/home/rich/.bashrc is a file
/home/rich/badtest doesn't exist
$

for 语句首先使用了文件扩展匹配来遍历通配符生成的文件列表,然后它会遍历列表中的下一个文件。可以将任意多的通配符放进列表中。

注意,你可以在数据列表中放入任何东西。即使文件或目录不存在,for 语句也会尝试处理列表中的内容。在处理文件或目录时,这可能会是个问题。你无法知道你正在尝试遍历的目录是否存在:在处理之前测试一下文件或目录总是好的。

# C 语言风格的 for 命令

如果你从事过 C 语言编程,可能会对 bash shell 中 for 命令的工作方式有点惊奇。在 C 语言中,for 循环通常定义一个变量,然后这个变量会在每次迭代时自动改变。通常程序员会将这个变量用作计数器,并在每次迭代中让计数器增一或减一。bash 的 for 命令也提供了这个功能。本节将会告诉你如何在 bash shell 脚本中使用 C 语言风格的 for 命令。

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

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

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

bash shell 也支持一种 for 循环,它看起来跟 C 语言风格的 for 循环类似,但有一些细微的不同,其中包括一些让 shell 脚本程序员困惑的东西。以下是 bash 中 C 语言风格的 for 循环的基本格式。

for (( variableassignment ; condition ; iterationprocess ))

C 语言风格的 for 循环的格式会让 bash shell 脚本程序员摸不着头脑,因为它使用了 C 语言风格的变量引用方式而不是 shell 风格的变量引用方式。C 语言风格的 for 命令看起来如下。

for (( a = 1; a < 10; a++ ))

注意,有些部分并没有遵循 bash shell 标准的 for 命令:

  • 变量赋值可以有空格;
  • 条件中的变量不以美元符开头;
  • 迭代过程的算式未用 expr 命令格式。

shell 开发人员创建了这种格式以更贴切地模仿 C 语言风格的 for 命令。这虽然对 C 语言程序员来说很好,但也会把专家级的 shell 程序员弄得一头雾水。在脚本中使用 C 语言风格的 for 循环时要小心。

以下例子是在 bash shell 程序中使用 C 语言风格的 for 命令

$ cat test8
#!/bin/bash
# testing the C-style for loop
for (( i=1; i <= 3; i++ ))
do
    echo "The next number is $i"
done
$ ./test8
The next number is 1
The next number is 2
The next number is 3
$

for 循环通过定义好的变量(本例中是变量 i)来迭代执行这些命令。在每次迭代中,$i 变量包含了 for 循环中赋予的值。在每次迭代后,循环的迭代过程会作用在变量上,在本例中,变量增一。

C 语言风格的 for 命令也允许为迭代使用多个变量。循环会单独处理每个变量,你可以为每个变量定义不同的迭代过程。尽管可以使用多个变量,但你只能在 for 循环中定义一种条件。

$ cat test9
#!/bin/bash
# multiple variables
for (( a=1, b=10; a <= 3; a++, b-- ))
do
    echo "$a - $b"
done
$ ./test9
1 - 10
2 - 9
3 - 8
$

变量 a 和 b 分别用不同的值来初始化并且定义了不同的迭代过程。循环的每次迭代在增加变量 a 的同时减小了变量 b。

# while 命令

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

while 命令的格式是:

while test command
do
    other commands
done

while 命令中定义的 test command 和 if-then 语句中的格式一模一样。可以使用任何普通的 bash shell 命令,或者用 test 命令进行条件测试,比如测试变量值。

while 命令的关键在于所指定的 test command 的退出状态码必须随着循环中运行的命令而改变。如果退出状态码不发生变化,while 循环就将一直不停地进行下去。 最常见的 test command 的用法是用方括号来检查循环命令中用到的 shell 变量的值。

$ cat test10
#!/bin/bash
# while command test

var1=10
while [ $var1 -gt 0 ]
do
    echo $var1
    var1=$[ $var1 - 1 ]
done
$ ./test10
10
9
8
7
6
5
4
3
2
1
$

while 命令定义了每次迭代时检查的测试条件:while [ $var1 -gt 0 ] 。只要测试条件成立,while 命令就会不停地循环执行定义好的命令。在这些命令中,测试条件中用到的变量必须被修改,否则就会陷入无限循环。在本例中,我们用 shell 算术来将变量值减一:var1=$[ $var1 - 1 ] 。while 循环会在测试条件不再成立时停止。

while 命令允许你在 while 语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。如果你不够小心,可能会导致一些有意思的结果。下面的例子将说明这一点。

$ cat test11
#!/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
$ ./test11
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

while 语句中定义了两个测试命令。第一个测试简单地显示了 var1 变量的当前值。第二个测试用方括号来判断 var1 变量的值。在循环内部,echo 语句会显示一条简单的消息,说明循环被执行了。注意当你运行本例时输出最后还有一个-1。

while 循环会在 var1 变量等于 0 时执行 echo 语句,然后将 var1 变量的值减一。接下来再次执行测试命令,用于下一次迭代。echo 测试命令被执行并显示了 var 变量的值(现在小于 0 了)。直到 shell 执行 test 测试命令,whle 循环才会停止。

# until 命令

until 命令和 while 命令工作的方式完全相反。until 命令要求你指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为 0,bash shell 才会执行循环中列出的命令。一旦测试命令返回了退出状态码 0,循环就结束了。和 while 命令类似,你可以在 until 命令语句中放入多个测试命令。只有最后一个命令的退出状态码决定了 bash shell 是否执行已定义的 other commands。下面是使用 until 命令的一个例子。

$ cat test12
#!/bin/bash
# using the until command
var1=100
until [ $var1 -eq 0 ]
do
    echo $var1
    var1=$[ $var1 - 25 ]
done
$ ./test12
100
75
50
25
$

本例中会测试 var1 变量来决定 until 循环何时停止。只要该变量的值等于 0,until 命令就会停止循环。同 while 命令一样,在 until 命令中使用多个测试命令时要注意。

$ cat test13
#!/bin/bash
# using the until command
var1=100
until echo $var1
    [ $var1 -eq 0 ]
do
    echo Inside the loop: $var1
    var1=$[ $var1 - 25 ]
done
$ ./test13
100
Inside the loop: 100
75
Inside the loop: 75
50
Inside the loop: 50
25
Inside the loop: 25
0
$

# 嵌套循环

循环语句可以在循环内使用任意类型的命令,包括其他循环命令。这种循环叫作嵌套循环(nested loop)。**注意,在使用嵌套循环时,你是在迭代中使用迭代,与命令运行的次数是乘积关系。**不注意这点的话,有可能会在脚本中造成问题。

$ cat test14
#!/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
$ ./test14
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
$

这个被嵌套的循环(也称为内部循环,inner loop)会在外部循环的每次迭代中遍历一次它所有的值。注意,两个循环的 do 和 done 命令没有任何差别。bash shell 知道当第一个 done 命令执行时是指内部循环而非外部循环。

在混用循环命令时也一样,比如在 while 循环内部放置一个 for 循环。

$ cat test15
#!/bin/bash
# placing 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
$ ./test15
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
$

同样,shell 能够区分开内部 for 循环和外部 while 循环各自的 do 和 done 命令。如果真的想挑战脑力,可以混用 until 和 while 循环。

$ cat test16
#!/bin/bash
# using until and while loops
var1=3
until [ $var1 -eq 0 ]
do
    echo "Outer loop: $var1"
    var2=1
    while [ $var2 -lt 5 ]
    do
        var3=$(echo "scale=4; $var1 / $var2" | bc)
        echo "   Inner loop: $var1 / $var2 = $var3"
        var2=$[ $var2 + 1 ]
    done
    var1=$[ $var1 - 1 ]
done
$ ./test16
Outer loop: 3
    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: 2
    Inner loop: 2 / 1 = 2.0000
    Inner loop: 2 / 2 = 1.0000
    Inner loop: 2 / 3 = .6666
    Inner loop: 2 / 4 = .5000
Outer loop: 1
    Inner loop: 1 / 1 = 1.0000
    Inner loop: 1 / 2 = .5000
    Inner loop: 1 / 3 = .3333
    Inner loop: 1 / 4 = .2500
$

外部的 until 循环以值 3 开始,并继续执行到值等于 0。内部 while 循环以值 1 开始并一直执行,只要值小于 5。每个循环都必须改变在测试条件中用到的值,否则循环就会无止尽进行下去。

# 循环处理文件数据

如果需要遍历存储在文件中的数据,则需要结合已经讲过的两种技术:

  • 使用嵌套循环
  • 修改 IFS 环境变量

通过修改 IFS 环境变量,就能强制 for 命令将文件中的每行都当成单独的一个条目来处理,即便数据中有空格也是如此。一旦从文件中提取出了单独的行,可能需要再次利用循环来提取行中的数据。

典型的例子是处理/etc/passwd 文件中的数据。这要求你逐行遍历/etc/passwd 文件,然后将 IFS 变量的值改成冒号,这样就能分隔开每行中的各个数据段了。

#!/bin/bash
# changing the IFS value
IFS.OLD=$IFS
IFS=$'\n'
for entry in $(cat /etc/passwd)
do
    echo "Values in $entry –"
    IFS=:
    for value in $entry
    do
        echo "   $value"
    done
done
$

这个脚本使用了两个不同的 IFS 值来解析数据。第一个 IFS 值解析出/etc/passwd 文件中的单独的行。内部 for 循环接着将 IFS 的值修改为冒号,允许你从/etc/passwd 的行中解析出单独的值。内部循环会解析出/etc/passwd 每行中的各个值。这种方法在处理外部导入电子表格所采用的逗号分隔的数据时也很方便。

# 控制循环

你可能会想,一旦启动了循环,就必须苦等到循环完成所有的迭代。并不是这样的。有两个命令能帮我们控制循环内部的情况:

  • break 命令
  • continue 命令

每个命令在如何控制循环的执行方面有不同的用法。下面几节将介绍如何使用这些命令来控制循环。

# break 命令

break 命令是退出循环的一个简单方法。可以用 break 命令来退出任意类型的循环,包括 while 和 until 循环。有几种情况可以使用 break 命令,本节将介绍这些方法。

  1. 跳出单个循环

在 shell 执行 break 命令时,它会尝试跳出当前正在执行的循环。

cat test17
#!/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"


$ ./test17
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 循环。

$ cat test18
#!/bin/bash
# breaking out 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 loop is completed"
$ ./test18
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
The while loop is completed
$

while 循环会在 if-then 的条件满足时执行 break 命令,终止。

  1. 跳出内部循环

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

$ cat test19
#!/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++ ))
    do
        if [ $b -eq 5 ]
        then
            break
        fi
        echo "   Inner loop: $b"
    done
done
$ ./test19
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
$

内部循环里的 for 语句指明当变量 b 等于 100 时停止迭代。但内部循环的 if-then 语句指明当变量 b 的值等于 5 时执行 break 命令。注意,即使内部循环通过 break 命令终止了,外部循环依然继续执行。

  1. 跳出外部循环

有时你在内部循环,但需要停止外部循环。break 命令接受单个命令行参数值:

break n

其中 ==n 指定了要跳出的循环层级。默认情况下,n 为 1,表明跳出的是当前的循环。如果你将 n 设为 2,break 命令就会停止下一级的外部循环==

$ cat test20
#!/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
$ ./test20
Outer loop: 1
    Inner loop: 1
    Inner loop: 2
    Inner loop: 3
    Inner loop: 4
$

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

# continue 命令

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

$ cat test21
#!/bin/bash
# using the continue command
for (( var1 = 1; var1 < 15; var1++ ))
do
    if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
    then
        continue
    fi
    echo "Iteration number: $var1"
done
$ ./test21
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 命令时,它会跳过剩余的命令。如果你在其中某个条件里对测试条件变量进行增值,问题就会出现。

$ cat badtest3
#!/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
$ ./badtest3 | more
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
while iteration: 6
while iteration: 6
while iteration: 6

你得确保将脚本的输出重定向到了 more 命令,这样才能停止输出。在 if-then 的条件成立之前,所有一切看起来都很正常,然后 shell 执行了 continue 命令。当 shell 执行 continue 命令时,它跳过了 while 循环中余下的命令。不幸的是,被跳过的部分正是$var1 计数变量增值的地方,而这个变量又被用于 while 测试命令中。这意味着这个变量的值不会再变化了,从前面连续的输出显示中你也可以看出来。

和 break 命令一样,continue 命令也允许通过命令行参数指定要继续执行哪一级循环:

continue n

其中 n 定义了要继续的循环层级。下面是继续外部 for 循环的一个例子。

$ cat test22
#!/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

$ ./test22
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
$

此处用 continue 命令来停止处理循环内的命令,但会继续处理外部循环。注意,值为 3 的那次迭代并没有处理任何内部循环语句。尽管 continue 命令停止了内部处理过程,但外部循环依然会继续。

# 处理循环的输出

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

for file in /home/rich/*
do
    if [ -d "$file" ]
    then
        echo "$file is a directory"
    elif
        echo "$file is a file"
    fi
done > output.txt

shell 会将 for 命令的结果重定向到文件 output.txt 中,而不是显示在屏幕上。考虑下面将 for 命令的输出重定向到文件的例子。

$ cat test23
#!/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 command is finished."
$ ./test23
The command is finished.
$ 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 语句。

这种方法同样适用于将循环的结果管接给另一个命令。

$ cat test24
#!/bin/bash
# piping a loop to another command
for state in "North Dakota" Connecticut Illinois Alabama Tennessee
do
    echo "$state is the next place to go"
done | sort
echo "This completes our travels"
$ ./test24
Alabama is the next place to go
Connecticut is the next place to go
Illinois is the next place to go
North Dakota is the next place to go
Tennessee is the next place to go
This completes our travels
$

state 值并没有在 for 命令列表中以特定次序列出。for 命令的输出传给了 sort 命令,该命令会改变 for 命令输出结果的顺序。运行这个脚本实际上说明了结果已经在脚本内部排好序了。

# 实战例子

现在你已经看到了 shell 脚本中各种循环的使用方法,来看一些实际应用的例子吧。循环是对系统数据进行迭代的常用方法,无论是目录中的文件还是文件中的数据。下面的一些例子演示了如何使用简单的循环来处理数据。

# 查找可执行文件

当你从命令行中运行一个程序的时候,Linux 系统会搜索一系列目录来查找对应的文件。这些目录被定义在环境变量 PATH 中。如果你想找出系统中有哪些可执行文件可供使用,只需要扫描 PATH 环境变量中所有的目录就行了。如果要徒手查找的话,就得花点时间了。不过我们可以编写一个小小的脚本,轻而易举地搞定这件事。

首先是创建一个 for 循环,对环境变量 PATH 中的目录进行迭代。处理的时候别忘了设置 IFS 分隔符。

IFS=:
for folder in $PATH
do

现在你已经将各个目录存放在了变量$folder 中,可以使用另一个 for 循环来迭代特定目录中的所有文件。

for file in $folder/*
do

最后一步是检查各个文件是否具有可执行权限,你可以使用 if-then 测试功能来实现。

if [ -x $file ]
then
    echo "   $file"
fi

好了,搞定了!将这些代码片段组合成脚本就行了。

$ cat test25
#!/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
$

运行这段代码时,你会得到一个可以在命令行中使用的可执行文件的列表。输出显示了在环境变量 PATH 所包含的所有目录中找到的全部可执行文件。

# 创建多个用户账户

shell 脚本的目标是让系统管理员过得更轻松。如果你碰巧工作在一个拥有大量用户的环境中,最烦人的工作之一就是创建新用户账户。好在可以使用 while 循环来降低工作的难度。

你不用为每个需要创建的新用户账户手动输入 useradd 命令,而是可以将需要添加的新用户账户放在一个文本文件中,然后创建一个简单的脚本进行处理。这个文本文件的格式如下:

userid,user name

第一个条目是你为新用户账户所选用的用户 ID。第二个条目是用户的全名。两个值之间使用逗号分隔,这样就形成了一种名为逗号分隔值的文件格式(或者是.csv)。这种文件格式在电子表格中极其常见,所以你可以轻松地在电子表格程序中创建用户账户列表,然后将其保存成.csv 格式,以备 shell 脚本读取及处理。

要读取文件中的数据,得用上一点 shell 脚本编程技巧。我们将 IFS 分隔符设置成逗号,并将其放入 while 语句的条件测试部分。然后使用 read 命令读取文件中的各行。实现代码如下:

while IFS=’,’ read –r userid name

read 命令会自动读取.csv 文本文件的下一行内容,所以不需要专门再写一个循环来处理。当 read 命令返回 FALSE 时(也就是读取完整个文件时),while 命令就会退出。妙极了! 要想把数据从文件中送入 while 命令,只需在 while 命令尾部使用一个重定向符就可以了。将各部分处理过程写成脚本如下。

$ cat test26
#!/bin/bash
# process new user accounts
input="users.csv"
while IFS=',' read -r userid name
do
    echo "adding $userid"
    useradd -c "$name" -m $userid
done < "$input"
$

$input 变量指向数据文件,并且该变量被作为 while 命令的重定向数据。users.csv 文件内容如下。

$ cat users.csv
rich,Richard Blum
christine,Christine Bresnahan
barbara,Barbara Blum
tim,Timothy Bresnahan
$

必须作为 root 用户才能运行这个脚本,因为 useradd 命令需要 root 权限。执行此脚本后,看一眼/etc/passwd 文件,你会发现账户已经创建好了。


循环是编程的一部分。bash shell 提供了三种可用于脚本中的循环命令。for 命令允许你遍历一系列的值,不管是在命令行里提供好的、包含在变量中的还是通过文件扩展匹配获得的文件名和目录名。while 命令使用普通命令或测试命令提供了基于命令条件的循环。只有在命令(或条件)产生退出状态码 0 时,while 循环才会继续迭代指定的一组命令。until 命令也提供了迭代命令的一种方法,但它的迭代是建立在命令(或条件)产生非零退出状态码的基础上。这个特性允许你设置一个迭代结束前都必须满足的条件。可以在 shell 脚本中对循环进行组合,生成多层循环。bash shell 提供了 continue 和 break 命令,允许你根据循环内的不同值改变循环的正常流程。bash shell 还允许使用标准的命令重定向和管道来改变循环的输出。你可以使用重定向来将循环的输出重定向到一个文件或是另一个命令。这就为控制 shell 脚本执行提供了丰富的功能。下一章将会讨论如何和 shell 脚本用户交互。shell 脚本通常并不完全是自成一体的。它们需要在运行时被提供某些外部数据。下一章将讨论各种可用来向 shell 脚本提供实时数据的方法。

# 参考

Linux命令行与Shell脚本教程 (opens new window) Linux命令行与Shell脚本教程 (opens new window)

Last Updated: 4/4/2022, 11:35:53 AM