前言
本篇笔记的参考资料是(《Linux命令行与shell脚本编程大全》(第3版),外加百度辅助,本篇笔记的主要内容是循环语句,即for、while和until语句。
for命令
如果遇到这样的场景:重复一组命令直到某个特定条件,例如处理某个目录下的所有文件、系统上所有用户或是某个文本文件中的所有行。此时就会用到for
命令,for命令可以创建一个遍历一系列值的的循环,每次迭代就会使用其中一个值来执行已定义好的一组命令,for命令的格式如下所示:
|
|
格式解释:
第一,在list参数中,用户需要提供推荐中要用到的一系列值;
第二,在每次迭代时,变量var会包含列表中的当前值,第一次迭代使用列表中的第1个值,第2次使用第2个值,以此类推;
第三,在do
和done
语句之间输入的命令可以是一条或多条标准的bash sehll命令,在这些命令中,$var
变量包含着这次迭代对应的当前列表项的中值。
第1案例:读取列表中的值
for命令最基本的用法就是遍历for命令自身所定义的一系列值,如下所示:
|
|
运行结果如下所示:
|
|
每次for命令遍历值列表,它都会将列表中的下个值赋给$test
变量,$test
变量可以像for命令语句中的其他脚本变量一样使用,在最后一次迭代后,$test
变量的值会在shell脚本的剩余部分一直保持有效,它会一直保持最后一次迭代的值,除非再次为变量赋值,看下面的案例:
|
|
运行结果如下所示:
|
|
转义字符的使用
for中有可能会遇到比较复杂的值,如下所示:
|
|
结果运行如下:
|
|
从结果中可以看出,如果for的列表中存在单引号,则在迭代中会出错误,它会把单引号去掉,并把单引号中间的字符串当作是一个值,如果要解决这个问题,可以使用两种方法:第一,使用转义字符来将单引号转义;第二,使用双引号来定义用到的单引号的值。
现在用这两种方法来解决这个问题,第一个引用用第一种方法,第二个引号用第二种方法,如下所示:
|
|
运行结果如下所示:
|
|
从结果可以看出两个单引号都能正常工作了。
多个词的分割
在for循环中,每个值都是用空格进行分割的,如果某个值包含空格,就会比较麻烦,如下面的案例所示:
|
|
运行结果如下所示:
|
|
从结果来看有问题,如果想要把含有空格的数值正确地迭代出来,必须使用双引号将这些值给圈起来,如下所示:
|
|
运行结果如下所示:
|
|
从变量读取列表
在shell脚本中会遇到这样的情况:将一系列的值都储存在了一个变量中,然后需要遍历变量中的整个列表。这种情况下,可以使用for命令,如下所示:
|
|
运行结果如下所示:
|
|
代码解释,在代码中有一行这样的代码,即list=$list" Heilongjiang"
,它表示在原来的列表中添加一个元素。
从命令读取值
生成列表中所需要值的另外一个途径就是使用命令的输出,可以用命令替换来执行任何能产生输出的 命令,然后在for命令中使用该命令的输出,如下所示:
|
|
运行结果如下所示:
|
|
在这个案例中,使用了cat命令来输出文件province的内容,在province文件中,每一行有一个省,其中最后一行是China HongKong,中间有一个空格。然后在输出时,for命令把China HongKong分开了。这是因为,在bash shell中,空格,换行符,制表符都是分割符,因此会被分开。
更改字段分割符
for在迭代变量时,使用空格作为分割符,这是因为存在一个特殊的环境就是IFS,它的全称是internal field separator,中文名是内部字段分割符。IFS环境变量定义了bash shell用作字段分割符的一系列字符。默认情况下,bash shell会将以下字符当作是字段分割符,其中包括①空格;②制表符;③换行符。bash shell会将数据中的这些字符都当成分割符,因此在处理含有空格的数据(例如某些文件名)时非常麻烦。如果要解决这个问题,需要修改IFS的值,例如,想让bash shell只识别换行符,就当IFS更改为IFS=$'/n'
。将这个语句加入到脚本中,就会告诉bash shell在数据中忽略空格和制表符,还以上面的案例为基础,看一下这段代码。
|
|
运行后,结果如下所示:
|
|
现在结果一切正常。如果处理的代码量比较大时,可能在一个地方需要修改IFS的值,然后忽略这次悠,在脚本的其他地方继续没用IFS的默认值,这个时候需要在改变IFS之前保存原来的IFS值,之后再恢复它,这种做法习惯是这样的:
|
|
此外,还有一种情况,如果某个文件中的分割隔都是冒号(:),例如etc/passwd
文件中就是这样,此时也可以将IFS的值更改炒冒号,就像这样IFS=:
,如果需要指定多个IFS字符,只要将它们在赋值行中一并输入就行,例如IFS=$'\n':;"
,这个赋值就是将换行符、冒号、分号和双引号作为字段分隔符。
使用通配符读取目录
使用for命令可以自动遍历目录中的文件,进行操作时,必须要在文件名或路径名中使用通配符,它会强制shell使用文件扩展匹配,文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程,如果不清楚所有的文件名,这个特性在处理上当中的文件时很好用,如下所示。
|
|
运行后结果如下所示:
|
|
代码及结果解释:for命令遍历/home/biotest/test/*
输出的结果,此代码使用了test命令测试了每个目录,以查看它是目录(-d参数)还是文件(-f参数)。在上面的代码中,if [ -d "$file" ]
这里比较特殊,因此它加了引号,因为目录名和文件中包含空格非常常见,因此为了适应这种情况,需要将$file
变量用双引号圈起来,如果不圈起来,就会输出错误,如下所示:
现在在/home/biotest/test
目录中添加一个目录,命令为a b
(中间有空格),再把上面的代码中的if [ -d "$file" ]
的引号去掉,运行,如下所示:
|
|
这里就显示出错。因为在test命令方面,bash shell会将额外的单词当作参数,从而造成错误。
在for命令中可以列出多个目录通配符,将目录查找和列表合并到同一个for语句中,看下面的案例:
|
|
运行结果如下所示:
|
|
for语句使用了文件扩展匹配来遍历通配符生成的文件列表,然后它会遍历列表中的下一个文件。
C语言风格的for命令
简单的C风格for命令
在C语言中,它的for命令有一个用来指明变量的特定方法,一个必须保持成立才能继续迭代的条件。以及另一个在每个迭代中改变变量的方法,当指定的条件不成立时,for循环就会停止,条件等式通过标准的数学符号定义,例如下面的C语言代码:
|
|
这段代码会产生一个简单的迭代循环,其中变量i
作为计数器,第一部分将一个默认值赋给该变量,中间的部分定义了循环重复的条件。当定义的条件不成立时,for循环就停止迭代。最后一部分定义了迭代的过程,在每次迭代之后,最后一部分占定义的表达式会被执行,在此案例中,每迭代一次,i
变量就会增加1。
bash shell也支持一种for循环,与C语言的for循环类似,它的格式如下所示:
|
|
在bash shell的C语言风格的for命令中,它有一些特殊的地方:第一,变量赋值可以有空格;第二,条件中的变量不以美元符号开头;第三,迭代过程中的算式并使用expr命令格式。看下面的案例:
|
|
运行结果如下所示:
|
|
使用多个变量
C风格的for命令也可以迭代多个变量,循环会单独处理每个变量,用户可以为每个变量定义不同的迭代过程,虽然可以使用多个变量,但在for循环中只能定义一种条件,如下所示:
|
运行结果如下所示:
|
|
代码及结果解释:变量a和b分别用不同的值来初始化并定义了不同的迭代过程,循环的每次迭代在增加变量a的同时,减小了变量b。
while命令
while命令在某种程度上是if-then
和for
循环的混合体。while命令允许定义一个要测试的命令,然后循环执行一组命令,只要定义的测试命令返回的是退出状态码0。它会在每次迭代的一开始测试test命令。在test命令返回非零退出状态码时,while命令会停止执行那组命令。
while命令的基本使用
while命令的基本格式如下所示:
|
|
while命令中定义的test command和if-then语句中的格式一样。可以使用任何普通bash shell命令,或者用test命令进行条件测试,比如测试变量值。while命令的关键在于所指定的test command的退出状态码必须随着循环中运行的命令而改变,如果退出状态码不发生变化,while循环就将一直不停地进行下去。常用的test command的用法是用方括号来检查循环命令中用到的shell变量的值,如下所示:
|
|
运行结果如下所示:
|
|
在这个例子中,while命令定义了每次迭代时检查的测试条件,即whle [ $var1 -gt 0 ]
,只要测试条件成立,while
命令就会不停地循环执行定义好的命令,在这些命令中,测试条件中用到的变量必须修改,否则就会降入无限循环,在这个案例中,每次循环,变量值就会减一,即var1=[ $var1 -1 ]
。while循环会在测试条件不再成立时停止。
使用多个测试命令
while命令允许用户在while语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。如果在输入命令时不小心,会导致一些意外,如下面的案例所示:
|
|
运行结果如下所示:
|
|
代码及结果解释:第一个测试显示了var1变量的当前值,即while echo $var1
,第二个测试用方括号来判断var1变量的值,即[ $var1 -ge 0 ]
,在循环内部,echo语句会显示一条信息,说明循环被执行了,在最后,变量成为-1,这是因为while循环会在var1变量等于0时执行echo语句,然后将var1送去1,接下来再执行测试命令,用于下一次迭代,echo不测试命令被执行并显示了var变量的值,直到shell执行test测试命令,while循环才停止。这就说明在含有多个命令的while语句中,在每次迭代中所有的测试命令都会被执行,包括测试命令失败的最后一交迭代,需要注意这种用法。
until命令
until命令和while命令的工作方式完全相反,until命令要求你指定一个通常返回非零退出状态码的测试命令。只有油门工命令的退出状态码不为0,bash shell才会执行循环中列出的命令。一旦测试命令返回了退出状态码0,循环就结果了,until命令的基本格式如下所示:
until命令的基本格式
|
|
与while命令类似,在until命令中也可以放入多个测试命令,只有最后个命令的退出状态码决定了bash shell是否执行已经定义的other commands,如下所示:
|
|
运行结果如下所示:
|
|
代码及结果解释:由于变量var1来决定until循环什么时候停止,只要该变量的的值等于0,until命令就会停止循环。until与while类似,在使用多个测试命令时要注意,如下所示:
|
|
运行结果如下所示:
|
|
直到var1=0,程序运行结束。
嵌套循环
循环语句可以在循环内使用任意类型的命令,包括其他循环命令。这种循环叫嵌套循环(nested loop)。需要注意的是,在使用嵌套循环时,是在迭代中使用迭代,与命令运行次数的关系是乘积的关系,在写代码时尤其要注意这一点,看下面的一个案例。
for循环与for循环的嵌套
|
|
运行结果如下所示:
|
|
代码及结果解释:在嵌套循环中,在外部循环的每次迭代中遍历一次它所有的值,两个循环的do和done命令没有任何差别,bash shell知道当第一个node命令执行的是内部循环,而非外部循环。
while循环与for循环的嵌套
在while循环内也可以放置一个for循环,如下所示:
|
|
运行结果如下所示:
|
|
until循环与for循环的嵌套
案例如下所示:
|
|
运行结果如下所示:
|
|
循环处理文件数据
当我们遇到一种情况,即必须要遍历储存在文件中的数据时,需要两种技术。第一,使用嵌套循环;第二,修改IFS环境变量。通过修改IFS环境变量,就能强制for命令将文件中的每行都当成单独的一个条目来处理,即使数据中有空格也是如此,一旦从文件中提取出了单独的行,可能需要再次利用循环来提取行中的数据,典型的案例就是处理/etc/passwd
文件中的数据,这要求你逐行遍历/etc/passwd
文件,并将IFS变量的值改为冒号,这样就能分隔开每行中的各个数据段了,如下所示:
|
|
运行结果如下所示:
|
|
这个脚本使用了两个不同的FIS值来解析数据,第一个IFS值解析出/etc/passwd
文件中的单独的行,内部for循环接着将FIS的值修改为冒号,然后从单独的行中解析出单独的值,因此在第一次运行时,会提取出一行数据,例如root:x:0:0:root:/root:/bin/bash
,接头使用for循环,提取出这一行中以冒号为分割符的元素,即root x 0 0 root /root /bin/bash
信息。
控制循环
如果遇到这样的情况:一旦开启了循环,必须要等到所有的循环完成迭代时才结束,如果我想提前想结束循环的话,此时就要用到break
命令和continue
命令。
break命令
break
命令可以退出任意类型的循环,包括until
和while
循环。
break跳出单个循环
在shell中执行break命令,它会深度跳出当前正在执行的循环,如下所示:
|
|
结果如下所示:
|
|
for循环通常都会遍历列表中指定的所有值。但当满足if-then的条件时,shell会执行break命令,停止for循环。同样的方法也适用于while和until循环,如下所示::
|
|
运行结果如下所示:
|
|
跳出内部循环
在处理多个循环时,break命令会自动终止你所在的最内层的循环。
|
|
结果如下所示:
|
|
跳出外部循环
有时候需要在内部循环,但外部循环停止,break命令可以接受单个命令行参数值,break -n
,其中n指定了要跳出的循环层级,默认情况下,n为1,表明跳出的是当前的循环,即最内层的是第1层循环,如果设为2,break命令就会停止下一级的外部循环,如下所示:
|
|
运行结果如下所示:
|
|
当shell执行了break命令后,外部循环就停止了。
continue命令
continue命令可以提前中止某次循环中的命令,但并不会完全终止整个循环,可以在循环内部设置shell不执行命令的条件,这里有个for循环中使用continue命令的简单例子,如下所示:
|
|
结果如下所示:
|
|
代码解释:当if-then语句的条件满足时,即值大于5小于10,shell会 执行continue命令,跳过此次循环中的剩余命令,但整个循环还会继续,当if-then的条件不再满足后,又继续循环。在while和until循环中也可以使用continue命令,但要谨慎,因为当shell执行continue命令时,它会跳过剩余的部分,如果在其中的条件具条件里对测试条件变量进行增值,问题就会出现,如下所示:
|
|
运行结果如下所示:
|
|
从结果可以看了同,在if-then的条件成立之后,一切都正常,但是当执行了continue命令后,它就跳过了while循环中的剩余部分,则跳过的部分正好是$var1计数变量增值的部分,而这个变量又被用于个渭测试命令中,因此这个值就不再变化,从而连续输出。
continue也可以与break一样,使用n来指定要继续执行哪一级循环,格式为continue n
,如下所示:
|
|
结果如下所示:
代码解释,其中的if-then部分为:
|
|
在此处用continue命令来停止处理循环内的命令,但会继续处理外部循环,值为3的那次迭代并没有处理任何内部循环语句,因为尽管continue命令命令会停止了处理过程,但外部循环会继续。(有关break n
与continue n
的这个原理,再写一篇笔记,我是没弄清楚原理)。
处理循环输出
在shell脚本中,可以对循环的输出使用管道或重定向,这可以通过在done命令之后添加一个处理命令来实现。如下所示:
|
|
运行结果如下所示:
|
|
第2个案例,如下所示:
|
|
运行结果如下所示:
|
|
代码及结果解释:shell创建了文件test23.txt并将for命令的输出重定向到这个文件,shell在for命令之后正常显示了echo语句。这种方法也同样适用于将循环的结果用通道导入另一个命令。
|
|
运行结果如下所示:
|
|
代码及结果解释:province的值并没有在for命令列表中以特定次序列出,for命令的输出结果导入到了sort命令中,该命令会改变for命令输出结果的顺序。运行这个脚本实际上说明了结果已经在脚本内部排好序了。
最终案例
第1案例:查找可执行文件
如果要找出系统中有哪些可执行文件,只需要扫描PATH环境变量中的所有目录即可,下面是这样的一个脚本,如下所示
|
|
运行,结果如下所示:
|
|
第2案例:创建多个用户账户
如果要创建大量的用户,可以使用while循环来实现,将需要添加的新用户账户放在一个文本文件中,然后创建一个简单的脚本处理,文件文件的格式是这样的userid, user name
,第一个条目是新用户的ID,第二个条目是用户的全名。两个值之间使用逗号分隔,这样就形成卫 种名为逗号分隔值的文本文件(或是csv格式文件)。如果要读取这里面的数据,需要将IFS
分隔符设置为逗号,并将其放入while语句的条件测试部分。然后使用read命令读取文件中的各行,这一部分 的代码就是while IFS=',',read -r userid name
,read命令会自动读取.csv
文件的下一行内容,因此不需要创建一个循环,当read命令返回FALSE时(也就是读取完整个文件时),while命令就会退出,完整的脚本如下所示:
|
|
运行结果如下所示:
|
|
从结果可以看出,这3个账户已经生成。