Linux shell - for
bash shell 提供了 for 命令,以允许创建遍历一系列值的循环。每次迭代都使用其中一个值来执行已定义好的一组命令。for 命令的基本格式如下:
for var in list
do
commands
done
需要提供用于迭代的一系列值作为 list 参数。指定这些值的方法不止一种。
在每次迭代中,变量 var 会包含列表中的当前值。第一次迭代使用列表中的第一个值,第二次迭代使用列表中的第二个值,以此类推,直到用完列表中的所有值。
do 语句和 done 语句之间的 commands 可以是一个或多个标准的 bash shell 命令。在这些命令中,$var 变量包含着此次迭代对应的列表中的当前值。
for test in Alabama Alaska Arizona Arkansas California Colorado
do
echo The next state is $test
done
# 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 脚本的剩余部分依然有效。它会一直保持最后一次迭代时的值(除非做了修改):
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"
# 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
$test 变量保持着它的值,也允许我们对其做出修改,在 for 循环之外跟其他变量一样使用。
在 shell 脚本中经常遇到的情况是,你将一系列值集中保存在了一个变量中,然后需要遍历该变量中的整个值列表。可以通过 for 命令完成这个任务:
list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut"
for state in $list
do
echo "Have you ever visited $state?"
done
# 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 命令中使用该命令的输出:
file="states.txt"
for state in $(cat $file)
do
echo "Visit beautiful $state"
done
# 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
最后,还可以用 for 命令来自动遍历目录中的文件。为此,必须在文件名或路径名中使用通配符,这会强制 shell 使用文件名通配符匹配(file globbing)。文件名通配符匹配是生成与指定通配符匹配的文件名或路径名的过程。
在处理目录中的文件时,如果不知道所有的文件名,上述特性是非常好用的:
for file in /home/roger/test/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done
# /home/roger/test/dir1 is a directory
# /home/roger/test/myprog.c is a file
# /home/roger/test/myprog is a file
# /home/roger/test/myscript is a file
# /home/roger/test/newdir is a directory
# /home/roger/test/newfile is a file
# /home/roger/test/newfile2 is a file
# /home/roger/test/testdir is a directory
# /home/roger/test/testing is a file
# /home/roger/test/testprog is a file
# /home/roger/test/testprog.c is a file
注意,我们在这个例子的 if 语句测试中做了一些不同的处理:
if [ -d "$file" ]
在 Linux 中,目录名和文件名中包含空格是完全合法的。要应对这种情况,应该将$file 变量放入双引号内。否则,遇到含有空格的目录名或文件名时会产生错误。
也可以在 for 命令中列出多个目录通配符:
for file in /home/roger/.b* /home/roger/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
# /home/roger/.backup.timestamp is a file
# /home/roger/.bash_history is a file
# /home/roger/.bash_logout is a file
# /home/roger/.bash_profile is a file
# /home/roger/.bashrc is a file
# /home/roger/badtest doesn't exist
for 语句首先遍历了由文件名通配符匹配生成的文件列表,然后遍历了列表中的下一个文件。你可以将任意多的通配符放进列表中。
注意,可以在值列表中放入任何东西。即使文件或目录不存在,for 语句也会尝试把列表处理完。如果是和文件或目录打交道,那就要出问题了。你无法知道正在遍历的目录是否存在:最好在处理之前先测试一下文件或目录。
C 语言风格的 for 命令(了解)
如果你从事过 C 语言编程,那么可能会惊讶于 bash shell 中 for 命令的工作方式。在 C 语言中,for 循环通常会定义一个变量,然后在每次迭代时更改。程序员通常会将这个变量用作计数器,每次迭代时让计数器增 1 或减 1。bash 的 for 命令也提供了这个功能。
for (( i=1; i <= 10; i++ ))
do
echo "The next number is $i"
done
# 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
仿 C 语言的 for 命令也允许为迭代使用多个变量。循环会单独处理每个变量,可以为每个变量定义不同的迭代过程。尽管可以使用多个变量,但只能在 for 循环中定义一种迭代条件:
for (( a=1, b=10; a <= 10; a++, b-- ))
do
echo "$a - $b"
done
# 1 - 10
# 2 - 9
# 3 - 8
# 4 - 7
# 5 - 6
# 6 - 5
# 7 - 4
# 8 - 3
# 9 - 2
# 10 - 1
循环语句可以在循环内使用任意类型的命令,包括其他循环命令,这称为嵌套循环。注意,在使用嵌套循环时是在迭代中再进行迭代,命令运行的次数是乘积关系。不注意这点有可能会在脚本中造成问题。
for (( a = 1; a <= 3; a++ ))
do
echo "Starting loop $a:"
for (( b = 1; b <= 3; b++ ))
do
echo " Inside loop: $b"
done
done
# 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
循环处理文件数据
如果经常需要遍历文件中保存的数据,需要综合运用两种技术。
- 使用嵌套循环。
- 修改 IFS 环境变量。
通过修改 IFS 环境变量,能强制 for 命令将文件中的每一行都作为单独的条目来处理,即便数据中有空格也是如此。从文件中提取出单独的行后,可能还得使用循环来提取行中的数据。
典型的例子是处理/etc/passwd 文件。这要求你逐行遍历该文件,将 IFS 变量的值改成冒号,以便分隔开每行中的各个字段。
具体做法如下:
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
# Values in roger:x:1000:1000:roger:/home/roger:/usr/bin/zsh -
# roger
# x
# 1000
# 1000
# roger
# /home/roger
# /usr/bin/zsh
这个脚本使用了两个不同的 IFS 值来解析数据。第一个 IFS 值解析出/etc/passwd 文件中的各行。内层 for 循环接着将 IFS 的值修改为冒号,以便解析出/etc/passwd 文件各行中的字段。
循环控制
你可能认为循环一旦启动,在结束之前就哪都去不了了。事实并非如此。有两个命令可以控制循环的结束时机。
- break 命令
- continue 命令
break
break 命令是退出循环的一种简单方法。你可以用 break 命令退出任意类型的循环,包括 while 循环和 until 循环。
跳出单个循环
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"
# Iteration number: 1
# Iteration number: 2
# Iteration number: 3
# Iteration number: 4
# The for loop is completed
for 循环通常会遍历列表中的所有值。但当满足 if-then 的条件时,shell 会执行 break 命令,结束 for 循环。
跳出内层循环
在处理多个循环时,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
# 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 命令就会停止下一级的外层循环:
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
# Outer loop: 1
# Inner loop: 1
# Inner loop: 2
# Inner loop: 3
# Inner loop: 4
注意,当 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
# 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 的条件不成立时,一切会恢复如常。
和 break 命令一样,continue 命令也允许通过命令行参数指定要继续执行哪一级循环:
continue n
其中 n 定义了要继续的循环层级。
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
# 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
其中的 if-then 语句:
if [ $a -gt 2 ] && [ $a -lt 4 ]
then
continue 2
fi
会使用 continue 命令停止处理循环内的命令,但会继续处理外层循环。注意值为 3 那次迭代并没有处理任何内部循环语句,因为尽管 continue 命令停止了处理,但外层循环依然会继续。
处理循环的输出
在 shell 脚本中,可以对循环的输出使用管道或进行重定向。这可以通过在 done 命令之后添加一个处理命令来实现:
for file in /home/roger/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
else
echo "$file is a file"
fi
done > output.txt
shell 会将 for 命令的结果重定向至文件 output.txt,而不再显示在屏幕上。
考虑下面这个将 for 命令的输出重定向至文件的例子:
for (( a = 1; a < 10; a++ ))
do
echo "The number is $a"
done > test.txt
echo "The command is finished."
# The command is finished.
shell 创建了文件 test.txt 并将 for 命令的输出重定向至该文件。for 命令结束之后,shell 一如往常地显示了 echo 语句。
这种方法同样适用于将循环的结果传输到另一个命令:
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"
# 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
for 命令的输出通过管道传给了 sort 命令,由后者对输出结果进行排序。运行该脚本,可以看出结果已经按 state 的值排好序了。
实战演练
查找可执行文件
当你在命令行中运行程序的时候,Linux 系统会搜索一系列目录来查找对应的文件。这些目录是在环境变量 PATH 中定义的。如果想找出系统中有哪些可执行文件可供使用,只需扫描 PATH 环境变量中所有的目录即可。但是要徒手查找的话,可就得花点儿时间了。不过可以编写一个小小的脚本,轻而易举地搞定这件事。
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 命令,可以将需要添加的新用户账户放在一个文本文件中,然后创建一个简单的脚本来进行处理。这个文本文件的格式如下:
loginname, name
第一项是为新用户账户所选用的用户 id。第二项是用户的全名。两个值之间以逗号分隔,这样就形成了一种叫作 CSV(comma-separated value,逗号分隔值)的文件格式。这种文件格式在电子表格中极其常见,所以你可以轻松地在电子表格程序中创建用户账户列表,然后将其保存成 CSV 格式,供 shell 脚本读取和处理。
要读取文件中的数据,就得用上一点儿 shell 脚本编程技巧。我们将 IFS 分隔符设置成逗号,并将其作为 while 语句的条件测试部分。然后使用 read 命令读取文件中的各行。实现代码如下所示:
while IFS=',' read -r userid name
read 命令会自动移往 CSV 文本文件的下一行,因此就无须专门再写一个循环了。当 read 命令返回假值的时候(也就是读取完整个文件),while 命令就会退出。
要想把数据从文件中传入 while 命令,只需在 while 命令尾部使用一个重定向符即可。
将各部分组合成脚本:
input="users.csv"
while IFS=',' read -r loginname name
do
echo "adding $loginname"
useradd -c "$name" -m $loginname
done < "$input"