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