魅力博客

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

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



Example 27-2 搜索与一个PID相关的进程
################################Start Script#######################################
 1 #!/bin/bash
 2 # pid-identifier.sh: 给出指定PID的进程的程序全路径.
 3
 4 ARGNO=1  # 此脚本期望的参数个数.
 5 E_WRONGARGS=65
 6 E_BADPID=66
 7 E_NOSUCHPROCESS=67
 8 E_NOPERMISSION=68
 9 PROCFILE=exe
10
11 if [ $# -ne $ARGNO ]
12 then
13   echo "Usage: `basename $0` PID-number" >&2  # 帮助信息重定向到标准出错.
14   exit $E_WRONGARGS
15 fi  
16
17 pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1 )
18 # 搜索命令"ps"输出的第一列.
19 # 然后再次确认是真正我们要寻找的进程,而不是这个脚本调用而产生的进程.
20 # 后一个"grep $1"会滤掉这个可能产生的进程.
21 #
22 #    pidno=$( ps ax | awk '{ print $1 }' | grep $1 )
23 #    也可以, 由 Teemu Huovila指出.
24
25 if [ -z "$pidno" ]  # 如果过滤完后结果是一个空字符串,
26 then                # 没有对应的PID进程在运行.
27   echo "No such process running."
28   exit $E_NOSUCHPROCESS
29 fi  
30
31 # 也可以用:
32 #   if ! ps $1 > /dev/null 2>&1
33 #   then                # 没有对应的PID进程在运行.
34 #     echo "No such process running."
35 #     exit $E_NOSUCHPROCESS
36 #    fi
37
38 # 为了简化整个进程,使用"pidof".
39
40
41 if [ ! -r "/proc/$1/$PROCFILE" ]  # 检查读权限.
42 then
43   echo "Process $1 running, but..."
44   echo "Can't get read permission on /proc/$1/$PROCFILE."
45   exit $E_NOPERMISSION  # 普通用户不能存取/proc目录的某些文件.
46 fi  
47
48 # 最后两个测试可以用下面的代替:
49 #    if ! kill -0 $1 > /dev/null 2>&1 # '0'不是一个信号,
50                                       # 但这样可以测试是否可以
51                                       # 向该进程发送信号.
52 #    then echo "PID doesn't exist or you're not its owner" >&2
53 #    exit $E_BADPID
54 #    fi
55
56
57
58 exe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' )
59 # 或       exe_file=$( ls -l /proc/$1/exe | awk '{print $11}' )
60 #
61 # /proc/pid-number/exe 是进程程序全路径的符号链接.
62 #
63
64 if [ -e "$exe_file" ]  # 如果 /proc/pid-number/exe 存在 ...
65 then                 # 则相应的进程存在.
66   echo "Process #$1 invoked by $exe_file."
67 else
68   echo "No such process running."
69 fi  
70
71
72 # 这个被详细讲解的脚本几乎可以用下面的命令代替:
73 # ps ax | grep $1 | awk '{ print $5 }'
74 # 然而, 这样并不会工作...
75 # 因为'ps'输出的第5列是进程的argv[0](即命令行第一个参数,调用时程序用的程序路径本身),
76 # 但不是可执行文件.
77 #
78 # 然而, 下面的两个都可以工作.
79 #       find /proc/$1/exe -printf '%l\n'
80 #       lsof -aFn -p $1 -d txt | sed -ne 's/^n//p'
81
82 # 由Stephane Chazelas附加注释.
83
84 exit 0
################################End Script#########################################

Example 27-3 网络连接状态
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 PROCNAME=pppd        # ppp 守护进程
 4 PROCFILENAME=status  # 在这儿寻找信息.
 5 NOTCONNECTED=65
 6 INTERVAL=2           # 两秒刷新一次.
 7
 8 pidno=$( ps ax | grep -v "ps ax" | grep -v grep | grep $PROCNAME | awk '{ print $1 }' )
 9 # 搜索ppp守护进程'pppd'的进程号.
10 # 一定要过滤掉由搜索进程产生的该行进程.
11 #
12 #  正如Oleg Philon指出的那样,
13 #+ 使用"pidof"命令会相当的简单.
14 #  pidno=$( pidof $PROCNAME )
15 #
16 #  颇有良心的建议:
17 #+ 当命令序列变得复杂的时候,去寻找更简洁的办法. .
18
19
20 if [ -z "$pidno" ]   # 如果没有找到此进程号,则进程没有运行.
21 then
22   echo "Not connected."
23   exit $NOTCONNECTED
24 else
25   echo "Connected."; echo
26 fi
27
28 while [ true ]       # 死循环,这儿可以有所改进.
29 do
30
31   if [ ! -e "/proc/$pidno/$PROCFILENAME" ]
32   # 进程运行时,对应的"status"文件会存在.
33   then
34     echo "Disconnected."
35     exit $NOTCONNECTED
36   fi
37
38 netstat -s | grep "packets received"  # 取得一些连接统计.
39 netstat -s | grep "packets delivered"
40
41
42   sleep $INTERVAL
43   echo; echo
44
45 done
46
47 exit 0
48
49 # 当要停止它时,可以用Control-C终止.
50
51 #    练习:
52 #    ---------
53 #    改进这个脚本,使它能按"q"键退出.
54 #    给脚本更友好的界面.
################################End Script#########################################

注意:    一般来说, 写/proc目录里的文件是危险 ,因为这样会破坏这个文件系统或摧毁机器.

注意事项:
[1]        一些系统命令, 例如 procinfo, free, vmstat, lsdev, 和uptime也能做类似的事情.


第28章    关于Zeros和Nulls
========================
/dev/zero和/dev/null

使用/dev/null

    把/dev/null看作"黑洞". 它非常等价于一个只写文件. 所有写入它的内容都会永远丢失.
而尝试从它那儿读取内容则什么也读不到. 然而, /dev/null对命令行和脚本都非常的有用.

    禁止标准输出.

   1 cat $filename >/dev/null
   2 # 文件内容丢失,而不会输出到标准输出.

    禁止标准错误 (来自例子 12-3).

   1 rm $badname 2>/dev/null
   2 #           这样错误信息[标准错误]就被丢到太平洋去了.

    禁止标准输出和标准错误的输出.

   1 cat $filename 2>/dev/null >/dev/null
   2 # 如果"$filename"不存在,将不会有任何错误信息提示.
   3 # 如果"$filename"存在, 文件的内容不会打印到标准输出.
   4 # 因此Therefore, 上面的代码根本不会输出任何信息.
   5 #
   6 #  当只想测试命令的退出码而不想有任何输出时非常有用.
   7 #
   8 #
   9 # cat $filename &>/dev/null
  10 #     也可以, 由 Baris Cicek 指出.

    删除一个文件的内容, 但是保留文件本身, 和所有的文件权限(来自于Example 2-1和
    Example 2-3):

   1 cat /dev/null > /var/log/messages
   2 #  : > /var/log/messages   有同样的效果, 但不会产生新的进程.(因为:是内建的)
   3
   4 cat /dev/null > /var/log/wtmp

    自动清空日志文件的内容 (特别适合处理这些由商业Web站点发送的讨厌的"cookies"):

Example 28-1 隐藏cookie而不再使用
################################Start Script#######################################
1 if [ -f ~/.netscape/cookies ]  # 如果存在则删除.
2 then
3   rm -f ~/.netscape/cookies
4 fi
5
6 ln -s /dev/null ~/.netscape/cookies
7 # 现在所有的cookies都会丢入黑洞而不会保存在磁盘上了.
################################End Script#########################################
使用/dev/zero
    像/dev/null一样, /dev/zero也是一个伪文件, 但它实际上产生连续不断的null的流
    (二进制的零流,而不是ASCII型的). 写入它的输出会丢失不见, 而从/dev/zero读出一
    连串的null也比较困难, 虽然这也能通过od或一个十六进制编辑器来做到. /dev/zero主
    要的用处是用来创建一个指定长度用于初始化的空文件,就像临时交换文件.

Example 28-2 用/dev/zero创建一个交换临时文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # 创建一个交换文件.
 3
 4 ROOT_UID=0         # Root 用户的 $UID 是 0.
 5 E_WRONG_USER=65    # 不是 root?
 6
 7 FILE=/swap
 8 BLOCKSIZE=1024
 9 MINBLOCKS=40
10 SUCCESS=0
11
12
13 # 这个脚本必须用root来运行.
14 if [ "$UID" -ne "$ROOT_UID" ]
15 then
16   echo; echo "You must be root to run this script."; echo
17   exit $E_WRONG_USER
18 fi  
19   
20
21 blocks=${1:-$MINBLOCKS}          #  如果命令行没有指定,
22                                  #+ 则设置为默认的40块.
23 # 上面这句等同如:
24 # --------------------------------------------------
25 # if [ -n "$1" ]
26 # then
27 #   blocks=$1
28 # else
29 #   blocks=$MINBLOCKS
30 # fi
31 # --------------------------------------------------
32
33
34 if [ "$blocks" -lt $MINBLOCKS ]
35 then
36   blocks=$MINBLOCKS              # 最少要有 40 个块长.
37 fi  
38
39
40 echo "Creating swap file of size $blocks blocks (KB)."
41 dd if=/dev/zero of=$FILE bs=$BLOCKSIZE count=$blocks  # 把零写入文件.
42
43 mkswap $FILE $blocks             # 将此文件建为交换文件(或称交换分区).
44 swapon $FILE                     # 激活交换文件.
45
46 echo "Swap file created and activated."
47
48 exit $SUCCESS
################################End Script#########################################
关于 /dev/zero 的另一个应用是为特定的目的而用零去填充一个指定大小的文件, 如挂载一个
文件系统到环回设备 (loopback device) (参考例子 13-8) 或"安全地" 删除一个文件
(参考例子 12-55).

Example 28-3 创建ramdisk
################################Start Script#######################################
 1 #!/bin/bash
 2 # ramdisk.sh
 3
 4 #  "ramdisk"是系统RAM内存的一段,
 5 #+ 它可以被当成是一个文件系统来操作.
 6 #  它的优点是存取速度非常快 (包括读和写).
 7 #  缺点: 易失性, 当计算机重启或关机时会丢失数据.
 8 #+       会减少系统可用的RAM.
 9 #
10 #  那么ramdisk有什么作用呢?
11 #  保存一个较大的数据集在ramdisk, 比如一张表或字典,
12 #+ 这样可以加速数据查询, 因为在内存里查找比在磁盘里查找快得多.
13
14
15 E_NON_ROOT_USER=70             # 必须用root来运行.
16 ROOTUSER_NAME=root
17
18 MOUNTPT=/mnt/ramdisk
19 SIZE=2000                      # 2K 个块 (可以合适的做修改)
20 BLOCKSIZE=1024                 # 每块有1K (1024 byte) 的大小
21 DEVICE=/dev/ram0               # 第一个 ram 设备
22
23 username=`id -nu`
24 if [ "$username" != "$ROOTUSER_NAME" ]
25 then
26   echo "Must be root to run \"`basename $0`\"."
27   exit $E_NON_ROOT_USER
28 fi
29
30 if [ ! -d "$MOUNTPT" ]         #  测试挂载点是否已经存在了,
31 then                           #+ 如果这个脚本已经运行了好几次了就不会再建这个目录了
32   mkdir $MOUNTPT               #+ 因为前面已经建立了.
33 fi
34
35 dd if=/dev/zero of=$DEVICE count=$SIZE bs=$BLOCKSIZE  # 把RAM设备的内容用零填充.
36                                                       # 为何需要这么做?
37 mke2fs $DEVICE                 # 在RAM设备上创建一个ext2文件系统.
38 mount $DEVICE $MOUNTPT         # 挂载设备.
39 chmod 777 $MOUNTPT             # 使普通用户也可以存取这个ramdisk.
40                                # 但是, 只能由root来缷载它.
41
42 echo "\"$MOUNTPT\" now available for use."
43 # 现在 ramdisk 即使普通用户也可以用来存取文件了.
44
45 #  注意, ramdisk是易失的, 所以当计算机系统重启或关机时ramdisk里的内容会消失.
46 #
47 #  拷贝所有你想保存文件到一个常规的磁盘目录下.
48
49 # 重启之后, 运行这个脚本再次建立起一个 ramdisk.
50 # 仅重新加载 /mnt/ramdisk 而没有其他的步骤将不会正确工作.
51
52 #  如果加以改进, 这个脚本可以放在 /etc/rc.d/rc.local,
53 #+ 以使系统启动时能自动设立一个ramdisk.
54 #  这样很合适速度要求高的数据库服务器.
55
56 exit 0
################################End Script#########################################
    最后值得一提的是, ELF二进制文件利用了/dev/zero.


第29章    调试
============
Debugging is twice as hard as writing the code in the first place. Therefore,
if you write the code as cleverly as possible, you are, by definition, not smart
enough to debug it.
                                                    Brian Kernighan

Bash shell 没有自带调试器, 甚至没有任何调试类型的命令或结构. [1]  脚本里的语法错误
或拼写错误会产生含糊的错误信息,通常这些在调试非功能性的脚本时没什么帮助.

Example 29-1 一个错误的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # ex74.sh
 3
 4 # 这是一个错误的脚本.
 5 # 哪里有错?
 6
 7 a=37
 8
 9 if [$a -gt 27 ]
10 then
11   echo $a
12 fi  
13
14 exit 0
################################End Script#########################################
脚本的输出:
 ./ex74.sh: [37: command not found
上面的脚本有什么错误(线索: 注意if的后面)?

Example 29-2 丢失关键字(keyword)
################################Start Script#######################################
1 #!/bin/bash
2 # missing-keyword.sh: 会产生什么样的错误信息?
3
4 for a in 1 2 3
5 do
6   echo "$a"
7 # done     # 第7行的必需的关键字 'done' 被注释掉了.
8
9 exit 0  
################################End Script#########################################
脚本的输出:
 missing-keyword.sh: line 10: syntax error: unexpected end of file

注意错误信息中说明的错误行不必一定要参考, 但那行是Bash解释器最终认识到是个错误的
地方.

出错信息可能在报告语法错误的行号时会忽略脚本的注释行.

如果脚本可以执行,但不是你所期望的那样工作怎么办? 这大多是由于常见的逻辑错误产生的.

Example 29-3 另一个错误脚本
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  这个脚本目的是为了删除当前目录下的所有文件,包括文件名含有空格的文件.
 4 #
 5 #  但不能工作.
 6 #  为什么?
 7
 8
 9 badname=`ls | grep ' '`
10
11 # 试试这个:
12 # echo "$badname"
13
14 rm "$badname"
15
16 exit 0
################################End Script#########################################
为了找出 例子 29-3  的错误可以把echo "$badname" 行的注释去掉. echo 出来的信息对你
判断是否脚本以你希望的方式运行时很有帮助.

在这个实际的例子里, rm "$badname" 不会达到想要的结果,因为$badname 没有引用起来.
加上引号以保证rm 命令只有一个参数(这就只能匹配一个文件名). 一个不完善的解决办法是
删除A partial fix is to remove to quotes from $badname and to reset $IFS to contain only a newline, IFS=$'\n'. 不过, 存在更简单的办法.<rojy bug>

   1 # 修正删除包含空格文件名时出错的办法.
   2 rm *\ *
   3 rm *" "*
   4 rm *' '*
   5 # Thank you. S.C.

总结该脚本的症状,

   1. 终止于一个"syntax error"(语法错误)的信息, 或
   2. 它能运行, 但不是按期望的那样运行(逻辑错误).
   3. 它能运行,运行的和期望的一样, 但有讨厌的副作用 (逻辑炸弹).

用来调试不能工作的脚本的工具包括


   1.  echo 语句可用在脚本中的有疑问的点上以跟踪了解变量的值, 并且也可以了解后续脚
        本的动作.

          注意: 最好只在调试时才使用echo语句.

           1 ### debecho (debug-echo), by Stefano Falsetto ###
           2 ### 只有变量 DEBUG 设置了值时才会打印传递进来的变量值. ###
           3 debecho () {
           4   if [ ! -z "$DEBUG" ]; then
           5      echo "$1" >&2
           6      #         ^^^ 打印到标准出错
           7   fi
           8 }
           9
          10 DEBUG=on
          11 Whatever=whatnot
          12 debecho $Whatever   # whatnot
          13
          14 DEBUG=
          15 Whatever=notwhat
          16 debecho $Whatever   # (这儿就不会打印了.)

   2.  使用 tee 过滤器来检查临界点的进程或数据流.

   3.  设置选项 -n -v -x

      sh -n scriptname 不会实际运行脚本,而只是检查脚本的语法错误. 这等同于把
        set -n 或 set -o noexec 插入脚本中. 注意还是有一些语法错误不能被这种检查找
        出来.

      sh -v scriptname 在实际执行一个命令前打印出这个命令. 这也等同于在脚本里设置
         set -v 或 set -o verbose.

      选项 -n 和 -v 可以一块使用. sh -nv scriptname 会打印详细的语法检查.

      sh -x scriptname 打印每个命令的执行结果, 但只用在某些小的方面. 它等同于脚本
        中插入 set -x 或 set -o xtrace.

      把 set -u 或 set -o nounset 插入到脚本里并运行它, 就会在每个试图使用没有申明
        过的变量的地方打印出一个错误信息.

   4.  使用一个"assert"(断言) 函数在脚本的临界点上测试变量或条件.
        (这是从C语言中借用来的.)

Example 29-4 用"assert"测试条件
################################Start Script#######################################
 1 #!/bin/bash
 2 # assert.sh
 3
 4 assert ()                 #  如果条件测试失败,
 5 {                         #+ 则打印错误信息并退出脚本.
 6   E_PARAM_ERR=98
 7   E_ASSERT_FAILED=99
 8
 9
10   if [ -z "$2" ]          # 没有传递足够的参数.
11   then
12     return $E_PARAM_ERR   # 什么也不做就返回.
13   fi
14
15   lineno=$2
16
17   if [ ! $1 ]
18   then
19     echo "Assertion failed:  \"$1\""
20     echo "File \"$0\", line $lineno"
21     exit $E_ASSERT_FAILED
22   # else
23   #   return
24   #   返回并继续执行脚本后面的代码.
25   fi  
26 }    
27
28
29 a=5
30 b=4
31 condition="$a -lt $b"     #  会错误信息并从脚本退出.
32                           #  把这个“条件”放在某个地方,
33                           #+ 然后看看有什么现象.
34
35 assert "$condition" $LINENO
36 # 脚本以下的代码只有当"assert"成功时才会继续执行.
37
38
39 # 其他的命令.
40 # ...
41 echo "This statement echoes only if the \"assert\" does not fail."
42 # ...
43 # 余下的其他命令.
44
45 exit 0
################################End Script#########################################

    5.  用变量$LINENO和内建的caller.

    6.  捕捉exit.
        脚本中的The exit 命令会触发信号0,终结进程,即脚本本身. [2] 这常用来捕捉
        exit命令做某事, 如强制打印变量值. trap 命令必须是脚本中第一个命令.

捕捉信号

trap
    当收到一个信号时指定一个处理动作; 这在调试时也很有用.
    注意: 信号是发往一个进程的非常简单的信息, 要么是由内核发出要么是由另一个进程,
            以告诉接收进程采取一些指定的动作 (一般是中止). 例如, 按Control-C, 发送
            一个用户中断( 即 INT 信号)到运行中的进程.
       1 trap '' 2
       2 # 忽略信号 2 (Control-C), 没有指定处理动作.
       3
       4 trap 'echo "Control-C disabled."' 2
       5 # 当按 Control-C 时显示一行信息.

Example 29-5 捕捉 exit
################################Start Script#######################################
 1 #!/bin/bash
 2 # 用trap捕捉变量值.
 3
 4 trap 'echo Variable Listing --- a = $a  b = $b' EXIT
 5 #  EXIT 是脚本中exit命令产生的信号的信号名.
 6 #
 7 #  由"trap"指定的命令不会被马上执行,只有当发送了一个适应的信号时才会执行.
 8 #
 9
10 echo "This prints before the \"trap\" --"
11 echo "even though the script sees the \"trap\" first."
12 echo
13
14 a=39
15
16 b=36
17
18 exit 0
19 #  注意到注释掉上面一行的'exit'命令也没有什么不同,
20 #+ 这是因为执行完所有的命令脚本都会退出.
################################End Script#########################################

Example 29-6 在Control-C后清除垃圾
################################Start Script#######################################
 1 #!/bin/bash
 2 # logon.sh: 简陋的检查你是否还处于连线的脚本.
 3
 4 umask 177  # 确定临时文件不是全部用户都可读的.
 5
 6
 7 TRUE=1
 8 LOGFILE=/var/log/messages
 9 #  注意 $LOGFILE 必须是可读的
10 #+ (用 root来做:chmod 644 /var/log/messages).
11 TEMPFILE=temp.$$
12 #  创建一个"唯一的"临时文件名, 使用脚本的进程ID.
13 #     用 'mktemp' 是另一个可行的办法.
14 #     举例:
15 #     TEMPFILE=`mktemp temp.XXXXXX`
16 KEYWORD=address
17 #  上网时, 把"remote IP address xxx.xxx.xxx.xxx"这行
18 #                      加到 /var/log/messages.
19 ONLINE=22
20 USER_INTERRUPT=13
21 CHECK_LINES=100
22 #  日志文件中有多少行要检查.
23
24 trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
25 #  如果脚本被control-c中断了,则清除临时文件.
26
27 echo
28
29 while [ $TRUE ]  #死循环.
30 do
31   tail -$CHECK_LINES $LOGFILE> $TEMPFILE
32   #  保存系统日志文件的最后100行到临时文件.
33   #  这是需要的, 因为新版本的内核在登录网络时产生许多日志文件信息.
34   search=`grep $KEYWORD $TEMPFILE`
35   #  检查"IP address" 短语是不是存在,
36   #+ 它指示了一次成功的网络登录.
37
38   if [ ! -z "$search" ] #  引号是必须的,因为变量可能会有一些空白符.
39   then
40      echo "On-line"
41      rm -f $TEMPFILE    #  清除临时文件.
42      exit $ONLINE
43   else
44      echo -n "."        #  -n 选项使echo不会产生新行符,
45                         #+ 这样你可以从该行的继续打印.
46   fi
47
48   sleep 1  
49 done  
50
51
52 #  注: 如果你更改KEYWORD变量的值为"Exit",
53 #+ 这个脚本就能用来在网络登录后检查掉线
54 #
55
56 # 练习: 修改脚本,像上面所说的那样,并修正得更好
57 #
58
59 exit 0
60
61
62 # Nick Drage 建议用另一种方法:
63
64 while true
65   do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
66   echo -n "."   # 在连接上之前打印点 (.....).
67   sleep 2
68 done
69
70 # 问题: 用 Control-C来终止这个进程可能是不够的.
71 #+         (点可能会继续被打印.)
72 # 练习: 修复这个问题.
73
74
75
76 # Stephane Chazelas 也提出了另一个办法:
77
78 CHECK_INTERVAL=1
79
80 while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
81 do echo -n .
82    sleep $CHECK_INTERVAL
83 done
84 echo "On-line"
85
86 # 练习: 讨论这几个方法的优缺点.
87 #
################################End Script#########################################
注意: trap 的DEBUG参数在每个命令执行完后都会引起一个指定的执行动作,例如,这可用来
    跟踪变量.

Example 29-7 跟踪变量
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
 4 # 在每个命令行显示变量$variable 的值.
 5
 6 variable=29
 7
 8 echo "Just initialized \"\$variable\" to $variable."
 9
10 let "variable *= 3"
11 echo "Just multiplied \"\$variable\" by 3."
12
13 exit $?
14
15 #  "trap 'command1 . . . command2 . . .' DEBUG" 的结构适合复杂脚本的环境
16 #+ 在这种情况下多次"echo $variable"比较没有技巧并且也耗时.
17 #
18 #
19
20 # Thanks, Stephane Chazelas 指出这一点.
21
22
23 脚本的输出:
24
25 VARIABLE-TRACE> $variable = ""
26 VARIABLE-TRACE> $variable = "29"
27 Just initialized "$variable" to 29.
28 VARIABLE-TRACE> $variable = "29"
29 VARIABLE-TRACE> $variable = "87"
30 Just multiplied "$variable" by 3.
31 VARIABLE-TRACE> $variable = "87"
################################End Script#########################################

当然, trap 命令除了调试还有其他的用处.

Example 29-8 运行多进程 (在多处理器的机器里)
################################Start Script#######################################
  1 #!/bin/bash
  2 # parent.sh
  3 # 在多处理器的机器里运行多进程.
  4 # 作者: Tedman Eng
  5
  6 #  这是要介绍的两个脚本的第一个,
  7 #+ 这两个脚本都在要在相同的工作目录下.
  8
  9
 10
 11
 12 LIMIT=$1         # 要启动的进程总数
 13 NUMPROC=4        # 当前进程数 (forks?)
 14 PROCID=1         # 启动的进程ID
 15 echo "My PID is $$"
 16
 17 function start_thread() {
 18         if [ $PROCID -le $LIMIT ] ; then
 19                 ./child.sh $PROCID&
 20                 let "PROCID++"
 21         else
 22            echo "Limit reached."
 23            wait
 24            exit
 25         fi
 26 }
 27
 28 while [ "$NUMPROC" -gt 0 ]; do
 29         start_thread;
 30         let "NUMPROC--"
 31 done
 32
 33
 34 while true
 35 do
 36
 37 trap "start_thread" SIGRTMIN
 38
 39 done
 40
 41 exit 0
 42
 43
 44
 45 # ======== 下面是第二个脚本 ========
 46
 47
 48 #!/bin/bash
 49 # child.sh
 50 # 在多处理器的机器里运行多进程.
 51 # 这个脚本由parent.sh脚本调用(即上面的脚本).
 52 # 作者: Tedman Eng
 53
 54 temp=$RANDOM
 55 index=$1
 56 shift
 57 let "temp %= 5"
 58 let "temp += 4"
 59 echo "Starting $index  Time:$temp" "$@"
 60 sleep ${temp}
 61 echo "Ending $index"
 62 kill -s SIGRTMIN $PPID
 63
 64 exit 0
 65
 66
 67 # ======================= 脚本作者注 ======================= #
 68 #  这不是完全没有bug的脚本.
 69 #  我运行LIMIT = 500 ,在过了开头的一二百个循环后,
 70 #+ 这些进程有一个消失了!
 71 #  不能确定是不是因为捕捉信号产生碰撞还是其他的原因.
 72 #  一但信号捕捉到,在下一个信号设置之前,
 73 #+ 会有一个短暂的时间来执行信号处理程序,
 74 #+ 这段时间内很可能会丢失一个信号捕捉,因此失去生成一个子进程的机会.
 75
 76 #  毫无疑问会有人能找出这个bug的原因,并且修复它
 77 #+ . . . 在将来的某个时候.
 78
 79
 80
 81 # ===================================================================== #
 82
 83
 84
 85 # ----------------------------------------------------------------------#
 86
 87
 88
 89 #################################################################
 90 # 下面的脚本由Vernia Damiano原创.
 91 # 不幸地是, 它不能正确工作.
 92 #################################################################
 93
 94 #!/bin/bash
 95
 96 #  必须以最少一个整数参数来调用这个脚本
 97 #+ (这个整数是协作进程的数目).
 98 #  所有的其他参数被传给要启动的进程.
 99
100
101 INDICE=8        # 要启动的进程数目
102 TEMPO=5         # 每个进程最大的睡眼时间
103 E_BADARGS=65    # 没有参数传给脚本的错误值.
104
105 if [ $# -eq 0 ] # 检查是否至少传了一个参数给脚本.
106 then
107   echo "Usage: `basename $0` number_of_processes [passed params]"
108   exit $E_BADARGS
109 fi
110
111 NUMPROC=$1              # 协作进程的数目
112 shift
113 PARAMETRI=( "$@" )      # 每个进程的参数
114
115 function avvia() {
116          local temp
117          local index
118          temp=$RANDOM
119          index=$1
120          shift
121          let "temp %= $TEMPO"
122          let "temp += 1"
123          echo "Starting $index Time:$temp" "$@"
124          sleep ${temp}
125          echo "Ending $index"
126          kill -s SIGRTMIN $$
127 }
128
129 function parti() {
130          if [ $INDICE -gt 0 ] ; then
131               avvia $INDICE "${PARAMETRI[@]}" &
132                 let "INDICE--"
133          else
134                 trap : SIGRTMIN
135          fi
136 }
137
138 trap parti SIGRTMIN
139
140 while [ "$NUMPROC" -gt 0 ]; do
141          parti;
142          let "NUMPROC--"
143 done
144
145 wait
146 trap - SIGRTMIN
147
148 exit $?
149
150 : <<SCRIPT_AUTHOR_COMMENTS
151 我需要运行能指定选项的一个程序,
152 能接受许多不同的文件,并在一个多处理器的机器上运行
153 所以我想(我也将会)使指定数目的进程运行,并且每个进程终止后都能启动一个新的
154
155
156 "wait"命令没什么帮助, 因为它是等候一个指定的或所有的后台进程.
157  所以我写了这个使用了trap指令的bash脚本来做这个任务.
158
159   --Vernia Damiano
160 SCRIPT_AUTHOR_COMMENTS
################################End Script#########################################
注意:    trap '' SIGNAL (两个引号引空) 在脚本中禁用了 SIGNAL 信号的动作(即忽略了).
        trap SIGNAL 则恢复了 SIGNAL 信号前次的处理动作. 这在保护脚本的某些临界点的
        位置不受意外的中断影响时很有用.

   1     trap '' 2  # 信号 2是  Control-C, 现在被忽略了.
   2     command
   3     command
   4     command
   5     trap 2     # 再启用Control-C
   6     

 Bash的版本 3 增加了下面的特殊变量用于调试.

   1.  $BASH_ARGC
   2.  $BASH_ARGV
   3.  $BASH_COMMAND
   4.  $BASH_EXECUTION_STRING
   5.  $BASH_LINENO
   6.  $BASH_SOURCE
   7.  $BASH_SUBSHELL

注意事项:
[1]        Rocky Bernstein的 Bash debugger 实际上填补了这个空白.
[2]        依据惯例,信号0 被指定为退出(exit).


第30章    选项
============
选项用来更改shell或/和脚本行为的机制.

set 命令用来在脚本里激活各种选项. 在脚本中任何你想让选项生效的地方,插入
set -o option-name 或, 用更简短的格式, set -option-abbrev. 这两种格式都是等价的.

   1       #!/bin/bash
   2
   3       set -o verbose
   4       # 执行前打印命令.

   1       #!/bin/bash
   2
   3       set -v
   4       # 和上面的有完全相同的效果.

注意:    为了在脚本里停用一个选项, 插入 set +o option-name 或 set +option-abbrev.

   1       #!/bin/bash
   2
   3       set -o verbose
   4       # 激活命令回显.
   5       command
   6       ...
   7       command
   8
   9       set +o verbose
  10       # 停用命令回显.
  11       command
  12       # 没有回显命令了.
  13
  14
  15       set -v
  16       # 激活命令回显.
  17       command
  18       ...
  19       command
  20
  21       set +v
  22       # 停用命令回显.
  23       command
  24
  25       exit 0
  26       

另一个在脚本里启用选项的方法是在脚本头部的#!后面指定选项.

   1       #!/bin/bash -x
   2       #
   3       # 下面是脚本的主要内容.
   4       

从命令行来激活脚本的选项也是可以办到的. 一些不能和set一起用的选项可以用在命令行指
定. -i是其中之一, 可以使脚本以交互方式运行.

bash -v script-name

bash -o verbose script-name

下面的表格列举了一些有用的选项. 它们都可以用简短格式来指定(以一个短横线开头)也可
以用完整的名字来指定(用双短横线开头或用-o来指定).

table 30-1 Bash 选项
==================================================================================
| 缩写               | 名称         | 作用
==================================================================================
| -C              | noclobber   | 防止重定向时覆盖文件 (此作用会被>|覆盖)
==================================================================================
| -D              | (none)         | 列出双引号引起的含有$前缀的字符串,但不执行脚本
|                  |                | 中的命令
==================================================================================
| -a              | allexport     | 导出所有定义的变量到环境变量中
==================================================================================
| -b              | notify         | 当后台任务终止时给出通知 (在脚本中用的不多)
==================================================================================
| -c...              | (none)         | 从...读命令
==================================================================================
| -e              | errexit     | 脚本发生第一个错误时就中止脚本运行,即当一个命令
|                  |                | 返回非零值时退出脚本 (除了until 或 while loops,
|                  |                | if-tests, list constructs)
==================================================================================
| -f              | noglob         | 文件名替换停用(指像*这样的符号不能替换为文件名了)
==================================================================================
| -i              | interactive | 使脚本以交互式方式运行
==================================================================================
| -n              | noexec         | 从脚本里读命令但不执行它们(语法检查)
==================================================================================
| -o Option-Name  | (none)         | 调用Option-Name 选项
==================================================================================
| -o posix          | POSIX         | 更改Bash或脚本的行为,使之符合POSIX标准.
==================================================================================
| -p              | privileged     | 脚本作为"suid"程序来运行 (小心!)
==================================================================================
| -r              | restricted     | 脚本在受限模式中运行 (参考第21章).
==================================================================================
| -s              | stdin         | 从标准输入读命令
==================================================================================
| -t              | (none)         | 第一个命令后就退出
==================================================================================
| -u              | nounset     | 当使用一个未定义的变量时产生一个错误信息,并强制
|                  |                | 退出脚本.
==================================================================================
| -v              | verbose     | 执行命令之前打印命令到标准输出
==================================================================================
| -x              | xtrace         | 与-v相似, 但打印完整的命令
==================================================================================
| -                  | (none)         | 选项列表结束的标志. 后面的参数是位置参数.
==================================================================================
| --              | (none)         | 释放位置参数. 如果参数列表被指定了(-- arg1 arg2),
|                  |                | 则位置参数被依次设置为参数列表中的值.
==================================================================================

第31章    Gotchas
===============
Turandot: Gli enigmi sono tre, la morte una!
Caleph: No, no! Gli enigmi sono tre, una la vita!

                                                Puccini

将保留字和字符声明为变量名.

   1 case=value0       # 引发错误.
   2 23skidoo=value1   # 也会有错误.
   3 # 以数字开头的变量名是由shell保留使用的.
   4 # 试试 _23skidoo=value1. 用下划线开头的变量名是允许的.
   5
   6 # 但是 . . .   仅使用下划线来用做变量名也是不行的.
   7 _=25
   8 echo $_           # $_ 是一个特殊的变量,被设置为最后命令的最后一个参数.
   9
  10 xyz((!*=value2    # 引起严重的错误.
  11 # 在第三版的Bash, 标点不能在变量名中出现.

用连字符或其他保留字符当做变量名(或函数名).

   1 var-1=23
   2 # 用 'var_1' 代替.
   3
   4 function-whatever ()   # 错误
   5 # 用 'function_whatever ()' 代替.
   6
   7  
   8 # 在第三版的 Bash, 标点不能在函数名中使用.
   9 function.whatever ()   # 错误
  10 # 用 'functionWhatever ()' 代替.

给变量和函数使用相同的名字. 这会使脚本不能分辨两者.

   1 do_something ()
   2 {
   3   echo "This function does something with \"$1\"."
   4 }
   5
   6 do_something=do_something
   7
   8 do_something do_something
   9
  10 # 这些都是合法的,但让人混淆.

不适当地使用宽白符(whitespace). 和其它的编程语言相比,Bash非常讲究空白字符的使用.

   1 var1 = 23   # 'var1=23' 正确.
   2 # 上面一行,Bash试图执行命令"var1"
   3 # 并且它的参数是"="和"23".
   4     
   5 let c = $a - $b   # 'let c=$a-$b' 或 'let "c = $a - $b"'是正确的.
   6
   7 if [ $a -le 5]    # if [ $a -le 5 ]   是正确的.
   8 # if [ "$a" -le 5 ]   会更好.
   9 # [[ $a -le 5 ]] 也可以.

未初始化的变量(指赋值前的变量)被认为是NULL值的,而不是有零值.

   1 #!/bin/bash
   2
   3 echo "uninitialized_var = $uninitialized_var"
   4 # uninitialized_var =

混淆测试里的= 和 -eq 操作符. 请记住, = 是比较字符变量而 -eq 比较整数.

   1 if [ "$a" = 273 ]      # $a 是一个整数还是一个字符串?
   2 if [ "$a" -eq 273 ]    # 如果$a 是一个整数,用这个表达式.
   3
   4 # 有时你能混用 -eq 和 = 而没有不利的结果.
   5 # 然而 . . .
   6
   7
   8 a=273.0   # 不是一个整数.
   9        
  10 if [ "$a" = 273 ]
  11 then
  12   echo "Comparison works."
  13 else  
  14   echo "Comparison does not work."
  15 fi    # Comparison does not work.
  16
  17 # 与   a=" 273"  和 a="0273" 一样.
  18
  19
  20 # 同样, 问题仍然是试图对非整数值使用 "-eq" 测试.
  21        
  22 if [ "$a" -eq 273.0 ]
  23 then
  24   echo "a = $a"
  25 fi  # 因错误信息而中断.  
  26 # test.sh: [: 273.0: integer expression expected

误用字符串比较操作符.

Example 31-1 数字和字符串比较是不相等同的
################################Start Script#######################################
 1 #!/bin/bash
 2 # bad-op.sh: 在整数比较中使用字符串比较.
 3
 4 echo
 5 number=1
 6
 7 # 下面的 "while" 循环有两个错误:
 8 #+ 一个很明显,另一个比较隐蔽.
 9
10 while [ "$number" < 5 ]    # 错误! 应该是:  while [ "$number" -lt 5 ]
11 do
12   echo -n "$number "
13   let "number += 1"
14 done  
15 #  尝试运行时会收到错误信息而退出:
16 #+ bad-op.sh: line 10: 5: No such file or directory
17 #  在单括号里, "<" 需要转义,
18 #+ 而即使是如此, 对此整数比较它仍然是错的.
19
20
21 echo "---------------------"
22
23
24 while [ "$number" \< 5 ]    #  1 2 3 4
25 do                          #
26   echo -n "$number "        #  看起来好像是能工作的, 但 . . .
27   let "number += 1"         #+ 它其实是在对 ASCII 码的比较,
28 done                        #+ 而非是对数值的比较.
29
30 echo; echo "---------------------"
31
32 # 下面这样便会引起问题了. 例如:
33
34 lesser=5
35 greater=105
36
37 if [ "$greater" \< "$lesser" ]
38 then
39   echo "$greater is less than $lesser"
40 fi                          # 105 is less than 5
41 #  事实上, "105" 小于 "5"
42 #+ 是因为使用了字符串比较 (以ASCII码的排序顺序比较).
43
44 echo
45
46 exit 0
################################End Script#########################################
有时在测试时的方括号([ ])里的变量需要引用起来(双引号). 如果没有这么做可能会引起不
可预料的结果. 参考例子 7-6, 例子 16-5, 和 例子 9-6.

在脚本里的命令可能会因为脚本没有运行权限而导致运行失败. 如果用户不能在命令行里调用
一个命令,即使把这个命令加到一个脚本中也一样会失败. 这时可以尝试更改访命令的属性,
甚至可能给它设置suid位(当然是以root来设置).

试图用 - 来做重定向操作(事实上它不是操作符)会导致令人讨厌的意外.

   1 command1 2> - | command2  # 试图把command1的错误重定向到一个管道里...
   2 #    ...不会工作.    
   3
   4 command1 2>& - | command2  # 也没有效果.
   5
   6 Thanks, S.C.

用 Bash 版本 2+ 的功能可以当有错误信息时引发修复动作. 老一些的 Linux机器可能默认的
安装是 1.XX 版本的Bash.

   1 #!/bin/bash
   2
   3 minimum_version=2
   4 # 因为 Chet Ramey 经常给Bash增加新的特性,
   5 # 你把 $minimum_version 设为 2.XX比较合适,或者是其他合适的值.
   6 E_BAD_VERSION=80
   7
   8 if [ "$BASH_VERSION" \< "$minimum_version" ]
   9 then
  10   echo "This script works only with Bash, version $minimum or greater."
  11   echo "Upgrade strongly recommended."
  12   exit $E_BAD_VERSION
  13 fi
  14
  15 ...

在非Linux的机器上使用Bourne shell脚本(#!/bin/sh)的Bash专有功能可能会引起不可预料的
行为. Linux系统通常都把sh 取别名为 bash, 但在其他的常见的UNIX系统却不一定是这样.

使用Bash中没有文档化的属性是危险的尝试. 在这本书的前几版中有几个脚本依赖于exit或
return的值没有限制不能用负整数(虽然限制了exit或return 的最大值是255). 不幸地是,
在版本 2.05b 以上这种情况就消失了. 参考See 例子 23-9.

一个带有DOS风格新行符 (\r\n) 的脚本会执行失败, 因为#!/bin/bash\r\n 不是合法的,不同
于合法的#!/bin/bash\n. 解决办法就是把脚本转换成UNIX风格的新行符.

   1 #!/bin/bash
   2
   3 echo "Here"
   4
   5 unix2dos $0    # 脚本先把自己改成DOS格式.
   6 chmod 755 $0   # 更改回执行权限.
   7                # 'unix2dos'命令会删除执行权限.
   8
   9 ./$0           # 脚本尝试再次运行自己本身.
  10                # 但它是一个DOS文件而不会正常工作了.
  11
  12 echo "There"
  13
  14 exit 0

shell脚本以 #!/bin/sh 行开头将不会在Bash兼容的模式下运行. 一些Bash专有的功能可能会
被禁用掉. 那些需要完全使用Bash专有扩展特性的脚本应该用#!/bin/bash开头.

脚本里在 here document 的终结输入的字符串前加入空白字符会引起不可预料的结果.

脚本不能export(导出)变量到它的父进程(parent process),或父进程的环境里. 就像我
们学的生物一样,一个子进程可以从父进程里继承但不能去影响父进程.

   1 WHATEVER=/home/bozo
   2 export WHATEVER
   3 exit 0

 bash$ echo $WHATEVER
 
 bash$

可以确定, 回到命令提示符, $WHATEVER 变量仍然没有设置.

在子SHELL(subshell)设置和操作变量 , 然后尝试在子SHELL的作用范围外使用相同名的变
量将会导致非期望的结果.

Example 31-2 子SHELL缺陷
################################Start Script#######################################
 1 #!/bin/bash
 2 # 在子SHELL中的变量缺陷.
 3
 4 outer_variable=outer
 5 echo
 6 echo "outer_variable = $outer_variable"
 7 echo
 8
 9 (
10 # 子SHELL开始
11
12 echo "outer_variable inside subshell = $outer_variable"
13 inner_variable=inner  # Set
14 echo "inner_variable inside subshell = $inner_variable"
15 outer_variable=inner  # Will value change globally?
16 echo "outer_variable inside subshell = $outer_variable"
17
18 # 导出变量会有什么不同吗?
19 #    export inner_variable
20 #    export outer_variable
21 # 试试看.
22
23 # 子SHELL结束
24 )
25
26 echo
27 echo "inner_variable outside subshell = $inner_variable"  # Unset.
28 echo "outer_variable outside subshell = $outer_variable"  # Unchanged.
29 echo
30
31 exit 0
32
33 # 如果你没有注释第 19 和 20行会怎么样?
34 # 会有什么不同吗?
################################End Script#########################################

把 echo 的输出用管道(Piping)输送给read命令可能会产生不可预料的结果. 在这个情况下,
 read  表现地好像它是在一个子SHELL里一样. 可用set 命令代替 (就像在例子 11-16里的一
样).

Example 31-3 把echo的输出用管道输送给read命令
################################Start Script#######################################
 1 #!/bin/bash
 2 #  badread.sh:
 3 #  尝试用 'echo 和 'read'
 4 #+ 来达到不用交互地给变量赋值的目的.
 5
 6 a=aaa
 7 b=bbb
 8 c=ccc
 9
10 echo "one two three" | read a b c
11 # 试图重新给 a, b, 和 c赋值.
12
13 echo
14 echo "a = $a"  # a = aaa
15 echo "b = $b"  # b = bbb
16 echo "c = $c"  # c = ccc
17 # 重新赋值失败.
18
19 # ------------------------------
20
21 # 用下面的另一种方法.
22
23 var=`echo "one two three"`
24 set -- $var
25 a=$1; b=$2; c=$3
26
27 echo "-------"
28 echo "a = $a"  # a = one
29 echo "b = $b"  # b = two
30 echo "c = $c"  # c = three
31 # 重新赋值成功.
32
33 # ------------------------------
34
35 #  也请注意echo值到'read'命令里是在一个子SHELL里起作用的.
36 #  所以,变量的值只在子SHELL里被改变了.
37
38 a=aaa          # 从头开始.
39 b=bbb
40 c=ccc
41
42 echo; echo
43 echo "one two three" | ( read a b c;
44 echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
45 # a = one
46 # b = two
47 # c = three
48 echo "-----------------"
49 echo "Outside subshell: "
50 echo "a = $a"  # a = aaa
51 echo "b = $b"  # b = bbb
52 echo "c = $c"  # c = ccc
53 echo
54
55 exit 0
################################End Script#########################################
事实上, 也正如 Anthony Richardson 指出的那样, 管道任何的数据到循环里都会引起相似的
问题.

   1 # 循环管道问题.
   2 #  Anthony Richardson编写此例,
   3 #+ Wilbert Berendsen补遗此例.
   4
   5
   6 foundone=false
   7 find $HOME -type f -atime +30 -size 100k |
   8 while true
   9 do
  10    read f
  11    echo "$f is over 100KB and has not been accessed in over 30 days"
  12    echo "Consider moving the file to archives."
  13    foundone=true
  14    # ------------------------------------
  15    echo "Subshell level = $BASH_SUBSHELL"
  16    # Subshell level = 1
  17    # 没错, 现在是在子shell里头运行.
  18    # ------------------------------------
  19 done
  20    
  21 #  foundone 变量在此总是有false值
  22 #+ 因此它是在子SHELL里被设为true值的
  23 if [ $foundone = false ]
  24 then
  25    echo "No files need archiving."
  26 fi
  27
  28 # =====================现在, 使用正确的方法:=================
  29
  30 foundone=false
  31 for f in $(find $HOME -type f -atime +30 -size 100k)  # 没有使用管道.
  32 do
  33    echo "$f is over 100KB and has not been accessed in over 30 days"
  34    echo "Consider moving the file to archives."
  35    foundone=true
  36 done
  37    
  38 if [ $foundone = false ]
  39 then
  40    echo "No files need archiving."
  41 fi
  42
  43 # ==================另一种方法==================
  44
  45 #  脚本中读变量值的相应部分替换在代码块里头读变量,
  46 #+ 这使变量能在相同的子SHELL里共享了.
  47 #  Thank you, W.B.
  48
  49 find $HOME -type f -atime +30 -size 100k | {
  50      foundone=false
  51      while read f
  52      do
  53        echo "$f is over 100KB and has not been accessed in over 30 days"
  54        echo "Consider moving the file to archives."
  55        foundone=true
  56      done
  57
  58      if ! $foundone
  59      then
  60        echo "No files need archiving."
  61      fi
  62 }

相关的问题是:当尝试写 tail -f 的输出给管道并传递给grep时会发生问题.

   1 tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
   2 # "error.log"文件里将不会写入任何东西.

--

在脚本中使用"suid" 的命令是危险的, 因为这会危及系统安全. [1]

用shell编写CGI程序是值得商榷的. Shell脚本的变量不是"类型安全的", 这样它用于CGI连接
使用时会引发不希望的结果. 其次, 它很难防范骇客的攻击.

Bash 不能正确处理双斜线 (//) 字符串.

在Linux 或 BSD上写的Bash脚本可能需要修正以使它们也能在商业的UNIX (或 Apple OSX)上运
行. 这些脚本常使用比一般的UNIX系统上的同类工具更强大功能的GNU 命令和过滤工具. 这方
面一个明显的例子是文本处理工具tr.

                                    Danger is near thee --

                                    Beware, beware, beware, beware.

                                    Many brave hearts are asleep in the deep.

                                    So beware --

                                    Beware.
                                                    A.J. Lamb and H.W. Petrie


注意事项:
[1]        给脚本设置suid 权限是没有用的.


第32章    脚本编程风格
====================
写脚本时养成结构化和系统方法的习惯. 即使你在信封背后随便做一下草稿也是有益的,要养
成在写代码前花几分钟来规划和组织你的想法.

这儿是一些风格的指南. 注意这节文档不是想成为一个官方Shell编程风格.

32.1. 非官方的Shell脚本风格
---------------------------
*    注释你的代码.这会使你的代码更容易让别人理解和赏识,同时也便于你维护.

       1 PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
       2 # 当你去年写下这句代码时非常的了解它在干什么事,但现在它完全是一个谜.
       3 # (摘自 Antek Sawicki的"pw.sh" 脚本.)

    给脚本和函数加上描述性的头部信息.

       1 #!/bin/bash
       2
       3 #************************************************#
       4 #                   xyz.sh                       #
       5 #           written by Bozo Bozeman              #
       6 #                July 05, 2001                   #
       7 #                                                #
       8 #                   清除项目文件.                #
       9 #************************************************#
      10
      11 E_BADDIR=65                       # 没有那样的目录.
      12 projectdir=/home/bozo/projects    # 要清除的目录.
      13
      14 # --------------------------------------------------------- #
      15 # cleanup_pfiles ()                                         #
      16 # 删除指定目录里的所有文件.                                 #
      17 # 参数: $target_directory                                   #
      18 # 返回: 成功返回0 , 失败返回$E_BADDIR值.                    #
      19 # --------------------------------------------------------- #
      20 cleanup_pfiles ()
      21 {
      22   if [ ! -d "$1" ]  # 测试目标目录是否存在.
      23   then
      24     echo "$1 is not a directory."
      25     return $E_BADDIR
      26   fi
      27
      28   rm -f "$1"/*
      29   return 0   # 成功.
      30 }  
      31
      32 cleanup_pfiles $projectdir
      33
      34 exit 0

    确认 #!/bin/bash 在脚本的第一行,在任何头部注释行之前.

*    避免使用 "魔数,"  [1]  它是硬编码的字符常量. 用有意义的变量名来代替. 这使脚本
        更容易理解并允许在不破坏应用的情况下做改变和更新.

       1 if [ -f /var/log/messages ]
       2 then
       3   ...
       4 fi
       5 # 一年以后,你决定让脚本改为检查 /var/log/syslog.
       6 # 那么现在就需要你手动修改脚本里每一处的要改动的代码,
       7 # 希望不要有你疏漏的地方.
       8
       9 # 更好的办法是:
      10 LOGFILE=/var/log/messages  # 只需要改动一行.
      11 if [ -f "$LOGFILE" ]
      12 then
      13   ...
      14 fi

*    为变量和函数选择描述性的名字.

       1 fl=`ls -al $dirname`                 # 含义含糊.
       2 file_listing=`ls -al $dirname`       # 更好的名字.
       3
       4
       5 MAXVAL=10   # 同一个脚本所有程序代码使用脚本常量.
       6 while [ "$index" -le "$MAXVAL" ]
       7 ...
       8
       9
      10 E_NOTFOUND=75                        #  把错误代码的代表的变量名大写U,
      11                                      # +并以"E_"开头.
      12 if [ ! -e "$filename" ]
      13 then
      14   echo "File $filename not found."
      15   exit $E_NOTFOUND
      16 fi  
      17
      18
      19 MAIL_DIRECTORY=/var/spool/mail/bozo  # 环境变量名用大写.
      20 export MAIL_DIRECTORY
      21
      22
      23 GetAnswer ()                         # 函数名用适当的大小写混合组成.
      24 {
      25   prompt=$1
      26   echo -n $prompt
      27   read answer
      28   return $answer
      29 }  
      30
      31 GetAnswer "What is your favorite number? "
      32 favorite_number=$?
      33 echo $favorite_number
      34
      35
      36 _uservariable=23                     # 语法允许, 但不推荐.
      37 # 用户定义的变量最好不要用下划线开头.
      38 # 把这个留给系统变量使用更好.

*    用有含义和系统的方法来使用退出代码(exit codes).
       1 E_WRONG_ARGS=65
       2 ...
       3 ...
       4 exit $E_WRONG_ARGS

    也参考附录 D.

    最后 建议在脚本中使用/usr/include/sysexits.h的退出码, 虽然它们主要由 C 和 C++
    语言编程时使用.

*    使用标准的参数选项. 最后 建议使用下面一组参数标志.

       1 -a      All: Return all information (including hidden file info).
       2 -b      Brief: Short version, usually for other scripts.
       3 -c      Copy, concatenate, etc.
       4 -d      Daily: Use information from the whole day, and not merely
       5         information for a specific instance/user.
       6 -e      Extended/Elaborate: (often does not include hidden file info).
       7 -h      Help: Verbose usage w/descs, aux info, discussion, help.
       8         See also -V.
       9 -l      Log output of script.
      10 -m      Manual: Launch man-page for base command.
      11 -n      Numbers: Numerical data only.
      12 -r      Recursive: All files in a directory (and/or all sub-dirs).
      13 -s      Setup & File Maintenance: Config files for this script.
      14 -u      Usage: List of invocation flags for the script.
      15 -v      Verbose: Human readable output, more or less formatted.
      16 -V      Version / License / Copy(right|left) / Contribs (email too).

    也参考附录 F.

*    把复杂的脚本分割成简单一些的模块. 用合适的函数来实现各个功能. 参考例子 34-4.

*    如果有简单的结构可以使用,不要使用复杂的结构.

       1 COMMAND
       2 if [ $? -eq 0 ]
       3 ...
       4 # 多余的并且也不直接明了.
       5
       6 if COMMAND
       7 ...
       8 # 更简练 (或者可能会损失一些可读性).

                    ... reading the UNIX source code to the Bourne shell
                    (/bin/sh). I was shocked at how much simple algorithms could
                    be made cryptic, and therefore useless, by a poor choice of
                    code style. I asked myself, "Could someone be proud of this
                    code?"
                                                                    Landon Noll
注意事项:
[1]        在上下文, "魔数" 和用来指明文件类型的 魔数(magic numbers)有完全不同的意思.


第33章    杂项
============
                Nobody really knows what the Bourne shell's grammar is. Even
                examination of the source code is little help.
                                                            Tom Duff

33.1. 交互式和非交互式的shells和脚本
------------------------------------
交互式的shell在 tty终端从用户的输入中读取命令. 另一方面, shell能在启动时读取启动文
件,显示一个提示符并默认激活作业控制. 用户能交互地使用shell.

运行脚本的shell一般都是非交互的shell. 但脚本仍然可以存取它拥有的终端. 脚本里甚至可
以仿效成可交互的shell.

   1 #!/bin/bash
   2 MY_PROMPT='$ '
   3 while :
   4 do
   5   echo -n "$MY_PROMPT"
   6   read line
   7   eval "$line"
   8   done
   9
  10 exit 0
  11
  12 # 这个例子脚本, 和上面的解释由
  13 # Stéphane Chazelas 提供(再次感谢).

让我们考虑一个要求用户交互式输入的脚本,通常用read语句 (参考例子 11-3). 真正的情况
可能有些混乱.以现在假设的情况来说,交互式脚本被限制在一个tty设备上,它本身已经是从
一个控制终端或一个中被用户调用的.

初始化和启动脚本不必是非交互式的,因为它们必须不需要人为地干预地运行.许多管理和系
统维护脚本也同样是非交互式的.不多变的重复性的任务可以自动地由非交互式脚本完成.

非交互式的脚本可以在后台运行,但交互脚本在后台运行则会被挂起,等待永远不会到达的输
入.解决这个难点的办法可以写预料这种情况的脚本或是内嵌here document 的脚本来获取脚
本期望的输入,这样就可作为后台任务运行了.在最简单的情况,重定向一个文件给一个read
语句提供输入(read variable <file). 这就可能适应交互和非交互的工作环境下都能达成脚
本运行的目的.

如果脚本需要测试当前是否运行在交互shell中,一个简单的办法是找一下是否有提示符变量,
即$PS1是否设置了. (如果脚本需要用户输入数据,则脚本会显示一个提示符.)

   1 if [ -z $PS1 ] # 没有提示符?
   2 then
   3   # 非交互式
   4   ...
   5 else
   6   # 交互式
   7   ...
   8 fi

另一个办法是脚本可以测试是否在变量$-中出现了选项"i".

   1 case $- in
   2 *i*)    # 交互式 shell
   3 ;;
   4 *)      # 非交互式 shell
   5 ;;
   6 # (Courtesy of "UNIX F.A.Q.," 1993)

注意:    脚本可以使用-i选项强制在交互式模式下运行或脚本头用#!/bin/bash -i. 注意这样
        可能会引起脚本古怪的行为或当没有错误出现时也会显示错误信息.

33.2. Shell 包装
----------------
包装脚本是指嵌有一个系统命令和程序的脚本,也保存了一组传给该命令的参数. [1]  包装
脚本使原本很复杂的命令行简单化. 这对 sed 和 awk 特别有用.

sed 和 awk 命令一般从命令行上以 sed -e 'commands' 和 awk 'commands' 来调用. 把sed
和awk的命令嵌入到Bash脚本里使调用变得更简单, 并且也可多次使用. 也可以综合地利用
sed 和 awk 的功能, 例如管道(piping)连接sed 命令的输出到awk命令中. 保存为可执行的
文件, 你可以用脚本编写的或修改的调用格式多次的调用它, 而不必在命令行上重复键入复杂
的命令行.

Example 33-1 shell 包装
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 这是一个把文件中的空行删除的简单脚本.
 4 # 没有参数检查.
 5 #
 6 # 你可能想增加类似下面的代码:
 7 #
 8 # E_NOARGS=65
 9 # if [ -z "$1" ]
10 # then
11 #  echo "Usage: `basename $0` target-file"
12 #  exit $E_NOARGS
13 # fi
14
15
16 # 就像从命令行调用下面的命令:
17 #    sed -e '/^$/d' filename
18 #
19
20 sed -e /^$/d "$1"
21 #  The '-e' 意味着后面跟的是编辑命令 (这是可选的).
22 #  '^' 匹配行的开头, '$' 则是行的结尾.
23 #  这个表达式匹配行首和行尾之间什么也没有的行,
24 #+ 即空白行.
25 #  'd'是删除命令.
26
27 #  引号引起命令行参数就允许在文件名中使用空白字符和特殊字符
28 #
29
30 #  注意这个脚本不能真正的修改目标文件.
31 #  如果你需要保存修改,就要重定向到某个输出文件里.
32
33 exit 0
################################End Script#########################################

Example 33-2 稍微复杂一些的shell包装
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  "subst", 把一个文件中的一个模式替换成一个模式的脚本
 4 #
 5 #  例如, "subst Smith Jones letter.txt".
 6
 7 ARGS=3         # 脚本要求三个参数.
 8 E_BADARGS=65   # 传递了错误的参数个数给脚本.
 9
10 if [ $# -ne "$ARGS" ]
11 # 测试脚本参数的个数 (这是好办法).
12 then
13   echo "Usage: `basename $0` old-pattern new-pattern filename"
14   exit $E_BADARGS
15 fi
16
17 old_pattern=$1
18 new_pattern=$2
19
20 if [ -f "$3" ]
21 then
22     file_name=$3
23 else
24     echo "File \"$3\" does not exist."
25     exit $E_BADARGS
26 fi
27
28
29 #  这儿是实现功能的代码.
30
31 # -----------------------------------------------
32 sed -e "s/$old_pattern/$new_pattern/g" $file_name
33 # -----------------------------------------------
34
35 #  's' 在sed命令里表示替换,
36 #+ /pattern/表示匹配地址.
37 #  The "g"也叫全局标志使sed会在每一行有$old_pattern模式出现的所有地方替换,
38 #+ 而不只是匹配第一个出现的地方.
39 #  参考'sed'的有关书籍了解更深入的解释.
40
41 exit 0    # 脚本成功调用会返回 0.
################################End Script#########################################

Example 33-3 写到日志文件的shell包装
################################Start Script#######################################
 1 #!/bin/bash
 2 #  普通的shell包装,执行一个操作并记录在日志里
 3 #
 4
 5 # 需要设置下面的两个变量.
 6 OPERATION=
 7 #         可以是一个复杂的命令链,
 8 #+        例如awk脚本或是管道 . . .
 9 LOGFILE=
10 #         不管怎么样,命令行参数还是要提供给操作的.
11
12
13 OPTIONS="$@"
14
15
16 # 记录操作.
17 echo "`date` + `whoami` + $OPERATION "$@"" >> $LOGFILE
18 # 现在, 执行操作.
19 exec $OPERATION "$@"
20
21 # 在操作之前记录日志是必须的.
22 # 为什么?
################################End Script#########################################

Example 33-4 包装awk的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # pr-ascii.sh: 打印 ASCII 码的字符表.
 3
 4 START=33   # 可打印的 ASCII 字符的范围 (十进制).
 5 END=125
 6
 7 echo " Decimal   Hex     Character"   # 表头.
 8 echo " -------   ---     ---------"
 9
10 for ((i=START; i<=END; i++))
11 do
12   echo $i | awk '{printf("  %3d       %2x         %c\n", $1, $1, $1)}'
13 # 在这个上下文,不会运行Bash的内建printf命令:
14 #     printf "%c" "$i"
15 done
16
17 exit 0
18
19
20 #  Decimal   Hex     Character
21 #  -------   ---     ---------
22 #    33       21         !
23 #    34       22         "
24 #    35       23         #
25 #    36       24         $
26 #
27 #    . . .
28 #
29 #   122       7a         z
30 #   123       7b         {
31 #   124       7c         |
32 #   125       7d         }
33
34
35 #  把脚本的输出重定向到一个文件或是管道给more命令来查看:
36 #+   sh pr-asc.sh | more
################################End Script#########################################

Example 33-5 另一个包装awk的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 给目标文件增加一列由数字指定的列.
 4
 5 ARGS=2
 6 E_WRONGARGS=65
 7
 8 if [ $# -ne "$ARGS" ] # 检查命令行参数个数是否正确.
 9 then
10    echo "Usage: `basename $0` filename column-number"
11    exit $E_WRONGARGS
12 fi
13
14 filename=$1
15 column_number=$2
16
17 #  传递shell变量给脚本的awk部分需要一点技巧.
18 #  方法之一是在awk脚本中使用强引用来引起bash脚本的变量
19 #
20 #     $'$BASH_SCRIPT_VAR'
21 #      ^                ^
22 #  这个方法在下面的内嵌的awk脚本中出现.
23 #  参考awk文档了解更多的细节.
24
25 # 多行的awk脚本调用格式为:  awk ' ..... '
26
27
28 # 开始 awk 脚本.
29 # -----------------------------
30 awk '
31
32 { total += $'"${column_number}"'
33 }
34 END {
35      print total
36 }     
37
38 ' "$filename"
39 # -----------------------------
40 # awk脚本结束.
41
42
43 #   把shell变量传递给awk变量可能是不安全的,
44 #+  因此Stephane Chazelas提出了下面另外一种方法:
45 #   ---------------------------------------
46 #   awk -v column_number="$column_number" '
47 #   { total += $column_number
48 #   }
49 #   END {
50 #       print total
51 #   }' "$filename"
52 #   ---------------------------------------
53
54
55 exit 0
################################End Script#########################################
对于要实现这些功能而只用一种多合一的瑞士军刀应该用Perl. Perl兼有sed和awk的能力, 并
且具有C的一个很大的子集. 它是标准的并支持面向对象编程的方方面面,甚至是很琐碎的东
西. 短的Perl脚本也可以嵌入到shell脚本中去,以至于有些人宣称Perl能够完全地代替shell
编程(本文作者对此持怀疑态度).

Example 33-6 把Perl嵌入Bash脚本
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # Shell命令可以包含 Perl 脚本.
 4 echo "This precedes the embedded Perl script within \"$0\"."
 5 echo "==============================================================="
 6
 7 perl -e 'print "This is an embedded Perl script.\n";'
 8 # 像sed脚本, Perl 也使用"-e"选项.
 9
10 echo "==============================================================="
11 echo "However, the script may also contain shell and system commands."
12
13 exit 0
################################End Script#########################################
把Bash脚本和Perl脚本放在同一个文件是可能的. 依赖于脚本如何被调用, 要么是Bash部分被
执行,要么是Perl部分被执行.

Example 33-7 Bash 和 Perl 脚本联合使用
################################Start Script#######################################
 1 #!/bin/bash
 2 # bashandperl.sh
 3
 4 echo "Greetings from the Bash part of the script."
 5 # 下面可以有更多的Bash命令.
 6
 7 exit 0
 8 # 脚本的Bash部分结束.
 9
10 # =======================================================
11
12 #!/usr/bin/perl
13 # 脚本的这个部分必须用-x选项来调用.
14
15 print "Greetings from the Perl part of the script.\n";
16 # 下面可以有更多的Perl命令.
17
18 # 脚本的Perl部分结束.
################################End Script#########################################

 bash$ bash bashandperl.sh
 Greetings from the Bash part of the script.
 
 bash$ perl -x bashandperl.sh
 Greetings from the Perl part of the script.

注意事项:
[1]        事实上,相当数量的Linux软件工具包是shell包装脚本. 例如/usr/bin/pdf2ps,
        /usr/bin/batch, 和 /usr/X11R6/bin/xmkmf.

33.3. 测试和比较: 另一种方法
----------------------------
对于测试,[[ ]]结构可能比[ ]更合适.同样地,算术比较可能用(( ))结构更有用.

   1 a=8
   2
   3 # 下面所有的比较是等价的.
   4 test "$a" -lt 16 && echo "yes, $a < 16"         # "与列表"
   5 /bin/test "$a" -lt 16 && echo "yes, $a < 16"
   6 [ "$a" -lt 16 ] && echo "yes, $a < 16"
   7 [[ $a -lt 16 ]] && echo "yes, $a < 16"          # 在[[ ]]和(( ))中不必用引号引起变量
   8 (( a < 16 )) && echo "yes, $a < 16"             #
   9
  10 city="New York"
  11 # 同样,下面的所有比较都是等价的.
  12 test "$city" \< Paris && echo "Yes, Paris is greater than $city"  # 产生 ASCII 顺序.
  13 /bin/test "$city" \< Paris && echo "Yes, Paris is greater than $city"
  14 [ "$city" \< Paris ] && echo "Yes, Paris is greater than $city"
  15 [[ $city < Paris ]] && echo "Yes, Paris is greater than $city"    # 不需要用引号引起$city.
  16
  17 # 多谢, S.C.

33.4. 递归
----------
脚本是否能 递归地  调用自己本身? 当然可以.

Example 33-8 递归调用自己本身的(无用)脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # recurse.sh
 3
 4 #  脚本能否递归地调用自己?
 5 #  是的, 但这有什么实际的用处吗?
 6 #  (看下面的.)
 7
 8 RANGE=10
 9 MAXVAL=9
10
11 i=$RANDOM
12 let "i %= $RANGE"  # 产生一个从 0 到 $RANGE - 1 之间的随机数.
13
14 if [ "$i" -lt "$MAXVAL" ]
15 then
16   echo "i = $i"
17   ./$0             #  脚本递归地调用再生成一个和自己一样的实例.
18 fi                 #  每个子脚本做的事都一样,
19                    #+ 直到产生的变量 $i 和变量 $MAXVAL 相等.
20
21 #  用"while"循环代替"if/then"测试会引起错误.
22 #  解释为什么会这样.
23
24 exit 0
25
26 # 注:
27 # ----
28 # 脚本要正确地工作必须有执行权限.
29 # 这是指用"sh"命令来调用这个脚本而没有设置正确权限导致的问题.
30 # 请解释原因.

返回顶部

发表评论:

Powered By Z-BlogPHP 1.7.3


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