天天看点

Linux shell 脚本编程-基础篇 (三)

继 Linux shell 脚本编程-基础篇 (二)

3. 更多的结构化命令

3.1 for 命令

重复执行一系列命令在编程中很常见。通常需要重复一组命令直至达到某个特定条件,比如处理某个目录下的所有文件、系统上的所有用户或是某个文本文件

中的所有行。

bash shell 提供了 for 命令,允许创建一个遍历一系列值的循环。每次迭代都使用其中一个值来执行已定义好的一组命令。下面是 bash shell 中

for 命令的基本格式:

    for var in list

    do

        commands

    done

在 list参数中,需要提供迭代中要用到的一系列值。可以通过几种不同的方法指定列表中的值。在每次迭代中,变量 var 会包含列表中的当前值。第一次

迭代会使用列表中的第一个值,第二次迭代使用第二个值,以此类推,直到列表中的所有值都过一遍。

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

    NOTE:

    -------------------------------------------------------------------------------------------------------------------------------------

    只要愿意,也可以将 do 语句和 for 语句放在同一行,但必须用分号将其同列表中的值分开:for var in list; do。

3.1.1 读取列表中的值

-----------------------------------------------------------------------------------------------------------------------------------------

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

示例:

    [[email protected] 13]$ cat test1.sh

    #!/bin/bash

    # basic for command

    #

    for test in Alabama Alaska Arizona Arkansas California Colorado

    do

        echo "The next state is $test"

    done

运行:

    [[email protected] 13]$ test1.sh

    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

每次 for 命令遍历值列表,它都会将列表中的下个值赋给 $test 变量。$test 变量可以像 for 命令语句中的其他脚本变量一样使用。在最后一次迭代后,

$test 变量的值会在 shell 脚本的剩余部分一直保持有效。它会一直保持最后一次迭代的值(除非修改了它)。

示例:

    [[email protected] 13]$ cat test1b.sh

    #!/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 visiting $test"

    test=Connecticut

    echo "Wait, now we're visiting $test"

运行:

    [[email protected] 13]$ test1b.sh

    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 visiting Colorado

    Wait, now we're visiting Connecticut

$test 变量保持了其值,也允许我们修改它的值,并在for命令循环之外跟其他变量一样使用。

3.1.2 读取列表中的复杂值

-----------------------------------------------------------------------------------------------------------------------------------------

事情并不会总像在 for 循环中看到的那么简单。有时会遇到难处理的数据。

示例:

    [[email protected] 13]$ cat badtest1

    #!/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

运行:

    [[email protected] 13]$ badtest1

    word:I

    word:dont know if thisll

    word:work

shell 看到了列表值中的单引号并尝试使用它们来定义一个单独的数据值。有两种办法解决这个问题:

    □ 使用转义字符(反斜线)来将单引号转义;

    □ 使用双引号来定义用到单引号的值。

示例:

    [[email protected] 13]$ cat test2.sh

    #!/bin/bash

    # another example of how not to use the for command

    for test in I don\'t known if "this'll" work

    do

            echo "word:$test"

    done

运行:

    [[email protected] 13]$ test2.sh

    word:I

    word:don't

    word:known

    word:if

    word:this'll

    word:work

在第一个有问题的地方添加了反斜线字符来转义 don' t中的单引号。在第二个有问题的地方将 this'll 用双引号圈起来。两种方法都能正常辨别出这个值。

可能遇到的另一个问题是有多个词的值。记住,for 循环假定每个值都是用空格分割的。如果有包含空格的数据值,就陷入麻烦了。

示例:

    [[email protected] 13]$ cat badtest2

    #!/bin/bash

    # another example of how not to use the for command

    for test in Nevada New Hampshire New Mexico New York North Carolina

    do

        echo "Now going to $test"

    done

运行:

    [[email protected] 13]$ badtest2

    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

这不是我们想要的结果。for 命令用空格来划分列表中的每个值。如果在单独的数据值中有空格,就必须用双引号将这些值圈起来。

示例:

    [[email protected] 13]$ cat test3.sh

    #!/bin/bash

    # an example of how to properly define values

    #

    for test in Nevada "New Hampshire" "New Mexico" "New York"

    do

            echo "Now going to $test"

    done

运行:

    [[email protected] 13]$ test3.sh

    Now going to Nevada

    Now going to New Hampshire

    Now going to New Mexico

    Now going to New York

现在 for 命令可以正确区分不同值了。另外要注意的是,在某个值两边使用双引号时,shell 并不会将双引号当成值的一部分。

3.1.3 从变量读取列表

-----------------------------------------------------------------------------------------------------------------------------------------

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

示例:

    [[email protected] 13]$ cat test4.sh

    #!/bin/bash

    # using a variale 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

运行:

    [[email protected] 13]$ test4.sh

    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 变量包含的已有列表中添加(或者说是拼接)了一个值。这是

向变量中存储的已有文本字符串尾部添加文本的一个常用方法。

3.1.4 从命令读取值

-----------------------------------------------------------------------------------------------------------------------------------------

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

示例:

    [[email protected] 13]$ cat test5.sh

    #!/bin/bash

    # reading values from a file

    #

    file="state"

    for state in $(cat $file)

    do

            echo "Visit beautiful $state"

    done

运行:

    [[email protected] 13]$ cat state

    Alabama

    Alaska

    Arizona

    Arkansas

    Colorado

    Connecticut

    Delaware

    Florida

    Georgia

    New York

    New Hampshire

    North Carolina

    [[email protected] 13]$ test5.sh

    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

    Visit beautiful New

    Visit beautiful York

    Visit beautiful New

    Visit beautiful Hampshire

    Visit beautiful North

    Visit beautiful Carolina

这个例子在命令替换中使用了 cat 命令来输出文件 states 的内容。states 文件中每一行有一个州,而不是通过空格分隔的。for 命令仍然以每次一行的

方式遍历了 cat 命令的输出,假定每个州都是在单独的一行上。但这并没有解决数据中有空格的问题。如果列出了一个名字中有空格的州,for 命令仍然

会将每个单词当作单独的值。

3.1.5 更改字段分隔符

-----------------------------------------------------------------------------------------------------------------------------------------

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

字符。默认情况下,bash shell 会将下列字符当作字段分隔符:

    □ 空格

    □ 制表符

    □ 换行符

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

这会非常麻烦,就像在上一个脚本示例中看到的。

要解决这个问题,可以在 shell 脚本中临时更改 IFS 环境变量的值来限制被 bash shell 当作字段分隔符的字符。例如,如果想修改 IFS 的值,使其只

能识别换行符,那就必须这么做:

    IFS=$'\n'

将这个语句加入到脚本中,告诉 bash shell 在数据值中忽略空格和制表符。对前一个脚本使用这种方法,将获得如下输出。

示例:

    [[email protected] 13]$ cat test5b.sh

    #!/bin/bash

    # reading values from a file

    #

    file="state"

    IFS=$'\n'

    for state in $(cat $file)

    do

            echo "Visit beautiful $state"

    done

运行:

    [[email protected] 13]$ test5b.sh

    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

    Visit beautiful New York

    Visit beautiful New Hampshire

    Visit beautiful North Carolina

现在,shell 脚本就能够使用列表中含有空格的值了。

    NOTE:

    -------------------------------------------------------------------------------------------------------------------------------------

    在处理代码量较大的脚本时,可能在一个地方需要修改 IFS 的值,然后忽略这次修改,在脚本的其他地方继续沿用 IFS 的默认值。一个可参考的安全

    实践是在改变 IFS 之前保存原来的 IFS 值,之后再恢复它。这种技术可以这样实现:

        IFS_OLD=$IFS

        IFS=$'\n'

        <在代码中使用新的IFS值>

        IFS=$IFS_OLD

    这就保证了在脚本的后续操作中使用的是IFS的默认值。

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

    IFS=:

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

    IFS="'\n':;"

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

3.1.6 用通配符读取目录

-----------------------------------------------------------------------------------------------------------------------------------------

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

匹配指定通配符的文件名或路径名的过程。

示例:

    [[email protected] 13]$ cat test6.sh

    #!/bin/bash

    # iterate through all the files in a directory

    for file in /home/devalone/study/shell-script/*

    do

            if [ -d "$file" ]

            then

                    echo "$file is a directory"

            elif [ -f "$file" ]

            then

                    echo "$file is a file"

            fi

    done

运行:

    [[email protected] 13]$ test6.sh

    /home/devalone/study/shell-script/12 is a directory

    /home/devalone/study/shell-script/13 is a directory

    /home/devalone/study/shell-script/14 is a directory

    /home/devalone/study/shell-script/15 is a directory

    /home/devalone/study/shell-script/16 is a directory

    /home/devalone/study/shell-script/17 is a directory

    /home/devalone/study/shell-script/18 is a directory

    /home/devalone/study/shell-script/19 is a directory

    /home/devalone/study/shell-script/20 is a directory

    /home/devalone/study/shell-script/21 is a directory

    /home/devalone/study/shell-script/22 is a directory

    /home/devalone/study/shell-script/23 is a directory

    /home/devalone/study/shell-script/24 is a directory

    /home/devalone/study/shell-script/log.180705 is a file

    /home/devalone/study/shell-script/rpm.list is a file

    /home/devalone/study/shell-script/test10.sh is a file

    /home/devalone/study/shell-script/test11.sh is a file

    /home/devalone/study/shell-script/test12.sh is a file

    /home/devalone/study/shell-script/test13.sh is a file

    ...

for 命令会遍历 /home/devalone/study/shell-script/* 输出的结果。该代码用 test 命令测试了每个条目(使用方括号方法),以查看它是目录(通过

-d 参数)还是文件(通过 -f 参数)。

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

    if [ -d "$file" ]

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

文件名时就会有错误产生。

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

示例:

    [[email protected] 13]$ cat test7.sh

    #!/bin/bash

    # iterate through all the files in a directory

    for file in /home/devalone/study/shell-script/* /home/devalone/.b*

    do

            if [ -d "$file" ]

            then

                    echo "$file is a directory"

            elif [ -f "$file" ]

            then

                    echo "$file is a file"

            fi

    done

运行:

    [[email protected] 13]$ test7.sh

    /home/devalone/study/shell-script/12 is a directory

    /home/devalone/study/shell-script/13 is a directory

    /home/devalone/study/shell-script/14 is a directory

    /home/devalone/study/shell-script/15 is a directory

    /home/devalone/study/shell-script/16 is a directory

    /home/devalone/study/shell-script/17 is a directory

    /home/devalone/study/shell-script/18 is a directory

    /home/devalone/study/shell-script/log.180705 is a file

    /home/devalone/study/shell-script/rpm.list is a file

    /home/devalone/study/shell-script/test10.sh is a file

    /home/devalone/study/shell-script/test11.sh is a file

    /home/devalone/study/shell-script/test12.sh is a file

    /home/devalone/study/shell-script/test13.sh is a file

    /home/devalone/study/shell-script/test14b.sh is a file

    /home/devalone/study/shell-script/test14.sh is a file

    /home/devalone/study/shell-script/test1.sh is a file

    /home/devalone/study/shell-script/test2.sh is a file

    /home/devalone/study/shell-script/test3.sh is a file

    ...

    /home/devalone/.bash_history is a file

    /home/devalone/.bash_logout is a file

    /home/devalone/.bash_profile is a file

    /home/devalone/.bashrc is a file

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

    NOTE:

    -------------------------------------------------------------------------------------------------------------------------------------

    注意,可以在数据列表中放入任何东西。即使文件或目录不存在,for 语句也会尝试处理列表中的内容。在处理文件或目录时,这可能会是个问题。无法

    知道正在尝试遍历的目录是否存在:在处理之前测试一下文件或目录总是好的。

3.2 C 语言风格的for 命令

-----------------------------------------------------------------------------------------------------------------------------------------

如果从事过 C 语言编程,可能会对 bash shell 中 for 命令的工作方式有点惊奇。在 C 语言中,for 循环通常定义一个变量,然后这个变量会在每次迭代

时自动改变。通常程序员会将这个变量用作计数器,并在每次迭代中让计数器增一或减一。bash 的 for 命令也提供了这个功能。

3.2.1 C 语言的for 命令

-----------------------------------------------------------------------------------------------------------------------------------------

C 语言的 for 命令有一个用来指明变量的特定方法,一个必须保持成立才能继续迭代的条件,以及另一个在每个迭代中改变变量的方法。当指定的条件不

成立时,for 循环就会停止。条件等式通过标准的数学符号定义。比如,考虑下面的C语言代码:

    for (i = 0; i < 10; i++)

    {

        printf("The next number is %d\n", i);

    }

bash shell 也支持一种 for 循环,它看起来跟 C语言风格的 for 循环类似,但有一些细微的不同,以下是bash中C语言风格的for循环的基本格式:

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

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

C 语言风格的 for 命令看起来如下:

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

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

    □ 变量赋值可以有空格;

    □ 条件中的变量不以美元符开头;

    □ 迭代过程的算式未用expr命令格式。

在脚本中使用 C 语言风格的 for 循环时要小心。

示例:

    [[email protected] 13]$ cat test8.sh

    #!/bin/bash

    # testing the C-style for loop

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

    do

            echo "the next number is $i"

    done

运行:

    [[email protected] 13]$ 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

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

过程会作用在变量上,在本例中,变量增一。

3.2.2 使用多个变量

-----------------------------------------------------------------------------------------------------------------------------------------

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

在 for 循环中定义一种条件。

示例:

    [[email protected] 13]$ cat test9.sh

    #!/bin/bash

    # multiple variables

    for (( a=1, b=10; a <= 10; a++, b-- ))

    do

            echo "$a - $b"

    done

运行:

    [[email protected] 13]$ 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。

3.3 while 命令

-----------------------------------------------------------------------------------------------------------------------------------------

while 命令某种意义上是 if-then 语句和 for 循环的混杂体。while 命令允许定义一个要测试的命令,只要定义的测试命令返回的是退出状态码 0。就会

循环执行一组命令。它会在每次迭代的一开始测试 test 命令。在 test 命令返回非零退出状态码时,while 命令会停止执行那组命令。

3.3.1 while 的基本格式

-----------------------------------------------------------------------------------------------------------------------------------------

while 命令的格式是:

    while test command

    do

        other commands

    done

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

测试变量值。

while 命令的关键在于所指定的 test command 的退出状态码必须随着循环中运行的命令而改变。如果退出状态码不发生变化, while 循环就将一直不停

地进行下去。

最常见的 test command 的用法是用方括号来检查循环命令中用到的 shell 变量的值。

示例:

    [[email protected] 13]$ cat test10.sh

    #!/bin/bash

    # while command test

    var1=10

    while [ $var1 -gt 0 ]

    do

        echo $var1

        var1=$[ $var1 - 1 ]

    done

运行:

    [[email protected] 13]$ test10.sh

    10

    9

    8

    7

    6

    5

    4

    3

    2

    1

while 命令定义了每次迭代时检查的测试条件:

    while [ $var1 -gt 0 ]

只要测试条件成立,while 命令就会不停地循环执行定义好的命令。在这些命令中,测试条件中用到的变量必须修改,否则就会陷入无限循环。在本例中,

用shell算术来将变量值减一:

    var1=$[ $var1 - 1 ]

while 循环会在测试条件不再成立时停止。

3.3.2 使用多个测试命令

-----------------------------------------------------------------------------------------------------------------------------------------

while 命令允许在 while 语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。

示例:

    [[email protected] 13]$ cat test11.sh

    #!/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

运行:

    [[email protected] 13]$ 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

    This is inside the loop

    -1

while 语句中定义了两个测试命令:

    while echo $var1

          [ $var1 -ge 0 ]

第一个测试简单地显示了 var1 变量的当前值。第二个测试用方括号来判断 var1 变量的值。在循环内部,echo 语句会显示一条简单的消息,说明循环被

执行了。注意运行本例时输出是如何结束的:

    This is inside the loop

    -1

while 循环会在 var1 变量等于 0 时执行 echo 语句,然后将 var1 变量的值减一。接下来再次执行测试命令,用于下一次迭代。echo 测试命令被执行并

显示了 var 变量的值(现在小于 0 了)。直到 shell 执行 test 测试命令,whle 循环才会停止。

这说明在含有多个命令的 while 语句中,在每次迭代中所有的测试命令都会被执行,包括测试命令失败的最后一次迭代。要留心这种用法。另一处要留意的

是该如何指定多个测试命令:每个测试命令都出现在单独的一行上。

3.4 until 命令

-----------------------------------------------------------------------------------------------------------------------------------------

until 命令和 while 命令工作的方式完全相反。until 命令要求指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为 0,bash

shell 才会执行循环中列出的命令。一旦测试命令返回了退出状态码 0,循环就结束了。

until 命令的格式如下:

    until test commands

    do

        other commands

    done

和 while命令类似,可以在 until 命令语句中放入多个测试命令。只有最后一个命令的退出状态码决定了 bash shell 是否执行已定义的 other commands。

示例:

    [[email protected] 13]$ cat test12.sh

    #!/bin/bash

    # using the until command

    #

    var1=100

    until [ $var1 -eq 0 ]

    do

            echo $var1

            var1=$[ $var1 -25 ]

    done

运行:

    [[email protected] 13]$ test12.sh

    100

    75

    50

    25

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

测试命令时要注意。

示例:

    [[email protected] 13]$ cat test13.sh

    #!/bin/bash

    # using the until command

    #

    var1=100

    until echo $var1

            [ $var1 -eq 0 ]

    do

            echo Inside the loop: $var1

            var1=$[ $var1 - 25]

    done

运行:

    [[email protected] 13]$ test13.sh

    100

    Inside the loop: 100

    75

    Inside the loop: 75

    50

    Inside the loop: 50

    25

    Inside the loop: 25

shell 会执行指定的多个测试命令,只有在最后一个命令成立时停止。

3.5 嵌套循环

-----------------------------------------------------------------------------------------------------------------------------------------

循环语句可以在循环内使用任意类型的命令,包括其他循环命令。这种循环叫作嵌套循环(nested loop)。注意,在使用嵌套循环时,是在迭代中使用迭代,

与命令运行的次数是乘积关系。不注意这点的话,有可能会在脚本中造成问题。

示例:

    [[email protected] 13]$ cat test14.sh

    #!/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

运行:

    [[email protected] 13]$ 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

这个被嵌套的循环(也称为内部循环,inner loop)会在外部循环的每次迭代中遍历一次它所有的值。两个循环的 do 和 done 命令没有任何差别。bash

shell 知道当第一个 done 命令执行时是指内部循环而非外部循环。

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

示例:

    [[email protected] 13]$ cat test15.sh

    #!/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

运行:

    [[email protected] 13]$ test15.sh

    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 循环示例:

    [[email protected] 13]$ cat test16.sh

    #!/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

示例:

    [[email protected] 13]$ test16.sh

    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

3.6 循环处理文件数据

-----------------------------------------------------------------------------------------------------------------------------------------

通常必须遍历存储在文件中的数据。这要求结合已经讲过的两种技术:

    □ 使用嵌套循环

    □ 修改IFS环境变量

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

可能需要再次利用循环来提取行中的数据。

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

示例:

    [[email protected] 13]$ cat test-passwd.sh

    #!/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_OLD

运行:

    [[email protected] 13]$ test-passwd.sh

    Values in root:x:0:0:root:/root:/bin/bash –

     root

     x

     root

     /root

     /bin/bash

    Values in bin:x:1:1:bin:/bin:/sbin/nologin –

     bin

     x

     1

     1

     bin

     /bin

     /sbin/nologin

    Values in daemon:x:2:2:daemon:/sbin:/sbin/nologin –

     daemon

     x

     2

     2

     daemon

     /sbin

     /sbin/nologin

    ...

3.7 控制循环

-----------------------------------------------------------------------------------------------------------------------------------------

有两个命令能帮我们控制循环内部的情况:

    □ break命令

    □ continue命令

3.7.1 break 命令

-----------------------------------------------------------------------------------------------------------------------------------------

break 命令是退出循环的一个简单方法。可以用 break 命令来退出任意类型的循环,包括 while 和 until 循环。

■ 跳出单个循环

-----------------------------------------------------------------------------------------------------------------------------------------

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

示例:

    [[email protected] 13]$ cat test17.sh

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

运行:

    [[email protected] 13]$ test17.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 循环。

示例:

    [[email protected] 13]$ cat test18.sh

    #!/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

运行:

    [[email protected] 13]$ test18.sh

    Iteration: 1

    Iteration: 2

    Iteration: 3

    Iteration: 4

    The while loop is completed

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

■ 跳出内部循环

-----------------------------------------------------------------------------------------------------------------------------------------

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

示例:

    [[email protected] 13]$ cat test19.sh

    #!/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

示例:

    [[email protected] 13]$ test19.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

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

循环通过 break 命令终止了,外部循环依然继续执行。

■ 跳出外部循环

-----------------------------------------------------------------------------------------------------------------------------------------

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

    break n

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

示例:

    [[email protected] 13]$ cat test20.sh

    #!/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

运行:

    [[email protected] 13]$ test20.sh

    Outer loop: 1

     Inner loop: 1

     Inner loop: 2

     Inner loop: 3

     Inner loop: 4

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

3.7.2 continue 命令

-----------------------------------------------------------------------------------------------------------------------------------------

continue 命令可以提前中止某次循环中的命令,但并不会完全终止整个循环。可以在循环内部设置 shell 不执行命令的条件。

示例:

    $ cat test21.sh

    #!/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

运行:

    [[email protected] 13]$ test21.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 命令时,它会跳过剩余的命令。如果在其中某个

条件里对测试条件变量进行增值,问题就会出现。

示例:

    [[email protected] 13]$ 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

运行:

    [[email protected] 13]$ 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

    ...

确保将脚本的输出重定向到了 more 命令,这样才能停止输出。在 if-then 的条件成立之前,所有一切看起来都很正常,然后 shell 执行了 continue

命令。当 shell 执行 continue 命令时,它跳过了 while 循环中余下的命令。不幸的是,被跳过的部分正是 $var1 计数变量增值的地方,而这个变量又

被用于 while 测试命令中。这意味着这个变量的值不会再变化了,因此进入无限循环。

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

    continue n

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

    [[email protected] 13]$ cat test22.sh

    #!/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

运行:

    [[email protected] 13]$ 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

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

命令停止了处理过程,但外部循环依然会继续。

3.8 处理循环的输出

-----------------------------------------------------------------------------------------------------------------------------------------

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

示例:

    [[email protected] 13]$ cat test23.sh

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

运行:

    [[email protected] 13]$ test23.sh

    The command is finished.

    [[email protected] 13]$ 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 语句。

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

    [[email protected] 13]$ cat test24.sh

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

运行:

    [[email protected] 13]$ test24.sh

    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 命令输出结果的顺序。运行这个脚本实际上

说明了结果已经在脚本内部排好序了。

3.9 实例

-----------------------------------------------------------------------------------------------------------------------------------------

循环是对系统数据进行迭代的常用方法,无论是目录中的文件还是文件中的数据。下面的一些例子演示了如何使用简单的循环来处理数据。

3.9.1 查找可执行文件

-----------------------------------------------------------------------------------------------------------------------------------------

从命令行中运行一个程序的时候,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

将这些代码片段组合成脚本就行了:

    [[email protected] 13]$ cat test25.sh

    #!/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

运行:

[[email protected] 13]$ test25.sh | more

/usr/local/protoc/bin:

 /usr/local/protoc/bin/protoc

/home/devalone/programs/apache-tomcat-8.5.11/bin:

 /home/devalone/programs/apache-tomcat-8.5.11/bin/bootstrap.jar

 /home/devalone/programs/apache-tomcat-8.5.11/bin/catalina.bat

 /home/devalone/programs/apache-tomcat-8.5.11/bin/catalina.sh

 /home/devalone/programs/apache-tomcat-8.5.11/bin/catalina-tasks.xml

 /home/devalone/programs/apache-tomcat-8.5.11/bin/commons-daemon.jar

 /home/devalone/programs/apache-tomcat-8.5.11/bin/commons-daemon-native.tar.gz

 /home/devalone/programs/apache-tomcat-8.5.11/bin/configtest.bat

 /home/devalone/programs/apache-tomcat-8.5.11/bin/configtest.sh

 /home/devalone/programs/apache-tomcat-8.5.11/bin/daemon.sh

 ...

3.9.2 创建多个用户账户

-----------------------------------------------------------------------------------------------------------------------------------------

将需要添加的新用户账户放在一个文本文件中,文本文件的格式如下:

    userid,user name

第一个条目是为新用户账户所选用的用户ID。第二个条目是用户的全名。两个值之间使用逗号分隔,这样就形成了一种名为逗号分隔值的文件格式。可以轻松

地在电子表格程序中创建用户账户列表,然后将其保存成 .csv 格式,以备 shell 脚本读取及处理。

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

中的各行。实现代码如下:

    while IFS=’,’ read –r userid name

read 命令会自动读取 .csv 文本文件的下一行内容,所以不需要专门再写一个循环来处理。当 read 命令返回 FALSE 时(也就是读取完整个文件时),

while 命令就会退出。

要想把数据从文件中送入 while 命令,只需在 while 命令尾部使用一个重定向符就可以了。

脚本如下:

    [[email protected] 13]$ cat test26.sh

    #!/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 文件内容如下:

    [[email protected] 13]$ cat users.csv

    user1,User Test1

    user2,User Test2

    user3,User Test3

    user4,User Test4

    user5,User Test5

运行: 需要 root 权限

    [[email protected] 13]$ sudo ./test26.sh

    adding user1

    adding user2

    adding user3

    adding user4

    adding user5

系列目录:

    Linux shell 脚本编程-基础篇 (一)

    Linux shell 脚本编程-基础篇 (二)

    Linux shell 脚本编程-基础篇 (三)

    Linux shell 脚本编程-基础篇 (四)

    Linux shell 脚本编程-基础篇 (五)

    Linux shell 脚本编程-基础篇 (六)

-----------------------------------------------------------------------------------------------------------------------------------------

参考:

    《Linux 命令行与 shell 脚本编程大全》 第 3 版 —— 2016.8(美)Richard Blum  Cristine Bresnahan

继续阅读