首页 > 技术文章 > shell编程:for循环

trafalgar999 2020-06-13 18:56 原文

shell编程 结构化命令

for 命令

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

for var in list
do
    commands
done

在 list 参数中,你需要提供迭代中要用到的一系列值。可以通过几种不同的方法指定列表 中的值。

在每次迭代中,变量 var 会包含列表中的当前值。第一次迭代会使用列表中的第一个值,第 二次迭代使用第二个值,以此类推,直到列表中的所有值都过一遍。

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

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

读取列表中的值

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

...
for test in Alabama Alaska Arizona Arkansas California Colorado
do
    echo The next state is $test
done
...

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

读取列表中的复杂值

事情并不会总像你在 for 循环中看到的那么简单。有时会遇到难处理的数据。下面是给 shell 脚本程序员带来麻烦的典型例子。

for test in I don't know if this'll work
do
    echo "word:$test"
done

shell 看到了列表值中的单引号并尝试使用它们来定义一个单独的数据值,这真是把 事情搞得一团糟。
有两种办法可解决这个问题:

  • 使用转义字符(反斜线)来将单引号转义;
  • 使用双引号来定义用到单引号的值。
for test in I don\'t know if "this'll" work
do
    echo "word:$test"
done

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

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

如果在单独的数据值中有 空格,就必须用双引号将这些值圈起来。

从变量读取列表

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

list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut"

for state in $list
do
    echo "Have you ever visited $state?"
done

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

从命令读取值

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

file="states"

for state in $(cat $file)
do
    echo "Visit beautiful $state"
done

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

更改字段分隔符

IFS 环境变量定义了 bash shell 用作字段分隔符的一系列字符。默认情况下,bash shell 会将下列字 符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

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

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

IFS=$'\n'

将这个语句加入到脚本中,告诉 bash shell 在数据值中忽略空格和制表符。

在处理代码量较大的脚本时,可能在一个地方需要修改 IFS 的值,然后忽略这次修改,在 脚本的其他地方继续沿用 IFS 的默认值。一个可参考的安全实践是在改变 IFS 之前保存原 来的 IFS 值,之后再恢复它。 这种技术可以这样实现:

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

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

IFS=$'\n':;"

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

用通配符读取目录

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

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


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

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

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

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

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

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


C 语言风格的 for 命令

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

C 语言的 for 命令

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

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

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

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

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

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

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

for (( i=1; i <= 10; i++ ))
do
    echo "The next number is $i"
done

使用多个变量

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

for (( a=1, b=10; a <= 10; a++, b-- ))
do
    echo "$a - $b"
done

while 命令

while 的基本格式

while 命令的格式是:

while test command
do
    other commands
done

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

var1=10
while [ $var1 -gt 0 ]
do
    echo $var1
    var1=$[ $var1 - 1 ]
done

使用多个测试命令

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

var1=10

while echo $var1
        [ $var1 -ge 0 ]
do
    echo "This is inside the loop"
    var1=$[ $var1 - 1 ]
done

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

在含有多个命令的 while 语句中,在每次迭代中所有的测试命令都会被执行,包括测 试命令失败的后一次迭代。要留心这种用法。另一处要留意的是该如何指定多个测试命令。注 意,每个测试命令都出现在单独的一行上。

until 命令

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

until test commands
do
    other commands
done

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

var1=100

until [ $var1 -eq 0 ]
do
    echo $var1
    var1=$[ $var1 - 25 ]
done

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

嵌套循环

在使用嵌套循环时,你是在迭代中使用迭代,与命令运行的次数是乘积关 系。

for (( a = 1; a <= 3; a++ ))
do
    echo "Starting loop $a:"
    for (( b = 1; b <= 3; b++ ))
    do
        echo "   Inside loop: $b"
    done
done

循环处理文件数据

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

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

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

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

#!/bin/bash

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

控制循环

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

  • break
  • continue

break 命令

可以用 break 命令来退出任意类型的循环,包括 while 和 until 循环。

有几种情况可以使用 break 命令

跳出单个循环

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

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"

for 循环通常都会遍历列表中指定的所有值。但当满足 if-then 的条件时,shell 会执行 break 命令,停止 for 循环。

这种方法同样适用于 while 和 until 循环。

跳出内部循环

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

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

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

跳出外部循环

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

break n

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

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

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

continue 命令

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

for (( var1 = 1; var1 < 15; var1++ ))
do
    if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
    then
        continue
    fi
    echo "Iteration number: $var1"
done

当 if-then 语句的条件被满足时(值大于 5 且小于 10),shell 会执行 continue 命令,跳过此 次循环中剩余的命令,但整个循环还会继续。当 if-then 的条件不再被满足时,一切又回到正轨。

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

continue n

其中 n 定义了要继续的循环层级。

处理循环的输出

在 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 state in "North Dakota" Connecticut Illinois Alabama Tennessee
do
    echo "$state is the next place to go"
done | sort
echo "This completes our travels"

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

实例

查找可执行文件

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

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

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

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

IFS=:
for folder in $PATH
do
    echo "$folder:"
    for file in $folder/*
    do
        if [ -x $file ]
        then
            echo "  $file"
        fi
    done
done

运行这段代码时,你会得到一个可以在命令行中使用的可执行文件的列表。

创建多个用户账户

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 命令尾部使用一个重定向符就可以了。

input="users.csv"
while IFS=',' read -r userid name
do
    echo "adding $userid"
    useradd -c "$name" -m $userid
done < "$input"

$input 变量指向数据文件,并且该变量被作为 while 命令的重定向数据。

推荐阅读