shell教程
OutOfMemory.CN技术专栏-> shell-> shell教程-> 第十章:循环和分支

第十章:循环和分支

对代码块的操作是构造组织shell脚本的关键.循环和分支结构为脚本编程提供了操作代码块的工具.循环就是重复一些命令的代码块,如果条件不满足就退出循环.forloopsforargin[list]这是一个基本的循环结构.它与C的for结构有很大不同.forargin[list]docommand(s)...done在循环的每次执行中,arg将顺序的存取list中列出的变量..forargin"$va

对代码块的操作是构造组织shell脚本的关键. 循环和分支结构为脚本编程提供了操作代码块的工具.

循环就是重复一些命令的代码块,如果条件不满足就退出循环.

for loops

for arg in [list]

这是一个基本的循环结构.它与C的for结构有很大不同.

for arg in [list]
do
   command(s)...
done

在循环的每次执行中,arg将顺序的存取list中列出的变量..

for arg in "$var1" "$var2" "$var3" ... "$varN"
# 在第1次循环中, arg = $var1
# 在第2次循环中, arg = $var2
# 在第3次循环中, arg = $var3
# ...
# 在第N次循环中, arg = $varN

# 在[list]中的参数加上双引号是为了防止单词被不合理地分割.

list中的参数允许包含通配符.

如果do和for想在同一行出现,那么在它们之间需要添加一个";".

for arg in [list] ; do


例子 10-1. 循环的一个简单例子

#!/bin/bash
# 列出所有的行星名称.

for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
do
  echo $planet  # 每个行星被单独打印在一行上.
done

echo

for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
# 所有的行星名称打印在同一行上.
# 整个'list'只有一个变量,用""封成一个变量.
do
  echo $planet
done

exit 0

每个[list]中的元素都可能包含多个参数.在处理参数组时,这是非常有用的.在这种情况下,使用set命令(见例子 11-15)来强制解析每个[list]中的元素,并且分配每个解析出来的部分到一个位置参数中.


例子 10-2. 每个[list]元素带两个参数的for循环

#!/bin/bash
# 再访行星.

# 分配行星的名字和它距太阳的距离.

for planet in "Mercury 36" "Venus 67" "Earth 93"  "Mars 142" "Jupiter 483"
do
  set -- $planet  # Parses variable "planet" and sets positional parameters.
  # "--" 将防止$planet为空,或者是以一个破折号开头.

  # 可能需要保存原始的位置参数,因为它们被覆盖了.
  # 一种方法就是使用数组,
  #        original_params=("$@")

  echo "$1		$2,000,000 miles from the sun"
  #-------two  tabs---把后边的0和$2连接起来
done

# (Thanks, S.C., for additional clarification.)

exit 0

可以在for循环中的[list]位置放入一个变量.


例子 10-3. 文件信息: 对包含在变量中的文件列表进行操作

#!/bin/bash
# fileinfo.sh

FILES="/usr/sbin/accept
/usr/sbin/pwck
/usr/sbin/chroot
/usr/bin/fakefile
/sbin/badblocks
/sbin/ypbind"     # 你关心的文件列表.
                  # 扔进去一个假文件, /usr/bin/fakefile.

echo

for file in $FILES
do

  if [ ! -e "$file" ]       # 检查文件是否存在.
  then
    echo "$file does not exist."; echo
    continue                # 继续下一个.
   fi

  ls -l $file | awk '{ print $9 "         file size: " $5 }'  # 打印2个域.
  whatis `basename $file`   # 文件信息.
  # 注意whatis数据库需要提前建立好.
  # 要想达到这个目的, 以root身份运行/usr/bin/makewhatis.
  echo
done

exit 0

如果在for循环的[list]中有通配符(*和?),那将会产生文件名扩展,也就是文件名扩展(globbing).


例子 10-4. 在for循环中操作文件

#!/bin/bash
# list-glob.sh:  在for循环中使用文件名扩展产生 [list]

echo

for file in *
#           ^  在表达式中识别文件扩展符时,
#+             Bash 将执行文件名扩展.
do
  ls -l "$file"  # Lists all files in $PWD (current directory).
  #  回想一下,通配符"*"能够匹配所有文件,
  #+ 然而,在"文件扩展符"中,是不能匹配"."文件的.

  #  如果没匹配到任何文件,那它将扩展成自己
  #  为了不让这种情况发生,那就设置nullglob选项
  #+   (shopt -s nullglob).
  #  Thanks, S.C.
done

echo; echo

for file in [jx]*
do
  rm -f $file    # 只删除当前目录下以"j"或"x"开头的文件.
  echo "Removed file \"$file\"".
done

echo

exit 0

在一个for循环中忽略in [list]部分的话,将会使循环操作$@(从命令行传递给脚本的参数列表).一个非常好的例子,见例子 A-16.


例子 10-5. 在for循环中省略in [list]

#!/bin/bash

#  使用两种方法来调用这个脚本,一种是带参数的情况,另一种不带参数.
#+ 观察此脚本的行为各是什么样的?

for a
do
 echo -n "$a "
done

#  没有[list],所以循环将操作'$@'
#+ (包括空白的命令参数列表).

echo

exit 0

也可以使用命令替换(command substitution)来产生for循环的[list].具体见例子 12-49, 例子 10-10例子 12-43.


例子10-6. 使用命令替换来产生for循环的[list]

#!/bin/bash
#  for-loopcmd.sh: 带[list]的for循环
#+ [list]是由命令替换产生的.

NUMBERS="9 7 3 8 37.53"

for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
do
  echo -n "$number "
done

echo
exit 0

下边是一个用命令替换来产生[list]的更复杂的例子.


例子 10-7. 对于二进制文件的grep替换

#!/bin/bash
# bin-grep.sh: 在一个二进制文件中定位匹配字串.

# 对于二进制文件的一个grep替换
# 与"grep -a"的效果相似

E_BADARGS=65
E_NOFILE=66

if [ $# -ne 2 ]
then
  echo "Usage: `basename $0` search_string filename"
  exit $E_BADARGS
fi

if [ ! -f "$2" ]
then
  echo "File \"$2\" does not exist."
  exit $E_NOFILE
fi


IFS="\n"         # 由Paulo Marcel Coelho Aragao提出的建议.
for word in $( strings "$2" | grep "$1" )
# "strings" 命令列出二进制文件中的所有字符串.
# 输出到管道交给"grep",然后由grep命令来过滤字符串.
do
  echo $word
done

# S.C. 指出, 行23 - 29 可以被下边的这行来代替,
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'


# 试试用"./bin-grep.sh mem /bin/ls"来运行这个脚本.

exit 0

大部分相同.


例子 10-8. 列出系统上的所有用户

#!/bin/bash
# userlist.sh

PASSWORD_FILE=/etc/passwd
n=1           # User number

for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
# 域分隔   = :           ^^^^^^
# 打印出第一个域                 ^^^^^^^^
# 从password文件中取得输入                    ^^^^^^^^^^^^^^^^^
do
  echo "USER #$n = $name"
  let "n += 1"
done


# USER #1 = root
# USER #2 = bin
# USER #3 = daemon
# ...
# USER #30 = bozo

exit 0

#  练习:
#  --------
#  一个普通用户(或者是一个普通用户运行的脚本)
#+ 怎么能读取/etc/password呢?
#  这是否是一个安全漏洞? 为什么是?为什么不是?

关于用命令替换来产生[list]的最后的例子.


例子 10-9. 在目录的所有文件中查找源字串

#!/bin/bash
# findstring.sh:
# 在一个指定目录的所有文件中查找一个特定的字符串.

directory=/usr/bin/
fstring="Free Software Foundation"  # 查看那个文件中包含FSF.

for file in $( find $directory -type f -name '*' | sort )
do
  strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
  #  在"sed"表达式中,
  #+ 我们必须替换掉正常的替换分隔符"/",
  #+ 因为"/"碰巧是我们需要过滤的字串之一.
  #  如果不用"%"代替"/"作为分隔符,那么这个操作将失败,并给出一个错误消息.(试试)
done

exit 0

#  练习 (容易):
#  ---------------
#  将内部用的$directory和$fstring变量,用从
#+ 命令行参数代替.

for循环的输出也可以通过管道传递到一个或多个命令中.


例子 10-10. 列出目录中所有的符号连接(symbolic links)

#!/bin/bash
# symlinks.sh: 列出目录中所有的符号连接文件.


directory=${1-`pwd`}
#  如果没有其他的特殊指定,
#+ 默认为当前工作目录.
#  下边的代码块,和上边这句等价.
# ----------------------------------------------------------
# ARGS=1                 # 需要一个命令行参数.
#
# if [ $# -ne "$ARGS" ]  # 如果不是一个参数的话...
# then
#   directory=`pwd`      # 当前工作目录
# else
#   directory=$1
# fi
# ----------------------------------------------------------

echo "symbolic links in directory \"$directory\""

for file in "$( find $directory -type l )"   # -type l 就是符号连接文件
do
  echo "$file"
done | sort                                  # 否则列出的文件将是未排序的
#  严格上说,此处并不一定非要一个循环不可,
#+ 因为"find"命令的结果将被扩展成一个单词.
#  然而,这种方式很容易理解和说明.

#  Dominik 'Aeneas' Schnitzer 指出,
#+ 如果没将 $( find $directory -type l )用""引用起来的话
#+ 那么将会把一个带有空白部分的文件名拆成以空白分隔的两部分(文件名中允许有空白).
#  即使这只将取出每个参数的第一个域.

exit 0


# Jean Helou 建议使用下边的方法:

echo "symbolic links in directory \"$directory\""
# 当前IFS的备份.要小心使用这个值.
OLDIFS=$IFS
IFS=:

for file in $(find $directory -type l -printf "%p$IFS")
do     #                              ^^^^^^^^^^^^^^^^
       echo "$file"
       done|sort

循环的输出可以重定向到文件中,我们对上边的例子做了一点修改.


例子 10-11. 将目录中的符号连接文件名保存到一个文件中

#!/bin/bash
# symlinks.sh: 列出目录中所有的符号连接文件.

OUTFILE=symlinks.list                         # 保存的文件

directory=${1-`pwd`}
#  如果没有其他的特殊指定,
#+ 默认为当前工作目录.


echo "symbolic links in directory \"$directory\"" > "$OUTFILE"
echo "---------------------------" >> "$OUTFILE"

for file in "$( find $directory -type l )"    # -type l 为寻找类型为符号链接的文件
do
  echo "$file"
done | sort >> "$OUTFILE"                     # 循环的输出
#           ^^^^^^^^^^^^^                       重定向到一个文件中.

exit 0

有一种非常像C语言的for循环的语法形式.这需要使用(()).


例子 10-12. 一个C风格的for循环

#!/bin/bash
# 两种循环到10的方法.

echo

# 标准语法.
for a in 1 2 3 4 5 6 7 8 9 10
do
  echo -n "$a "
done

echo; echo

# +==========================================+

# 现在, 让我们用C风格的语法做同样的事.

LIMIT=10

for ((a=1; a <= LIMIT ; a++))  # 双圆括号, 并且"LIMIT"变量前边没有 "$".
do
  echo -n "$a "
done                           # 这是一个借用'ksh93'的结构.

echo; echo

# +=========================================================================+

# 让我们使用C的逗号操作符,来同时增加两个变量的值.

for ((a=1, b=1; a <= LIMIT ; a++, b++))  # 逗号将同时进行2条操作.
do
  echo -n "$a-$b "
done

echo; echo

exit 0

参考例子 26-15, 例子 26-16, 和 例子 A-6.

---

现在来一个现实生活中使用的for循环.


例子 10-13. 在batch mode中使用efax

#!/bin/bash
# Faxing ('fax' 必须已经被安装过了).

EXPECTED_ARGS=2
E_BADARGS=65

if [ $# -ne $EXPECTED_ARGS ]
# 检查命令行参数的个数是否正确.
then
   echo "Usage: `basename $0` phone# text-file"
   exit $E_BADARGS
fi


if [ ! -f "$2" ]
then
  echo "File $2 is not a text file"
  exit $E_BADARGS
fi


fax make $2              # 从文本文件中创建传真格式的文件.

for file in $(ls $2.0*)  # 连接转换过的文件.
                         # 在变量列表中使用通配符.
do
  fil="$fil $file"
done

efax -d /dev/ttyS3 -o1 -t "T$1" $fil   # 干活的地方.


# S.C. 指出, 通过下边的命令可以省去for循环.
#    efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
# 但这并不十分有讲解意义[嘿嘿].

exit 0

while

这种结构在循环的开头判断条件是否满足,如果条件一直满足,那就一直循环下去(0为退出码[exit status]).与for 循环的区别是,这种结构适合用在循环次数未知的情况下.

while [condition]
do
  command...
done

和for循环一样,如果想把do和条件放到同一行上还是需要一个";".

while [condition] ; do

注意一下某种特定的while循环,比如getopts结构,好像和这里所介绍的模版有点脱节.


例子 10-14. 简单的while循环

#!/bin/bash

var0=0
LIMIT=10

while [ "$var0" -lt "$LIMIT" ]
do
  echo -n "$var0 "        # -n 将会阻止产生新行.
  #             ^           空格,数字之间的分隔.

  var0=`expr $var0 + 1`   # var0=$(($var0+1))  也可以.
                          # var0=$((var0 + 1)) 也可以.
                          # let "var0 += 1"    也可以.
done                      # 使用其他的方法也行.

echo

exit 0


例子 10-15. 另一个while循环

#!/bin/bash

echo
                               # 等价于:
while [ "$var1" != "end" ]     # while test "$var1" != "end"
do
  echo "Input variable #1 (end to exit) "
  read var1                    # 为什么不使用'read $var1'?
  echo "variable #1 = $var1"   # 因为包含"#"字符,所以需要"" . . .
  # 如果输入为'end',那么就在这里打印.
  # 不在这里判断结束,在循环顶判断.
  echo
done

exit 0

一个while循环可以有多个判断条件,但是只有最后一个才能决定是否退出循环.然而这需要一种有点不同的循环语法.


例子 10-16. 多条件的while循环

#!/bin/bash

var1=unset
previous=$var1

while echo "previous-variable = $previous"
      echo
      previous=$var1
      [ "$var1" != end ] # 记录之前的$var1.
      # 这个"while"循环中有4个条件, 但是只有最后一个能控制循环.
      # 退出状态由第4个条件决定.
do
echo "Input variable #1 (end to exit) "
  read var1
  echo "variable #1 = $var1"
done

# 尝试理解这个脚本的运行过程.
# 这里还是有点小技巧的.

exit 0

与for循环一样,while循环也可通过(())来使用C风格语法.(见例子 9-30).


例子 10-17. C风格的while循环

#!/bin/bash
# wh-loopc.sh: 循环10次的while循环.

LIMIT=10
a=1

while [ "$a" -le $LIMIT ]
do
  echo -n "$a "
  let "a+=1"
done           # 到目前为止都没什么令人惊奇的地方.

echo; echo

# +=================================================================+

# 现在, 重复C风格的语法.

((a = 1))      # a=1
# 双圆括号允许赋值两边的空格,就像C语言一样.

while (( a <= LIMIT ))   # 双圆括号, 变量前边没有"$".
do
  echo -n "$a "
  ((a += 1))   # let "a+=1"
  # Yes, 看到了吧.
  # 双圆括号允许像C风格的语法一样增加变量的值.
done

echo

# 现在,C程序员可以在Bash中找到回家的感觉了吧.

exit 0

while循环的stdin可以用<来重定向到文件.

whild循环的stdin支持管道.

until

这个结构在循环的顶部判断条件,并且如果条件一直为false那就一直循环下去.(与while相反).

until [condition-is-true]
do
  command...
done

注意: until循环的判断在循环的顶部,这与某些编程语言是不同的.

与for循环一样,如果想把do和条件放在一行里,就使用";".

until [condition-is-true] ; do


例子 10-18. until循环

#!/bin/bash

END_CONDITION=end

until [ "$var1" = "$END_CONDITION" ]
# 在循环的顶部判断条件.
do
  echo "Input variable #1 "
  echo "($END_CONDITION to exit)"
  read var1
  echo "variable #1 = $var1"
  echo
done

exit 0

© 内存溢出 OutOfMemory.CN