Shell for循环用法

作者:IT技术圈子 浏览量:181   发表于 2024-08-15 09:31 标签:

for 循环

语法:

for arg in [list]
1

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

for arg in [list]
do
  command(s)...
done
1
2
3
4

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

for arg in "$var1" "$var2" "$var3" ... "$varN"
# 第一次循环中,arg = $var1
# 第二次循环中,arg = $var2
# 第三次循环中,arg = $var3
# ...
# 第 N 次循环中,arg = $varN
# 为了防止可能的字符分割问题,[list] 中的参数都需要被引用。
1
2
3
4
5
6
7

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

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

for arg in [list] ; do
1

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

#!/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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

[list] 中的每一个元素中都可能含有多个参数。这在处理参数组中非常有用。在这种情况下,使用 set 命令(查看 样例 15-16)强制解析 [list] 中的每一个元素,并将元素的每一个部分分配给位置参数。

样例 11-2. for 循环 [list] 中的每一个变量有两个参数的情况

#!/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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

一个单一变量也可以成为 for 循环中的 [list]。

样例 11-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 $8 "         file size: " $5 }'  # 输出其中的两个域。
  whatis `basename $file`   # 文件信息。
  # 脚本正常运行需要注意提前设置好 whatis 的数据。
  # 使用 root 权限运行 /usr/bin/makewhatis 可以完成。
  echo
done
exit 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

for 循环中的 [list] 可以是一个参数。

样例 11-4. 操作含有一系列文件的参数

#!/bin/bash
filename="*txt"
for file in $filename
do
 echo "Contents of $file"
 echo "---"
 cat "$file"
 echo
done
1
2
3
4
5
6
7
8
9

如果在匹配文件扩展名的 for 循环中的 [list] 含有通配符(* 和 ?),那么将会进行文件名扩展。

样例 11-5. 在 for 循环中操作文件

#!/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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

如果在 for 循环中省略 in [list] 部分,那么循环将会遍历位置参数($@)。样例 A-15 中使用到了这一点。也可以查看 样例 15-17。

样例 11-6. 缺少 in [list] 的 for 循环

#!/bin/bash
# 尝试在带参数和不带参数两种情况下调用这个脚本,观察发生了什么。
for a
do
 echo -n "$a "
done
#  缺失 'in list' 的情况下,循环会遍历 '$@'
#+(命令行参数列表,包括空格)。
echo
exit 0
1
2
3
4
5
6
7
8
9
10

可以在 for 循环中使用 命令代换 生成 [list]。查看 样例 16-54,样例 11-11 和 样例 16-48。

样例 11-7. 在 for 循环中使用命令代换生成 [list]

#!/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
1
2
3
4
5
6
7
8
9

下面是使用命令代换生成 [list] 的更加复杂的例子。

样例 11-8. 一种替代 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=$'\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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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

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

#!/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 的文件权限。
# 这算不算是一个安全漏洞?为什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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

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

#!/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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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

generate_list ()
{
  echo "one two three"
}
for word in $(generate_list)  # "word" 获得函数执行的结果。
do
  echo "$word"
done
# one
# two
# three
1
2
3
4
5
6
7
8
9
10
11

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

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

#!/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
#  上面的代码可以在目录名包含冒号(前一个允许包含空格)
#+ 的情况下仍旧正常工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

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

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

#!/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