第33章:流程控制:for 循环

在关于流程控制的最后一章中,我们将学习另一个 shell 循环结构。for 循环(for loop)不同于 whileuntil 循环,它提供了在循环内处理序列的方法。这在编程时会变得很有用。因此,for 循环在 bash 脚本中广为流行。

很自然地,实现 for 循环,需要使用 for 复合命令。在 bash 中,for 有两种形式。

for:传统的 Shell 形式

原始的 for 命令句法如下:

for variable [in words]; do
    command
done

其中 variable 是一个会在循环执行过程中自增的变量的名称,words 是一个可选的会被按序分配到 variable 中的列表项,commands 是会在每一次循环迭代中被执行的命令。

for 命令在命令行中也很有用。我们可以很方便地演示其如何工作。

[me@linuxbox ~]$ for i in A B C D; do echo $i; done
A
B
C
D

上例中,有四个词给到 forABCD。循环用这四个词执行了四次。每次执行循环,就分配一个词给变量 i。在循环内部,有一个 echo 命令,会显示 i 的值,以显示当前的分配。和 whileuntil 循环一样,done 关键词关闭循环。

for 真正强大的特性是有众多有趣的途径可以创建 words 列表。例如,可以用花括号扩展,如:

[me@linuxbox ~]$ for i in {A..D}; do echo $i; done
A
B
C
D

或者可以使用路径名扩展,如下:

[me@linuxbox ~]$ for i in distros*.txt; do echo "$i"; done
distros-by-date.txt
distros-dates.txt
distros-key-names.txt
distros-key-vernums.txt
distros-names.txt
distros.txt
distros-vernums.txt
distros-versions.txt

路径名扩展提供了一个漂亮干净的路径名列表,使其可以在循环中被处理。需要的一个预防措施是要检查这扩展事实上匹配了什么。默认情况下,如果扩展没有匹配任何文件,则返回通配符自身(在上例中是 "distros*.txt")。要防止此类情况,我们可以如下修改上面的代码:

for i in distros*.txt; do
    if [[ -e "$i" ]]; then
        echo "$i"
    fi
done

通过增加一个对文件存在与否的测试,可以忽略失败的扩展。

另一个常见的制造 words 的方法是命令替换。

#!/bin/bash

# longest-word: find longest string in a file

while [[ -n "$1" ]]; do
    if [[ -r "$1" ]]; then
        max_word=
        max_len=0
        for i in $(strings "$1"); do
            len="$(echo -n "$i" | wc -c)"
            if (( len > max_len )); then
                max_len="$len"
                max_word="$i"
            fi
        done
        echo "$1: '$max_word' ($max_len characters)"
    fi
    shift
done

在这个例子中,我们寻找一个文件中最长的字符串。当在命令行上给出一个或更多文件名时,程序会使用 strings 程序(包含在 GNU binutils 包中)在每个文件中生成一个可读文本列表 "words"。for 循环反过来处理每个单词并判断当前单词是否是已找过单词中最长的一个。当循环得出结论,就会显示最长的那个单词。

这里有一件事需要记住,与通常的练习相反,我们没有把命令替换 $(strings "$1") 放在双引号内。这是因为我们实际上是想分词,以便给出一份列表。如果我们把命令替换放在引号内,则仅会产生单个单词,它会包含文件中每个字符串。那不是我们想要查找的。

如果 for 命令中可选的 words 部分被省略了,for 默认去处理位置参数。我们来使用这种方法修改 longest-word 脚本:

#!/bin/bash

# longest-word2: find longest string in a file

for i; do
    if [[ -r "$i" ]]; then
        max_word=
        max_len=0
        for j in $(strings "$i"); do
            len="$(echo -n "$j" | wc -c)"
            if (( len > max_len )); then
                max_len="$len"
                max_word="$j"
            fi
        done
        echo "$i: '$max_word' ($max_len characters)"
    fi
done

我们可以看到,已经用 for 替换了 while ,变更了最外层的循环。在省略了 for 命令中的 words 列表后,用位置参数替代了。在循环内,前面例子中的变量 i 已经被改成 j 了。同时还移除了 shift

为什么是 i?

可能你已经注意到了上面的 for 循环示例中,总是选择 i 作为变量。为何?除了传统,实际上没有什么特殊的理由。用在 for 里的变量可以是任意合法的变量名,但是 i 最常用,之后就是 jk

这个传统的基础来自 Fortran 编程语言。在 Fortran 中,未声明的变量从字母 I、J、K、L、M 开始,类型自动设置为整数,而其它字母开头的变量类型则为实数(有小数部分的数字)。这一行为引领程序员使用 I、J、K 作为循环变量,因为在需要临时变量(通常是循环变量)时,使用它们的工作量较小。

这也引出了下面这句基于 Fortran 的妙语:

「 上帝是实数,除非声明为整数。」

for:C 语言形式

近几个版本的 bash 已经加入了 for 命令的第二种句法形式,和 C 编程语言的形式类似。许多其它语言也支持这种形式。

for (( expression1; expression2; expression3 )); do
    commands
done

这里,expression1expression2expression3 都是算术表达式,commands 是在循环中每次迭代都会被执行的命令。

就行为而言,这种形式等效于下列的结构:

(( expression1 ))
while (( expression2 )); do
    commands
    (( expression3 ))
done

expression1 用来初始化循环条件,expression2 用来确定何时结束循环,而 expression3 在每次循环迭代的最后被执行。

这里有个典型的应用:

#!/bin/bash

# simple_counter: demo of C style for command

for (( i=0; i<5; i=i+1 )); do
    echo $i
done

当执行时,产生下列输出:

[me@linuxbox ~]$ simple_counter
0
1
2
3
4

本例中,expression1 以数值 0 初始化变量 iexpression2 的作用是,只要变量 i 的值小于 5,就继续循环,而 expression3 在每次循环重复时会使变量 i 增长 1

for 的 C 语言形式在任何需要数字序列的时候都很有用。在下面两章中我们可以看到。

总结

掌握了 for 命令,我们现在可以对 sys_info_page 脚本做最后的改进。当前,report_home_space 函数是这样的:

report_home_space () {
    if [[ "$(id -u)" -eq 0 ]]; then
        cat <<- _EOF_
            <h2>Home Space Utilization (All Users)</h2>
            <pre>$(du -sh /home/*)</pre>
            _EOF_
    else
        cat <<- _EOF_
            <h2>Home Space Utilization ($USER)</h2>
            <pre>$(du -sh "$HOME")</pre>
            _EOF_
    fi
    return
}

接下来,我们将重写这部分,以提供针对每个用户家目录更多的细节,并包含所有文件和子目录的数目。

report_home_space () {

    local format="%8s%10s%10s\n"
    local i dir_list total_files total_dirs total_size user_name

    if [[ "$(id -u)" -eq 0 ]]; then
        dir_list=/home/*
        user_name="All Users"
    else
        dir_list="$HOME"
        user_name="$USER"
    fi

    echo "<h2>Home Space Utilization ($user_name)</h2>"

    for i in $dir_list; do

        total_files="$(find "$i" -type f | wc -l)"
        total_dirs="$(find "$i" -type d | wc -l)"
        total_size="$(du -sh "$i" | cut -f 1)"

        echo "<H3>$i</H3>"
        echo "<pre>"
        printf "$format" "Dirs" "Files" "Size"
        printf "$format" "----" "-----" "----"
        printf "$format" "$total_dirs" "$total_files" "$total_size"
        echo "</pre>"
    done
    return
}

本次改写应用了迄今为止我们学到的许多知识。我们还是测试其是否为超级用户,不过,不是执行if 部分中的整套行为,而是设置了一些随后用在 for 循环里的变量。我们给函数加入了几个局部变量,并用 printf 格式化了一些输出。

扩展阅读

Last updated

Was this helpful?