流程控制:for循环
1、for的传统形式
for的传统形式如下:
bash
for variable [in words]; do
commands
done
其中,variable是在循环期间递增的变量名,words是一个可选的条目列表,各个条目会被依次赋给variable,command是在每次循环迭代时要执行的命令。
for在命令行中也可以发挥作用。我们通过一个简单的例子来演示其工作方式:
bash
[me@linuxbox ~]$ for i in A B C D; do echo $i; done
A
B
C
D
在这个例子中,for得到了一个包含4个单词(A、B、C、D)的列表,因此该循环会执行4次。每次循环时,其中一个单词就会被赋给变量i。在循环内部,echo命令显示变量i的值。与while和until循环一样,关键字done用于结束循环。
for真正的强大之处在于创建单词列表的多种方式。例如,可以使用花括号扩展来创建:
bash
[me@linuxbox ~]$ for i in {A..D}; do echo $i; done
A
B
C
D
也可以使用路径名扩展来创建:
bash
[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)。为了防范这种情况,我们要在脚本中加入相应的代码:
bash
for i in distros*.txt; do
if [[ -e "$i" ]]; then
echo "$i"
fi
done
通过加入文件存在测试,就能忽略失败的扩展。
另一种创建单词列表的常见方法是命令替换:
bash
#!/bin/bash
# longest-word: 在文件中查找长字符串
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软件包内)生成每个文件中可读的文本"单词"列表。for循环依次处理各个单词,确定当前单词是否为目前最长的。当循环结束时,显示出找到的最长单词。
有一处地方要注意,不同于惯常的实践,我们这次并没有将命令替换(strings"1")放入双引号中。这是因为我们正是想要对命令替换的结果进行单词分割,以此得到单词列表。如果加上了双引号,得到的就只是一个包含了文件中所有字符串的单个单词,这可不是我们想要的。
如果忽略了for命令中可选的in words部分,则默认处理位置参数。我们来按照这种方法修改脚本longest-word:
bash
#!/bin/bash
# longest-word2: 在文件中查找长字符串
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
可以看出,我们将外围的while循环改成了for循环。由于忽略了for命令中的单词列表部分,因此循环转而处理位置参数。在循环内部,先前的变量i被修改成了变量j。另外,末尾的shift也用不上了。
为什么要用i
你也许已经注意到上述每个for循环示例中选用的变量都是i,这是为什么呢?其实除了沿用惯例之外,也没什么特别原因。for可以使用任何有效变量,只不过i是最常用的,然后就是j和k。
这种惯例源自Fortran。在Fortran中,以字母I、J、K、L、M开头的未声明变量自动被归类为整数,而以其他字母开头的变量被归类为实数(带小数的数字)。这就促使程序员在需要临时变量(通常用作循环变量)的时候使用变量I、J、K,毕竟这样比较省事。
2、for的C语言形式
最近的Bash加入了另一种for形式,其与C语言中的for形式类似,很多其他编程语言也支持这种形式:
bash
for (( expression1; expression2; expression3 )); do
commands
done
其中,expression1、expression2、expression3都是算术表达式,commands是在每次循环时执行的命令。
就其功能而言,这种形式等价于:
bash
(( expression1 ))
while (( expression2 )); do
commands
(( expression3 ))
done
expression1用于初始化循环条件,expression2用于确定循环何时结束,expression3在每次循环末尾执行。
下面是for的C语言形式的典型应用:
bash
#!/bin/bash
# simple_counter: 演示C风格的命令
for (( i=0; i<5; i=i+1 )); do
echo $i
done
执行结果如下:
bash
[me@linuxbox ~]$ simple_counter
0
1
2
3
4
在这个例子中,expression1使用0初始化变量i;expression2在i的值小于5时,允许循环继续执行;expression3在每次循环时将i的值加1。
只要需要数值序列,C语言形式的for就能派上用场。在第33章和第34章中,我们还会多次看到for的相关应用。
3、总结
学习了for命令之后,我们就可以对sys_info_page脚本进行最终的改进了。report_home_space函数目前是这样的:
bash
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
}
接下来,我们要对其进行重写,以提供有关各个用户主目录的更多细节信息,包含其中的文件和子目录的总量:
bash
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格式化部分输出。