shell结构化命令——for命令

重复执行一系列命令在编程中很常见。你经常需要重复多个命令直至达到某个特定条件,比如处理目录下的所有文件、系统中的所有用户或是文本文件中的所有行。

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

for var in list

do

        commands

done

你需要提供用于迭代的一系列值作为list参数。指定这些值的方法不止一种。

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

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

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

前面提到过,有几种方法可以指定列表中的值。下面将介绍这些方法。

读取列表中的值 

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

$ cat test1
#!/bin/bash
# basic for command

for test in Alabama Alaska Arizona Arkansaa 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
# testing the for variable after the looping
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循环之外跟其他变量一样使用。

读取列表中的复杂值 

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

$ cat badtest1
#!/bin/bash
# another example of how not to use the for command

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看到了列表值中的单引号并尝试使用它们来定义一个单独的数据值,这下着实把事情搞得一团糟。

有两种方法可以解决这个问题。

  • 使用转义字符(反斜线)将单引号转义。
  • 使用双引号来定义含有单引号的值。

两种解决方法并没有什么出奇之处,但都能解决这个问题:

$ cat test2
#!/bin/bash
# solutions to the quote problem

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

在第一个有问题的地方,添加了反斜线来转义don't中的单引号。在第二个有问题的地方,将this'll放在了双引号内。两种方法都管用。

你可能遇到的另一个问题是多单词值(multiword value)。记住,for循环假定各个值之间是以空格(也可以是制表符或换行符)分隔的。如果某个值含有空格,那可就又麻烦了:

$ cat badtest2
#!/bin/bash
# another example of how not to use the for command

for test in Nevada New Hampshire New Mexico New York North Carolina
do
    echo "Now going to $test"
done
$ ./badtest2
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
# an example of how to properly define values

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 Hampashire
Now going to New Mexico
Now going to New York

现在for命令可以正确地区分不同值了。另外要注意的是,当你使用双引号引用某个值时,shell并不会将双引号当成值的一部分。

从变量中读取值列表 

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

$ cat test4
#!/bin/bash
# using a variable to hold the list

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?
...

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

从命令中读取值列表 

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

$ cat test5
#!/bin/bash
# reading values from a file

file="states.txt"

for state in $(cat $file)
do 
    echo "Visit beautiful $state"
done
$ cat states.txt
Alabama
Alaska
Arizona
Arkansas
Colorado
Connecticut
Delaware
Georgia
$ ./test5
Visit beautiful Alabama
...

这个例子在命令替换中使用cat命令来输出文件states.txt的内容。注意,states.txt文件中每个值各占一行,而不是以空格分隔。for命令仍然以每次一行的方式遍历cat命令的输出。但这并没有解决数据中含有空格的问题。如果你列出了一个名字中有空格的州,则for命令仍然会用空格来分隔值。具体原理接下来会介绍。

注意        test5的代码示例将不包含路径的文件名赋给了变量。这要求文件和脚本位于同一个目录中。如果并非如此,则需要使用完整路径名(不管是绝对路径还是相对路径)来引用文件位置。

更改字段分隔符 

造成这个问题的原因是特殊的环境变量IFS(internal field separator,内部字段分隔符)。IFS环境变量定义了bash shell用作字段分隔符的一系列字符。在默认情况下,bash shell会将下列字符视为字段分隔符。

  • 空格
  • 制表符
  • 换行符

如果bash shell在数据中看到了这些字符中的任意一个,那么它就会认为这是列表中的一个新字段的开始。在处理可能含有空格的数据(比如文件名)时,这就很烦人了,就像你在先前脚本示例中看到的那样。

解决这个问题的办法是在shell脚本中临时更改IFS环境变量的值来限制被bash shell视为字段分隔符的字符。如果想修改IFS的值,使其只能识别换行符,可以这么做:

IFS=$'\n'

将该语句加入脚本,告诉bash shell忽略数据中的空格和制表符。对前一个脚本使用这种方法,将获得如下输出:

$ cat test5b
#!/bin/bash
# reading values from a file

file="states.txt"

IFS=$'\n'
for state in $(cat $file)
do 
    echo "Visit beautiful $state"
done
$ ./test5b
Visit beautiful Alabama
...
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使用文件名通配符匹配(file globbing)。文件名通配符匹配是生成与指定通配符匹配的文件名或路径名的过程。

在处理目录中的文件时,如果不知道所有的文件名,上述特性是非常好用的:

$ cat test6
#!/bin/bash
# iterate through all the files in a directory

for file in /home/rich/test/*
do
    if [ -d "$file" ]
    then
        echo "$file is a directory"
    elis [ -f "$file" ]
    then
        echo "$file is a file"
    fi
done
$ ./test6
/home/rich/test/dir1 is a directory
...

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命令中列出多个目录通配符:

$ cat test7
#!/bin/bash
# iterating through multiple directries

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
...

for语句首先遍历了由文件名通配符匹配生成的文件列表,然后遍历了列表中的下一个文件。你可以将任意多的通配符放进列表中。

警告        注意,可以在值列表中放入任何东西。即使文件或目录不存在,for语句也会尝试把列表处理完。如果是和文件或目录打交道,那就要出问题了。你无法知道正在遍历的目录是否存在:最好在处理之前先测试一下文件或目录。 

 

 

 

 

 

  • 64
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值