魅力博客

魅力Linux|魅力空间|魅力博客|学习Linux|ubuntu日记|电脑教程|手机软件

深入学习shell脚本艺术-高级Bash脚本编程指南5



Example 9-17 参数替换中的模式匹配
################################Start Script#######################################
 1 #!/bin/bash
 2 # patt-matching.sh
 3
 4 # 使用# ## % %%来进行参数替换操作的模式匹配.
 5
 6 var1=abcd12345abc6789
 7 pattern1=a*c  # * (通配符) 匹配a - c之间的任何字符.
 8
 9 echo
10 echo "var1 = $var1"           # abcd12345abc6789
11 echo "var1 = ${var1}"         # abcd12345abc6789
12                               # (alternate form)
13 echo "Number of characters in ${var1} = ${#var1}"
14 echo
15
16 echo "pattern1 = $pattern1"   # a*c  (everything between 'a' and 'c')
17 echo "--------------"
18 echo '${var1#$pattern1}  =' "${var1#$pattern1}"    #         d12345abc6789
19 # 最短的可能匹配, 去掉abcd12345abc6789的前3个字符
20 #                     |-|               ^^^
21 echo '${var1##$pattern1} =' "${var1##$pattern1}"   #                  6789      
22 # 最远的匹配,去掉abcd12345abc6789的前12个字符.
23 #                |----------|      ^^^^
24
25 echo; echo; echo
26
27 pattern2=b*9            # 'b' 到'9'之间的任何字符
28 echo "var1 = $var1"     # 还是 abcd12345abc6789
29 echo
30 echo "pattern2 = $pattern2"
31 echo "--------------"
32 echo '${var1%pattern2}  =' "${var1%$pattern2}"     #     abcd12345a
33 # 最近的匹配, 去掉abcd12345abc6789的最后6个字符
34 #                           |----|  ^^^^
35 echo '${var1%%pattern2} =' "${var1%%$pattern2}"    #     a
36 # 最远匹配, 去掉abcd12345abc6789的最后12个字符
37 #                |-------------|  ^^^^^^
38
39 # 记住, # 和## 从字符串的左边开始,并且去掉左边的字符串,
40 #           % 和 %% 从字符串的右边开始,并且去掉右边的子串.
41
42 echo
43
44 exit 0
################################End Script#########################################

Example 9-18 重命名文件扩展名
################################Start Script#######################################
 1 #!/bin/bash
 2 # rfe.sh: 重命名文件扩展名.
 3 #
 4 # 用法:   rfe old_extension new_extension
 5 #
 6 # 例子:
 7 # 将指定目录的所有 *.gif 文件都重命名为 *.jpg,
 8 # 用法:   rfe gif jpg
 9
10
11 E_BADARGS=65
12
13 case $# in
14   0|1)             # "|" 在这里的意思是或操作.
15   echo "Usage: `basename $0` old_file_suffix new_file_suffix"
16   exit $E_BADARGS  # 如果只有0个或1个参数,那么就退出.
17   ;;
18 esac
19
20
21 for filename in *.$1
22 # 以第一个参数为扩展名的全部文件的列表
23 do
24   mv $filename ${filename%$1}$2
25   #  从筛选出的文件中先去掉以第一参数结尾的扩展名部门,
26   #+ 然后作为扩展名把第2个参数添加上.
27 done
28
29 exit 0
################################End Script#########################################

变量扩展/子串替换
    这些结构都是从ksh中吸收来的.

    ${var:pos}
        变量var从位置pos开始扩展.

    ${var:pos:len}
        从位置pos开始,并扩展len长度个字符.见Example A-14(这个例子里有这种操作的一个
        创造性用法)

    ${var/Pattern/Replacement}
        使用Replacement来替换var中的第一个Pattern的匹配.

    ${var//Pattern/Replacement}
        全局替换.在var中所有的匹配,都会用Replacement来替换.

        向上边所说,如果Replacement被忽略的话,那么所有匹配到的Pattern都会被删除.

Example 9-19 使用模式匹配来分析比较特殊的字符串
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 var1=abcd-1234-defg
 4 echo "var1 = $var1"
 5
 6 t=${var1#*-*}
 7 echo "var1 (with everything, up to and including first - stripped out) = $t"
 8 #  t=${var1#*-}  在这个例子中作用是一样的,
 9 #+ 因为 # 匹配这个最近的字符串,
10 #+ 并且 * 匹配前边的任何字符串,包括一个空字符.
11 # (Thanks, Stephane Chazelas, for pointing this out.)
12
13 t=${var1##*-*}
14 echo "If var1 contains a \"-\", returns empty string...   var1 = $t"
15
16
17 t=${var1%*-*}
18 echo "var1 (with everything from the last - on stripped out) = $t"
19
20 echo
21
22 # -------------------------------------------
23 path_name=/home/bozo/ideas/thoughts.for.today
24 # -------------------------------------------
25 echo "path_name = $path_name"
26 t=${path_name##/*/}
27 echo "path_name, stripped of prefixes = $t"
28 # 在这个特定的例子中,与 t=`basename $path_name` 的作用一致.
29 #  t=${path_name%/}; t=${t##*/}   是一个更一般的解决办法,
30 #+ 但有时还是不行.
31 #  如果 $path_name 以一个新行结束, 那么`basename $path_name` 将不能工作,
32 #+ 但是上边这个表达式可以.
33 # (Thanks, S.C.)
34
35 t=${path_name%/*.*}
36 # 与 t=`dirname $path_name` 效果相同.
37 echo "path_name, stripped of suffixes = $t"
38 # 在某些情况下将失效,比如 "../", "/foo////", # "foo/", "/".
39 #  删除后缀,尤其是在basename没有后缀的时候,
40 #+ 但是dirname还是会使问题复杂化.
41 # (Thanks, S.C.)
42
43 echo
44
45 t=${path_name:11}
46 echo "$path_name, with first 11 chars stripped off = $t"
47 t=${path_name:11:5}
48 echo "$path_name, with first 11 chars stripped off, length 5 = $t"
49
50 echo
51
52 t=${path_name/bozo/clown}
53 echo "$path_name with \"bozo\" replaced  by \"clown\" = $t"
54 t=${path_name/today/}
55 echo "$path_name with \"today\" deleted = $t"
56 t=${path_name//o/O}
57 echo "$path_name with all o's capitalized = $t"
58 t=${path_name//o/}
59 echo "$path_name with all o's deleted = $t"
60
61 exit 0
################################End Script#########################################

    ${var/#Pattern/Replacement}
        如果var的前缀匹配到了Pattern,那么就用Replacement来替换Pattern.

    ${var/%Pattern/Replacement}
        如果var的后缀匹配到了Pattern,那么就用Replacement来替换Pattern.

Example 9-20 对字符串的前缀或后缀使用匹配模式
################################Start Script#######################################
 1 #!/bin/bash
 2 # var-match.sh:
 3 # 对字符串的前后缀使用匹配替换的一个样本
 4
 5 v0=abc1234zip1234abc    # 原始变量.
 6 echo "v0 = $v0"         # abc1234zip1234abc
 7 echo
 8
 9 # 匹配字符串的前缀
10 v1=${v0/#abc/ABCDEF}    # abc1234zip1234abc
11                         # |-|
12 echo "v1 = $v1"         # ABCDEF1234zip1234abc
13                         # |----|
14
15 # 匹配字符串的后缀
16 v2=${v0/%abc/ABCDEF}    # abc1234zip123abc
17                         #              |-|
18 echo "v2 = $v2"         # abc1234zip1234ABCDEF
19                         #               |----|
20
21 echo
22
23 #  ----------------------------------------------------
24 #  必须在开头或结尾匹配,否则,
25 #+ 将不会产生替换结果.
26 #  ----------------------------------------------------
27 v3=${v0/#123/000}       # 匹配上了,但不是在字符串的开头
28 echo "v3 = $v3"         # abc1234zip1234abc
29                         # 没替换.
30 v4=${v0/%123/000}       # 匹配上了,但不是在字符串结尾.
31 echo "v4 = $v4"         # abc1234zip1234abc
32                         # 没替换.
33
34 exit 0    
################################End Script#########################################

${!varprefix*}, ${!varprefix@}
    使用变量的前缀来匹配前边所有声明过的变量.
    1 xyz23=whatever
    2 xyz24=
    3
    4 a=${!xyz*}      # 以"xyz"作为前缀,匹配所有前边声明过的变量.
    5 echo "a = $a"   # a = xyz23 xyz24
    6 a=${!xyz@}      # 同上.
    7 echo "a = $a"   # a = xyz23 xyz24
    8
    9 # Bash, version 2.04, 添加了这个特征.

注意事项:
[1]        如果在一个非交互脚本中,$parameter为空的话,那么这个脚本将以127返回.
        (127退出码对应的Bash错误码为"command not found").



9.4 指定类型的变量:declare或者typeset
-------------------------------------
declare或者typeset内建命令(这两个命令是完全一样的)允许指定变量的具体类型.在某些特
定的语言中,这是一种指定类型的很弱的形式.declare命令是在Bash版本2或之后的版本才被
加入的.typeset命令也可以工作在ksh脚本中.

declare/typeset 选项

    -r 只读
        1 declare -r var1
        (declare -r var1与readonly var1是完全一样的)
        这和C语言中的const关键字一样,都是强制指定只读.如果你尝试修改一个只读变量
        的值,那么你将得到一个错误消息.

    -i 整形
        1 declare -i number
        2 # 这个脚本将把变量"number"后边的赋值视为一个整形.
        3
        4 number=3
        5 echo "Number = $number"     # Number = 3
        6
        7 number=three
        8 echo "Number = $number"     # Number = 0
        9 # 尝试把"three"解释为整形.

        如果把一个变量指定为整形,那么即使没有expr和let命令,也允许使用特定的算术运算
        1 n=6/3
        2 echo "n = $n"       # n = 6/3
        3
        4 declare -i n
        5 n=6/3
        6 echo "n = $n"       # n = 2

    -a 数组
        1 declae -a indices
        变量indices将被视为数组.

    -f 函数
        1 declare -f
        如果使用declare -f而不带参数的话,将会列出这个脚本中之前定义的所有函数.
        1 declare -f function_name
        如果使用declare -f function_name这种形式的话,将只会列出这个函数的名字.

    -x export
        1 declare -x var3
        这种使用方式,将会把var3 export出来.

Example 9-21 使用declare来指定变量的类型
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 func1 ()
 4 {
 5 echo This is a function.
 6 }
 7
 8 declare -f        # 列出之前的所有函数.
 9
10 echo
11
12 declare -i var1   # var1 是个整形.
13 var1=2367
14 echo "var1 declared as $var1"
15 var1=var1+1       # 变量声明不需使用'let'命令.
16 echo "var1 incremented by 1 is $var1."
17 # 尝试将变量修改为整形.
18 echo "Attempting to change var1 to floating point value, 2367.1."
19 var1=2367.1       # 结果将是一个错误消息,并且变量并没有被修改.
20 echo "var1 is still $var1"
21
22 echo
23
24 declare -r var2=13.36         # 'declare' 允许设置变量的属性,
25                               #+ 并且同时分配变量的值.
26 echo "var2 declared as $var2" # 尝试修改只读变量.
27 var2=13.37                    # 产生一个错误消息,并且从脚本退出了.
28
29 echo "var2 is still $var2"    # 这行将不会被执行.
30
31 exit 0                        # 脚本将不会在此处退出.
################################End Script#########################################

    注意:使用declare内建命令将会限制变量的作用域.
     1 foo ()
     2 {
     3 FOO="bar"
     4 }
     5
     6 bar ()
     7 {
     8 foo
     9 echo $FOO
    10 }
    11
    12 bar   # Prints bar.

    然而...
     1 foo (){
     2 declare FOO="bar"
     3 }
     4
     5 bar ()
     6 {
     7 foo
     8 echo $FOO
     9 }
    10
    11 bar  # Prints nothing.
    12
    13
    14 # Thank you, Michael Iatrou, for pointing this out.



9.5 变量的间接引用
------------------
假设一个变量的值是另一个变量的名字.我们有可能从第一个变量中取得第2个变量的值么?
比如,如果a=letter_of_alphabet接着letter_of_alphabet=z,那么我们能从a中得到z么?
答案是:当然可以,并且这被称为间接引用.它使用一个不常用的符号eval var1=\$$var2.


Example 9-22 间接引用
################################Start Script#######################################
 1 #!/bin/bash
 2 # ind-ref.sh: 间接变量引用
 3 # 存取一个变量的值的值(这里翻译得有点拗口,不过凑合吧)
 4
 5 a=letter_of_alphabet   # 变量"a"的值是另一个变量的名字.
 6 letter_of_alphabet=z
 7
 8 echo
 9
10 # 直接引用.
11 echo "a = $a"          # a = letter_of_alphabet
12
13 # 间接引用.
14 eval a=\$$a
15 echo "Now a = $a"      # Now a = z
16
17 echo
18
19
20 # 现在,让我们试试修改第2个引用的值.
21
22 t=table_cell_3
23 table_cell_3=24
24 echo "\"table_cell_3\" = $table_cell_3"            # "table_cell_3" = 24
25 echo -n "dereferenced \"t\" = "; eval echo \$$t    # 解引用 "t" = 24
26 # 在这个简单的例子中,下边的表达式也能正常工作(为什么?).
27 #         eval t=\$$t; echo "\"t\" = $t"
28
29 echo
30
31 t=table_cell_3
32 NEW_VAL=387
33 table_cell_3=$NEW_VAL
34 echo "Changing value of \"table_cell_3\" to $NEW_VAL."
35 echo "\"table_cell_3\" now $table_cell_3"
36 echo -n "dereferenced \"t\" now "; eval echo \$$t
37 # "eval" 将获得两个参数 "echo" 和 "\$$t" (与$table_cell_3等价)
38
39 echo
40
41 # (Thanks, Stephane Chazelas, 澄清了上边的行为.)
42
43
44 # 另一个方法是使用${!t}符号,见"Bash, 版本2"小节.
45 # 也请参阅ex78.sh.
46
47 exit 0
################################End Script#########################################
间接应用到底有什么应用价值?它给Bash添加了一种类似于C语言指针的功能,在Example 34-3
中有例子.并且,还有一些其它的有趣的应用....

Nils Radtke展示了如何建立一个"dynamic"变量名字并且取出其中的值.当sourcing(包含)配置
文件时,这很有用.
################################Start Script#######################################
 1 #!/bin/bash
 2
 3
 4 # ---------------------------------------------
 5 # 这部分内容可能来自于单独的文件.
 6 isdnMyProviderRemoteNet=172.16.0.100
 7 isdnYourProviderRemoteNet=10.0.0.10
 8 isdnOnlineService="MyProvider"
 9 # ---------------------------------------------
10       
11
12 remoteNet=$(eval "echo \$$(echo isdn${isdnOnlineService}RemoteNet)")
13 remoteNet=$(eval "echo \$$(echo isdnMyProviderRemoteNet)")
14 remoteNet=$(eval "echo \$isdnMyProviderRemoteNet")
15 remoteNet=$(eval "echo $isdnMyProviderRemoteNet")
16
17 echo "$remoteNet"    # 172.16.0.100
18
19 # ================================================================
20
21 #  同时,它甚至能更好.
21 #  
22
23 #  考虑下边的脚本,给出了一个变量getSparc,
24 #+ 但是没给出变量getIa64:
25
26 chkMirrorArchs () {
27   arch="$1";
28   if [ "$(eval "echo \${$(echo get$(echo -ne $arch |
29        sed 's/^\(.\).*/\1/g' | tr 'a-z' 'A-Z'; echo $arch |
30        sed 's/^.\(.*\)/\1/g')):-false}")" = true ]
31   then
32      return 0;
33   else
34      return 1;
35   fi;
36 }
37
38 getSparc="true"
39 unset getIa64
40 chkMirrorArchs sparc
41 echo $?        # 0
42                # True
43
44 chkMirrorArchs Ia64
45 echo $?        # 1
46                # False
47
48 # 注意:
49 # -----<rojy bug>
50 # Even the to-be-substituted variable name part is built explicitly.
51 # The parameters to the chkMirrorArchs calls are all lower case.
52 # The variable name is composed of two parts: "get" and "Sparc" . . .
################################End Script#########################################

Example 9-23 传递一个间接引用给awk
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  "column totaler"脚本的另一个版本
 4 #+ 这个版本在目标文件中添加了一个特殊的列(数字的).
 5 #  这个脚本使用了间接引用.
 6
 7 ARGS=2
 8 E_WRONGARGS=65
 9
10 if [ $# -ne "$ARGS" ] # 检查命令行参数是否是合适的个数.
11 then
12    echo "Usage: `basename $0` filename column-number"
13    exit $E_WRONGARGS
14 fi
15
16 filename=$1
17 column_number=$2
18
19 #===== 上边的这部分,与原来的脚本一样 =====#
20
21
22 # 一个多行的awk脚本被调用,通过 ' ..... '
23
24
25 # awk 脚本开始.
26 # ------------------------------------------------
27 awk "
28
29 { total += \$${column_number} # 间接引用.
30 }
31 END {
32      print total
33      }
34
35      " "$filename"
36 # ------------------------------------------------
37 # awk 脚本结束.
38
39 #  间接的变量引用避免了在一个内嵌的awk脚本中引用
40 #+ 一个shell变量的问题.
41 #  Thanks, Stephane Chazelas.
42
43
44 exit 0
################################End Script#########################################
注意: 这个脚本有些狡猾.如果第2个变量修改了它的值,那么第一个变量必须被适当的解引用
      (像上边的例子一样).幸运的是,在Bash版本2中引入的${!variable}(参见Example 34-2)
      是的间接引用更加直观了.

注意: Bash并不支持指针的算术运算,并且这严格的限制了间接引用的使用.事实上,在脚本语言
      中,间接引用本来就是丑陋的部分.



9.6 $RANDOM: 产生随机整数
-------------------------
$RANDOM是Bash的内部函数(并不是常量),这个函数将返回一个范围在0 - 32767之间的一个伪
随机整数.它不应该被用来产生密匙.

Example 9-24 产生随机数
################################Start Script#######################################
  1 #!/bin/bash
  2
  3 # $RANDOM 在每次调用的时候,返回一个不同的随机整数.
  4 # 指定的范围是: 0 - 32767 (有符号的16-bit 整数).
  5
  6 MAXCOUNT=10
  7 count=1
  8
  9 echo
 10 echo "$MAXCOUNT random numbers:"
 11 echo "-----------------"
 12 while [ "$count" -le $MAXCOUNT ]      # 产生10 ($MAXCOUNT) 个随机整数.
 13 do
 14   number=$RANDOM
 15   echo $number
 16   let "count += 1"  # 数量加1.
 17 done
 18 echo "-----------------"
 19
 20 # 如果你需要在一个特定范围内产生一个随机int,那么使用'modulo'(模)操作.
 21 # 这将返回一个除法操作的余数.
 22
 23 RANGE=500
 24
 25 echo
 26
 27 number=$RANDOM
 28 let "number %= $RANGE"
 29 #           ^^
 30 echo "Random number less than $RANGE  ---  $number"
 31
 32 echo
 33
 34
 35
 36 #  如果你需要产生一个比你指定的最小边界大的随机数,
 37 #+ 那么建立一个test循环,来丢弃所有产生对比这个数小的随机数.
 38
 39 FLOOR=200
 40
 41 number=0   #initialize
 42 while [ "$number" -le $FLOOR ]
 43 do
 44   number=$RANDOM
 45 done
 46 echo "Random number greater than $FLOOR ---  $number"
 47 echo
 48
 49    # 让我们对上边的循环尝试一个小改动,也就是
 50    #       让"number = $RANDOM + $FLOOR"
 51    # 这将不再需要那个while循环,并且能够运行得更快.
 52    # 但是, 这可能会产生一个问题.那么这个问题是什么呢?(译者:这很简单,有可能溢出)
 53
 54
 55
 56 # 结合上边两个例子的技术,来达到获得在指定的上下限之间来产生随机数.
 57 number=0   #initialize
 58 while [ "$number" -le $FLOOR ]
 59 do
 60   number=$RANDOM
 61   let "number %= $RANGE"  # 让$number依比例落在$RANGE范围内.
 62 done
 63 echo "Random number between $FLOOR and $RANGE ---  $number"
 64 echo
 65
 66
 67
 68 # 产生一个二元选择,就是"true"和"false"两个值.
 69 BINARY=2
 70 T=1
 71 number=$RANDOM
 72
 73 let "number %= $BINARY"
 74 #  注意,让"number >>= 14" 将给出一个更好的随机分配
 75 #+ (右移14位将把所有为全部清空,除了第15位,因为有符号,所以第16位是符号位).
 76 if [ "$number" -eq $T ]
 77 then
 78   echo "TRUE"
 79 else
 80   echo "FALSE"
 81 fi  
 82
 83 echo
 84
 85
 86 # 抛骰子
 87 SPOTS=6   # 模6给出的范围就是0-5.
 88           # 加1就会得到期望的范围1 - 6.
 89           # Thanks, Paulo Marcel Coelho Aragao, for the simplification.
 90 die1=0
 91 die2=0
 92 # 是否让SPOTS=7比加1更好呢?解释行或者不行的原因?
 93
 94 # 每次抛骰子,都会给出均等的机会.
 95
 96     let "die1 = $RANDOM % $SPOTS +1" # 抛第一次.
 97     let "die2 = $RANDOM % $SPOTS +1" # 抛第二次.
 98     #  上边的那个算术操作,具有更高的优先级呢 --
 99     #+ 模操作(%)还是加法操作(+)?
100
101
102 let "throw = $die1 + $die2"
103 echo "Throw of the dice = $throw"
104 echo
105
106
107 exit 0
################################End Script#########################################

Example 9-25 从一副扑克牌中取出一张随机的牌
################################Start Script#######################################
 1 #!/bin/bash
 2 # pick-card.sh
 3
 4 # 这是一个从数组中取出随机元素的一个例子.
 5
 6
 7 # 取出一张牌,任何一张.
 8
 9 Suites="Clubs
10 Diamonds
11 Hearts
12 Spades"
13
14 Denominations="2
15 3
16 4
17 5
18 6
19 7
20 8
21 9
22 10
23 Jack
24 Queen
25 King
26 Ace"
27
28 # 注意变量的多行展开.
29
30
31 suite=($Suites)                # 读到数组变量中.
32 denomination=($Denominations)
33
34 num_suites=${#suite[*]}        # 计算有多少个元素.
35 num_denominations=${#denomination[*]}
36
37 echo -n "${denomination[$((RANDOM%num_denominations))]} of "
38 echo ${suite[$((RANDOM%num_suites))]}
39
40
41 # $bozo sh pick-cards.sh
42 # Jack of Clubs
43
44
45 # Thank you, "jipe," for pointing out this use of $RANDOM.
46 exit 0
################################End Script#########################################

Jipe展示了一系列的在一定范围中产生随机数的方法.
 1 #  在6到30之间产生随机数.
 2    rnumber=$((RANDOM%25+6))    
 3
 4 #  还是产生6-30之间的随机数,
 5 #+ 但是这个数字必须被3均分.
 6    rnumber=$(((RANDOM%30/3+1)*3))
 7
 8 #  注意,这可能不会在所有时候都能正常地运行.
 9 #  It fails if $RANDOM returns 0.
10
11 #  Frank Wang 建议用下班的方法来取代:
12    rnumber=$(( RANDOM%27/3*3+6 ))

Bill Gradwohl 提出了一个重要的规则来产生正数.
1 rnumber=$(((RANDOM%(max-min+divisibleBy))/divisibleBy*divisibleBy+min))

这里Bill给出了一个通用函数,这个函数返回一个在两个指定值之间的随机数

Example 9-26 两个指定值之间的随机数
################################Start Script#######################################
  1 #!/bin/bash
  2 # random-between.sh
  3 # 在两个指定值之间的随机数.
  4 # Bill Gradwohl编写的本脚本,本文作者作了较小的修改.
  5 # 允许使用.
  6
  7
  8 randomBetween() {
  9    #  产生一个正的或者负的随机数.
 10    #+ 在$max和$max之间
 11    #+ 并且可被$divisibleBy整除的.
 12    #  给出一个合理的随机分配的返回值.
 13    #
 14    #  Bill Gradwohl - Oct 1, 2003
 15
 16    syntax() {
 17    # 在函数中内嵌函数
 18       echo
 19       echo    "Syntax: randomBetween [min] [max] [multiple]"
 20       echo
 21       echo    "Expects up to 3 passed parameters, but all are completely optional."
 22       echo    "min is the minimum value"
 23       echo    "max is the maximum value"
 24       echo    "multiple specifies that the answer must be a multiple of this value."
 25       echo    "    i.e. answer must be evenly divisible by this number."
 26       echo    
 27       echo    "If any value is missing, defaults area supplied as: 0 32767 1"
 28       echo    "Successful completion returns 0, unsuccessful completion returns"
 29       echo    "function syntax and 1."
 30       echo    "The answer is returned in the global variable randomBetweenAnswer"
 31       echo    "Negative values for any passed parameter are handled correctly."
 32    }
 33
 34    local min=${1:-0}
 35    local max=${2:-32767}
 36    local divisibleBy=${3:-1}
 37    # 默认值分配,用来处理没有参数传递进来的时候.
 38
 39    local x
 40    local spread
 41
 42    # 确认divisibleBy是正值.
 43    [ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy))
 44
 45    # 完整性检查.
 46    if [ $# -gt 3 -o ${divisibleBy} -eq 0 -o  ${min} -eq ${max} ]; then
 47       syntax
 48       return 1
 49    fi
 50
 51    # 察看是否min和max颠倒了.
 52    if [ ${min} -gt ${max} ]; then
 53       # 交换它们.
 54       x=${min}
 55       min=${max}
 56       max=${x}
 57    fi
 58
 59    #  如果min自己并不能够被$divisibleBy整除,
 60    #+ 那么就调整min的值,使其能够被$divisibleBy整除,前提是不能放大范围.
 61    if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ]; then
 62       if [ ${min} -lt 0 ]; then
 63          min=$((min/divisibleBy*divisibleBy))
 64       else
 65          min=$((((min/divisibleBy)+1)*divisibleBy))
 66       fi
 67    fi
 68
 69    #  如果min自己并不能够被$divisibleBy整除,
 70    #+ 那么就调整max的值,使其能够被$divisibleBy整除,前提是不能放大范围.
 71    if [ $((max/divisibleBy*divisibleBy)) -ne ${max} ]; then
 72       if [ ${max} -lt 0 ]; then
 73          max=$((((max/divisibleBy)-1)*divisibleBy))
 74       else
 75          max=$((max/divisibleBy*divisibleBy))
 76       fi
 77    fi
 78
 79    #  ---------------------------------------------------------------------
 80    #  现在,来做真正的工作.
 81
 82    #  注意,为了得到对于端点来说合适的分配,
 83    #+ 随机值的范围不得不落在
 84    #+ 0 和 abs(max-min)+divisibleBy之间, 而不是 abs(max-min)+1.
 85
 86    #  对于端点来说,
 87    #+ 这个少量的增加将会产生合适的分配.
 88
 89    #  修改这个公式,使用abs(max-min)+1来代替abs(max-min)+divisibleBy的话,
 90    #+ 也能够产生正确的答案, 但是在这种情况下生成的随机值对于正好为端点倍数
 91    #+ 的这种情况来说将是不完美的,因为在正好为端点倍数的情况的随机率比较低,
 92    #+ 因为你才加1而已,这比正常的公式所产生的机率要小得多(正常为加divisibleBy)
 93    #  ---------------------------------------------------------------------
 94
 95    spread=$((max-min))
 96    [ ${spread} -lt 0 ] && spread=$((0-spread))
 97    let spread+=divisibleBy
 98    randomBetweenAnswer=$(((RANDOM%spread)/divisibleBy*divisibleBy+min))
 99
100    return 0
101
102    #  然而,Paulo Marcel Coelho Aragao指出
103    #+ 当$max和$min不能被$divisibleBy整除时,
104    #+ 这个公式将会失败.
105    #
106    #  他建议使用如下的公式:
107    #    rnumber = $(((RANDOM%(max-min+1)+min)/divisibleBy*divisibleBy))
108
109 }
110
111 # 让我们测试一下这个函数.
112 min=-14
113 max=20
114 divisibleBy=3
115
116
117 #  产生一个数组answers,answers的下标用来表示在范围内可能出现的值,
118 #+ 而内容记录的是对于这个值出现的次数,如果我们循环足够多次,一定会得到
119 #+ 一次出现机会.
120 declare -a answer
121 minimum=${min}
122 maximum=${max}
123    if [ $((minimum/divisibleBy*divisibleBy)) -ne ${minimum} ]; then
124       if [ ${minimum} -lt 0 ]; then
125          minimum=$((minimum/divisibleBy*divisibleBy))
126       else
127          minimum=$((((minimum/divisibleBy)+1)*divisibleBy))
128       fi
129    fi
130
131
132    #  如果maximum自己并不能够被$divisibleBy整除,
133    #+ 那么就调整maximum的值,使其能够被$divisibleBy整除,前提是不能放大范围.
134
135    if [ $((maximum/divisibleBy*divisibleBy)) -ne ${maximum} ]; then
136       if [ ${maximum} -lt 0 ]; then
137          maximum=$((((maximum/divisibleBy)-1)*divisibleBy))
138       else
139          maximum=$((maximum/divisibleBy*divisibleBy))
140       fi
141    fi
142
143
144 #  我们需要产生一个下标全为正的数组,
145 #+ 所以我们需要一个displacement来保正都为正的结果.
146
147
148 displacement=$((0-minimum))
149 for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
150    answer[i+displacement]=0
151 done
152
153
154 # 现在我们循环足够多的次数来得到我们想要的答案.
155 loopIt=1000   #  脚本作者建议 100000,
156               #+ 但是这实在是需要太长的时间了.
157
158 for ((i=0; i<${loopIt}; ++i)); do
159
160    #  注意,我们在这里调用randomBetween函数时,故意将min和max颠倒顺序
161    #+ 我们是为了测试在这种情况下,此函数是否还能得到正确的结果.
162
163    randomBetween ${max} ${min} ${divisibleBy}
164
165    # 如果答案不是我们所预期的,那么就报告一个错误.
166    [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo MIN or MAX error - ${randomBetweenAnswer}!
167    [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo DIVISIBLE BY error - ${randomBetweenAnswer}!
168
169    # 将统计值存到answer之中.
170    answer[randomBetweenAnswer+displacement]=$((answer[randomBetweenAnswer+displacement]+1))
171 done
172
173
174
175 # 让我们察看一下结果
176
177 for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do
178    [ ${answer[i+displacement]} -eq 0 ] && echo "We never got an answer of $i." || echo "${i} occurred ${answer[i+displacement]} times."
179 done
180
181
182 exit 0
################################End Script#########################################

$RANDOM到底有多随机?最好的办法就是写个脚本来测试一下.跟踪随机数的分配情况.
让我们用随机数摇一个骰子.
Example 9-27 使用随机数来摇一个骰子
################################Start Script#######################################
 1 #!/bin/bash
 2 # RANDOM到底有多random?
 3
 4 RANDOM=$$       # 使用脚本的进程ID来作为随机数的产生种子.
 5
 6 PIPS=6          # 一个骰子有6面.
 7 MAXTHROWS=600   # 如果你没别的事干,那么可以增加这个数值.
 8 throw=0         # 抛骰子的次数.
 9
10 ones=0          #  必须把所有count都初始化为0,
11 twos=0          #+ 因为未初始化的变量为null,不是0.
12 threes=0
13 fours=0
14 fives=0
15 sixes=0
16
17 print_result ()
18 {
19 echo
20 echo "ones =   $ones"
21 echo "twos =   $twos"
22 echo "threes = $threes"
23 echo "fours =  $fours"
24 echo "fives =  $fives"
25 echo "sixes =  $sixes"
26 echo
27 }
28
29 update_count()
30 {
31 case "$1" in
32   0) let "ones += 1";;   # 因为骰子没有0,所以给1.
33   1) let "twos += 1";;   # 对tows做同样的事.
34   2) let "threes += 1";;
35   3) let "fours += 1";;
36   4) let "fives += 1";;
37   5) let "sixes += 1";;
38 esac
39 }
40
41 echo
42
43
44 while [ "$throw" -lt "$MAXTHROWS" ]
45 do
46   let "die1 = RANDOM % $PIPS"
47   update_count $die1
48   let "throw += 1"
49 done  
50
51 print_result
52
53 exit 0
54
55 #  如果RANDOM是真正的随机,那么摇出来结果应该平均的.
56 #  $MAXTHROWS设为600,那么每面都应该为100,上下的出入不应该超过20.
57 #
58 #  记住RANDOM毕竟只是一个伪随机数,
59 #+ 并且不是十分完美的.
60
61 #  随机数的产生是一个深奥并复杂的问题.
62 #  足够长的随机序列,不但会展现杂乱无章的一面,
63 #+ 而且会展现机会均等的一面.
64
65 # 一个很简单的练习:
66 # -----------------
67 # 重写这个例子,做成抛1000次硬币的形式.
68 # 分为正反两面.
################################End Script#########################################
像我们在上边的例子中看到的,最好在每次随机数产生时都使用新的种子.应为如果使用同样的
种子的话,那么随机数将产生相同的序列.[2](C中random()函数也会有这样的行为)

Example 9-28 重新分配随机数种子
################################Start Script#######################################
 1 #!/bin/bash
 2 # seeding-random.sh: 设置RANDOM变量作为种子.
 3
 4 MAXCOUNT=25       # 决定产生多少个随机数.
 5
 6 random_numbers ()
 7 {
 8 count=0
 9 while [ "$count" -lt "$MAXCOUNT" ]
10 do
11   number=$RANDOM
12   echo -n "$number "
13   let "count += 1"
14 done  
15 }
16
17 echo; echo
18
19 RANDOM=1          # 为随机数的产生设置RANDOM种子.
20 random_numbers
21
22 echo; echo
23
24 RANDOM=1          # 设置同样的种子...
25 random_numbers    # ...将会和上边产生的随机数列相同.
26                   #
27                   # 复制一个相同的随机数序列在什么时候有用呢?
28
29 echo; echo
30
31 RANDOM=2          # 再试一下,但这次使用不同的种子...
32 random_numbers    # 将给出一个不同的随机数序列.
33
34 echo; echo
35
36 # RANDOM=$$  使用脚本的进程id 作为随机数的种子.
37 # 从'time'或'date'命令中取得RANDOM作为种子也是很常用的办法.
38
39 # 一个有想象力的方法...
40 SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
41 #  首先从/dev/urandom(系统伪随机设备文件)中取出1行,
42 #+ 然后着这个可打印行转换为(8进制)数,通过使用"od"命令,
43 #+ 最后使用"awk"来获得一个数,
44 #+ 这个数将作为随机数产生的种子.
45 RANDOM=$SEED
46 random_numbers
47
48 echo; echo
49
50 exit 0
################################End Script#########################################
注意:/dev/urandom设备文件提供了一种比单独使用$RANDOM更好的,能产生更"随机"的随机数
    的方法.
    dd if=/dev/urandom of=targetfile bs=1 count=XX能够产生一个很分散的为随机数.
    然而,将这个数赋值到一个脚本文件的变量中,还需要可操作性,比如使用"od"命令
    (就像上边的例子,见Example 12-13),或者使用dd命令(见Example 12-55),或者管道到
    "md5sum"命令中(见Example 33-14).
    
    当然还有其它的产生伪随机数的方法.Awk就可以提供一个方便的方法.
Example 9-29 使用awk产生伪随机数
################################Start Script#######################################
 1 #!/bin/bash
 2 # random2.sh: 产生一个范围0 - 1的为随机数.
 3 # 使用awk的rand()函数.
 4
 5 AWKSCRIPT=' { srand(); print rand() } '
 6 #            Command(s) / 传到awk中的参数
 7 # 注意,srand()函数用来产生awk的随机数种子.
 8
 9
10 echo -n "Random number between 0 and 1 = "
11
12 echo | awk "$AWKSCRIPT"
13 # 如果你省去'echo'那么将发生什么?
14
15 exit 0
16
17
18 # Exercises:
18 # 练习:
19 # -----
20
21 # 1) 使用循环结构,打印出10个不同的随机数.
22 #      (提示: 在循环的每次执行过程中,你必须使用"srand()"函数来生成不同的
23 #+     种子.如果你没做这件事那么将发生什么?
24
25 # 2) 使用一个整数乘法作为一个放缩因子,在10到100的范围之间,
26 #+   来产生随机数.
27
28 # 3) 同上边的练习 #2,但这次产生随机整数.
################################End Script#########################################
    "data"命令也可以用来产生伪随机整数序列.

注意事项:
[1]        真正的随机事件(在它存在的范围内),只发生在特定的几个未知的自然界现象中,比如
        放射性衰变.计算机只能产生模拟的随机事件,并且计算机产生的"随机"数因此只能称
        为伪随机数.
[2]        计算机产生的伪随机数序列用的种子可以被看成是一种标识标签.比如,使用种子23所
        产生的伪随机数序列就被称作序列#23.
        
        一个伪随机序列的特点就是在这个序列开始重复之前的所有元素的个数的和,也就是
        这个序列的长度.一个好的伪随机产生算法将可以产生一个非常长的不重复的序列.


9.7 双圆括号结构
----------------
((...))与let命令很像,允许算术扩展和赋值.举个简单的例子a=$(( 5 + 3 )),将把a设为
"5+3"或者8.然而,双圆括号也是一种在Bash中允许使用C风格的变量处理的机制.

Example 9-30 C风格的变量处理
################################Start Script#######################################
   1 #!/bin/bash
   2 # 处理一个变量,C风格,使用((...))结构.
   3
   4
   5 echo
   6
   7 (( a = 23 ))  # 给一个变量赋值,从"="两边的空格就能看出这是c风格的处理.
   8 echo "a (initial value) = $a"
   9
  10 (( a++ ))     # 变量'a'后加1,C风格.
  11 echo "a (after a++) = $a"
  12
  13 (( a-- ))     # 变量'a'后减1,C风格.
  14 echo "a (after a--) = $a"
  15
  16
  17 (( ++a ))     # 变量'a'预加1,C风格.
  18 echo "a (after ++a) = $a"
  19
  20 (( --a ))     # 变量'a'预减1,C风格.
  21 echo "a (after --a) = $a"
  22
  23 echo
  24
  25 ########################################################
  26 #  注意:在C语言中,预减和后减操作
  27 #+ 会有些不同的副作用.
  28
  29 n=1; let --n && echo "True" || echo "False"  # False
  30 n=1; let n-- && echo "True" || echo "False"  # True
  31
  32 #  Thanks, Jeroen Domburg.
  33 ########################################################
  34
  35 echo
  36
  37 (( t = a<45?7:11 ))   # C风格的3元操作.
  38 echo "If a < 45, then t = 7, else t = 11."
  39 echo "t = $t "        # Yes!
  40
  41 echo
  42
  43
  44 # ----------------
  45 # 复活节彩蛋注意!
  46 # ----------------
  47 #  Chet Ramey 显然的偷偷摸摸的做了一些未公开的C风格的结构
  48 #+ 放在Bash中(准确地说是根据ksh来改写的,这更接近些)
  49 #  在Bash文档中,Ramey调用((...))shell算法,
  50 #+ 但是它可以走得更远.
  51 #  对不起, Chet, 现在秘密被公开了.
  52
  53 # See also "for" and "while" loops using the ((...)) construct.
  53 # 也参考一些"for"和"while"循环中使用((...))结构的例子.
  54
  55 # 这些只能工作在2.04或者更高版本的Bash中.
  56
  57 exit 0
################################End Script#########################################
见Example 10-12.



第10章    循环和分支
==================
对代码块进行操作是有组织的结构化的shell脚本的关键.为了达到这个目的,循环和分支提供
帮助.


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

for loops

for arg in [list]
    这是一个基本的循环结构.它与C的相似结构有很大不同.

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

    注意:在循环的每次执行中,arg将顺序的存取list中列出的变量.
    1 for arg in "$var1" "$var2" "$var3" ... "$varN"  
    2 # 在第1次循环中, arg = $var1        
    3 # 在第2次循环中, arg = $var2        
    4 # 在第3次循环中, arg = $var3        
    5 # ...
    6 # 在第n次循环中, arg = $varN
    7
    8 # 在[list]中的参数加上双引号是为了阻止单词分离.

    list中的参数允许包含通配符.
    如果do和for想在同一行出现,那么在它们之间需要添加一个";".
    for arg in [list]; do

Example 10-1 循环的一个简单例子
################################Start Script#######################################
 1 #!/bin/bash
 2 # 列出所有行星.
 3
 4 for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
 5 do
 6   echo $planet  # Each planet on a separate line.
 7 done
 8
 9 echo
10
11 for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
12 # 所有的行星都在同一行上.
13 # 完整的'list'作为一个变量都封在""中
14 do
15   echo $planet
16 done
17
18 exit 0
################################End Script#########################################

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

Example 10-2 每个[list]元素带两个参数的for循环
################################Start Script#######################################
 1 #!/bin/bash
 2 # 还是行星.
 3
 4 # 分配行星的名字和它距太阳的距离.
 5
 6 for planet in "Mercury 36" "Venus 67" "Earth 93"  "Mars 142" "Jupiter 483"
 7 do
 8   set -- $planet  # 解析变量"planet"并且设置位置参数.
 9   # "--" 将防止$planet为空,或者是以一个破折号开头.
10
11   # 可能需要保存原始的位置参数,因为它们被覆盖了.
12   # 一种方法就是使用数组,
13   #        original_params=("$@")
14
15   echo "$1        $2,000,000 miles from the sun"
16   #-------two  tabs---把后边的0和$2连接起来
17 done
18
19 # (Thanks, S.C., for additional clarification.)
20
21 exit 0
################################End Script#########################################

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

Example 10-3 文件信息:对包含在变量中的文件列表进行操作
################################Start Script#######################################
 1 #!/bin/bash
 2 # fileinfo.sh
 3
 4 FILES="/usr/sbin/accept
 5 /usr/sbin/pwck
 6 /usr/sbin/chroot
 7 /usr/bin/fakefile
 8 /sbin/badblocks
 9 /sbin/ypbind"     # 你关心的文件列表.
10                   # 扔进去一个假文件, /usr/bin/fakefile.
11
12 echo
13
14 for file in $FILES
15 do
16
17   if [ ! -e "$file" ]       # 检查文件是否存在.
18   then
19     echo "$file does not exist."; echo
20     continue                # 继续下一个.
21    fi
22
23   ls -l $file | awk '{ print $9 "         file size: " $5 }'  # 打印2个域.
24   whatis `basename $file`   # 文件信息.
25   # 注意whatis数据库需要提前建立好.
26   # 要想达到这个目的, 以root身份运行/usr/bin/makewhatis.
27   echo
28 done  
29
30 exit 0
################################End Script#########################################

    如果在for循环的[list]中有通配符(*和?),那将会产生文件名扩展,也就是file globbing.
Example 10-4 在for循环中操作文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # list-glob.sh: 产生 [list] 在for循环中, 使用 "globbing"
 3
 4 echo
 5
 6 for file in *
 7 #           ^  在表达式中识别file globbing时,
 8 #+             Bash 将执行文件名扩展
 9 do
10   ls -l "$file"  # 列出所有在$PWD(当前目录)中的所有文件.
11   #  回想一下,通配符"*"能够匹配所有文件,
12   #+ 然而,在"globbing"中,是不能比配"."文件的.
13
14   #  If the pattern matches no file, it is expanded to itself.
14   #  如果没匹配到任何文件,那它将扩展成自己.
15   #  为了不让这种情况发生,那就设置nullglob选项
16   #+   (shopt -s nullglob).
17   #  Thanks, S.C.
18 done
19
20 echo; echo
21
22 for file in [jx]*
23 do
24   rm -f $file    # 只删除当前目录下以"j"或"x"开头的文件.
25   echo "Removed file \"$file\"".
26 done
27
28 echo
29
30 exit 0
################################End Script#########################################

    在一个for循环中忽略[list]的话,将会使循环操作$@(从命令行传递给脚本的参数列表).
    一个非常好的例子,见Example A-16.
Example 10-5 在for循环中省略[list]
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  使用两种方法来调用这个脚本,一种是带参数的情况,另一种不带参数.
 4 #+ 观察此脚本的行为各是什么样的?
 5
 6 for a
 7 do
 8  echo -n "$a "
 9 done
10
11 #  The 'in list' missing, therefore the loop operates on '$@'
11 #  没有[list],所以循环将操作'$@'
12 #+ (包括空白的命令参数列表).
13
14 echo
15
16 exit 0
################################End Script#########################################

    也可以使用命令替换来产生for循环的[list].具体见Example 12-49,Example 10-10,
    和Example 12-43.
Example 10-6 使用命令替换来产生for循环的[list]
################################Start Script#######################################
 1 #!/bin/bash
 2 #  for-loopcmd.sh: 带[list]的for循环
 3 #+ [list]是由命令替换产生的.
 4
 5 NUMBERS="9 7 3 8 37.53"
 6
 7 for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
 8 do
 9   echo -n "$number "
10 done
11
12 echo
13 exit 0
################################End Script#########################################
    下边是一个用命令替换来产生[list]的更复杂的例子.
Example 10-7 对于二进制文件的一个grep替换
################################Start Script#######################################
 1 #!/bin/bash
 2 # bin-grep.sh: 在一个二进制文件中定位匹配字串.
 3
 4 # 对于二进制文件的一个grep替换
 5 # 与"grep -a"的效果相似
 6
 7 E_BADARGS=65
 8 E_NOFILE=66
 9
10 if [ $# -ne 2 ]
11 then
12   echo "Usage: `basename $0` search_string filename"
13   exit $E_BADARGS
14 fi
15
16 if [ ! -f "$2" ]
17 then
18   echo "File \"$2\" does not exist."
19   exit $E_NOFILE
20 fi  
21
22
23 IFS="\n"         # 由Paulo Marcel Coelho Aragao提出的建议.
24 for word in $( strings "$2" | grep "$1" )
25 # "strings" 命令列出二进制文件中的所有字符串.
26 # 输出到管道交给"grep",然后由grep命令来过滤字符串.
27 do
28   echo $word
29 done
30
31 # S.C. 指出, 行23 - 29 可以被下边的这行来代替,
32 #    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'
33
34
35 # 试试用"./bin-grep.sh mem /bin/ls"来运行这个脚本.
36
37 exit 0
################################End Script#########################################
    大部分相同.
Example 10-8 列出系统上的所有用户
################################Start Script#######################################
 1 #!/bin/bash
 2 # userlist.sh
 3
 4 PASSWORD_FILE=/etc/passwd
 5 n=1           # User number
 6
 7 for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
 8 # 域分隔   = :           ^^^^^^
 9 # 打印出第一个域                 ^^^^^^^^
10 # 从password文件中取得输入                   ^^^^^^^^^^^^^^^^^
11 do
12   echo "USER #$n = $name"
13   let "n += 1"
14 done  
15
16
17 # USER #1 = root
18 # USER #2 = bin
19 # USER #3 = daemon
20 # ...
21 # USER #30 = bozo
22
23 exit 0
24
25 #  练习 :
26 #  ------
27 #  一个普通用户(或者是一个普通用户运行的脚本)
28 #+ 怎么能读取/etc/password呢?
29 #  这是否是一个安全漏洞? 为什么是?为什么不是?
################################End Script#########################################
    关于用命令替换来产生[list]的最后的例子.
Example 10-9 在目录的所有文件中查找源字串
################################Start Script#######################################
 1 #!/bin/bash
 2 # findstring.sh:
 3 # 在一个指定目录的所有文件中查找一个特定的字符串.
 4
 5 directory=/usr/bin/
 6 fstring="Free Software Foundation"  # 查看那个文件中包含FSF.
 7
 8 for file in $( find $directory -type f -name '*' | sort )
 9 do
10   strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
11   #  在"sed"表达式中,
12   #+ 我们必须替换掉正常的替换分隔符"/",
13   #+ 因为"/"碰巧是我们需要过滤的字串之一.
14   #  如果不用"%"代替"/"作为分隔符,那么这个操作将失败,并给出一个错误消息.(试试)
15 done  
16
17 exit 0
18
19 #  练习 (easy):
20 #  ------------
21 #  将内部用的$directory和$fstring变量,用从
22 #+ 命令行参数代替.
################################End Script#########################################

    for循环的输出也可以通过管道传递到一个或多个命令中.
Example 10-10 列出目录中所有的符号连接文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # symlinks.sh: 列出目录中所有的符号连接文件.
 3
 4
 5 directory=${1-`pwd`}
 6 #  如果没有其他的特殊指定,
 7 #+ 默认为当前工作目录.
 8 #  下边的代码块,和上边这句等价.
 9 # ----------------------------------------------------------
10 # ARGS=1                 # 需要一个命令行参数.
11 #
12 # if [ $# -ne "$ARGS" ]  # 如果不是一个参数的话...
13 # then
14 #   directory=`pwd`      # 当前工作目录
15 # else
16 #   directory=$1
17 # fi
18 # ----------------------------------------------------------
19
20 echo "symbolic links in directory \"$directory\""
21
22 for file in "$( find $directory -type l )"   # -type l 就是符号连接文件
23 do
24   echo "$file"
25 done | sort                                  # 否则列出的文件将是未排序的
26 #  严格上说,此处并不一定非要一个循环不可,
27 #+ 因为"find"命令的结果将被扩展成一个单词.
28 #  然而,这种方式很容易理解和说明.
29
30 #  Dominik 'Aeneas' Schnitzer 指出,
31 #+ 如果没将 $( find $directory -type l )用""引用起来的话
32 #+ 那么将会把一个带有空白部分的文件名拆成以空白分隔的两部分(文件名中允许有空白).
33 #  即使这只将取出每个参数的第一个域.
34
35 exit 0
36
37
38 # Jean Helou 建议使用下边的方法:
39
40 echo "symbolic links in directory \"$directory\""
41 # 当前IFS的备份.要小心使用这个值.
42 OLDIFS=$IFS
43 IFS=:
44
45 for file in $(find $directory -type l -printf "%p$IFS")
46 do     #                              ^^^^^^^^^^^^^^^^
47        echo "$file"
48        done|sort
################################End Script#########################################

    循环的输出可以重定向到文件中,我们对上边的例子做了一点修改.
Example 10-11 将目录中的符号连接文件名保存到一个文件中
################################Start Script#######################################
 1 #!/bin/bash
 2 # symlinks.sh: 列出目录中所有的符号连接文件.
 3
 4 OUTFILE=symlinks.list                         # 保存的文件
 5
 6 directory=${1-`pwd`}
 7 #  如果没有其他的特殊指定,
 8 #+ 默认为当前工作目录.
 9
10
11 echo "symbolic links in directory \"$directory\"" > "$OUTFILE"
12 echo "---------------------------" >> "$OUTFILE"
13
14 for file in "$( find $directory -type l )"    # -type l 为符号链接
15 do
16   echo "$file"
17 done | sort >> "$OUTFILE"                     # 循环的输出
18 #           ^^^^^^^^^^^^^                       重定向到一个文件中
19
20 exit 0
################################End Script#########################################

    有一种非常像C语言的for循环的语法形式.这需要使用(()).
Example 10-12 一个C风格的for循环
################################Start Script#######################################
 1 #!/bin/bash
 2 # 两种循环到10的方法.
 3
 4 echo
 5
 6 # 标准语法.
 7 for a in 1 2 3 4 5 6 7 8 9 10
 8 do
 9   echo -n "$a "
10 done  
11
12 echo; echo
13
14 # +==========================================+
15
16 # 现在, 让我们用C风格的语法做同样的事.
17
18 LIMIT=10
19
20 for ((a=1; a <= LIMIT ; a++))  # Double parentheses, and "LIMIT" with no "$".
20 for ((a=1; a <= LIMIT ; a++))  # 双圆括号, 并且"LIMIT"变量前边没有 "$".
21 do
22   echo -n "$a "
23 done                           # 这是一个借用'ksh93'的结构.
24
25 echo; echo
26
27 # +=========================================================================+
28
29 # 让我们使用C的逗号操作符,来同时增加两个变量的值.
30
31 for ((a=1, b=1; a <= LIMIT ; a++, b++))  # 逗号将同时进行2条操作.
32 do
33   echo -n "$a-$b "
34 done
35
36 echo; echo
37
38 exit 0
################################End Script#########################################
    参考Example 26-15,Example 26-16,和Example A-6.
    ---
    现在来一个现实生活中使用的for循环.
Example 10-13 在batch mode中使用efax
################################Start Script#######################################
 1 #!/bin/bash
 2 # Faxing ('fax' 必须已经被安装过了).
 3
 4 EXPECTED_ARGS=2
 5 E_BADARGS=65
 6
 7 if [ $# -ne $EXPECTED_ARGS ]
 8 # 检查命令行参数的个数是否正确.
 9 then
10    echo "Usage: `basename $0` phone# text-file"
11    exit $E_BADARGS
12 fi
13
14
15 if [ ! -f "$2" ]
16 then
17   echo "File $2 is not a text file"
18   exit $E_BADARGS
19 fi
20   
21
22 fax make $2              # 从文本文件中创建传真格式的文件.
23
24 for file in $(ls $2.0*)  # 连接转换过的文件.
25                          # 在变量列表中使用通配符.
26 do
27   fil="$fil $file"
28 done  
29
30 efax -d /dev/ttyS3 -o1 -t "T$1" $fil   # 干活的地方.
31
32
33 # S.C. 指出, 通过下边的命令可以省去for循环.
34 #    efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
35 # 但这并不十分有讲解意义[嘿嘿].
36
37 exit 0
################################End Script#########################################

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

    while [condition]
    do
        command...
    done

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

    while [condition]; do

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

Example 10-14 简单的while循环
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 var0=0
 4 LIMIT=10
 5
 6 while [ "$var0" -lt "$LIMIT" ]
 7 do
 8   echo -n "$var0 "        # -n 将会阻止产生新行.
 9   #             ^           空格,数字之间的分隔.
10
11   var0=`expr $var0 + 1`   # var0=$(($var0+1))  也可以.
12                           # var0=$((var0 + 1)) 也可以.
13                           # let "var0 += 1"    也可以.
14 done                      # 使用其他的方法也行.
15
16 echo
17
18 exit 0
################################End Script#########################################

Example 10-15 另一个while循环
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 echo
 4                                # 等价于:
 5 while [ "$var1" != "end" ]     # while test "$var1" != "end"
 6 do
 7   echo "Input variable #1 (end to exit) "
 8   read var1                    # 为什么不使用'read $var1'?
 9   echo "variable #1 = $var1"   # 因为包含"#"字符,所以需要""
10   # 如果输入为'end',那么就在这里echo.
11   # 不在这里判断结束,在循环顶判断.
12   echo
13 done  
14
15 exit 0
################################End Script#########################################

    一个while循环可以有多个判断条件,但是只有最后一个才能决定是否退出循环.然而这需
    要一种有点不同的循环语法.
Example 10-16 多条件的while循环
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 var1=unset
 4 previous=$var1
 5
 6 while echo "previous-variable = $previous"
 7       echo
 8       previous=$var1
 9       [ "$var1" != end ] # 记录之前的$var1.
10       # 这个"while"循环中有4个条件, 但是只有最后一个能控制循环.
11       # 退出状态由第4个条件决定.
12 do
13 echo "Input variable #1 (end to exit) "
14   read var1
15   echo "variable #1 = $var1"
16 done  
17
18 # 尝试理解这个脚本的运行过程.
19 # 这里还是有点小技巧的.
20
21 exit 0
################################End Script#########################################

    与for循环一样,while循环也可通过(())来使用C风格语法.(见Example 9-30)
Example 10-17 C风格的while循环
################################Start Script#######################################
 1 #!/bin/bash
 2 # wh-loopc.sh: 循环10次的while循环.
 3
 4 LIMIT=10
 5 a=1
 6
 7 while [ "$a" -le $LIMIT ]
 8 do
 9   echo -n "$a "
10   let "a+=1"
11 done           # 到目前为止都没什么令人惊奇的地方.
12
13 echo; echo
14
15 # +=================================================================+
16
17 # 现在, 重复C风格的语法.
18
19 ((a = 1))      # a=1
20 # 双圆括号允许赋值两边的空格,就像C语言一样.
21
22 while (( a <= LIMIT ))   # 双圆括号, 变量前边没有"$".
23 do
24   echo -n "$a "
25   ((a += 1))   # let "a+=1"
26   # Yes, 看到了吧.
27   # 双圆括号允许像C风格的语法一样增加变量的值.
28 done
29
30 echo
31
32 # 现在,C程序员可以在Bash中找到回家的感觉了吧.
33
34 exit 0
################################End Script#########################################
    注意:while循环的stdin可以用<来重定向到文件.
        whild循环的stdin支持管道.

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

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

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

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

    until [condition-is-true]; do

Example 10-18 until循环
################################Start Script#######################################
1 #!/bin/bash
2
3 END_CONDITION=end
4
5 until [ "$var1" = "$END_CONDITION" ]
6 # 在循环的顶部判断条件.
7 do
8   echo "Input variable #1 "
 9   echo "($END_CONDITION to exit)"
10   read var1
11   echo "variable #1 = $var1"
12   echo
13 done  
14
15 exit 0
################################End Script#########################################



10.2 嵌套循环
-------------
嵌套循环就是在一个循环中还有一个循环,内部循环在外部循环体中.在外部循环的每次执行过
程中都会触发内部循环,直到内部循环执行结束.外部循环执行了多少次,内部循环就完成多少
次.当然,不论是外部循环或内部循环的break语句都会打断处理过程.

Example 10-19 嵌套循环
################################Start Script#######################################
 1 #!/bin/bash
 2 # nested-loop.sh: 嵌套的"for" 循环.
 3
 4 outer=1             # 设置外部循环计数.
 5
 6 # 开始外部循环.
 7 for a in 1 2 3 4 5
 8 do
 9   echo "Pass $outer in outer loop."
10   echo "---------------------"
11   inner=1           # 重设内部循环的计数.
12
13   # ===============================================
14   # 开始内部循环.
15   for b in 1 2 3 4 5
16   do
17     echo "Pass $inner in inner loop."
18     let "inner+=1"  # 增加内部循环计数.
19   done
20   # 内部循环结束.
21   # ===============================================
22
23   let "outer+=1"    # 增加外部循环的计数.
24   echo              # 每次外部循环之间的间隔.
25 done               
26 # 外部循环结束.
27
28 exit 0
################################End Script#########################################



10.3 循环控制
-------------
影响循环行为的命令

break,continue
    break和continue这两个循环控制命令[1]与其它语言的类似命令的行为是相同的.break
    命令将会跳出循环,continue命令将会跳过本次循环下边的语句,直接进入下次循环.

Example 10-20 break和continue命令在循环中的效果
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 LIMIT=19  # 上限
 4
 5 echo
 6 echo "Printing Numbers 1 through 20 (but not 3 and 11)."
 7
 8 a=0
 9
10 while [ $a -le "$LIMIT" ]
11 do
12  a=$(($a+1))
13
14  if [ "$a" -eq 3 ] || [ "$a" -eq 11 ]  # 除了3和11.
15  then
16    continue      # 跳过本次循环剩下的语句.
17  fi
18
19  echo -n "$a "   # 在$a等于3和11的时候,这句将不会执行.
20 done
21
22 # 练习:
23 # 为什么循环会打印出20?
24
25 echo; echo
26
27 echo Printing Numbers 1 through 20, but something happens after 2.
28
29 ##################################################################
30
31 # Same loop, but substituting 'break' for 'continue'.
31 # 同样的循环, 但是用'break'来代替'continue'.
32
33 a=0
34
35 while [ "$a" -le "$LIMIT" ]
36 do
37  a=$(($a+1))
38
39  if [ "$a" -gt 2 ]
40  then
41    break  # 将会跳出整个循环.
42  fi
43
44  echo -n "$a "
45 done
46
47 echo; echo; echo
48
49 exit 0
################################End Script#########################################

    break命令可以带一个参数.一个不带参数的break循环只能退出最内层的循环,而break N
    可以退出N层循环.
Example 10-21 多层循环的退出
################################Start Script#######################################
 1 #!/bin/bash
 2 # break-levels.sh: 退出循环.
 3
 4 # "break N" 退出N层循环.
 5
 6 for outerloop in 1 2 3 4 5
 7 do
 8   echo -n "Group $outerloop:   "
 9
10   # --------------------------------------------------------
11   for innerloop in 1 2 3 4 5
12   do
13     echo -n "$innerloop "
14
15     if [ "$innerloop" -eq 3 ]
16     then
17       break  # 试试 break 2 来看看发生什么.
18              # (内部循环和外部循环都被退出了.)
19     fi
20   done
21   # --------------------------------------------------------
22
23   echo
24 done  
25
26 echo
27
28 exit 0
################################End Script#########################################

    continue命令也可以带一个参数.一个不带参数的continue命令只去掉本次循环的剩余代码
    .而continue N将会把N层循环剩余的代码都去掉,但是循环的次数不变.
Example 10-22 多层循环的continue
################################Start Script#######################################
 1 #!/bin/bash
 2 # "continue N" 命令, 将让N层的循环全部被continue.
 3
 4 for outer in I II III IV V           # 外部循环
 5 do
 6   echo; echo -n "Group $outer: "
 7
 8   # --------------------------------------------------------------------
 9   for inner in 1 2 3 4 5 6 7 8 9 10  # 内部循环
10   do
11
12     if [ "$inner" -eq 7 ]
13     then
14       continue 2  # continue 2层, 也就是到outer循环上.
15                   # 将"continue 2"替换为一个单独的"continue"
16                   # 来看一下一个正常循环的行为.
17     fi  
18
19     echo -n "$inner "  # 7 8 9 10 将不会被echo
20   done  
21   # --------------------------------------------------------------------
22    #译者注:如果在此处添加echo的话,当然也不会输出.
23 done
24
25 echo; echo
26
27 # 练习:
28 # 准备一个有意义的"continue N"的使用,放在脚本中.
29
30 exit 0
################################End Script#########################################

Example 10-23 在实际的任务中使用"continue N"
################################Start Script#######################################
 1 # Albert Reiner 给出了一个关于使用"continue N"的例子:
 2 # ---------------------------------------------------
 3
 4 #  Suppose I have a large number of jobs that need to be run, with
 5 #+ any data that is to be treated in files of a given name pattern in a
 6 #+ directory. There are several machines that access this directory, and
 7 #+ I want to distribute the work over these different boxen. Then I
 8 #+ usually nohup something like the following on every box:
 9
10 while true
11 do
12   for n in .iso.*
13   do
14     [ "$n" = ".iso.opts" ] && continue
15     beta=${n#.iso.}
16     [ -r .Iso.$beta ] && continue
17     [ -r .lock.$beta ] && sleep 10 && continue
18     lockfile -r0 .lock.$beta || continue
19     echo -n "$beta: " `date`
20     run-isotherm $beta
21     date
22     ls -alF .Iso.$beta
23     [ -r .Iso.$beta ] && rm -f .lock.$beta
24     continue 2
25   done
26   break
27 done
28
29 #  The details, in particular the sleep N, are particular to my
30 #+ application, but the general pattern is:
31
32 while true
33 do
34   for job in {pattern}
35   do
36     {job already done or running} && continue
37     {mark job as running, do job, mark job as done}
38     continue 2
39   done
40   break        # Or something like `sleep 600' to avoid termination.
41 done
42
43 #  This way the script will stop only when there are no more jobs to do
44 #+ (including jobs that were added during runtime). Through the use
45 #+ of appropriate lockfiles it can be run on several machines
46 #+ concurrently without duplication of calculations [which run a couple
47 #+ of hours in my case, so I really want to avoid this]. Also, as search
48 #+ always starts again from the beginning, one can encode priorities in
49 #+ the file names. Of course, one could also do this without `continue 2',
50 #+ but then one would have to actually check whether or not some job
51 #+ was done (so that we should immediately look for the next job) or not
52 #+ (in which case we terminate or sleep for a long time before checking
53 #+ for a new job).
################################End Script#########################################
    注意:continue N结构如果被用在一个有意义的上下文中的话,往往都很难理解,并且技巧性
        很高.所以最好的方法就是尽量避免它.

注意事项:
[1]        这两个命令是shell的内建命令,而不像其它的循环命令那样,比如while和case,这两个
        是关键字.

返回顶部

发表评论:

Powered By Z-BlogPHP 1.7.3


知识共享许可协议
本作品采用知识共享署名 3.0 中国大陆许可协议进行许可。
网站备案号粤ICP备15104741号-1