shell编程之循环

循环是当循环控制条件为真时,一系列命令迭代执行的代码块。

1、for 循环

语法:

bash 复制代码
for arg in [list]

这是 shell 中最基本的循环结构,它与C语言形式的循环有着明显的不同。

bash 复制代码
for arg in [list]
do
  command(s)...
done

在循环的过程中,arg 会从 list 中连续获得每一个变量的值。

bash 复制代码
for arg in "$var1" "$var2" "$var3" ... "$varN"
# 第一次循环中,arg = $var1
# 第二次循环中,arg = $var2
# 第三次循环中,arg = $var3
# ...
# 第 N 次循环中,arg = $varN
# 为了防止可能的字符分割问题,[list] 中的参数都需要被引用。

参数 list 中允许含有 通配符。

如果 do 和 for 写在同一行时,需要在 list 之后加上一个分号。

bash 复制代码
for arg in [list] ; do

样例 11-1. 简单的 for 循环

bash 复制代码
#!/bin/bash
# 列出太阳系的所有行星。
for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
do
  echo $planet  # 每一行输出一个行星。
done
echo; echo
for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
    # 所有的行星都输出在一行上。
    # 整个 'list' 被包裹在引号中时是作为一个单一的变量。
    # 为什么?因为空格也是变量的一部分。
do
  echo $planet
done
echo; echo "Whoops! Pluto is no longer a planet!"
exit 0

list\] 中的每一个元素中都可能含有多个参数。这在处理参数组中非常有用。在这种情况下,使用 set 命令(查看 样例 15-16)强制解析 \[list\] 中的每一个元素,并将元素的每一个部分分配给位置参数。 样例 11-2. for 循环 \[list\] 中的每一个变量有两个参数的情况 ```bash #!/bin/bash # 让行星再躺次枪。 # 将每个行星与其到太阳的距离放在一起。 for planet in "Mercury 36" "Venus 67" "Earth 93" "Mars 142" "Jupiter 483" do set -- $planet # 解析变量 "planet" #+ 并将其每个部分赋值给位置参数。 # "--" 防止一些极端情况,比如 $planet 为空或者以破折号开头。 # 因为位置参数会被覆盖掉,因此需要先保存原先的位置参数。 # 你可以使用数组来保存 # original_params=("$@") echo "$1 $2,000,000 miles from the sum" #-------两个制表符---将后面的一系列 0 连到参数 $2 上。 done # (感谢 S.C. 做出的额外注释。) exit 0 ``` 一个单一变量也可以成为 for 循环中的 \[list\]。 样例 11-3. 文件信息:查看一个单一变量中含有的文件列表的文件信息 ```bash #!/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 $8 " file size: " $5 }' # 输出其中的两个域。 whatis `basename $file` # 文件信息。 # 脚本正常运行需要注意提前设置好 whatis 的数据。 # 使用 root 权限运行 /usr/bin/makewhatis 可以完成。 echo done exit 0 ``` for 循环中的 \[list\] 可以是一个参数。 样例 11-4. 操作含有一系列文件的参数 ```bash #!/bin/bash filename="*txt" for file in $filename do echo "Contents of $file" echo "---" cat "$file" echo done ``` 如果在匹配文件扩展名的 for 循环中的 \[list\] 含有通配符(\* 和 ?),那么将会进行文件名扩展。 样例 11-5. 在 for 循环中操作文件 ```bash #!/bin/bash # list-glob.sh: 通过文件名扩展在 for 循环中产生 [list]。 # 通配 = 文件名扩展。 echo for file in * # ^ Bash 在检测到通配表达式时, #+ 会进行文件名扩展。 do ls -l "$file" # 列出 $PWD(当前工作目录)下的所有文件。 # 回忆一下,通配符 "*" 会匹配所有的文件名, #+ 但是,在文件名扩展中,他将不会匹配以点开头的文件。 # 如果没有匹配到文件,那么它将会扩展为它自身。 # 为了防止出现这种情况,需要设置 nullglob 选项。 #+ (shopt -s nullglob)。 # 感谢 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-15 中使用到了这一点。也可以查看 样例 15-17。 样例 11-6. 缺少 in \[list\] 的 for 循环 ```bash #!/bin/bash # 尝试在带参数和不带参数两种情况下调用这个脚本,观察发生了什么。 for a do echo -n "$a " done # 缺失 'in list' 的情况下,循环会遍历 '$@' #+(命令行参数列表,包括空格)。 echo exit 0 ``` 可以在 for 循环中使用 命令代换 生成 \[list\]。查看 样例 16-54,样例 11-11 和 样例 16-48。 样例 11-7. 在 for 循环中使用命令代换生成 \[list

bash 复制代码
#!/bin/bash
# for-loopcmd.sh: 带命令代换所生成 [list] 的 for 循环
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] 的更加复杂的例子。

样例 11-8. 一种替代 grep 搜索二进制文件的方法

bash 复制代码
#!/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=$'\012'       # 按照 Anton Filippov 的意见应该是
                  # IFS="\n"
for word in $( strings "$2" | grep "$1" )
# "strings" 命令列出二进制文件中的所有字符串。
# 将结果通过管道输出到 "grep" 中,检查是不是匹配的字符串。
do
  echo $word
done
# 就像 S.C. 指出的那样,第 23-30 行可以换成下面的形式:
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'
# 尝试运行脚本 "./bin-grep.sh mem /bin/ls"
exit 0

下面的例子同样展示了如何使用命令代换生成 [list]。

样例 11-9. 列出系统中的所有用户

bash 复制代码
#!/bin/bash
# userlist.sh
PASSWORD_FILE=/etc/passwd
n=1           # 用户数量
for name in $(awk 'BEGIN{fs=":"}{print $1}' < "$PASSWORD_FILE" )
# 分隔符 = :              ^^^^^^
# 输出第一个域                    ^^^^^^^^
# 读取密码文件 /etc/passwd                    ^^^^^^^^^^^^^^^^^
do
  echo "USER #$n = $name"
  let "n += 1"
done
# USER #1 = root
# USER #2 = bin
# USER #3 = daemon
# ...
# USER #33 = bozo
exit $?
# 讨论:
# -----
# 一个普通用户是如何读取 /etc/passwd 文件的?
# 提示:检查 /etc/passwd 的文件权限。
# 这算不算是一个安全漏洞?为什么?

另外一个关于 [list] 的例子也来自于命令代换。

样例 11-10. 检查目录中所有二进制文件的原作者

bash 复制代码
#!/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%$driectory%%"
  #  在 "sed" 表达式中,你需要替换掉 "/" 分隔符,
  #+ 因为 "/" 是一个会被过滤的字符。
  #  如果不做替换,将会产生一个错误。(你可以尝试一下。)
done
exit $?
# 简单的练习:
# ----------
# 修改脚本,使其可以从命令行参数中获取 $directory 和 $fstring。

最后一个关于 [list] 和命令代换的例子,但这个例子中的命令是一个函数。

bash 复制代码
generate_list ()
{
  echo "one two three"
}
for word in $(generate_list)  # "word" 获得函数执行的结果。
do
  echo "$word"
done
# one
# two
# three

for 循环的结果可以通过管道导向至一个或多个命令中。

样例 11-11. 列出目录中的所有符号链接。

bash 复制代码
#!/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 1 )"   # -type 1 = 符号链接
do
  echo "$file"
done | sort                                  # 否则文件顺序会是乱序。
#  严格的来说这里并不需要使用循环,
#+ 因为 "find" 命令的输出结果已经被扩展成一个单一字符串了。
#  然而,为了方便大家理解,我们使用了循环的方式。
#  Dominik 'Aeneas' Schnitzer 指出,
#+ 不引用 $( find $directory -type 1 ) 的话,
#  脚本将在文件名包含空格时阻塞。
exit 0
# --------------------------------------------------------
# Jean Helou 提供了另外一种方法:
echo "symbolic links in directory \"$directory\""
# 备份当前的内部字段分隔符。谨慎永远没有坏处。
OLDIFS=$IFS
IFS=:
for file in $(find $directory -type 1 -printf "%p$IFS")
do     #                              ^^^^^^^^^^^^^^^^
       echo "$file"
       done|sort
# James "Mike" Conley 建议将 Helou 的代码修改为:
OLDIFS=$IFS
IFS='' # 空的内部字段分隔符意味着将不会分隔任何字符串
for file in $( find $directory -type 1 )
do
  echo $file
  done | sort
#  上面的代码可以在目录名包含冒号(前一个允许包含空格)
#+ 的情况下仍旧正常工作。

还有另外一种看起来非常像C语言中循环那样的语法。你需要使用到 双圆括号 语法。

样例 11-13. C语言风格的循环

bash 复制代码
#!/bin/bash
# 用多种方式数到10。
echo
# 基础版
for a in 1 2 3 4 5 6 7 8 9 10
do
  echo -n "$a "
done
echo; echo
# +==========================================+
# 使用 "seq"
for a in `seq 10`
do
  echo -n "$a "
done
echo; echo
# +==========================================+
# 使用大括号扩展语法
# Bash 3+ 版本有效。
for a in {1..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++))
do  # 逗号连接操作。
  echo -n "$a-$b "
done
echo; echo
exit 0

2、 while 循环

while 循环结构会在循环顶部检测循环条件,若循环条件为真( 退出状态 为0)则循环持续进行。与 for 循环 不同的是,while 循环是在不知道循环次数的情况下使用的。

bash 复制代码
while [ condition ]
do
  command(s)...
done

在 while 循环结构中,你不仅可以使用像 if/test 中那样的 括号结构,也可以使用用途更广泛的 双括号结构(while [[ condition ]])。

就像在 for 循环中那样,将 do 和循环条件放在同一行时需要加一个分号。

while [ condition ] ; do

在 while 循环中,括号结构 并不是必须存在的。比如说 getopts 结构。

样例 11-15. 简单的 while 循环

bash 复制代码
#!/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

样例 11-16. 另一个例子

bash 复制代码
#!/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 循环可以有多个测试条件,但只有最后的那一个条件决定了循环是否终止。这是一种你需要注意到的不同于其他循环的语法。

样例 11-17. 多条件 while 循环

bash 复制代码
#!/bin/bash
var1=unset
previous=$var1
while echo "previous-variable = $previous"
      echo
      previous=$var1
      [ "$var1" != end ] # 记录下 $var1 之前的值。
      # 在 while 循环中有4个条件,但只有最后的那个控制循环。
      # 最后一个条件的退出状态才会被记录。
do
echo "Input variable #1 (end to exit) "
  read var1
  echo "variable #1 = $var1"
done
# 猜猜这是怎样实现的。
# 这是一个很小的技巧。
exit 0

就像 for 循环一样, while 循环也可以使用双圆括号结构写得像C语言那样。

样例 11-18. C语言风格的 while 循环

bash 复制代码
#!/bin/bash
# wh-loopc.sh: 在 "while" 循环中计数到10。
LIMIT=10                 # 循环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"
  # 是的,就是这样。
  # 双圆括号结构允许像C语言一样自增一个变量。
done
echo
# 这可以让C和Java程序猿感觉更加舒服。
exit 0

3、until

与 while 循环相反,until 循环测试其顶部的循环条件,直到其中的条件为真时停止。

bash 复制代码
until [ condition-is-true ]
do
  commands(s)...
done

注意到,跟其他的一些编程语言不同,until 循环的测试条件在循环顶部。

就像在 for 循环中那样,将 do 和循环条件放在同一行时需要加一个分号。

until[ condition-is-true ] ; do

样例 11-19. until 循环

bash 复制代码
#!/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
#                ---                   #
#  就像 "for" 和 "while" 循环一样,
#+ "until" 循环也可以写的像C语言一样。
LIMIT=10
var=0
until (( var > LIMIT ))
do  # ^^ ^     ^     ^^   没有方括号,没有 $ 前缀。
  echo -n "$var "
  (( var++ ))
done    # 0 1 2 3 4 5 6 7 8 9 10
exit 0
相关推荐
dingdingfish3 天前
GNU Parallel 学习 - 第1章:How to read this book
bash·shell·gnu·parallel
似霰6 天前
Linux Shell 脚本编程——核心基础语法
linux·shell
似霰6 天前
Linux Shell 脚本编程——脚本自动化基础
linux·自动化·shell
偷学技术的梁胖胖yo7 天前
Shell脚本中连接数据库查询数据报错 “No such file or directory“以及函数传参数组
linux·mysql·shell
纵有疾風起16 天前
【Linux 系统开发】基础开发工具详解:软件包管理器、编辑器。编译器开发实战
linux·服务器·开发语言·经验分享·bash·shell
gis分享者18 天前
Shell 脚本中如何使用 here document 实现多行文本输入? (中等)
shell·脚本·document·多行·文本输入·here
柏木乃一18 天前
基础IO(上)
linux·服务器·c语言·c++·shell
angushine19 天前
CPU脚本并远程部署
shell
赵民勇24 天前
Linux/Unix中install命令全面用法解析
linux·shell
gis分享者24 天前
Shell 脚本中如何使用 trap 命令捕捉和处理信号(中等)
shell·脚本·信号·处理·trap·捕捉