本篇内容均摘自《Linux命令行与shell脚本编程大全》,个人认为需要重点学习的章节。【免费】Linux命令行与Shell脚本编程大全 第3版 PDF全本 21MB 百度网盘下载 - 今夕是何夕 - 博客园
重复执行一系列命令在编程中很常见。通常你需要重复一组命令直至达到某个特定条件,比如处理某个目录下的所有文件、系统上的所有用户或是某个文本文件中的所有行。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命令自身所定义的一系列值。
$ cat test1
#!/bin/bash
for test in Alabama Alaska Arizona Arkansas California Colorado
do
echo The next state is $test
done
$ ./test1
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脚本的剩余部分一直保持有效。它会一直保持最后一次迭代的值(除非你修改了它)。
$ cat test1b
#!/bin/bash
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"
$ ./test1b
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脚本程序员带来麻烦的典型例子:
$ cat badtest1
#!/bin/bash
for test in I don't know if this'll work
do
echo "word:$test"
done
$ ./badtest1
word:I
word:dont know if thisll
word:work
shell看到了列表值中的单引号并尝试使用它们来定义一个单独的数据值,有两种办法可解决这个问题:
1.使用转义字符(反斜线)来将单引号转义;
2.使用双引号来定义用到单引号的值。
$ cat test2
#!/bin/bash
for test in I don\'t know if "this'll" work
do
echo "word:$test"
done
$ ./test2
word:I
word:don't
word:know
word:if
word:this'll
word:work
记住for循环假定每个值都是用空格分割的。如果有包含空格的数据值,你就陷入麻烦了。
$ cat badtest2
#!/bin/bash
for test in Nevada New Hampshire New Mexico New York North Carolina
do
echo "Now going to $test"
done
$ ./badtest1
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命令用空格来划分列表中的每个值。如果在单独的数据值中有空格,就必须用双引号将这些值圈起来。
$ cat test3
#!/bin/bash
for test in Nevada "New Hampshire" "New Mexico" "New York"
do
echo "Now going to $test"
done
$ ./test3
Now going to Nevada
Now going to New Hampshire
Now going to New Mexico
Now going to New York
现在for命令可以正确区分不同值了。另外要注意的是,在某个值两边使用双引号时, shell并不会将双引号当成值的一部分。从变量读取列表通常shell脚本遇到的情况是,你将一系列值都集中存储在了一个变量中,然后需要遍历变量中的整个列表。也可以通过for命令完成这个任务。
$ cat test4
#!/bin/bash
list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut"
for state in $list
do
echo "Have you ever visited $state?"
done
$ ./test4
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命令中使用该命令的输出。
$ cat test5
#!/bin/bash
file="states"
for state in $(cat $file)
do
echo "Visit beautiful $state"
done
$ cat states
Alabama
Alaska
Arizona
Arkansas
Colorado
Connecticut
Delaware
Florida
Georgia
$ ./test5
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
这个例子在命令替换中使用了cat命令来输出文件states的内容。你会注意到states文件中每一行有一个州,而不是通过空格分隔的。 for命令仍然以每次一行的方式遍历了cat命令的输出,假定每个州都是在单独的一行上。但这并没有解决数据中有空格的问题。如果你列出了一个名字中有空格的州, for命令仍然会将每个单词当作单独的值。说明: test5的代码范例将文件名赋给变量,文件名中没有加入路径。这要求文件和脚本位于同一个目录中。如果不是的话,你需要使用全路径名(不管是绝对路径还是相对路径)来引用文件位置。
更改字段分隔符
造成这个问题的原因是特殊的环境变量IFS,叫作内部字段分隔符( internal field separator)。IFS环境变量定义了bash shell用作字段分隔符的一系列字符。默认情况下, bash shell会将下列字符当作字段分隔符:空格,制表符(\t),换行符(\n)。如果bash shell在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。在处理可能含有空格的数据(比如文件名)时,这会非常麻烦,就像你在上一个脚本示例中看到的。要解决这个问题,可以在shell脚本中临时更改IFS环境变量的值来限制被bash shell当作字段分隔符的字符。例如,如果你想修改IFS的值,使其只能识别换行符,那就必须这么做:
IFS=$'\n'
将这个语句加入到脚本中,告诉bash shell在数据值中忽略空格和制表符。对前一个脚本使用这种方法,将获得如下输出。
$ cat test5b
#!/bin/bash
file="states"
IFS=$'\n'
for state in $(cat $file)
do
echo "Visit beautiful $state"
done
$ ./test5b
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脚本就能够使用列表中含有空格的值了。
警告:在处理代码量较大的脚本时,可能在一个地方需要修改IFS的值,然后忽略这次修改,在脚本的其他地方继续沿用IFS的默认值。一个可参考的安全实践是在改变IFS之前保存原来的IFS值,之后再恢复它。这种技术可以这样实现:
IFS.OLD=$IFS
IFS=$'\n' #在代码中使用新的IFS值
IFS=$IFS.OLD
这就保证了在脚本的后续操作中使用的是IFS的默认值。
还有其他一些IFS环境变量的绝妙用法。假定你要遍历一个文件中用冒号分隔的值(比如在/etc/passwd文件中)。你要做的就是将IFS的值设为冒号。
IFS=:
如果要指定多个IFS字符,只要将它们在赋值行串起来就行。
IFS=$'\n':;"
这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。如何使用IFS字符解析数据没有任何限制。
用通配符读取目录
最后,可以用for命令来自动遍历目录中的文件。进行此操作时,必须在文件名或路径名中使用通配符。它会强制shell使用文件扩展匹配。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程。如果不知道所有的文件名,这个特性在处理目录中的文件时就非常好用。
$ cat test6
#!/bin/bash
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
$ ./test6
/home/rich/test/dir1 is a directory
/home/rich/test/myprog.c is a file
/home/rich/test/myprog is a file
/home/rich/test/myscript is a file
/home/rich/test/newdir is a directory
/home/rich/test/newfile is a file
/home/rich/test/newfile2 is a file
/home/rich/test/testdir is a directory
/home/rich/test/testing is a file
/home/rich/test/testprog is a file
/home/rich/test/testprog.c is a file
for命令会遍历/home/rich/test/*输出的结果。该代码用test命令测试了每个条目(使用方括号方法),以查看它是目录(通过-d参数)还是文件(通过-f参数)。
注意:我们在这个例子的if语句中做了一些不同的处理:
if [ -d "$file" ]
在Linux中,目录名和文件名中包含空格当然是合法的。要适应这种情况,应该将$file变量用双引号圈起来。如果不这么做,遇到含有空格的目录名或文件名时就会有错误产生。
./test6: line 6: [: too many arguments
./test6: line 9: [: too many arguments
在test命令中, bash shell会将额外的单词当作参数,进而造成错误。
也可以在for命令中列出多个目录通配符,将目录查找和列表合并进同一个for语句。
$ cat test7
#!/bin/bash
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
$ ./test7
/home/rich/.backup.timestamp is a file
/home/rich/.bash_history is a file
/home/rich/.bash_logout is a file
/home/rich/.bash_profile is a file
/home/rich/.bashrc is a file
/home/rich/badtest doesn't exist
for语句首先使用了文件扩展匹配来遍历通配符生成的文件列表,然后它会遍历列表中的下一个文件。可以将任意多的通配符放进列表中。