魅力博客

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

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



Example 33-9 递归调用自己本身的(有用)脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # pb.sh: 电话本(phone book)
 3
 4 # 由Rick Boivie编写,已得到使用许可.
 5 # 由ABS文档作者修改.
 6
 7 MINARGS=1     #  脚本需要至少一个参数.
 8 DATAFILE=./phonebook
 9               #  在当前目录下名为"phonebook"的数据文件必须存在
10               #
11 PROGNAME=$0
12 E_NOARGS=70   #  没有参数的错误值.
13
14 if [ $# -lt $MINARGS ]; then
15       echo "Usage: "$PROGNAME" data"
16       exit $E_NOARGS
17 fi      
18
19
20 if [ $# -eq $MINARGS ]; then
21       grep $1 "$DATAFILE"
22       #  如果$DATAFILE文件不存在,'grep' 会打印一个错误信息.
23 else
24       ( shift; "$PROGNAME" $* ) | grep $1
25       # 脚本递归调用本身.
26 fi
27
28 exit 0        #  脚本在这儿退出.
29               #  因此Therefore, 从这行开始可以写没有#开头的的注释行
30           #
31
32 # ------------------------------------------------------------------------
33 "phonebook"文件的例子:
34
35 John Doe        1555 Main St., Baltimore, MD 21228          (410) 222-3333
36 Mary Moe        9899 Jones Blvd., Warren, NH 03787          (603) 898-3232
37 Richard Roe     856 E. 7th St., New York, NY 10009          (212) 333-4567
38 Sam Roe         956 E. 8th St., New York, NY 10009          (212) 444-5678
39 Zoe Zenobia     4481 N. Baker St., San Francisco, SF 94338  (415) 501-1631
40 # ------------------------------------------------------------------------
41
42 $bash pb.sh Roe
43 Richard Roe     856 E. 7th St., New York, NY 10009          (212) 333-4567
44 Sam Roe         956 E. 8th St., New York, NY 10009          (212) 444-5678
45
46 $bash pb.sh Roe Sam
47 Sam Roe         956 E. 8th St., New York, NY 10009          (212) 444-5678
48
49 #  当超过一个参数传给这个脚本时,
50 #+ 它只打印包含所有参数的行.
################################End Script#########################################

Example 33-10 另一个递归调用自己本身的(有用)脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # usrmnt.sh, 由Anthony Richardson编写
 3 # 得到允许在此使用.
 4
 5 # usage:       usrmnt.sh
 6 # 描述: 挂载设备, 调用者必须列在/etc/sudoers文件的MNTUSERS组里
 7 #
 8
 9 # ----------------------------------------------------------
10 #  这是一个用户挂载设备的脚本,它用sudo来调用自己.
11 #  只有拥有合适权限的用户才能用
12
13 #   usermount /dev/fd0 /mnt/floppy
14
15 # 来代替
16
17 #   sudo usermount /dev/fd0 /mnt/floppy
18
19 #  我使用相同的技术来处理我所有的sudo脚本,
20 #+ 因为我觉得它很方便.
21 # ----------------------------------------------------------
22
23 #  如果 SUDO_COMMAND 变量没有设置,我们不能通过sudo来运行脚本本身.
24 #+ 传递用户的真实ID和组ID . . .
25
26 if [ -z "$SUDO_COMMAND" ]
27 then
28    mntusr=$(id -u) grpusr=$(id -g) sudo $0 $*
29    exit 0
30 fi
31
32 # 如果我们以sudo来调用运行,就会运行这儿.
33 /bin/mount $* -o uid=$mntusr,gid=$grpusr
34
35 exit 0
36
37 # 附注 (由脚本作者加注):
38 # -------------------------------------------------
39
40 # 1) Linux允许在/etc/fstab文件中使用"users"选项
41 #    以使任何用户能挂载可移动的介质.
42 #    但是, 在一个服务器上,
43 #    我只想有限的几个用户可以存取可移动介质.
44 #    我发现使用sudo可以有更多的控制.
45
46 # 2) 我也发现sudo能通过组更方便地达成目的.
47 #
48
49 # 3) 这个方法使给予任何想给合适权限的人使用mount命令
50 #    所以要小心使用.
51 #    你也可以开发类似的脚本mntfloppy, mntcdrom,和 mntsamba来使mount命令得到更好的控制
52 #
53 #
54 #
################################End Script#########################################
注意:    过多层次的递归调用会耗尽脚本的堆栈空间,会引起段错误.

33.5. 彩色脚本
--------------
ANSI [1]  定义了屏幕属性的转义序列集合,例如粗体文本,背景和前景颜色. DOS批处理文
件(batch files) 一般使用ANSI的转义代码来控制色彩输出,Bash脚本也是这么做的.

Example 33-11 一个 "彩色的" 地址资料库
################################Start Script#######################################
 1 #!/bin/bash
 2 # ex30a.sh: ex30.sh的"彩色" 版本.
 3 #            没有加工处理的地址资料库
 4
 5
 6 clear                                   # 清除屏幕.
 7
 8 echo -n "          "
 9 echo -e '\E[37;44m'"\033[1mContact List\033[0m"
10                                         # 白色为前景色,蓝色为背景色
11 echo; echo
12 echo -e "\033[1mChoose one of the following persons:\033[0m"
13                                         # 粗体
14 tput sgr0
15 echo "(Enter only the first letter of name.)"
16 echo
17 echo -en '\E[47;34m'"\033[1mE\033[0m"   # 蓝色
18 tput sgr0                               # 把色彩设置为"常规"
19 echo "vans, Roland"                     # "[E]vans, Roland"
20 echo -en '\E[47;35m'"\033[1mJ\033[0m"   # 红紫色
21 tput sgr0
22 echo "ones, Mildred"
23 echo -en '\E[47;32m'"\033[1mS\033[0m"   # 绿色
24 tput sgr0
25 echo "mith, Julie"
26 echo -en '\E[47;31m'"\033[1mZ\033[0m"   # 红色
27 tput sgr0
28 echo "ane, Morris"
29 echo
30
31 read person
32
33 case "$person" in
34 # 注意变量被引起来了.
35
36   "E" | "e" )
37   # 接受大小写的输入.
38   echo
39   echo "Roland Evans"
40   echo "4321 Floppy Dr."
41   echo "Hardscrabble, CO 80753"
42   echo "(303) 734-9874"
43   echo "(303) 734-9892 fax"
44   echo "revans@zzy.net"
45   echo "Business partner & old friend"
46   ;;
47
48   "J" | "j" )
49   echo
50   echo "Mildred Jones"
51   echo "249 E. 7th St., Apt. 19"
52   echo "New York, NY 10009"
53   echo "(212) 533-2814"
54   echo "(212) 533-9972 fax"
55   echo "milliej@loisaida.com"
56   echo "Girlfriend"
57   echo "Birthday: Feb. 11"
58   ;;
59
60 # 稍后为 Smith 和 Zane 增加信息.
61
62           * )
63    # 默认选项Default option.      
64    # 空的输入(直接按了回车) 也会匹配这儿.
65    echo
66    echo "Not yet in database."
67   ;;
68
69 esac
70
71 tput sgr0                               # 把色彩重设为"常规".
72
73 echo
74
75 exit 0
################################End Script#########################################

Example 33-12 画盒子
################################Start Script#######################################
  1 #!/bin/bash
  2 # Draw-box.sh: 用ASCII字符画一个盒子.
  3
  4 # Stefano Palmeri编写,文档作者作了少量编辑.
  5 # 征得作者同意在本书使用.
  6
  7
  8 ######################################################################
  9 ###  draw_box 函数的注释  ###
 10
 11 #  "draw_box" 函数使用户可以在终端上画一个盒子.
 12 #
 13 #
 14 #  用法: draw_box ROW COLUMN HEIGHT WIDTH [COLOR]
 15 #  ROW 和 COLUMN 定位要画的盒子的左上角.       
 16 #
 17 #  ROW 和 COLUMN 必须要大于0且小于目前终端的尺寸.
 18 #
 19 #  HEIGHT 是盒子的行数,必须 > 0.
 20 #  HEIGHT + ROW 必须 <= 终端的高度.
 21 #  WIDTH 是盒子的列数,必须 > 0.
 22 #  WIDTH + COLUMN 必须 <= 终端的宽度.
 23 #
 24 #  例如: 如果你当前终端的尺寸是 20x80,
 25 #  draw_box 2 3 10 45 是合法的
 26 #  draw_box 2 3 19 45 的 HEIGHT 值是错的 (19+2 > 20)
 27 #  draw_box 2 3 18 78 的 WIDTH 值是错的 (78+3 > 80)
 28 #
 29 #  COLOR 是盒子边框的颜色.
 30 #  它是第5个参数,并且它是可选的.
 31 #  0=黑色 1=红色 2=绿色 3=棕褐色 4=蓝色 5=紫色 6=青色 7=白色.
 32 #  如果你传给这个函数错的参数,
 33 #+ 它就会以代码65退出,
 34 #+ 没有其他的信息打印到标准出错上.
 35 #
 36 #  在画盒子之前要清屏.
 37 #  函数内不包含有清屏命令.
 38 #  这使用户可以画多个盒子,甚至叠接多个盒子.
 39
 40 ###  draw_box 函数注释结束  ###
 41 ######################################################################
 42
 43 draw_box(){
 44
 45 #=============#
 46 HORZ="-"
 47 VERT="|"
 48 CORNER_CHAR="+"
 49
 50 MINARGS=4
 51 E_BADARGS=65
 52 #=============#
 53
 54
 55 if [ $# -lt "$MINARGS" ]; then                 # 如果参数小于4,退出.
 56     exit $E_BADARGS
 57 fi
 58
 59 # 搜寻参数中的非数字的字符.
 60 # 能用其他更好的办法吗 (留给读者的练习?).
 61 if echo $@ | tr -d [:blank:] | tr -d [:digit:] | grep . &> /dev/null; then
 62    exit $E_BADARGS
 63 fi
 64
 65 BOX_HEIGHT=`expr $3 - 1`   #  -1 是需要的,因为因为边角的"+"是高和宽共有的部分.
 66 BOX_WIDTH=`expr $4 - 1`    #
 67 T_ROWS=`tput lines`        #  定义当前终端长和宽的尺寸,
 68 T_COLS=`tput cols`         #
 69          
 70 if [ $1 -lt 1 ] || [ $1 -gt $T_ROWS ]; then    #  如果参数是数字就开始检查有效性.
 71    exit $E_BADARGS                             #
 72 fi
 73 if [ $2 -lt 1 ] || [ $2 -gt $T_COLS ]; then
 74    exit $E_BADARGS
 75 fi
 76 if [ `expr $1 + $BOX_HEIGHT + 1` -gt $T_ROWS ]; then
 77    exit $E_BADARGS
 78 fi
 79 if [ `expr $2 + $BOX_WIDTH + 1` -gt $T_COLS ]; then
 80    exit $E_BADARGS
 81 fi
 82 if [ $3 -lt 1 ] || [ $4 -lt 1 ]; then
 83    exit $E_BADARGS
 84 fi                                 # 参数检查完毕.
 85
 86 plot_char(){                       # 函数内的函数.
 87    echo -e "\E[${1};${2}H"$3
 88 }
 89
 90 echo -ne "\E[3${5}m"               # 如果传递了盒子边框颜色参数,则设置它.
 91
 92 # start drawing the box
 93
 94 count=1                                         #  用plot_char函数画垂直线
 95 for (( r=$1; count<=$BOX_HEIGHT; r++)); do      #
 96   plot_char $r $2 $VERT
 97   let count=count+1
 98 done
 99
100 count=1
101 c=`expr $2 + $BOX_WIDTH`
102 for (( r=$1; count<=$BOX_HEIGHT; r++)); do
103   plot_char $r $c $VERT
104   let count=count+1
105 done
106
107 count=1                                        #  用plot_char函数画水平线
108 for (( c=$2; count<=$BOX_WIDTH; c++)); do      #
109   plot_char $1 $c $HORZ
110   let count=count+1
111 done
112
113 count=1
114 r=`expr $1 + $BOX_HEIGHT`
115 for (( c=$2; count<=$BOX_WIDTH; c++)); do
116   plot_char $r $c $HORZ
117   let count=count+1
118 done
119
120 plot_char $1 $2 $CORNER_CHAR                   # 画盒子的角.
121 plot_char $1 `expr $2 + $BOX_WIDTH` +
122 plot_char `expr $1 + $BOX_HEIGHT` $2 +
123 plot_char `expr $1 + $BOX_HEIGHT` `expr $2 + $BOX_WIDTH` +
124
125 echo -ne "\E[0m"             #  恢复最初的颜色.
126
127 P_ROWS=`expr $T_ROWS - 1`    #  在终端的底部打印提示符.
128
129 echo -e "\E[${P_ROWS};1H"
130 }      
131
132
133 # 现在, 让我们来画一个盒子.
134 clear                       # 清屏.
135 R=2      # 行
136 C=3      # 列
137 H=10     # 高
138 W=45     # 宽
139 col=1    # 颜色(红)
140 draw_box $R $C $H $W $col   # 画盒子.
141
142 exit 0
143
144 # 练习:
145 # --------
146 # 增加可以在盒子里打印文本的选项
################################End Script#########################################
最简单也可能是最有用的ANSI转义序列是加粗文本, \033[1m ... \033[0m. \033 触发转义序
列, 而 "[1" 启用加粗属性, 而"[0" 表示切换回禁用加粗状态. "m"则表示终止一个转义序列.

 bash$ echo -e "\033[1mThis is bold text.\033[0m"

一种相似的转义序列可切换下划线效果 (在 rxvt 和 aterm 上).

 bash$ echo -e "\033[4mThis is underlined text.\033[0m"
           
注意:    echo使用-e选项可以启用转义序列.

其他的转义序列可用于更改文本或/和背景色彩.

 bash$ echo -e '\E[34;47mThis prints in blue.'; tput sgr0
 
 bash$ echo -e '\E[33;44m'"yellow text on blue background"; tput sgr0
 
 bash$ echo -e '\E[1;33;44m'"BOLD yellow text on blue background"; tput sgr0
           
注意:    通常为淡色的前景色文本设置粗体效果是较好的.

tput sgr0 把终端设置恢复为原样. 如果省略这一句会使后续在该终端的输出仍为蓝色.

注意:    因为tput sgr0 在某些环境下不能恢复终端设置, echo -ne \E[0m 会是更好的选择.

可以在有色的背景上用下面的模板写有色彩的文本.

echo -e '\E[COLOR1;COLOR2mSome text goes here.'

"\E[" 开始转义序列. 分号分隔的数值"COLOR1" 和 "COLOR2" 指定前景色和背景色, 数值和
色彩的对应参见下面的表格. (数值的顺序不是有关系的,因为前景色和背景色数值都落在不
重叠的范围里.) "m"终止该转义序列, 然后文本以结束的转义指定的属性显示.

也要注意到用单引号引用了echo -e后面的余下命令序列.

下表的数值是在 rxvt 终端运行的结果. 具体效果可能在其他的各种终端上不一样.

table 33-1. 转义序列中数值和彩色的对应

==========================
| 色彩  | 前景色 | 背景色|
|  黑     |  30     |  40   |
|  红     |  31     |  41   |
|  绿     |  32     |  42   |
|  黄     |  33     |  43   |
|  蓝     |  34     |  44   |
|  洋红    |  35     |  45   |
|  青     |  36     |  46   |
|  白     |  37     |  47   |
==========================

Example 33-13 显示彩色文本
################################Start Script#######################################
 1 #!/bin/bash
 2 # color-echo.sh: 用彩色来显示文本.
 3
 4 # 依照需要修改这个脚本.
 5 # 这比手写彩色的代码更容易一些.
 6
 7 black='\E[30;47m'
 8 red='\E[31;47m'
 9 green='\E[32;47m'
10 yellow='\E[33;47m'
11 blue='\E[34;47m'
12 magenta='\E[35;47m'
13 cyan='\E[36;47m'
14 white='\E[37;47m'
15
16
17 alias Reset="tput sgr0"      #  把文本属性重设回原来没有清屏前的
18                              #
19
20
21 cecho ()                     # Color-echo.
22                              # 参数 $1 = 要显示的信息
23                              # 参数 $2 = 颜色
24 {
25 local default_msg="No message passed."
26                              # 不是非要一个本地变量.
27
28 message=${1:-$default_msg}   # 默认的信息.
29 color=${2:-$black}           # 如果没有指定,默认使用黑色.
30
31   echo -e "$color"
32   echo "$message"
33   Reset                      # 重设文本属性.
34
35   return
36 }  
37
38
39 # 现在,让我们试试.
40 # ----------------------------------------------------
41 cecho "Feeling blue..." $blue
42 cecho "Magenta looks more like purple." $magenta
43 cecho "Green with envy." $green
44 cecho "Seeing red?" $red
45 cecho "Cyan, more familiarly known as aqua." $cyan
46 cecho "No color passed (defaults to black)."
47        # 缺失 $color (色彩)参数.
48 cecho "\"Empty\" color passed (defaults to black)." ""
49        # 空的 $color (色彩)参数.
50 cecho
51        # $message(信息) 和 $color (色彩)参数都缺失.
52 cecho "" ""
53        # 空的 $message (信息)和 $color (色彩)参数.
54 # ----------------------------------------------------
55
56 echo
57
58 exit 0
59
60 # 练习:
61 # ---------
62 # 1) 为'cecho ()'函数增加粗体的效果.
63 # 2) 增加可选的彩色背景.
################################End Script#########################################

Example 33-14 "赛马" 游戏
################################Start Script#######################################
  1 #!/bin/bash
  2 # horserace.sh: 非常简单的赛马模拟.
  3 # 作者: Stefano Palmeri
  4 # 已取得使用许可.
  5
  6 ################################################################
  7 #  脚本目的:
  8 #  使用转义字符和终端颜色.
  9 #
 10 #  练习:
 11 #  编辑脚本使其更具有随机性,
 12 #+ 设置一个假的赌场 . . .     
 13 #  嗯 . . . 嗯 . . . 这个开始使我想起了一部电影 . . .
 14 #
 15 #  脚本给每匹马一个随机的障碍.
 16 #  不均等会以障碍来计算
 17 #+ 并且用一种欧洲风格表达出来.
 18 #  例如: 机率(odds)=3.75 意味着如果你押1美元赢,
 19 #+ 你可以赢得3.75美元.
 20 #
 21 #  脚本已经在GNU/Linux操作系统上测试过 OS,
 22 #+ 测试终端有xterm 和 rxvt, 及 konsole.
 23 #  测试机器有AMD 900 MHz 的处理器,
 24 #+ 平均比赛时间是75秒.    
 25 #  在更快的计算机上比赛时间应该会更低.
 26 #  所以, 如果你想有更多的悬念,重设USLEEP_ARG 变量的值.
 27 #
 28 #  由Stefano Palmeri编写.
 29 ################################################################
 30
 31 E_RUNERR=65
 32
 33 # 检查 md5sum 和 bc 是不是安装了.
 34 if ! which bc &> /dev/null; then
 35    echo bc is not installed.  
 36    echo "Can\'t run . . . "
 37    exit $E_RUNERR
 38 fi
 39 if ! which md5sum &> /dev/null; then
 40    echo md5sum is not installed.  
 41    echo "Can\'t run . . . "
 42    exit $E_RUNERR
 43 fi
 44
 45 #  更改下面的变量值可以使脚本执行的更慢.
 46 #  它会作为usleep的参数 (man usleep)  
 47 #+ 并且它的单位是微秒 (500000微秒 = 半秒).
 48 USLEEP_ARG=0  
 49
 50 #  如果脚本接收到ctrl-c中断,清除临时目录, 恢复终端光标和颜色
 51 #
 52 trap 'echo -en "\E[?25h"; echo -en "\E[0m"; stty echo;\
 53 tput cup 20 0; rm -fr  $HORSE_RACE_TMP_DIR'  TERM EXIT
 54 #  参考调试的章节了解'trap'的更多解释
 55
 56 # 给脚本设置一个唯一(实际不是绝对唯一的)的临时目录名.
 57 HORSE_RACE_TMP_DIR=$HOME/.horserace-`date +%s`-`head -c10 /dev/urandom | md5sum | head -c30`
 58
 59 # 创建临时目录,并切换到该目录下.
 60 mkdir $HORSE_RACE_TMP_DIR
 61 cd $HORSE_RACE_TMP_DIR
 62
 63
 64 #  这个函数把光标移动到行为 $1 列为 $2 然后打印 $3.
 65 #  例如: "move_and_echo 5 10 linux" 等同于
 66 #+ "tput cup 4 9; echo linux", 但是用一个命令代替了两个.
 67 #  注: "tput cup" 表示在终端左上角的 0 0 位置,
 68 #+ echo 是在终端的左上角的 1 1 位置.
 69 move_and_echo() {
 70           echo -ne "\E[${1};${2}H""$3"
 71 }
 72
 73 # 产生1-9之间伪随机数的函数.
 74 random_1_9 () {
 75                 head -c10 /dev/urandom | md5sum | tr -d [a-z] | tr -d 0 | cut -c1
 76 }
 77
 78 #  画马时模拟运动的两个函数.
 79 draw_horse_one() {
 80                echo -n " "//$MOVE_HORSE//
 81 }
 82 draw_horse_two(){
 83               echo -n " "\\\\$MOVE_HORSE\\\\
 84 }   
 85
 86
 87 # 取得当前的终端尺寸.
 88 N_COLS=`tput cols`
 89 N_LINES=`tput lines`
 90
 91 # 至少需要 20-行 X 80-列 的终端尺寸. 检查一下.
 92 if [ $N_COLS -lt 80 ] || [ $N_LINES -lt 20 ]; then
 93    echo "`basename $0` needs a 80-cols X 20-lines terminal."
 94    echo "Your terminal is ${N_COLS}-cols X ${N_LINES}-lines."
 95    exit $E_RUNERR
 96 fi
 97
 98
 99 # 开始画赛场.
100
101 # 需要一个80个字符的字符串,看下面的.
102 BLANK80=`seq -s "" 100 | head -c80`
103
104 clear
105
106 # 把前景和背景颜色设置成白色的.
107 echo -ne '\E[37;47m'
108
109 # 把光标移到终端的左上角.
110 tput cup 0 0
111
112 # 画六条白线.
113 for n in `seq 5`; do
114       echo $BLANK80        # 线是用80个字符组成的字符串.  
115 done
116
117 # 把前景色设置成黑色.
118 echo -ne '\E[30m'
119
120 move_and_echo 3 1 "START  1"            
121 move_and_echo 3 75 FINISH
122 move_and_echo 1 5 "|"
123 move_and_echo 1 80 "|"
124 move_and_echo 2 5 "|"
125 move_and_echo 2 80 "|"
126 move_and_echo 4 5 "|  2"
127 move_and_echo 4 80 "|"
128 move_and_echo 5 5 "V  3"
129 move_and_echo 5 80 "V"
130
131 # 把前景色设置成红色.
132 echo -ne '\E[31m'
133
134 # 一些ASCII艺术.
135 move_and_echo 1 8 "..@@@..@@@@@...@@@@@.@...@..@@@@..."
136 move_and_echo 2 8 ".@...@...@.......@...@...@.@......."
137 move_and_echo 3 8 ".@@@@@...@.......@...@@@@@.@@@@...."
138 move_and_echo 4 8 ".@...@...@.......@...@...@.@......."
139 move_and_echo 5 8 ".@...@...@.......@...@...@..@@@@..."
140 move_and_echo 1 43 "@@@@...@@@...@@@@..@@@@..@@@@."
141 move_and_echo 2 43 "@...@.@...@.@.....@.....@....."
142 move_and_echo 3 43 "@@@@..@@@@@.@.....@@@@...@@@.."
143 move_and_echo 4 43 "@..@..@...@.@.....@.........@."
144 move_and_echo 5 43 "@...@.@...@..@@@@..@@@@.@@@@.."
145
146
147 # 把前景和背景颜色设为绿色.
148 echo -ne '\E[32;42m'
149
150 # 画11行绿线.
151 tput cup 5 0
152 for n in `seq 11`; do
153       echo $BLANK80
154 done
155
156 # 把前景色设为黑色.
157 echo -ne '\E[30m'
158 tput cup 5 0
159
160 # 画栅栏.
161 echo "++++++++++++++++++++++++++++++++++++++\
162 ++++++++++++++++++++++++++++++++++++++++++"
163
164 tput cup 15 0
165 echo "++++++++++++++++++++++++++++++++++++++\
166 ++++++++++++++++++++++++++++++++++++++++++"
167
168 # 把前景和背景色设回白色.
169 echo -ne '\E[37;47m'
170
171 # 画3条白线.
172 for n in `seq 3`; do
173       echo $BLANK80
174 done
175
176 # 把前景色设为黑色.
177 echo -ne '\E[30m'
178
179 # 创建9个文件来保存障碍物.
180 for n in `seq 10 7 68`; do
181       touch $n
182 done  
183
184 # 设置脚本要画的马的类型为第一种类型.
185 HORSE_TYPE=2
186
187 #  为每匹马创建位置文件和机率文件.
188 #+ 在这些文件里保存了该匹马当前的位置,
189 #+ 类型和机率.
190 for HN in `seq 9`; do
191       touch horse_${HN}_position
192       touch odds_${HN}
193       echo \-1 > horse_${HN}_position
194       echo $HORSE_TYPE >>  horse_${HN}_position
195       # 给马定义随机的障碍物.
196        HANDICAP=`random_1_9`
197       # 检查random_1_9函数是否返回了有效值.
198       while ! echo $HANDICAP | grep [1-9] &> /dev/null; do
199                 HANDICAP=`random_1_9`
200       done
201       # 给马定义最后的障碍的位置.
202       LHP=`expr $HANDICAP \* 7 + 3`
203       for FILE in `seq 10 7 $LHP`; do
204             echo $HN >> $FILE
205       done   
206      
207       # 计算机率.
208       case $HANDICAP in
209               1) ODDS=`echo $HANDICAP \* 0.25 + 1.25 | bc`
210                                  echo $ODDS > odds_${HN}
211               ;;
212               2 | 3) ODDS=`echo $HANDICAP \* 0.40 + 1.25 | bc`
213                                        echo $ODDS > odds_${HN}
214               ;;
215               4 | 5 | 6) ODDS=`echo $HANDICAP \* 0.55 + 1.25 | bc`
216                                              echo $ODDS > odds_${HN}
217               ;;
218               7 | 8) ODDS=`echo $HANDICAP \* 0.75 + 1.25 | bc`
219                                        echo $ODDS > odds_${HN}
220               ;;
221               9) ODDS=`echo $HANDICAP \* 0.90 + 1.25 | bc`
222                                   echo $ODDS > odds_${HN}
223       esac
224
225
226 done
227
228
229 # 打印机率.
230 print_odds() {
231 tput cup 6 0
232 echo -ne '\E[30;42m'
233 for HN in `seq 9`; do
234       echo "#$HN odds->" `cat odds_${HN}`
235 done
236 }
237
238 # 在起跑线上画马.
239 draw_horses() {
240 tput cup 6 0
241 echo -ne '\E[30;42m'
242 for HN in `seq 9`; do
243       echo /\\$HN/\\"                               "
244 done
245 }
246
247 print_odds
248
249 echo -ne '\E[47m'
250 # 等待回车按键开始赛马.
251 # 转义序列'\E[?25l'禁显了光标.
252 tput cup 17 0
253 echo -e '\E[?25l'Press [enter] key to start the race...
254 read -s
255
256 #  禁用了终端的常规显示功能.
257 #  这避免了赛跑时不小心按了按键键入显示字符而弄乱了屏幕.
258 #  
259 stty -echo
260
261 # --------------------------------------------------------
262 # 开始赛跑.
263
264 draw_horses
265 echo -ne '\E[37;47m'
266 move_and_echo 18 1 $BLANK80
267 echo -ne '\E[30m'
268 move_and_echo 18 1 Starting...
269 sleep 1
270
271 # 设置终点线的列数.
272 WINNING_POS=74
273
274 # 记录赛跑开始的时间.
275 START_TIME=`date +%s`
276
277 # COL 是由下面的"while"结构使用的.
278 COL=0    
279
280 while [ $COL -lt $WINNING_POS ]; do
281                    
282           MOVE_HORSE=0     
283           
284           # 检查random_1_9函数是否返回了有效值.
285           while ! echo $MOVE_HORSE | grep [1-9] &> /dev/null; do
286                 MOVE_HORSE=`random_1_9`
287           done
288           
289           # 取得随机取得的马的类型和当前位置.
290           HORSE_TYPE=`cat  horse_${MOVE_HORSE}_position | tail -1`
291           COL=$(expr `cat  horse_${MOVE_HORSE}_position | head -1`)
292           
293           ADD_POS=1
294           # 检查当前的位置是否是障碍物的位置.
295           if seq 10 7 68 | grep -w $COL &> /dev/null; then
296                 if grep -w $MOVE_HORSE $COL &> /dev/null; then
297                       ADD_POS=0
298                       grep -v -w  $MOVE_HORSE $COL > ${COL}_new
299                       rm -f $COL
300                       mv -f ${COL}_new $COL
301                       else ADD_POS=1
302                 fi
303           else ADD_POS=1
304           fi
305           COL=`expr $COL + $ADD_POS`
306           echo $COL >  horse_${MOVE_HORSE}_position  # 保存新位置.
307                             
308          # 选择要画的马的类型.         
309           case $HORSE_TYPE in
310                 1) HORSE_TYPE=2; DRAW_HORSE=draw_horse_two
311                 ;;
312                 2) HORSE_TYPE=1; DRAW_HORSE=draw_horse_one
313           esac       
314           echo $HORSE_TYPE >>  horse_${MOVE_HORSE}_position # 保存当前类型.
315          
316           # 把前景色设为黑,背景色设为绿.
317           echo -ne '\E[30;42m'
318           
319           # 把光标位置移到新的马的位置.
320           tput cup `expr $MOVE_HORSE + 5`  `cat  horse_${MOVE_HORSE}_position | head -1`
321           
322           # 画马.
323           $DRAW_HORSE
324            usleep $USLEEP_ARG
325           
326            # 当所有的马都越过15行的之后,再次打印机率.          
327            touch fieldline15
328            if [ $COL = 15 ]; then
329              echo $MOVE_HORSE >> fieldline15  
330            fi
331            if [ `wc -l fieldline15 | cut -f1 -d " "` = 9 ]; then
332                print_odds
333                : > fieldline15
334            fi           
335           
336           # 取得领头的马.
337           HIGHEST_POS=`cat *position | sort -n | tail -1`          
338           
339           # 把背景色重设为白色.
340           echo -ne '\E[47m'
341           tput cup 17 0
342           echo -n Current leader: `grep -w $HIGHEST_POS *position | cut -c7`"                              "           
343
344 done  
345
346 # 取得赛马结束的时间.
347 FINISH_TIME=`date +%s`
348
349 # 背景色设为绿色并且启用闪动的功能.
350 echo -ne '\E[30;42m'
351 echo -en '\E[5m'
352
353 # 使获胜的马闪动.
354 tput cup `expr $MOVE_HORSE + 5` `cat  horse_${MOVE_HORSE}_position | head -1`
355 $DRAW_HORSE
356
357 # 禁用闪动文本.
358 echo -en '\E[25m'
359
360 # 把前景和背景色设为白色.
361 echo -ne '\E[37;47m'
362 move_and_echo 18 1 $BLANK80
363
364 # 前景色设为黑色.
365 echo -ne '\E[30m'
366
367 # 闪动获胜的马.
368 tput cup 17 0
369 echo -e "\E[5mWINNER: $MOVE_HORSE\E[25m""  Odds: `cat odds_${MOVE_HORSE}`"\
370 "  Race time: `expr $FINISH_TIME - $START_TIME` secs"
371
372 # 恢复光标和最初的颜色.
373 echo -en "\E[?25h"
374 echo -en "\E[0m"
375
376 # 恢复回显功能.
377 stty echo
378
379 # 删除赛跑的临时文件.
380 rm -rf $HORSE_RACE_TMP_DIR
381
382 tput cup 19 0
383
384 exit 0
################################End Script#########################################
参考 例子 A-22.

注意:    然而,有一个主要的问题,那就是ANSI 转义序列是不可移植的. 在一些终端运行的
        很好的代码可能在另外一些终端上可能运行地很糟糕. 在彩色脚本作者终端上运行的
        很好的脚本可能在另外一些终端上就产生不可阅读的输出了. 这给彩色脚本的用处大
        大打了个折扣,而很可能使这些技术变成一个暗机关或只是一个玩具而已.

Moshe Jacobson的颜色工具(http://runslinux.net/projects.html#color)能相当容易地使用
ANSI转义序列. 它用清晰和较有逻辑的语法来代替刚才讨论的难用的结构.

Henry/teikedvl 也同样开发了一个软件包来简化彩色脚本的一些操作
(http://scriptechocolor.sourceforge.net/).

注意事项:
[1]        当然,ANSI是American National Standards Institute(美国国家标准组织)的缩
        写. 这个令人敬畏的组织建立和维护着许多技术和工业的标准.

33.6. 优化
----------
大多数shell脚本处理不复杂的问题时会有很快的解决办法. 正因为这样,优化脚本速度不是
一个问题. 考虑这样的情况, 一个脚本处理很重要的任务, 虽然它确实运行的很好很正确,但
是处理速度太慢. 用一种可编译的语言重写它可能不是非常好的选择. 最简单的办法是重写使
这个脚本效率低下的部分. 这个代码优化的原理是否同样适用于效率低下的shell脚本?

检查脚本中的循环. 反复执行操作的时间消耗增长非常的快. 如果可能, 可以从循环中删除时
间消耗的操作.

优先使用内建(builtin)命令而不是系统命令. 内建命令执行起来更快并且一般调用时不会
产生新的子shell.

避免不需要的命令, 特别是管道(pipe).

   1 cat "$file" | grep "$word"
   2
   3 grep "$word" "$file"
   4
   5 #  上面的命令行有同样的效果,
   6 #+ 但第二个运行的更有效率,因为它不产生新的子进程.

cat 命令似乎特别常在脚本中被滥用.

用time和times工具去了解计算花费的时间. 考虑用C甚至是汇编重写关键的消耗时间的部分.

尝试最小化文件I/O. Bash在文件处理上不是特别地有效率, 所以要考虑在脚本中使用更合适
地工具来处理, 比如说awk 或 Perl.

采用结构化的思想来写脚本, 使各个模块能够依据需要组织和合并起来.一些适用于高级语言
的优化技术也可以用在脚本上 , 但有些技术, 比如说循环优化, 几乎是不相关的. 上面的讨
论, 依据经验来判断.

怎样优化减少执行时间的优秀脚本示例, 请参考例子 12-42.

33.7. 各种小技巧
----------------
*    为了记录在一个实际的会话期或多个会话期内运行的用户脚本,可以加下面的代码到每
    个你想追踪记录的脚本里. 这会记录下连续的脚本名记录和调用的次数.

       1 # 添加(>>)下面几行到你想追踪记录的脚本末尾处.
       2
       3 whoami>> $SAVE_FILE    # 记录调用脚本的用户.
       4 echo $0>> $SAVE_FILE   # 记录脚本名.
       5 date>> $SAVE_FILE      # 记录日期和时间.
       6 echo>> $SAVE_FILE      # 空行作为分隔行.
       7
       8 #  当然, SAVE_FILE 变量应在~/.bashrc中定义并导出(export)
       9 #+ (变量值类似如 ~/.scripts-run)

*    >> 操作符可以在文件尾添加内容. 如果你想在文件头添加内容,那应该怎么办?

       1 file=data.txt
       2 title="***This is the title line of data text file***"
       3
       4 echo $title | cat - $file >$file.new
       5 # "cat -" 连接标准输出的内容和$file的内容.
       6 #  最后的结果就是生成了一个新文件,
       7 #+ 文件的头添加了 $title 的值,后跟$file的内容.

    这是早先例子 17-13中的简化变体. 当然, , sed 也可以办到.

*    脚本也可以像内嵌到另一个shell脚本的普通命令一样调用, 如 Tcl 或 wish 脚本, 甚至
    可以是Makefile. 它们可以作为外部shell命令用C语言的 system() 函数调用, 例如.,
    system("script_name");.

*    把内嵌的 sed 或 awk 脚本的内容赋值给一个变量可以增加包装脚本(shell wrapper)
    的可读性. 参考 例子 A-1 和 例子 11-18.

*    把你最喜欢和最有用的定义和函数放在一些文件中. 当需要的使用的时候, 在脚本中使用
    dot (.) 或 source 命令来"包含(include)"这些"库文件"的一个或多个.

       1 # 脚本库
       2 # ------ -------
       3
       4 # 注:
       5 # 本文件没有"#!"开头.
       6 # 也没有真正做执行动作的代码.
       7
       8
       9 # 有用的变量定义
      10
      11 ROOT_UID=0             # Root用户的 $UID 值是0.
      12 E_NOTROOT=101          # 非root用户出错代码.
      13 MAXRETVAL=255          # 函数最大的的返回值(正值).
      14 SUCCESS=0
      15 FAILURE=-1
      16
      17
      18
      19 # 函数
      20
      21 Usage ()               # "Usage:" 信息(即帮助信息).
      22 {
      23   if [ -z "$1" ]       # 没有传递参数.
      24   then
      25     msg=filename
      26   else
      27     msg=$@
      28   fi
      29
      30   echo "Usage: `basename $0` "$msg""
      31 }  
      32
      33
      34 Check_if_root ()       # 检查是不是root在运行脚本.
      35 {                      # 取自例子"ex39.sh".
      36   if [ "$UID" -ne "$ROOT_UID" ]
      37   then
      38     echo "Must be root to run this script."
      39     exit $E_NOTROOT
      40   fi
      41 }  
      42
      43
      44 CreateTempfileName ()  # 创建一个"唯一"的临时文件.
      45 {                      # 取自例子"ex51.sh".
      46   prefix=temp
      47   suffix=`eval date +%s`
      48   Tempfilename=$prefix.$suffix
      49 }
      50
      51
      52 isalpha2 ()            # 测试字符串是不是都是字母组成的.
      53 {                      # 取自例子"isalpha.sh".
      54   [ $# -eq 1 ] || return $FAILURE
      55
      56   case $1 in
      57   *[!a-zA-Z]*|"") return $FAILURE;;
      58   *) return $SUCCESS;;
      59   esac                 # Thanks, S.C.
      60 }
      61
      62
      63 abs ()                           # 绝对值.
      64 {                                # 注意: 最大的返回值 = 255.
      65   E_ARGERR=-999999
      66
      67   if [ -z "$1" ]                 # 要传递参数.
      68   then
      69     return $E_ARGERR             # 返回错误.
      70   fi
      71
      72   if [ "$1" -ge 0 ]              # 如果非负的值,
      73   then                           #
      74     absval=$1                    # 绝对值是本身.
      75   else                           # 否则,
      76     let "absval = (( 0 - $1 ))"  # 改变它的符号.
      77   fi  
      78
      79   return $absval
      80 }
      81
      82
      83 tolower ()             #  把传递的字符串转为小写
      84 {                      #
      85
      86   if [ -z "$1" ]       #  如果没有传递参数,
      87   then                 #+ 打印错误信息
      88     echo "(null)"      #+ (C风格的void指针的错误信息)
      89     return             #+ 然后从函数中返回.
      90   fi  
      91
      92   echo "$@" | tr A-Z a-z
      93   # 转换传递过来的所有参数($@).
      94
      95   return
      96
      97 # 用命令替换功能把函数的输出赋给变量.
      98 # 例如:
      99 #    oldvar="A seT of miXed-caSe LEtTerS"
     100 #    newvar=`tolower "$oldvar"`
     101 #    echo "$newvar"    # 一串混合大小写的字符转换成了全部小写字符
     102 #
     103 # 练习: 重写这个函数把传递的参数变为大写
     104 #       ... toupper()  [容易].
     105 }
*    在脚本中添加特殊种类的注释开头标识有助于条理清晰和可读性.
       1 ## 表示注意.
       2 rm -rf *.zzy   ##  "rm"命令的"-rf"组合选项非常的危险,
       3                ##+ 尤其是对通配符而言.
       4
       5 #+ 表示继续上一行.
       6 #  这是第一行
       7 #+ 这是多行的注释,
       8 #+ 这里是最后一行.
       9
      10 #* 表示标注.
      11
      12 #o 表示列表项.
      13
      14 #> 表示另一个观点.
      15 while [ "$var1" != "end" ]    #> while test "$var1" != "end"

*    if-test 结构的一种聪明用法是用来注释一块代码块.
       1 #!/bin/bash
       2
       3 COMMENT_BLOCK=
       4 #  给上面的变量设置某个值就会产生讨厌的结果
       5 #
       6
       7 if [ $COMMENT_BLOCK ]; then
       8
       9 Comment block --
      10 =================================
      11 This is a comment line.
      12 This is another comment line.
      13 This is yet another comment line.
      14 =================================
      15
      16 echo "This will not echo."
      17
      18 Comment blocks are error-free! Whee!
      19
      20 fi
      21
      22 echo "No more comments, please."
      23
      24 exit 0
    把这种方法和使用here documents来注释代码块作一个比较.
*    测试$? 退出状态变量, 因为一个脚本可能想要测试一个参数是否只包含数字,以便后面
    可以把它当作一个整数.
       1 #!/bin/bash
       2
       3 SUCCESS=0
       4 E_BADINPUT=65
       5
       6 test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null
       7 # 整数要么等于零要么不等于零.
       8 # 2>/dev/null 可以抑制错误信息.
       9
      10 if [ $? -ne "$SUCCESS" ]
      11 then
      12   echo "Usage: `basename $0` integer-input"
      13   exit $E_BADINPUT
      14 fi
      15
      16 let "sum = $1 + 25"             # 如果$1不是整数就会产生错误.
      17 echo "Sum = $sum"
      18
      19 # 任何变量,而不仅仅命令行参数可用这种方法来测试.
      20
      21 exit 0

*    0 - 255 范围的函数返回值是个严格的限制. 用全局变量和其他方法常常出问题. 函数内
    返回值给脚本主体的另一个办法是让函数写值到标准输出(通常是用echo) 作为"返回值",
    并且将其赋给一个变量. 这实际是命令替换(command substitution)的变体.

Example 33-15 返回值技巧
################################Start Script#######################################
 1 #!/bin/bash
 2 # multiplication.sh
 3
 4 multiply ()                     # 传递乘数.
 5 {                               # 能接受多个参数.
 6
 7   local product=1
 8
 9   until [ -z "$1" ]             # 直到所有参数都处理完毕...
10   do
11     let "product *= $1"
12     shift
13   done
14
15   echo $product                 #  不会打印到标准输出,
16 }                               #+ 因为要把它赋给一个变量.
17
18 mult1=15383; mult2=25211
19 val1=`multiply $mult1 $mult2`
20 echo "$mult1 X $mult2 = $val1"
21                                 # 387820813
22
23 mult1=25; mult2=5; mult3=20
24 val2=`multiply $mult1 $mult2 $mult3`
25 echo "$mult1 X $mult2 X $mult3 = $val2"
26                                 # 2500
27
28 mult1=188; mult2=37; mult3=25; mult4=47
29 val3=`multiply $mult1 $mult2 $mult3 $mult4`
30 echo "$mult1 X $mult2 X $mult3 X $mult4 = $val3"
31                                 # 8173300
32
33 exit 0
################################End Script#########################################
    相同的技术也可用在字符串中. 这意味着函数可以"返回"一个非数字的值.

       1 capitalize_ichar ()          #  把传递来的参数字符串的第一个字母大写
       2 {                            #
       3
       4   string0="$@"               # 能接受多个参数.
       5
       6   firstchar=${string0:0:1}   # 第一个字符.
       7   string1=${string0:1}       # 余下的字符.
       8
       9   FirstChar=`echo "$firstchar" | tr a-z A-Z`
      10                              # 第一个字符转换成大写字符.
      11
      12   echo "$FirstChar$string1"  # 打印到标准输出.
      13
      14 }  
      15
      16 newstring=`capitalize_ichar "every sentence should start with a capital letter."`
      17 echo "$newstring"          # Every sentence should start with a capital letter.

    用这种办法甚至可能"返回" 多个值.

Example 33-16 整型还是string?
################################Start Script#######################################
 1 #!/bin/bash
 2 # sum-product.sh
 3 # 函数可以"返回"多个值.
 4
 5 sum_and_product ()   # 计算所传参数的总和与乘积.
 6 {
 7   echo $(( $1 + $2 )) $(( $1 * $2 ))
 8 # 打印每个计算的值到标准输出,各值用空格分隔开.
 9 }
10
11 echo
12 echo "Enter first number "
13 read first
14
15 echo
16 echo "Enter second number "
17 read second
18 echo
19
20 retval=`sum_and_product $first $second`      # 把函数的输出赋值给变量.
21 sum=`echo "$retval" | awk '{print $1}'`      # 把第一个域的值赋给sum变量.
22 product=`echo "$retval" | awk '{print $2}'`  # 把第二个域的值赋给product变量.
23
24 echo "$first + $second = $sum"
25 echo "$first * $second = $product"
26 echo
27
28 exit 0
################################End Script#########################################

*    下一个技巧是传递数组给函数的技术, 然后 "返回" 一个数组给脚本.

    用 变量替换(command substitution)把数组的所有元素用空格分隔开来并赋给一个变量
    就可以实现给函数传递数组. 用先前介绍的方法函数内echo一个数组并"返回此值",然后
    调用命令替换用 ( ... ) 操作符赋值给一个数组.

Example 33-17 传递和返回数组
################################Start Script#######################################
 1 #!/bin/bash
 2 # array-function.sh: 传递一个数组给函数并且...
 3 #                   从函数"返回"一个数组
 4
 5
 6 Pass_Array ()
 7 {
 8   local passed_array   # 局部变量.
 9   passed_array=( `echo "$1"` )
10   echo "${passed_array[@]}"
11   #  列出新数组中的所有元素
12   #+ 新数组是在函数内声明和赋值的.
13 }
14
15
16 original_array=( element1 element2 element3 element4 element5 )
17
18 echo
19 echo "original_array = ${original_array[@]}"
20 #                      列出最初的数组元素.
21
22
23 # 下面是传递数组给函数的技巧.
24 # **********************************
25 argument=`echo ${original_array[@]}`
26 # **********************************
27 #  把原数组的所有元素用空格分隔开合成一个字符串并赋给一个变量
28 #
29 #
30 # 注意:只是把数组本身传给函数是不会工作的.
31
32
33 # 下面是允许数组作为"返回值"的技巧.
34 # *****************************************
35 returned_array=( `Pass_Array "$argument"` )
36 # *****************************************
37 # 把函数的输出赋给数组变量.
38
39 echo "returned_array = ${returned_array[@]}"
40
41 echo "============================================================="
42
43 #  现在,再试一次Now, try it again,
44 #+ 尝试在函数外存取(列出)数组.
45 Pass_Array "$argument"
46
47 # 函数本身可以列出数组,但...
48 #+ 函数外存取数组被禁止.
49 echo "Passed array (within function) = ${passed_array[@]}"
50 # 因为变量是函数内的局部变量,所以只有NULL值.
51
52 echo
53
54 exit 0
################################End Script#########################################
    在例子 A-10中有一个更精心制作的给函数传递数组的例子.

*    利用双括号结构,使在for 和 while 循环中可以使用C风格的语法来设置和增加变量. 参
    考例子 10-12 和 例子 10-17.

*    在脚本开头设置 path 和 umask 增加脚本的"可移植性" -- 在某些把 $PATH 和 umask
    弄乱的系统里也可以运行.

       1 #!/bin/bash
       2 PATH=/bin:/usr/bin:/usr/local/bin ; export PATH
       3 umask 022   # 脚本的创建的文件有 755 的权限设置.
       4
       5 # 多谢Ian D. Allen提出这个技巧.

*    一个有用的脚本技术是:重复地把一个过滤器的输出回馈(用管道)给另一个相同过滤器,
    但过滤器有不同的参数和/或选项. 尤其对 tr 和 grep 更合适.

       1 # 取自例子"wstrings.sh".
       2
       3 wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
       4 tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`

Example 33-18 anagrams游戏
################################Start Script#######################################
 1 #!/bin/bash
 2 # agram.sh: 用anagrams玩游戏.
 3
 4 # 寻找 anagrams ...
 5 LETTERSET=etaoinshrdlu
 6 FILTER='.......'       # 最小有多少个字母?
 7 #       1234567
 8
 9 anagram "$LETTERSET" | # 找出这串字符中所有的 anagrams ...
10 grep "$FILTER" |       # 至少7个字符,
11 grep '^is' |           # 以'is'开头
12 grep -v 's$' |         # 不是复数的(指英文单词复数)
13 grep -v 'ed$'          # 不是过去式的(当然也是英文单词)
14 # 可以加许多组合条件和过滤器.
15
16 #  使用 "anagram" 软件
17 #+ 它是作者 "yawl" 单词列表软件包的一部分.
18 #  http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
19 #  http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz
20
21 exit 0                 # 代码结束.
22
23
24 bash$ sh agram.sh
25 islander
26 isolate
27 isolead
28 isotheral
29
30
31
32 #  练习:
33 #  ---------
34 #  修改这个脚本使 LETTERSET 能作为命令行参数来接受.
35 #  能够传递参数给第 11 - 13 行的过滤器(就像 $FILTER),
36 #+ 以便能靠传递参数来指定一种功能.
37
38 #  参考agram2.sh了解些微不同的anagram的一种方法
39 #
################################End Script#########################################
    See also Example 27-3, Example 12-22, and Example A-9.

*    使用"匿名的 here documents" 来注释代码块,这样避免了对代码块的每一块单独用#来
    注释了. 参考例子 17-11.

*    当依赖某个命令脚本在一台没有安装该命令的机器上运行时会出错. 使用 whatis 命令可
    以避免此问题.

       1 CMD=command1                 # 第一选择First choice.
       2 PlanB=command2               # 第二选择Fallback option.
       3
       4 command_test=$(whatis "$CMD" | grep 'nothing appropriate')
       5 #  如果'command1'没有在系统里发现 , 'whatis'会返回:
       6 #+ "command1: nothing appropriate."
       7 #
       8 #  另一种更安全的办法是:
       9 #     command_test=$(whereis "$CMD" | grep \/)
      10 #  但后面的测试判断应该翻转过来,
      11 #+ 因为$command_test只有当系统存在$CMD命令时才有内容.
      12 #
      13 #     (Thanks, bojster.)
      14
      15
      16 if [[ -z "$command_test" ]]  # 检查命令是否存在.
      17 then
      18   $CMD option1 option2       #  调用command1.
      19 else                         #  否则,
      20   $PlanB                     #+ 调用command2.
      21 fi

*    在发生错误的情况下 if-grep test 可能不会返回期望的结果,因为文本是打印在标准出
    错而不是标准输出上.

       1 if ls -l nonexistent_filename | grep -q 'No such file or directory'
       2   then echo "File \"nonexistent_filename\" does not exist."
       3 fi

    把标准出错重定向到标准输出上可以修改这个.

       1 if ls -l nonexistent_filename 2>&1 | grep -q 'No such file or directory'
       2 #                             ^^^^
       3   then echo "File \"nonexistent_filename\" does not exist."
       4 fi
       5
       6 # 多谢Chris Martin指出.

*    The run-parts 命令很容易依次运行一组命令脚本,特别是和 cron 或 at 组合起来.

*    在shell脚本里能调用 X-Windows 的窗口小部件将多么美好. 已经存在有几种工具包实现
    这个了, 它们称为Xscript, Xmenu, 和 widtools. 头两个已经不再维护. 幸运地是仍然
    可以从这儿下载widtools.

    注意:    widtools (widget tools) 工具包要求安装了 XForms 库. 另外, 它的 Makefile
            在典型的Linux系统上安装前需要做一些合适的编辑. 最后, 提供的6个部件有3个
            不能工作 (事实上会发生段错误).

    dialog 工具集提供了shell脚本使用一种称为"对话框"的窗口部件. 原始的 dialog 软件
    包工作在文本模式的控制台下, 但它的后续软件 gdialog, Xdialog, 和 kdialog 使用基
    于X-Windows的窗口小部件集.

Example 33-19 在shell脚本中调用的窗口部件
################################Start Script#######################################
 1 #!/bin/bash
 2 # dialog.sh: 使用 'gdialog' 窗口部件.
 3 # 必须在你的系统里安装'gdialog'才能运行此脚本.
 4 # 版本 1.1 (04/05/05 修正)
 5
 6 # 这个脚本的灵感源自下面的文章.
 7 #     "Scripting for X Productivity," by Marco Fioretti,
 8 #      LINUX JOURNAL, Issue 113, September 2003, pp. 86-9.
 9 # Thank you, all you good people at LJ.
10
11
12 # 在窗口中的输入错误.
13 E_INPUT=65
14 # 输入窗口显示的尺寸.
15 HEIGHT=50
16 WIDTH=60
17
18 # 输出文件名 (由脚本名构建而来).
19 OUTFILE=$0.output
20
21 # 把这个脚本的内容显示在窗口中.
22 gdialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH
23
24
25
26 # 现在,保存输入到输出文件中.
27 echo -n "VARIABLE=" > $OUTFILE
28 gdialog --title "User Input" --inputbox "Enter variable, please:" \
29 $HEIGHT $WIDTH 2>> $OUTFILE
30
31
32 if [ "$?" -eq 0 ]
33 # 检查退出状态是一个好习惯.
34 then
35   echo "Executed \"dialog box\" without errors."
36 else
37   echo "Error(s) in \"dialog box\" execution."
38         # 或者, 点击"Cancel", 而不是"OK" 按钮.
39   rm $OUTFILE
40   exit $E_INPUT
41 fi
42
43
44
45 # 现在,我们重新取得并显示保存的变量.
46 . $OUTFILE   # 'Source' 保存的文件(即执行).
47 echo "The variable input in the \"input box\" was: "$VARIABLE""
48
49
50 rm $OUTFILE  # 清除临时文件.
51              # 有些应用可能需要保留这些文件.
52
53 exit $?
################################End Script#########################################
    其他的在脚本中使用窗口的工具还有 Tk 或 wish (Tcl 派生物), PerlTk (Perl 的 Tk
    扩展), tksh (ksh 的 Tk 扩展), XForms4Perl (Perl 的 XForms 扩展), Gtk-Perl
    (Perl 的 Gtk 扩展), 或 PyQt (Python 的 Qt 扩展).

*    为了对复杂的脚本做多次的版本修订管理, 可以使用 rcs 软件包.

    使用这个软件包的好处之一是会自动地升级ID头标识.在 rcs 的co命令处理一些预定义的
    关键字参数替换,例如,代替脚本里头#$Id$的,如类似下面的行:

       1 #$Id: hello-world.sh,v 1.1 2004/10/16 02:43:05 bozo Exp $


33.8. 安全话题
--------------

33.8.1. 被感染的脚本
--------------------
有一个简短的关于脚本安全的介绍是适当的. 脚本程序可能会包含蠕虫病毒,特洛伊木马, 或
是其他的病毒. 由于这些原因, 决不要以root身份运行脚本 (或允许它被插入到系统的
/etc/rc.d里的启动脚本中) 除非你确定这是值得信赖的源码或你已经很小心地分析过了脚本
并确信它不会有什么危害.

Bell实验室及其他地方的病毒研究人员, 包括 M. Douglas McIlroy, Tom Duff, 和
Fred Cohen 已经调查过了shell脚本病毒的实现. 他们认为写脚本病毒甚至对于新手来说也是
很容易的,就像“脚本小子(script kiddie)”也能容易地写出. [1]

这也是学习脚本编程的原因之一:学习读懂脚本和理解脚本可以保护你的系统免受骇客攻击或
破坏.

33.8.2. 隐藏Shell脚本源码
-------------------------
为了安全, 使脚本不可读是有必要的. 如果有软件可以把脚本转化成相应的二进制执行文件就
好了. Francisco Rosales的 shc - 通用的Shell脚本编译器(generic shell script
compiler) 可以出色地完成目标.

不幸地, 依照发表在2005年十月的Linux Journal杂志上的一篇文章, 二进制文件可以,至少
在某些情况下能被恢复回脚本的原始代码. 但不管怎么说,这对大多数技术不高超的骇客来说
仍然是一个保持脚本安全的有效的方法.

注意事项:
[1]        参考 Marius van Oers 的文章(Unix Shell Scripting Malware),和列在参考书目
        (bibliography)的参考资料.


33.9. 移植话题
--------------
这本书是关于在GNU/Linux系统下的Bash编程. 但同样,sh  和 ksh 用户也能在这儿得到许多
有用的价值.

以现在的情况来看,许多种shell和脚本语言都尽力使自己符合 POSIX 1003.2 标准. 用
--posix 选项调用Bash或在脚本开头插入 set -o posix 就能使Bash能以很接近这个标准的方
式运行. 在脚本开头用

   1 #!/bin/sh

比用

   1 #!/bin/bash

会更好.注意在Linux和一些UNIX风格的系统里/bin/sh是/bin/bash的一个链接(link), 并且
如果脚本以/bin/sh调用时会禁用Bash的扩展功能.

大多数的Bash脚本能不作修改就能运行在 ksh下, 反之亦然, 因为 Chet Ramey 辛勤地把 ksh
的属性移植到了最新的Bash版本.

在商业的 UNIX 机器上, 使用了GNU扩展属性的标准命令的脚本可能不会工作. 这个问题在最
近几年已经有所改观了, 因为GNU软件包完美地代替了在这些"大块头的"UNIX运行的相应工具.
源码分发 给传统UNIX上加快了这种趋势.

Bash 有传统的 Bourne shell 缺乏的一些属性. 下面是其中一些:
*    一些扩展的 调用选项(invocation options)
*    使用 $( ) 结构来完成命令替换(Command substitution)
*    一些 字符串处理(string manipulation)  操作符
*    进程替换(Process substitution)
*    Bash的 内建(builtins) 命令

参考 Bash F.A.Q. 查看完整的列表.


33.10. 在Windows下进行Shell编程
-------------------------------
使用其他操作系统用户希望能运行UNIX类型的脚本能在他们的系统上运行, 因此也希望能在这
本书里能学到这方面的知识. 来自Cygnus的 Cygwin 软件结合来自Mortice Kern的MKS软件包
(MKS utilities)可以给Windows添加shell脚本的兼容.

已经有正式宣布Windows的将来版本会包含Bash风格的命令行和脚本能力,但目前为止还没有
结果.


第34章    Bash, 版本 2 和 3
=========================

34.1. Bash, 版本2
-----------------
当前运行在你的机器里的Bash版本号是版本 2.xx.y 或 3.xx.y.

 bash$ echo $BASH_VERSION
 2.05.b.0(1)-release

经典的Bash版本2编程语言升级版增加了数组变量, [1] 字符串和参数扩展, 和间接变量引用
的更好的方法,及其他的属性.

Example 34-1 字符串扩展
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 字符串扩展.
 4 # Bash版本2引入的特性.
 5
 6 #  具有$'xxx'格式的字符串
 7 #+ 将会解释里面的标准的转义字符.
 8
 9 echo $'Ringing bell 3 times \a \a \a'
10      # 可能在一些终端只能响铃一次.
11 echo $'Three form feeds \f \f \f'
12 echo $'10 newlines \n\n\n\n\n\n\n\n\n\n'
13 echo $'\102\141\163\150'   # Bash
14                            # 八进制相等的字符.
15
16 exit 0
################################End Script#########################################

Example 34-2 间接变量引用 - 新方法
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 间接变量引用.
 4 # 这有点像C++的引用属性.
 5
 6
 7 a=letter_of_alphabet
 8 letter_of_alphabet=z
 9
10 echo "a = $a"           # 直接引用.
11
12 echo "Now a = ${!a}"    # 间接引用.
13 # ${!variable} 形式比老的"eval var1=\$$var2"更高级
14
15 echo
16
17 t=table_cell_3
18 table_cell_3=24
19 echo "t = ${!t}"                      # t = 24
20 table_cell_3=387
21 echo "Value of t changed to ${!t}"    # 387
22
23 #  这在用来引用数组或表格的成员时非常有用,
24 #+ 或用来模拟多维数组.
25 #  如果有可索引的选项 (类似于指针运算)
26 #+ 会更好. 唉.
27
28 exit 0
################################End Script#########################################

Example 34-3 使用间接变量引用的简单数据库应用
################################Start Script#######################################
 1 #!/bin/bash
 2 # resistor-inventory.sh
 3 # 使用间接变量引用的简单数据库应用.
 4
 5 # ============================================================== #
 6 # 数据
 7
 8 B1723_value=470                                   # 值
 9 B1723_powerdissip=.25                             # 是什么
10 B1723_colorcode="yellow-violet-brown"             # 色彩带宽
11 B1723_loc=173                                     # 它们存在哪儿
12 B1723_inventory=78                                # 有多少
13
14 B1724_value=1000
15 B1724_powerdissip=.25
16 B1724_colorcode="brown-black-red"
17 B1724_loc=24N
18 B1724_inventory=243
19
20 B1725_value=10000
21 B1725_powerdissip=.25
22 B1725_colorcode="brown-black-orange"
23 B1725_loc=24N
24 B1725_inventory=89
25
26 # ============================================================== #
27
28
29 echo
30
31 PS3='Enter catalog number: '
32
33 echo
34
35 select catalog_number in "B1723" "B1724" "B1725"
36 do
37   Inv=${catalog_number}_inventory
38   Val=${catalog_number}_value
39   Pdissip=${catalog_number}_powerdissip
40   Loc=${catalog_number}_loc
41   Ccode=${catalog_number}_colorcode
42
43   echo
44   echo "Catalog number $catalog_number:"
45   echo "There are ${!Inv} of [${!Val} ohm / ${!Pdissip} watt] resistors in stock."
46   echo "These are located in bin # ${!Loc}."
47   echo "Their color code is \"${!Ccode}\"."
48
49   break
50 done
51
52 echo; echo
53
54 # 练习:
55 # ---------
56 # 1) 重写脚本,使其从外部文件里读数据.
57 # 2) 重写脚本,用数组代替间接变量引用
58 #
59 #    用数组会更简单明了
60
61
62 # 注:
63 # -----
64 #  Shell脚本除了最简单的数据应用,其实并不合适数据库应用,
65 #+ 它过多地依赖实际工作的环境和命令.
66 #  写数据库应用更好的还是用一门自然支持数据结构的语言,
67 #+ 如 C++ 或 Java (或甚至是 Perl).
68
69 exit 0
################################End Script#########################################

Example 34-4 用数组和其他的小技巧来处理四人随机打牌
################################Start Script#######################################
  1 #!/bin/bash
  2
  3 # Cards:
  4 # 处理四人打牌.
  5
  6 UNPICKED=0
  7 PICKED=1
  8
  9 DUPE_CARD=99
 10
 11 LOWER_LIMIT=0
 12 UPPER_LIMIT=51
 13 CARDS_IN_SUIT=13
 14 CARDS=52
 15
 16 declare -a Deck
 17 declare -a Suits
 18 declare -a Cards
 19 #  用一个三维数据来描述数据会更容易实现也更明了一些.
 20 #
 21 #  可能Bash将来的版本会支持多维数组.
 22
 23
 24 initialize_Deck ()
 25 {
 26 i=$LOWER_LIMIT
 27 until [ "$i" -gt $UPPER_LIMIT ]
 28 do
 29   Deck[i]=$UNPICKED   # 把整副牌的每张牌都设为没人持牌.
 30   let "i += 1"
 31 done
 32 echo
 33 }
 34
 35 initialize_Suits ()
 36 {
 37 Suits[0]=C #梅花
 38 Suits[1]=D #方块
 39 Suits[2]=H #红心
 40 Suits[3]=S #黑桃
 41 }
 42
 43 initialize_Cards ()
 44 {
 45 Cards=(2 3 4 5 6 7 8 9 10 J Q K A)
 46 # 另一种初始化数组的方法.
 47 }
 48
 49 pick_a_card ()
 50 {
 51 card_number=$RANDOM
 52 let "card_number %= $CARDS"
 53 if [ "${Deck[card_number]}" -eq $UNPICKED ]
 54 then
 55   Deck[card_number]=$PICKED
 56   return $card_number
 57 else  
 58   return $DUPE_CARD
 59 fi
 60 }
 61
 62 parse_card ()
 63 {
 64 number=$1
 65 let "suit_number = number / CARDS_IN_SUIT"
 66 suit=${Suits[suit_number]}
 67 echo -n "$suit-"
 68 let "card_no = number % CARDS_IN_SUIT"
 69 Card=${Cards[card_no]}
 70 printf %-4s $Card
 71 # 优雅地打印各张牌.
 72 }
 73
 74 seed_random ()  # 随机产生牌上数值的种子.
 75 {               # 如果你没有这么做会有什么发生?
 76 seed=`eval date +%s`
 77 let "seed %= 32766"
 78 RANDOM=$seed
 79 #  其他的产生随机用的种子的方法还有什么W?
 80 #
 81 }
 82
 83 deal_cards ()
 84 {
 85 echo
 86
 87 cards_picked=0
 88 while [ "$cards_picked" -le $UPPER_LIMIT ]
 89 do
 90   pick_a_card
 91   t=$?
 92
 93   if [ "$t" -ne $DUPE_CARD ]
 94   then
 95     parse_card $t
 96
 97     u=$cards_picked+1
 98     # 改回1步进的索引(临时的). 为什么?
 99     let "u %= $CARDS_IN_SUIT"
100     if [ "$u" -eq 0 ]   # 内嵌的 if/then 条件测试.
101     then
102      echo
103      echo
104     fi
105     # Separate hands.
106
107     let "cards_picked += 1"
108   fi  
109 done  
110
111 echo
112
113 return 0
114 }
115
116
117 # 结构化编程:
118 # 整个程序逻辑模块化.
119
120 #================
121 seed_random
122 initialize_Deck
123 initialize_Suits
124 initialize_Cards
125 deal_cards
126 #================
127
128 exit 0
129
130
131
132 # 练习 1:
133 # 把这个脚本完整地做注释.
134
135 # 练习 2:
136 # 增加一个处理例程 (函数) 来以花色排序打印出每个人手中的牌.
137 # 如果你高兴,可增加你喜欢的各种酷的代码.
138
139 # 练习 3:
140 # 简化和理顺脚本的逻辑.
################################End Script#########################################

注意事项:
[1]        Chet Ramey 承诺会在Bash的未来版本中实现关联数组(associative arrays)
        (一个Perl特性). 到了版本3,这个特性还没有实现.


34.2. Bash版本3
---------------
在2004年7月27日, Chet Ramey 发布了Bash的第三版本. 它修复了许多bug并加入了一些新的
属性.

增加的一些属性有:
*    新的,更特别的不可移植的 {a..z} 花括号扩展(brace expansion) 操作符.

       1 #!/bin/bash
       2
       3 for i in {1..10}
       4 #  比下面的更简单并且更易于理解
       5 #+ for i in $(seq 10)
       6 do
       7   echo -n "$i "
       8 done
       9
      10 echo
      11
      12 # 1 2 3 4 5 6 7 8 9 10

*    ${!array[@]} 操作符, 它扩展给定的数组(array)的所有元素下标.

       1 #!/bin/bash
       2
       3 Array=(element-zero element-one element-two element-three)
       4
       5 echo ${Array[0]}   # 元素0
       6                    # 数组的第一个元素.
       7
       8 echo ${!Array[@]}  # 0 1 2 3
       9                    # 数组所有的下标.
      10
      11 for i in ${!Array[@]}
      12 do
      13   echo ${Array[i]} # element-zero
      14                    # element-one
      15                    # element-two
      16                    # element-three
      17                    #
      18                    # 在数组里的所有元素.
      19 done

*    =~ 正则表达式(Regular Expression) 匹配操作符在双方括号(double brackets)
    测试表达式中使用. (Perl也有一个相似的操作符.)

       1 #!/bin/bash
       2
       3 variable="This is a fine mess."
       4
       5 echo "$variable"
       6
       7 if [[ "$variable" =~ "T*fin*es*" ]]
       8 # 在双方括号([[]])里用=~操作符进行正则匹配.
       9 then
      10   echo "match found"
      11       # match found
      12 fi

    或, 更有用的用法:

       1 #!/bin/bash
       2
       3 input=$1
       4
       5
       6 if [[ "$input" =~ "[1-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]" ]]
       7 # NNN-NN-NNNN
       8 # 每个N是一个数字.
       9 # 但, 开头的第一个数字不能是 0.
      10 then
      11   echo "Social Security number."
      12   # 处理 SSN.
      13 else
      14   echo "Not a Social Security number!"
      15   # 或者, 要求正确的输入.
      16 fi

    还有一个使用 =~ 操作符的例子, 参考例子 A-28.

注意:    升级到Bash版本3使原来在早先版本可以工作的少部分脚本不能工作了. 要重新测试
        原来的脚本看是否它们仍然可以工作!

        确实发生不能工作的情况, 在 Advanced Bash Scripting Guide 里的部分脚本代码
        不得不修复 (例如,例子 A-20 和 例子 9-4).


第35章    后记
============

35.1. 作者后记
--------------

                                        doce ut discas

                                        (Teach, that you yourself may learn.)

我怎么会写这本脚本编程的书?这有一个很奇怪的故事.时间回到前几年,那时我准备学习
shell脚本编程--这最好的办法莫过于读一本这方面的好书了.我一直在找一本能覆盖这个
主题方方面面的指南参考书.我也在找一本能把难点说得清楚容易并能用实际的代码和代码注
释解释这些难以理解的细节. [1]  事实上,我在找一本非常满意的书, 或者是类似的东西.
不幸的是,这是不存在的.如果我想要一本,那我不得不自己写一本.于是,它就写出来了.

这使我想起一个关于疯子教授的故事.下面的事像笨蛋那样荒谬.当这本书上架之前,对于所
有的在图书馆的书使他有了想写一本书的主意.他就这样做了,开始了这项好处多多的工作.
他争分夺秒地开始完成和现在这本书标题很像的书,他每天都很快地奔跑回家来做这件事.当
几年之后他死掉了,他有了写几千本书保存下来,可能会被和其他的破书一块放在书架上.可
能他写的书不是那么的好,但这有什么关系呢?这是一个活在幻想里的一个朋友.即使他没有
被幻想给迷惑驱使...但我忍不住地钦佩这个老笨蛋.

注意事项:
[1]        这是声名狼藉使人郁闷到死的技术.

35.2. 关于作者
--------------
这家伙到底是谁?

作者没有外交特权,不是被强迫写作的. [1] 这本书有点违背他的其他的主要工作,
HOW-2 Meet Women: The Shy Man's Guide to Relationships. 他另外也写了
Software-Building HOWTO. 近来, 他尝试写一些虚构的短篇小说.

自1995年成为一个Linux用户以来(Slackware 2.2, kernel 1.2.1), 作者已经发表了一些软件
包,包括 cruft 一次性加密软件包(one-time pad encryption utility), 软件 mcalc 可
用做计算器, 软件judge是Scrabble拼字游戏的自动求解包, 和软件包yawl 一起组成猜词表.
他从一台CDC 3800上使用FORTRAN IV开始他的编程之旅, 但那种日子一点也不值得怀念.

作者和他的妻子还有他们的狗隐居在一个偏远的地方,他幻想着人性是善良的.

注意事项:
[1]         这些谁可以做,谁不可以做...拿到一个MCSE证书.


35.3. 哪里可以取得帮助?
-----------------------
作者不是太忙(并且心情不错)的话,会回答一般性的脚本编程问题.但是,如果你的特定应
用的脚本不工作,建议你最好把问题发到 comp.os.unix.shell 新闻组去.


35.4. 制作这本书的工具
----------------------

35.4.1 硬件
-----------
一个运行着Red Hat 7.1/7.3的IBM Thinkpad, model 760XL(P166, 104 meg RAM)的笔记本.
是的,它非常的缓慢并且还有一个更人胆战心惊的键盘,但它总比一根铅笔加一个巨大的写字
板好多了.

升级: 升级到了运行着FC3的 770Z Thinkpad (P2-366, 192 meg RAM)笔记本.谁想捐赠一个
更新的一点笔记本给这个快饿死的作者 <g>?

35.4.2 软件和排版软件
---------------------
   1.  Bram Moolenaar的功能强大的SGML软件 vim文本编辑器.
   2.  OpenJade, 一个把SGML文档转换为其他格式的DSSSL翻译引擎.
   3.  Norman Walsh的DSSSL排版框架.
   4.  由Norman Walsh和Leonard Muellner (O'Reilly, ISBN 1-56592-580-7)写的最权威的
        指南:DocBook. 它仍然是任何一个想写Docbook SGML格式的书的标准参考书.



todo
====
12.22    第2部分翻译结束.阶段性总结.
12.02    本打算自己看,做做笔记就算了,如果要大家看,就不行了.补全前3章所有未译的地方.
01.06    所有的没翻译或翻译不对的地方标记<rojy bug>.
01.08    一些朋友提出想要,可惜没译完,最近效率比较低,手里还有些其它的活:(.
        丑媳妇总要见公婆,还是放了吧.
01.13    11章结束
01.14    12章开始, 翻译html版本.
01.19    开始12.4节.
02.16    开始12.6节.
03.23    12章终于结束了...
04.15    前3部分终于结束了, 历时4个多月:(, 后边的部分就交给
05.15    终于完成了, 没有黄毅兄的帮助, 真是不敢想象, 恐怕就要流产了
        接下来就要搞sgml版本的了, 唉... 到底走弯路了...

本书至此完。完整文档在此下载:http://boke.58zn.cn//UpFile/2009/12/ABS_Guide_cn.txt

【全文完。本文转载自网络收集。版权归原作者所有。本博客转载只为了知识的普及和认知,若侵犯了你的版权,请与我联系,我将在确认后删除。】


返回顶部

发表评论:

Powered By Z-BlogPHP 1.7.3


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