高级Bash脚本编程指南(三)
第10章 循环和分支
==================
对代码块进行操作是有组织的结构化的shell脚本的关键.为了达到这个目的,循环和分支提供
帮助.
10.1 循环
---------
循环就是重复一些命令的代码块,如果条件不满足就退出循环.
for loops
for arg in [list]
这是一个基本的循环结构.它与C的相似结构有很大不同.
for arg in [list]
do
command(s)...
done
注意:在循环的每次执行中,arg将顺序的存取list中列出的变量.
1 for arg in "$var1" "$var2" "$var3" ... "$varN"
2 # 在第1次循环中, arg = $var1
3 # 在第2次循环中, arg = $var2
4 # 在第3次循环中, arg = $var3
5 # ...
6 # 在第n次循环中, arg = $varN
7
8 # 在[list]中的参数加上双引号是为了阻止单词分离.
list中的参数允许包含通配符.
如果do和for想在同一行出现,那么在它们之间需要添加一个";".
for arg in [list]; do
Example 10-1 循环的一个简单例子
################################Start Script#######################################
1 #!/bin/bash
2 # 列出所有行星.
3
4 for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
5 do
6 echo $planet # Each planet on a separate line.
7 done
8
9 echo
10
11 for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
12 # 所有的行星都在同一行上.
13 # 完整的'list'作为一个变量都封在""中
14 do
15 echo $planet
16 done
17
18 exit 0
################################End Script#########################################
注意:每个[list]中的元素都可能包含多个参数.在处理参数组时,这是非常有用的.
在这种情况下,使用set命令(见Example 11-15)来强制解析每个[list]中的元素,
并且分配每个解析出来的部分到一个位置参数中.
Example 10-2 每个[list]元素带两个参数的for循环
################################Start Script#######################################
1 #!/bin/bash
2 # 还是行星.
3
4 # 分配行星的名字和它距太阳的距离.
5
6 for planet in "Mercury 36" "Venus 67" "Earth 93" "Mars 142" "Jupiter 483"
7 do
8 set -- $planet # 解析变量"planet"并且设置位置参数.
9 # "--" 将防止$planet为空,或者是以一个破折号开头.
10
11 # 可能需要保存原始的位置参数,因为它们被覆盖了.
12 # 一种方法就是使用数组,
13 # original_params=("$@")
14
15 echo "$1 $2,000,000 miles from the sun"
16 #-------two tabs---把后边的0和$2连接起来
17 done
18
19 # (Thanks, S.C., for additional clarification.)
20
21 exit 0
################################End Script#########################################
可以在for循环中的[list]位置放入一个变量
Example 10-3 文件信息:对包含在变量中的文件列表进行操作
################################Start Script#######################################
1 #!/bin/bash
2 # fileinfo.sh
3
4 FILES="/usr/sbin/accept
5 /usr/sbin/pwck
6 /usr/sbin/chroot
7 /usr/bin/fakefile
8 /sbin/badblocks
9 /sbin/ypbind" # 你关心的文件列表.
10 # 扔进去一个假文件, /usr/bin/fakefile.
11
12 echo
13
14 for file in $FILES
15 do
16
17 if [ ! -e "$file" ] # 检查文件是否存在.
18 then
19 echo "$file does not exist."; echo
20 continue # 继续下一个.
21 fi
22
23 ls -l $file | awk '{ print $9 " file size: " $5 }' # 打印2个域.
24 whatis `basename $file` # 文件信息.
25 # 注意whatis数据库需要提前建立好.
26 # 要想达到这个目的, 以root身份运行/usr/bin/makewhatis.
27 echo
28 done
29
30 exit 0
################################End Script#########################################
如果在for循环的[list]中有通配符(*和?),那将会产生文件名扩展,也就是file globbing.
Example 10-4 在for循环中操作文件
################################Start Script#######################################
1 #!/bin/bash
2 # list-glob.sh: 产生 [list] 在for循环中, 使用 "globbing"
3
4 echo
5
6 for file in *
7 # ^ 在表达式中识别file globbing时,
8 #+ Bash 将执行文件名扩展
9 do
10 ls -l "$file" # 列出所有在$PWD(当前目录)中的所有文件.
11 # 回想一下,通配符"*"能够匹配所有文件,
12 #+ 然而,在"globbing"中,是不能比配"."文件的.
13
14 # If the pattern matches no file, it is expanded to itself.
14 # 如果没匹配到任何文件,那它将扩展成自己.
15 # 为了不让这种情况发生,那就设置nullglob选项
16 #+ (shopt -s nullglob).
17 # Thanks, S.C.
18 done
19
20 echo; echo
21
22 for file in [jx]*
23 do
24 rm -f $file # 只删除当前目录下以"j"或"x"开头的文件.
25 echo "Removed file \"$file\"".
26 done
27
28 echo
29
30 exit 0
################################End Script#########################################
在一个for循环中忽略[list]的话,将会使循环操作$@(从命令行传递给脚本的参数列表).
一个非常好的例子,见Example A-16.
Example 10-5 在for循环中省略[list]
################################Start Script#######################################
1 #!/bin/bash
2
3 # 使用两种方法来调用这个脚本,一种是带参数的情况,另一种不带参数.
4 #+ 观察此脚本的行为各是什么样的?
5
6 for a
7 do
8 echo -n "$a "
9 done
10
11 # The 'in list' missing, therefore the loop operates on
11 # 没有[list],所以循环将操作
12 #+ (包括空白的命令参数列表).
13
14 echo
15
16 exit 0
################################End Script#########################################
也可以使用命令替换来产生for循环的[list].具体见Example 12-49,Example 10-10,
和Example 12-43.
Example 10-6 使用命令替换来产生for循环的[list]
################################Start Script#######################################
1 #!/bin/bash
2 # for-loopcmd.sh: 带[list]的for循环
3 #+ [list]是由命令替换产生的.
4
5 NUMBERS="9 7 3 8 37.53"
6
7 for number in `echo $NUMBERS` # for number in 9 7 3 8 37.53
8 do
9 echo -n "$number "
10 done
11
12 echo
13 exit 0
################################End Script#########################################
下边是一个用命令替换来产生[list]的更复杂的例子.
Example 10-7 对于二进制文件的一个grep替换
################################Start Script#######################################
1 #!/bin/bash
2 # bin-grep.sh: 在一个二进制文件中定位匹配字串.
3
4 # 对于二进制文件的一个grep替换
5 # 与"grep -a"的效果相似
6
7 E_BADARGS=65
8 E_NOFILE=66
9
10 if [ $# -ne 2 ]
11 then
12 echo "Usage: `basename $0` search_string filename"
13 exit $E_BADARGS
14 fi
15
16 if [ ! -f "$2" ]
17 then
18 echo "File \"$2\" does not exist."
19 exit $E_NOFILE
20 fi
21
22
23 IFS="\n" # 由Paulo Marcel Coelho Aragao提出的建议.
24 for word in $( strings "$2" | grep "$1" )
25 # "strings" 命令列出二进制文件中的所有字符串.
26 # 输出到管道交给"grep",然后由grep命令来过滤字符串.
27 do
28 echo $word
29 done
30
31 # S.C. 指出, 行23 - 29 可以被下边的这行来代替,
32 # strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'
33
34
35 # 试试用"./bin-grep.sh mem /bin/ls"来运行这个脚本.
36
37 exit 0
################################End Script#########################################
大部分相同.
Example 10-8 列出系统上的所有用户
################################Start Script#######################################
1 #!/bin/bash
2 # userlist.sh
3
4 PASSWORD_FILE=/etc/passwd
5 n=1 # User number
6
7 for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
8 # 域分隔 = : ^^^^^^
9 # 打印出第一个域 ^^^^^^^^
10 # 从password文件中取得输入 ^^^^^^^^^^^^^^^^^
11 do
12 echo "USER #$n = $name"
13 let "n += 1"
14 done
15
16
17 # USER #1 = root
18 # USER #2 = bin
19 # USER #3 = daemon
20 # ...
21 # USER #30 = bozo
22
23 exit 0
24
25 # 练习 :
26 # ------
27 # 一个普通用户(或者是一个普通用户运行的脚本)
28 #+ 怎么能读取/etc/password呢?
29 # 这是否是一个安全漏洞? 为什么是?为什么不是?
################################End Script#########################################
关于用命令替换来产生[list]的最后的例子.
Example 10-9 在目录的所有文件中查找源字串
################################Start Script#######################################
1 #!/bin/bash
2 # findstring.sh:
3 # 在一个指定目录的所有文件中查找一个特定的字符串.
4
5 directory=/usr/bin/
6 fstring="Free Software Foundation" # 查看那个文件中包含FSF.
7
8 for file in $( find $directory -type f -name '*' | sort )
9 do
10 strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
11 # 在"sed"表达式中,
12 #+ 我们必须替换掉正常的替换分隔符"/",
13 #+ 因为"/"碰巧是我们需要过滤的字串之一.
14 # 如果不用"%"代替"/"作为分隔符,那么这个操作将失败,并给出一个错误消息.(试试)
15 done
16
17 exit 0
18
19 # 练习 (easy):
20 # ------------
21 # 将内部用的$directory和$fstring变量,用从
22 #+ 命令行参数代替.
################################End Script#########################################
for循环的输出也可以通过管道传递到一个或多个命令中.
Example 10-10 列出目录中所有的符号连接文件
################################Start Script#######################################
1 #!/bin/bash
2 # symlinks.sh: 列出目录中所有的符号连接文件.
3
4
5 directory=${1-`pwd`}
6 # 如果没有其他的特殊指定,
7 #+ 默认为当前工作目录.
8 # 下边的代码块,和上边这句等价.
9 # ----------------------------------------------------------
10 # ARGS=1 # 需要一个命令行参数.
11 #
12 # if [ $# -ne "$ARGS" ] # 如果不是一个参数的话...
13 # then
14 # directory=`pwd` # 当前工作目录
15 # else
16 # directory=$1
17 # fi
18 # ----------------------------------------------------------
19
20 echo "symbolic links in directory \"$directory\""
21
22 for file in "$( find $directory -type l )" # -type l 就是符号连接文件
23 do
24 echo "$file"
25 done | sort # 否则列出的文件将是未排序的
26 # 严格上说,此处并不一定非要一个循环不可,
27 #+ 因为"find"命令的结果将被扩展成一个单词.
28 # 然而,这种方式很容易理解和说明.
29
30 # Dominik 'Aeneas' Schnitzer 指出,
31 #+ 如果没将 $( find $directory -type l )用""引用起来的话
32 #+ 那么将会把一个带有空白部分的文件名拆成以空白分隔的两部分(文件名中允许有空白).
33 # 即使这只将取出每个参数的第一个域.
34
35 exit 0
36
37
38 # Jean Helou 建议使用下边的方法:
39
40 echo "symbolic links in directory \"$directory\""
41 # 当前IFS的备份.要小心使用这个值.
42 OLDIFS=$IFS
43 IFS=:
44
45 for file in $(find $directory -type l -printf "%p$IFS")
46 do # ^^^^^^^^^^^^^^^^
47 echo "$file"
48 done|sort
################################End Script#########################################
循环的输出可以重定向到文件中,我们对上边的例子做了一点修改.
Example 10-11 将目录中的符号连接文件名保存到一个文件中
################################Start Script#######################################
1 #!/bin/bash
2 # symlinks.sh: 列出目录中所有的符号连接文件.
3
4 OUTFILE=symlinks.list # 保存的文件
5
6 directory=${1-`pwd`}
7 # 如果没有其他的特殊指定,
8 #+ 默认为当前工作目录.
9
10
11 echo "symbolic links in directory \"$directory\"" > "$OUTFILE"
12 echo "---------------------------" >> "$OUTFILE"
13
14 for file in "$( find $directory -type l )" # -type l 为符号链接
15 do
16 echo "$file"
17 done | sort >> "$OUTFILE" # 循环的输出
18 # ^^^^^^^^^^^^^ 重定向到一个文件中
19
20 exit 0
################################End Script#########################################
有一种非常像C语言的for循环的语法形式.这需要使用(()).
Example 10-12 一个C风格的for循环
################################Start Script#######################################
1 #!/bin/bash
2 # 两种循环到10的方法.
3
4 echo
5
6 # 标准语法.
7 for a in 1 2 3 4 5 6 7 8 9 10
8 do
9 echo -n "$a "
10 done
11
12 echo; echo
13
14 # +==========================================+
15
16 # 现在, 让我们用C风格的语法做同样的事.
17
18 LIMIT=10
19
20 for ((a=1; a <= LIMIT ; a++)) # Double parentheses, and "LIMIT" with no "$".
20 for ((a=1; a <= LIMIT ; a++)) # 双圆括号, 并且"LIMIT"变量前边没有 "$".
21 do
22 echo -n "$a "
23 done # 这是一个借用'ksh93'的结构.
24
25 echo; echo
26
27 # +=========================================================================+
28
29 # 让我们使用C的逗号操作符,来同时增加两个变量的值.
30
31 for ((a=1, b=1; a <= LIMIT ; a++, b++)) # 逗号将同时进行2条操作.
32 do
33 echo -n "$a-$b "
34 done
35
36 echo; echo
37
38 exit 0
################################End Script#########################################
参考Example 26-15,Example 26-16,和Example A-6.
---
现在来一个现实生活中使用的for循环.
Example 10-13 在batch mode中使用efax
################################Start Script#######################################
1 #!/bin/bash
2 # Faxing ('fax' 必须已经被安装过了).
3
4 EXPECTED_ARGS=2
5 E_BADARGS=65
6
7 if [ $# -ne $EXPECTED_ARGS ]
8 # 检查命令行参数的个数是否正确.
9 then
10 echo "Usage: `basename $0` phone# text-file"
11 exit $E_BADARGS
12 fi
13
14
15 if [ ! -f "$2" ]
16 then
17 echo "File $2 is not a text file"
18 exit $E_BADARGS
19 fi
20
21
22 fax make $2 # 从文本文件中创建传真格式的文件.
23
24 for file in $(ls $2.0*) # 连接转换过的文件.
25 # 在变量列表中使用通配符.
26 do
27 fil="$fil $file"
28 done
29
30 efax -d /dev/ttyS3 -o1 -t "T$1" $fil # 干活的地方.
31
32
33 # S.C. 指出, 通过下边的命令可以省去for循环.
34 # efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
35 # 但这并不十分有讲解意义[嘿嘿].
36
37 exit 0
################################End Script#########################################
while
这种结构在循环的开头判断条件是否满足,如果条件一直满足,那就一直循环下去(0为退出
码).与for循环的区别是,这种结构适合用在循环次数未知的情况下.
while [condition]
do
command...
done
和for循环一样,如果想把do和条件放到同一行上还是需要一个";".
while [condition]; do
注意一下某种特定的while循环,比如getopts结构,好像和这里所介绍的模版有点脱节.
Example 10-14 简单的while循环
################################Start Script#######################################
1 #!/bin/bash
2
3 var0=0
4 LIMIT=10
5
6 while [ "$var0" -lt "$LIMIT" ]
7 do
8 echo -n "$var0 " # -n 将会阻止产生新行.
9 # ^ 空格,数字之间的分隔.
10
11 var0=`expr $var0 + 1` # var0=$(($var0+1)) 也可以.
12 # var0=$((var0 + 1)) 也可以.
13 # let "var0 += 1" 也可以.
14 done # 使用其他的方法也行.
15
16 echo
17
18 exit 0
################################End Script#########################################
Example 10-15 另一个while循环
################################Start Script#######################################
1 #!/bin/bash
2
3 echo
4 # 等价于:
5 while [ "$var1" != "end" ] # while test "$var1" != "end"
6 do
7 echo "Input variable #1 (end to exit) "
8 read var1 # 为什么不使用'read $var1'?
9 echo "variable #1 = $var1" # 因为包含"#"字符,所以需要""
10 # 如果输入为'end',那么就在这里echo.
11 # 不在这里判断结束,在循环顶判断.
12 echo
13 done
14
15 exit 0
################################End Script#########################################
一个while循环可以有多个判断条件,但是只有最后一个才能决定是否退出循环.然而这需
要一种有点不同的循环语法.
Example 10-16 多条件的while循环
################################Start Script#######################################
1 #!/bin/bash
2
3 var1=unset
4 previous=$var1
5
6 while echo "previous-variable = $previous"
7 echo
8 previous=$var1
9 [ "$var1" != end ] # 记录之前的$var1.
10 # 这个"while"循环中有4个条件, 但是只有最后一个能控制循环.
11 # 退出状态由第4个条件决定.
12 do
13 echo "Input variable #1 (end to exit) "
14 read var1
15 echo "variable #1 = $var1"
16 done
17
18 # 尝试理解这个脚本的运行过程.
19 # 这里还是有点小技巧的.
20
21 exit 0
################################End Script#########################################
与for循环一样,while循环也可通过(())来使用C风格语法.(见Example 9-30)
Example 10-17 C风格的while循环
################################Start Script#######################################
1 #!/bin/bash
2 # wh-loopc.sh: 循环10次的while循环.
3
4 LIMIT=10
5 a=1
6
7 while [ "$a" -le $LIMIT ]
8 do
9 echo -n "$a "
10 let "a+=1"
11 done # 到目前为止都没什么令人惊奇的地方.
12
13 echo; echo
14
15 # +=================================================================+
16
17 # 现在, 重复C风格的语法.
18
19 ((a = 1)) # a=1
20 # 双圆括号允许赋值两边的空格,就像C语言一样.
21
22 while (( a <= LIMIT )) # 双圆括号, 变量前边没有"$".
23 do
24 echo -n "$a "
25 ((a += 1)) # let "a+=1"
26 # Yes, 看到了吧.
27 # 双圆括号允许像C风格的语法一样增加变量的值.
28 done
29
30 echo
31
32 # 现在,C程序员可以在Bash中找到回家的感觉了吧.
33
34 exit 0
################################End Script#########################################
注意:while循环的stdin可以用<来重定向到文件.
whild循环的stdin支持管道.
until
这个结构在循环的顶部判断条件,并且如果条件一直为false那就一直循环下去.(与while
相反)
until [condition-is-true]
do
command...
done
注意: until循环的判断在循环的顶部,这与某些编程语言是不同的.
与for循环一样,如果想把do和条件放在一行里,就使用";".
until [condition-is-true]; do
Example 10-18 until循环
################################Start Script#######################################
1 #!/bin/bash
2
3 END_CONDITION=end
4
5 until [ "$var1" = "$END_CONDITION" ]
6 # 在循环的顶部判断条件.
7 do
8 echo "Input variable #1 "
9 echo "($END_CONDITION to exit)"
10 read var1
11 echo "variable #1 = $var1"
12 echo
13 done
14
15 exit 0
################################End Script#########################################
10.2 嵌套循环
-------------
嵌套循环就是在一个循环中还有一个循环,内部循环在外部循环体中.在外部循环的每次执行过
程中都会触发内部循环,直到内部循环执行结束.外部循环执行了多少次,内部循环就完成多少
次.当然,不论是外部循环或内部循环的break语句都会打断处理过程.
Example 10-19 嵌套循环
################################Start Script#######################################
1 #!/bin/bash
2 # nested-loop.sh: 嵌套的"for" 循环.
3
4 outer=1 # 设置外部循环计数.
5
6 # 开始外部循环.
7 for a in 1 2 3 4 5
8 do
9 echo "Pass $outer in outer loop."
10 echo "---------------------"
11 inner=1 # 重设内部循环的计数.
12
13 # ===============================================
14 # 开始内部循环.
15 for b in 1 2 3 4 5
16 do
17 echo "Pass $inner in inner loop."
18 let "inner+=1" # 增加内部循环计数.
19 done
20 # 内部循环结束.
21 # ===============================================
22
23 let "outer+=1" # 增加外部循环的计数.
24 echo # 每次外部循环之间的间隔.
25 done
26 # 外部循环结束.
27
28 exit 0
################################End Script#########################################
10.3 循环控制
-------------
影响循环行为的命令
break,continue
break和continue这两个循环控制命令[1]与其它语言的类似命令的行为是相同的.break
命令将会跳出循环,continue命令将会跳过本次循环下边的语句,直接进入下次循环.
Example 10-20 break和continue命令在循环中的效果
################################Start Script#######################################
1 #!/bin/bash
2
3 LIMIT=19 # 上限
4
5 echo
6 echo "Printing Numbers 1 through 20 (but not 3 and 11)."
7
8 a=0
9
10 while [ $a -le "$LIMIT" ]
11 do
12 a=$(($a+1))
13
14 if [ "$a" -eq 3 ] || [ "$a" -eq 11 ] # 除了3和11.
15 then
16 continue # 跳过本次循环剩下的语句.
17 fi
18
19 echo -n "$a " # 在$a等于3和11的时候,这句将不会执行.
20 done
21
22 # 练习:
23 # 为什么循环会打印出20?
24
25 echo; echo
26
27 echo Printing Numbers 1 through 20, but something happens after 2.
28
29 ##################################################################
30
31 # Same loop, but substituting 'break' for 'continue'.
31 # 同样的循环, 但是用'break'来代替'continue'.
32
33 a=0
34
35 while [ "$a" -le "$LIMIT" ]
36 do
37 a=$(($a+1))
38
39 if [ "$a" -gt 2 ]
40 then
41 break # 将会跳出整个循环.
42 fi
43
44 echo -n "$a "
45 done
46
47 echo; echo; echo
48
49 exit 0
################################End Script#########################################
break命令可以带一个参数.一个不带参数的break循环只能退出最内层的循环,而break N
可以退出N层循环.
Example 10-21 多层循环的退出
################################Start Script#######################################
1 #!/bin/bash
2 # break-levels.sh: 退出循环.
3
4 # "break N" 退出N层循环.
5
6 for outerloop in 1 2 3 4 5
7 do
8 echo -n "Group $outerloop: "
9
10 # --------------------------------------------------------
11 for innerloop in 1 2 3 4 5
12 do
13 echo -n "$innerloop "
14
15 if [ "$innerloop" -eq 3 ]
16 then
17 break # 试试 break 2 来看看发生什么.
18 # (内部循环和外部循环都被退出了.)
19 fi
20 done
21 # --------------------------------------------------------
22
23 echo
24 done
25
26 echo
27
28 exit 0
################################End Script#########################################
continue命令也可以带一个参数.一个不带参数的continue命令只去掉本次循环的剩余代码
.而continue N将会把N层循环剩余的代码都去掉,但是循环的次数不变.
Example 10-22 多层循环的continue
################################Start Script#######################################
1 #!/bin/bash
2 # "continue N" 命令, 将让N层的循环全部被continue.
3
4 for outer in I II III IV V # 外部循环
5 do
6 echo; echo -n "Group $outer: "
7
8 # --------------------------------------------------------------------
9 for inner in 1 2 3 4 5 6 7 8 9 10 # 内部循环
10 do
11
12 if [ "$inner" -eq 7 ]
13 then
14 continue 2 # continue 2层, 也就是到outer循环上.
15 # 将"continue 2"替换为一个单独的"continue"
16 # 来看一下一个正常循环的行为.
17 fi
18
19 echo -n "$inner " # 7 8 9 10 将不会被echo
20 done
21 # --------------------------------------------------------------------
22 #译者注:如果在此处添加echo的话,当然也不会输出.
23 done
24
25 echo; echo
26
27 # 练习:
28 # 准备一个有意义的"continue N"的使用,放在脚本中.
29
30 exit 0
################################End Script#########################################
Example 10-23 在实际的任务中使用"continue N"
################################Start Script#######################################
1 # Albert Reiner 给出了一个关于使用"continue N"的例子:
2 # ---------------------------------------------------
3
4 # Suppose I have a large number of jobs that need to be run, with
5 #+ any data that is to be treated in files of a given name pattern in a
6 #+ directory. There are several machines that access this directory, and
7 #+ I want to distribute the work over these different boxen. Then I
8 #+ usually nohup something like the following on every box:
9
10 while true
11 do
12 for n in .iso.*
13 do
14 [ "$n" = ".iso.opts" ] && continue
15 beta=${n#.iso.}
16 [ -r .Iso.$beta ] && continue
17 [ -r .lock.$beta ] && sleep 10 && continue
18 lockfile -r0 .lock.$beta || continue
19 echo -n "$beta: " `date`
20 run-isotherm $beta
21 date
22 ls -alF .Iso.$beta
23 [ -r .Iso.$beta ] && rm -f .lock.$beta
24 continue 2
25 done
26 break
27 done
28
29 # The details, in particular the sleep N, are particular to my
30 #+ application, but the general pattern is:
31
32 while true
33 do
34 for job in {pattern}
35 do
36 {job already done or running} && continue
37 {mark job as running, do job, mark job as done}
38 continue 2
39 done
40 break # Or something like `sleep 600' to avoid termination.
41 done
42
43 # This way the script will stop only when there are no more jobs to do
44 #+ (including jobs that were added during runtime). Through the use
45 #+ of appropriate lockfiles it can be run on several machines
46 #+ concurrently without duplication of calculations [which run a couple
47 #+ of hours in my case, so I really want to avoid this]. Also, as search
48 #+ always starts again from the beginning, one can encode priorities in
49 #+ the file names. Of course, one could also do this without `continue 2',
50 #+ but then one would have to actually check whether or not some job
51 #+ was done (so that we should immediately look for the next job) or not
52 #+ (in which case we terminate or sleep for a long time before checking
53 #+ for a new job).
################################End Script#########################################
注意:continue N结构如果被用在一个有意义的上下文中的话,往往都很难理解,并且技巧性
很高.所以最好的方法就是尽量避免它.
注意事项:
[1] 这两个命令是shell的内建命令,而不像其它的循环命令那样,比如while和case,这两个
是关键字.
10.4 测试与分支(case和select结构)
---------------------------------
case和select结构在技术上说不是循环,因为它们并不对可执行的代码块进行迭代.但是和循环
相似的是,它们也依靠在代码块的顶部或底部的条件判断来决定程序的分支.
在代码块中控制程序分支
case (in) / esac
在shell中的case同C/C++中的switch结构是相同的.它允许通过判断来选择代码块中多条
路径中的一条.
case "$variable" in
"$condition1")
command...
;;
"$condition1")
command...
;;
esac
注意: 对变量使用""并不是强制的,因为不会发生单词分离.
每句测试行,都以右小括号)结尾.
每个条件块都以两个分号结尾;;.
case块的结束以esac(case的反向拼写)结尾.
Example 10-24 使用case
################################Start Script#######################################
1 #!/bin/bash
2 # 测试字符串范围
3
4 echo; echo "Hit a key, then hit return."
5 read Keypress
6
7 case "$Keypress" in
8 [[:lower:]] ) echo "Lowercase letter";;
9 [[:upper:]] ) echo "Uppercase letter";;
10 [0-9] ) echo "Digit";;
11 * ) echo "Punctuation, whitespace, or other";;
12 esac # Allows ranges of characters in [square brackets],
12 esac # 允许字符串的范围出现在[]中,
13 #+ or POSIX ranges in [[double square brackets.
13 #+ 或者POSIX范围在[[中.
14
15 # 在这个例子的第一个版本中,
16 #+ 测试大写和小写字符串使用的是
17 #+ [a-z] 和 [A-Z].
18 # 这种用法将不会在某些特定的场合或Linux发行版中正常工作.
19 # POSIX 更具可移植性.
20 # 感谢Frank Wang 指出这点.
21
22 # 练习:
23 # -----
24 # 就像这个脚本所表现的,它只允许单个按键,然后就结束了.
25 # 修改这个脚本,让它能够接受重复输入,
26 #+ 报告每个按键,并且只有在"X"被键入时才结束.
27 # 暗示: 将这些代码都用"while"循环圈起来.
28
29 exit 0
################################End Script#########################################
Example 10-25 使用case来创建菜单
################################Start Script#######################################
1 #!/bin/bash
2
3 # 未经处理的地址资料
4
5 clear # 清屏.
6
7 echo " Contact List"
8 echo " ------- ----"
9 echo "Choose one of the following persons:"
10 echo
11 echo "[E]vans, Roland"
12 echo "[J]ones, Mildred"
13 echo "[S]mith, Julie"
14 echo "[Z]ane, Morris"
15 echo
16
17 read person
18
19 case "$person" in
20 # 注意,变量是被引用的.
21
22 "E" | "e" )
23 # 接受大写或小写输入.
24 echo
25 echo "Roland Evans"
26 echo "4321 Floppy Dr."
27 echo "Hardscrabble, CO 80753"
28 echo "(303) 734-9874"
29 echo "(303) 734-9892 fax"
30 echo ""
31 echo "Business partner & old friend"
32 ;;
33 # 注意,在每个选项后边都需要以;;结尾.
34
35 "J" | "j" )
36 echo
37 echo "Mildred Jones"
38 echo "249 E. 7th St., Apt. 19"
39 echo "New York, NY 10009"
40 echo "(212) 533-2814"
41 echo "(212) 533-9972 fax"
42 echo ""
43 echo "Ex-girlfriend"
44 echo "Birthday: Feb. 11"
45 ;;
46
47 # 后边的Smith和Zane的信息在这里就省略了.
48
49 * )
50 # 默认选项.
51 # 空输入(敲RETURN).
52 echo
53 echo "Not yet in database."
54 ;;
55
56 esac
57
58 echo
59
60 # 练习:
61 # -----
62 # 修改这个脚本,让它能够接受多输入,
63 #+ 并且能够显示多个地址.
64
65 exit 0
################################End Script#########################################
一个case的特殊用法,用来测试命令行参数.
################################Start Script#######################################
1 #! /bin/bash
2
3 case "$1" in
4 "") echo "Usage: ${0##*/} "; exit $E_PARAM;; # 没有命令行参数,
5 # 或者第一个参数为空.
6 # Note that ${0##*/} is ${var##pattern} param substitution. Net result is $0.
6 # 注意:${0##*/} 是${var##pattern} 这种模式的替换. 得到的结果是$0.
7
8 -*) FILENAME=./$1;; # 如果传递进来的文件名参数($1)以一个破折号开头,
9 #+ 那么用./$1来代替
10 #+ 这样后边的命令将不会把它作为一个选项来解释.
11
12 * ) FILENAME=$1;; # 否则, $1.
13 esac
################################End Script#########################################
这是一个更容易懂的命令行参数处理的一个例子.
################################Start Script#######################################
1 #! /bin/bash
2
3
4 while [ $# -gt 0 ]; do # 直到你用完所有的参数...
5 case "$1" in
6 -d|--debug)
7 # "-d" or "--debug" parameter?
8 DEBUG=1
9 ;;
10 -c|--conf)
11 CONFFILE="$2"
12 shift
13 if [ ! -f $CONFFILE ]; then
14 echo "Error: Supplied file doesn't exist!"
15 exit $E_CONFFILE # 文件没发现错误.
16 fi
17 ;;
18 esac
19 shift # 检查剩下的参数.
20 done
21
22 # 来自Stefano Falsetto的 "Log2Rot" 脚本,
23 #+ 他的"rottlog" 包的一部分.
24 # 授权使用.
################################End Script#########################################
Example 10-26 使用命令替换来产生case变量
################################Start Script#######################################
1 #!/bin/bash
2 # case-cmd.sh: 使用命令替换来产生"case"变量
3
4 case $( arch ) in # "arch" 返回机器的类型.
5 # 等价于 'uname -m' ...
6 i386 ) echo "80386-based machine";;
7 i486 ) echo "80486-based machine";;
8 i586 ) echo "Pentium-based machine";;
9 i686 ) echo "Pentium2+-based machine";;
10 * ) echo "Other type of machine";;
11 esac
12
13 exit 0
################################End Script#########################################
case结构也可以过滤globbing模式的字符串.
Example 10-27 简单字符串匹配
################################Start Script#######################################
1 #!/bin/bash
2 # match-string.sh: 简单字符串匹配
3
4 match_string ()
5 {
6 MATCH=0
7 NOMATCH=90
8 PARAMS=2 # 函数需要2个参数.
9 BAD_PARAMS=91
10
11 [ $# -eq $PARAMS ] || return $BAD_PARAMS
12
13 case "$1" in
14 "$2") return $MATCH;;
15 * ) return $NOMATCH;;
16 esac
17
18 }
19
20
21 a=one
22 b=two
23 c=three
24 d=two
25
26
27 match_string $a # 参数个数错误.
28 echo $? # 91
29
30 match_string $a $b # 不匹配
31 echo $? # 90
32
33 match_string $b $d # 匹配
34 echo $? # 0
35
36
37 exit 0
################################End Script#########################################
Example 10-28 检查是否是字母输入
################################Start Script#######################################
1 #!/bin/bash
2 # isalpha.sh: 使用"case"结构来过滤字符串.
3
4 SUCCESS=0
5 FAILURE=-1
6
7 isalpha () # 检查输入的*第一个字符*是不是字母表上的字符.
8 {
9 if [ -z "$1" ] # 没有参数传进来?
10 then
11 return $FAILURE
12 fi
13
14 case "$1" in
15 [a-zA-Z]*) return $SUCCESS;; # 以一个字母开头?
16 * ) return $FAILURE;;
17 esac
18 } # 同C语言的"isalpha()"函数相比较.
19
20
21 isalpha2 () # 测试是否*整个字符串*为字母表字符.
22 {
23 [ $# -eq 1 ] || return $FAILURE
24
25 case $1 in
26 *[!a-zA-Z]*|"") return $FAILURE;;
27 *) return $SUCCESS;;
28 esac
29 }
30
31 isdigit () # 测试是否*整个字符串*都是数字.
32 { # 换句话说就是测试是否是整数变量.
33 [ $# -eq 1 ] || return $FAILURE
34
35 case $1 in
36 *[!0-9]*|"") return $FAILURE;;
37 *) return $SUCCESS;;
38 esac
39 }
40
41
42
43 check_var () # 测试 isalpha ().
44 {
45 if isalpha "$@"
46 then
47 echo "\"$*\" begins with an alpha character."
48 if isalpha2 "$@"
49 then # 不需要测试第一个字符是否是non-alpha.
50 echo "\"$*\" contains only alpha characters."
51 else
52 echo "\"$*\" contains at least one non-alpha character."
53 fi
54 else
55 echo "\"$*\" begins with a non-alpha character."
56 # 如果没有参数传递进来,也是"non-alpha".
57 fi
58
59 echo
60
61 }
62
63 digit_check () # 测试 isdigit ().
64 {
65 if isdigit "$@"
66 then
67 echo "\"$*\" contains only digits [0 - 9]."
68 else
69 echo "\"$*\" has at least one non-digit character."
70 fi
71
72 echo
73
74 }
75
76 a=23skidoo
77 b=H3llo
78 c=-What?
79 d=What?
80 e=`echo $b` # 命令替换.
81 f=AbcDef
82 g=27234
83 h=27a34
84 i=27.34
85
86 check_var $a
87 check_var $b
88 check_var $c
89 check_var $d
90 check_var $e
91 check_var $f
92 check_var # 没有参数传进来,将发生什么?
93 #
94 digit_check $g
95 digit_check $h
96 digit_check $i
97
98
99 exit 0 # S.C改进过这个脚本.
100
101 # Exercise:
102 # --------
103 # 编写一个 'isfloat ()'函数来测试浮点数.
104 # 暗示: 这个函数基本上与'isdigit ()'一样,
105 #+ 但是要添加一部分小数点的处理.
################################End Script#########################################
select
select结构是建立菜单的另一种工具,这种结构是从ksh中引入的.
select variable [in list]
do
command...
break
done
提示用户选择的内容比如放在变量列表中.注意:select命令使用PS3提示符[默认为(#? )]
但是可以修改PS3.
Example 10-29 用select来创建菜单
################################Start Script#######################################
1 #!/bin/bash
2
3 PS3='Choose your favorite vegetable: ' # 设置提示符字串.
4
5 echo
6
7 select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
8 do
9 echo
10 echo "Your favorite veggie is $vegetable."
11 echo "Yuck!"
12 echo
13 break # 如果这里没有'break'会发生什么?
14 done
15
16 exit 0
################################End Script#########################################
如果忽略了in list列表,那么select命令将使用传递到脚本的命令行参数,或者是函数参数
前提是将select写到这个函数中.
与for variable [in list]结构在忽略[in list]时的行为相比较.
Example 10-30 用函数中select结构来创建菜单
################################Start Script#######################################
1 #!/bin/bash
2
3 PS3='Choose your favorite vegetable: '
4
5 echo
6
7 choice_of()
8 {
9 select vegetable
10 # [in list] 被忽略, 所以'select'用传递给函数的参数.
11 do
12 echo
13 echo "Your favorite veggie is $vegetable."
14 echo "Yuck!"
15 echo
16 break
17 done
18 }
19
20 choice_of beans rice carrots radishes tomatoes spinach
21 # $1 $2 $3 $4 $5 $6
22 # 传递给choice_of() 函数的参数
23
24 exit 0
################################End Script#########################################
参见Example 34-3.
第11章 内部命令与内建
======================
内建命令指的就是包含在Bash工具集中的命令.这主要是考虑到执行效率的问题--内建命令将
比外部命令的执行得更快,外部命令通常需要fork出一个单独的进程来执行.另外一部分原因
是特定的内建命令需要直接存取shell内核部分.
当一个命令或者是shell本身需要初始化(或者创建)一个新的子进程来执行一个任务的时候,这
种行为被称为forking.这个新产生的进程被叫做子进程,并且这个进程是从父进程中分离出来
的.当子进程执行它的任务时,同时父进程也在运行.
注意:当父进程取得子进程的进程ID的时候,父进程可以传递给子进程参数,而反过来则不行.
这将产生不可思议的并且很难追踪的问题.
Example 11-1 一个fork出多个自己实例的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # spawn.sh
3
4
5 PIDS=$(pidof sh $0) # 这个脚本不同实例的进程ID.
6 P_array=( $PIDS ) # 把它们放到数组里(为什么?).
7 echo $PIDS # 显示父进程和子进程的进程ID.
8 let "instances = ${#P_array[*]} - 1" # 计算元素个数,至少为1.
9 # 为什么减1?
10 echo "$instances instance(s) of this script running."
11 echo "[Hit Ctl-C to exit.]"; echo
12
13
14 sleep 1 # 等.
15 sh $0 # 再来一次.
16
17 exit 0 # 没必要: 脚本永远不会走到这里.
18 # 为什么走不到这里?
19
20 # 在使用Ctl-C退出之后,
21 #+ 是否所有产生的进程都会被kill掉?
22 # 如果是这样的话, 为什么?
23
24 # 注意:
25 # ----
26 # 小心,不要让这个脚本运行太长时间.
27 # 它最后将吃掉你大部分的系统资源.
28
29 # 对于用脚本产生大量的自身实例来说,
30 #+ 是否有适当的脚本技术.
31 # 为什么是为什么不是?
################################End Script#########################################
一般的,脚本中的内建命令在执行时将不会fork出一个子进程.但是脚本中的外部或过滤命令
通常会fork一个子进程.
一个内建命令通常与一个系统命令同名,但是Bash在内部重新实现了这些命令.比如,Bash的
echo命令与/bin/echo就不尽相同,虽然它们的行为绝大多数情况下是一样的.
1 #!/bin/bash
2
3 echo "This line uses the \"echo\" builtin."
4 /bin/echo "This line uses the /bin/echo system command."
关键字的意思就是保留字.对于shell来说关键字有特殊的含义,并且用来构建shell的语法结构.
比如,"for","while","do"和"!"都是关键字.与内建命令相同的是,关键字也是Bash的骨干部分,
但是与内建命令不同的是,关键字自身并不是命令,而是一个比较大的命令结构的一部分.[1]
I/O类
echo
打印(到stdout)一个表达式或变量(见Example 4-1).
1 echo Hello
2 echo $a
echo需要使用-e参数来打印转移字符.见Example 5-2.
一般的每个echo命令都会在终端上新起一行,但是-n选项将会阻止新起一行.
注意:echo命令可以用来作为一系列命令的管道输入.
1 if echo "$VAR" | grep -q txt # if [[ $VAR = *txt* ]]
2 then
3 echo "$VAR contains the substring sequence \"txt\""
4 fi
注意:echo命令与命令替换相组合可以用来设置一个变量.
a=`echo "HELLO" | tr A-Z a-z`
参见Example 12-19,Example 12-3,Example 12-42,和Example 12-43.
注意:echo `command`将会删除任何有命令产生的换行符.
$IFS(内部域分隔符)一般都会将\n(换行符)包含在它的空白字符集合中.Bash因此会根据
参数中的换行来分离命令的输出.然后echo将以空格代替换行来输出这些参数.
bash$ ls -l /usr/share/apps/kjezz/sounds
-rw-r--r-- 1 root root 1407 Nov 7 2000 reflect.au
-rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au
bash$ echo `ls -l /usr/share/apps/kjezz/sounds`
total 40 -rw-r--r-- 1 root root 716 Nov 7 2000 reflect.au -rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au
所以,我们怎么才能在一个需要echo出来的字符串中嵌入换行呢?
################################Start Script#######################################
1 # 嵌入一个换行?
2 echo "Why doesn't this string \n split on two lines?"
3 # 上边这句的\n将被打印出来.达不到换行的目的.
4
5 # 让我们在试试其他方法.
6
7 echo
8
9 echo $"A line of text containing
10 a linefeed."
11 # 打印出2个独立的行,(潜入换行成功了).
12 # 但是,"$"前缀是否是必要的?
13
14 echo
15
16 echo "This string splits
17 on two lines."
18 # 不用非得有"$"前缀.
19
20 echo
21 echo "---------------"
22 echo
23
24 echo -n $"Another line of text containing
25 a linefeed."
26 # 打印出2个独立的行,(潜入换行成功了).
27 # 即使-n选项,也没能阻止换行(译者:-n 阻止了第2个换行)
28
29 echo
30 echo
31 echo "---------------"
32 echo
33 echo
34
35 # 然而,下边的代码就没能像期望的那样运行.
36 # Why not? Hint: Assignment to a variable.
36 # 为什么失败? 提示: 因为分配到了变量.
37 string1=$"Yet another line of text containing
38 a linefeed (maybe)."
39
40 echo $string1
41 # Yet another line of text containing a linefeed (maybe).
42 # ^
43 # 换行变成了空格.
44
45 # Thanks, Steve Parker, for pointing this out.
################################End Script#########################################
注意: 这个命令是shell的一个内建命令,与/bin/echo不同,虽然行为相似.
bash$ type -a echo
echo is a shell builtin
echo is /bin/echo
printf
printf命令,格式化输出,是echo命令的增强.它是C语言printf()库函数的一个有限的变形,
并且在语法上有些不同.
printf format-string... parameter...
这是Bash的内建版本,与/bin/printf或/usr/bin/printf命令不同.想更深入的了解,请
察看printf(系统命令)的man页.
注意:老版本的Bash可能不支持printf.
Example 11-2 printf
################################Start Script#######################################
1 #!/bin/bash
2 # printf demo
3
4 PI=3.14159265358979
5 DecimalConstant=31373
6 Message1="Greetings,"
7 Message2="Earthling."
8
9 echo
10
11 printf "Pi to 2 decimal places = %1.2f" $PI
12 echo
13 printf "Pi to 9 decimal places = %1.9f" $PI # 都能正确地结束.
14
15 printf "\n" # 打印一个换行,
16 # 等价于 'echo' . . .
17
18 printf "Constant = \t%d\n" $DecimalConstant # 插入一个 tab (\t).
19
20 printf "%s %s \n" $Message1 $Message2
21
22 echo
23
24 # ==========================================#
25 # 模仿C函数, sprintf().
26 # 使用一个格式化的字符串来加载一个变量.
27
28 echo
29
30 Pi12=$(printf "%1.12f" $PI)
31 echo "Pi to 12 decimal places = $Pi12"
32
33 Msg=`printf "%s %s \n" $Message1 $Message2`
34 echo $Msg; echo $Msg
35
36 # 向我们看到的一样,现在'sprintf'函数可以
37 #+ 作为一个可被加载的模块
38 #+ 但这是不可移植的.
39
40 exit 0
################################End Script#########################################
使用printf的最主要的应用就是格式化错误消息.
1 E_BADDIR=65
2
3 var=nonexistent_directory
4
5 error()
6 {
7 printf "$@" >&2
8 # 格式化传递进来的位置参数,并把它们送到stderr.
9 echo
10 exit $E_BADDIR
11 }
12
13 cd $var || error $"Can't cd to %s." "$var"
14
15 # Thanks, S.C.
read
从stdin中读取一个变量的值,也就是与键盘交互取得变量的值.使用-a参数可以取得数组
变量(见Example 26-6).
Example 11-3 使用read,变量分配
################################Start Script#######################################
1 #!/bin/bash
2 # "Reading" 变量.
3
4 echo -n "Enter the value of variable 'var1': "
5 # -n选项,阻止换行.
6
7 read var1
8 # 注意在var1前面没有'$',因为变量正在被设置.
9
10 echo "var1 = $var1"
11
12
13 echo
14
15 # 一个'read'命令可以设置多个变量.
16 echo -n "Enter the values of variables 'var2' and 'var3' (separated by a space or tab): "
17 read var2 var3
18 echo "var2 = $var2 var3 = $var3"
19 # 如果你只输入了一个值,那么其他的变量还是未设置(null).
20
21 exit 0
################################End Script#########################################
一个不带变量参数的read命令,将把来自键盘的输入存入到专用变量$REPLY中.
Example 11-4 当使用一个不带变量参数的read命令时,将会发生什么?
################################Start Script#######################################
1 #!/bin/bash
2 # read-novar.sh
3
4 echo
5
6 # -------------------------- #
7 echo -n "Enter a value: "
8 read var
9 echo "\"var\" = "$var""
10 # 到这里为止,都与期望的相同.
11 # -------------------------- #
12
13 echo
14
15 # ------------------------------------------------------------------- #
16 echo -n "Enter another value: "
17 read # 没有变量分配给'read'命令,因此...
18 #+ 输入将分配给默认变量,$REPLY.
19 var="$REPLY"
20 echo "\"var\" = "$var""
21 # 这部分代码和上边的代码等价.
22 # ------------------------------------------------------------------- #
23
24 echo
25
26 exit 0
################################End Script#########################################
通常情况下,在使用read命令时,输入一个\然后回车,将会阻止产生一个新行.-r选项将会
让\转义.
Example 11-5 read命令的多行输入
################################Start Script#######################################
1 #!/bin/bash
2
3 echo
4
5 echo "Enter a string terminated by a \\, then press ."
6 echo "Then, enter a second string, and again press ."
7 read var1 # "\"将会阻止产生新行,当read $var1时.
8 # first line \
9 # second line
10
11 echo "var1 = $var1"
12 # var1 = first line second line
13
14 # For each line terminated by a "\"
14 # 对于每个一个"\"结尾的行
15 #+ 你都会看到一个下一行的提示符,让你继续向var1输入内容.
16
17 echo; echo
18
19 echo "Enter another string terminated by a \\ , then press ."
20 read -r var2 # -r选项将会让"\"转义.
21 # first line \
22
23 echo "var2 = $var2"
24 # var2 = first line \
25
26 # 第一个就会结束var2变量的录入.
27
28 echo
29
30 exit 0
################################End Script#########################################
read命令有些有趣的选项,这些选项允许打印出一个提示符,然后在不输入的情况
下,可以读入你的按键字符.
1 # Read a keypress without hitting ENTER.
1 # 不敲回车,读取一个按键字符.
2
3 read -s -n1 -p "Hit a key " keypress
4 echo; echo "Keypress was "\"$keypress\""."
5
6 # -s 选项意味着不打印输入.
7 # -n N 选项意味着直接受N个字符的输入.
8 # -p 选项意味着在读取输入之前打印出后边的提示符.
9
10 # 使用这些选项是有技巧的,因为你需要使用正确的循序来使用它们.
read的-n选项也可以检测方向键,和一些控制按键.
Example 11-6 检测方向键
################################Start Script#######################################
1 #!/bin/bash
2 # arrow-detect.sh: 检测方向键,和一些非打印字符的按键.
3 # Thank you, Sandro Magi告诉了我怎么做.
4
5 # --------------------------------------------
6 # 按键产生的字符编码.
7 arrowup='\[A'
8 arrowdown='\[B'
9 arrowrt='\[C'
10 arrowleft='\[D'
11 insert='\[2'
12 delete='\[3'
13 # --------------------------------------------
14
15 SUCCESS=0
16 OTHER=65
17
18 echo -n "Press a key... "
19 # 如果不是上边列表所列出的按键,可能还是需要按回车.(译者:因为一般按键是一个字符)
20 read -n3 key # 读3个字符.
21
22 echo -n "$key" | grep "$arrowup" #检查输入字符是否匹配.
23 if [ "$?" -eq $SUCCESS ]
24 then
25 echo "Up-arrow key pressed."
26 exit $SUCCESS
27 fi
28
29 echo -n "$key" | grep "$arrowdown"
30 if [ "$?" -eq $SUCCESS ]
31 then
32 echo "Down-arrow key pressed."
33 exit $SUCCESS
34 fi
35
36 echo -n "$key" | grep "$arrowrt"
37 if [ "$?" -eq $SUCCESS ]
38 then
39 echo "Right-arrow key pressed."
40 exit $SUCCESS
41 fi
42
43 echo -n "$key" | grep "$arrowleft"
44 if [ "$?" -eq $SUCCESS ]
45 then
46 echo "Left-arrow key pressed."
47 exit $SUCCESS
48 fi
49
50 echo -n "$key" | grep "$insert"
51 if [ "$?" -eq $SUCCESS ]
52 then
53 echo "\"Insert\" key pressed."
54 exit $SUCCESS
55 fi
56
57 echo -n "$key" | grep "$delete"
58 if [ "$?" -eq $SUCCESS ]
59 then
60 echo "\"Delete\" key pressed."
61 exit $SUCCESS
62 fi
63
64
65 echo " Some other key pressed."
66
67 exit $OTHER
68
69 # 练习:
70 # -----
71 # 1) 通过使用'case'结构来代替'if'结构
72 #+ 来简化这个脚本.
73 # 2) Add detection of the "Home," "End," "PgUp," and "PgDn" keys.
73 # 2) 添加"Home," "End," "PgUp," 和 "PgDn"这些按键的检查.
################################End Script#########################################
注意: 对read命令来说,-n 选项将不会检测ENTER(新行)键.
read命令的-t选项允许时间输入(见Example 9-4).
read命令也可以从重定向的文件中读入变量的值.如果文件中的内容超过一行,那么只有第
一行被分配到这个变量中.如果read命令有超过一个参数,那么每个变量都会从文件中取得
以定义的空白分隔的字符串作为变量的值.小心!
Example 11-7 通过文件重定向来使用read
################################Start Script#######################################
1 #!/bin/bash
2
3 read var1 4 echo "var1 = $var1"
5 # var1将会把data-file的第一行的全部内容都作为它的值.
6
7 read var2 var3 8 echo "var2 = $var2 var3 = $var3"
9 # 注意,这里"read"命令将会产生一种不直观的行为.
10 # 1) 重新从文件的开头开始读入变量.
11 # 2) 每个变量都设置成了以空白分割的字符串,
12 # 而不是之前的以整行的内容作为变量的值.
13 # 3) 而最后一个变量将会取得第一行剩余的全部部分(不管是否以空白分割).
14 # 4) 如果需要赋值的变量的个数比文件中第一行一空白分割的字符串的个数多的话,
15 # 那么这些变量将会被赋空值.
16
17 echo "------------------------------------------------"
18
19 # 如何用循环来解决上边所提到的问题:
20 while read line
21 do
22 echo "$line"
23 done 24 # Thanks, Heiner Steven for pointing this out.
25
26 echo "------------------------------------------------"
27
28 # 使用$IFS (内部域分隔变量)来将每行的输入单独的放到"read"中,
29 # 如果你不想使用默认空白的话.
30
31 echo "List of all users:"
32 OIFS=$IFS; IFS=: # /etc/passwd 使用 ":" 作为域分隔符.
33 while read name passwd uid gid fullname ignore
34 do
35 echo "$name ($fullname)"
36 done 37 IFS=$OIFS # 恢复原始的 $IFS.
38 # 这个代码片段也是Heiner Steven写的.
39
40
41
42 # 在循环内部设置$IFS变量
43 #+ 而不用把原始的$IFS
44 #+ 保存到临时变量中.
45 # Thanks, Dim Segebart, for pointing this out.
46 echo "------------------------------------------------"
47 echo "List of all users:"
48
49 while IFS=: read name passwd uid gid fullname ignore
50 do
51 echo "$name ($fullname)"
52 done 53
54 echo
55 echo "\$IFS still $IFS"
56
57 exit 0
################################End Script#########################################
注意:管道输出到一个read命令中,使用管道echo输出到read会失败.
然而使用管道cat输出看起来能够正常运行.
1 cat file1 file2 |
2 while read line
3 do
4 echo $line
5 done
但是,像Bjon Eriksson指出的:
Example 11-8 管道输出到read中的问题
################################Start Script#######################################
1 #!/bin/sh
2 # readpipe.sh
3 # 这个例子是Bjon Eriksson捐献的.
4
5 last="(null)"
6 cat $0 |
7 while read line
8 do
9 echo "{$line}"
10 last=$line
11 done
12 printf "\nAll done, last:$last\n"
13
14 exit 0 # 代码结束.
15 # 下边是这个脚本的部分输出.
16 # 打印出了多余的大括号.
17
18 #############################################
19
20 ./readpipe.sh
21
22 {#!/bin/sh}
23 {last="(null)"}
24 {cat $0 |}
25 {while read line}
26 {do}
27 {echo "{$line}"}
28 {last=$line}
29 {done}
30 {printf "nAll done, last:$lastn"}
31
32
33 All done, last:(null)
34
35 变量(last)是设置在子shell中的而没设在外边.
################################End Script#########################################
在许多linux发行版上,gendiff脚本通常在/usr/bin下,将find的输出使用管道传递到一个
while循环中.
1 find $1 \( -name "*$2" -o -name ".*$2" \) -print |
2 while read f; do
3 . . .
文件系统类
cd
cd,修改目录命令,在脚本中用得最多的时候就是,命令需要在指定目录下运行时,需要用cd
修改当前工作目录.
1 (cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)
[之前有个例子,Alan Cox写的]
-P(physical)选项的作用是忽略符号连接.
cd - 将把工作目录改为$OLDPWD,就是之前的工作目录.
注意:当我们用两个/来作为cd命令的参数时,结果却出乎我们的意料.
bash$ cd //
bash$ pwd
//
输出应该,并且当然是/.无论在命令行下还是在脚本中,这都是个问题.
pwd
打印当前的工作目录.这将给用户(或脚本)当前的工作目录(见Example 11-9).使用这个
命令的结果和从内键变量$PWD中读取的值是相同的.
pushd, popd, dirs
这几个命令可以使得工作目录书签化,就是可以按顺序向前或向后移动工作目录.
压栈的动作可以保存工作目录列表.选项可以允许对目录栈作不同的操作.
pushd dir-name 把路径dir-name压入目录栈,同时修改当前目录到dir-name.
popd 将目录栈中最上边的目录弹出,同时修改当前目录到弹出来的那个目录.
dirs 列出所有目录栈的内容(与$DIRSTACK便两相比较).一个成功的pushd或者popd将会
自动的调用dirs命令.
对于那些并没有对当前工作目录做硬编码,并且需要对当前工作目录做灵活修改的脚本来说
,使用这些命令是再好不过的了.注意内建$DIRSTACK数组变量,这个变量可以在脚本内存取,
并且它们保存了目录栈的内容.
Example 11-9 修改当前的工作目录
################################Start Script#######################################
1 #!/bin/bash
2
3 dir1=/usr/local
4 dir2=/var/spool
5
6 pushd $dir1
7 # 将会自动运行一个 'dirs' (把目录栈的内容列到stdout上).
8 echo "Now in directory `pwd`." # Uses back-quoted 'pwd'.
9
10 # 现在对'dir1'做一些操作.
11 pushd $dir2
12 echo "Now in directory `pwd`."
13
14 # 现在对'dir2'做一些操作.
15 echo "The top entry in the DIRSTACK array is $DIRSTACK."
16 popd
17 echo "Now back in directory `pwd`."
18
19 # 现在,对'dir1'做更多的操作.
20 popd
21 echo "Now back in original working directory `pwd`."
22
23 exit 0
24
25 # 如果你不使用 'popd'将会发生什么 -- 然后退出这个脚本?
26 # 你最后将落在那个目录中?为什么?
################################End Script#########################################
变量类
let
let命令将执行变量的算术操作.在许多情况下,它被看作是复杂的expr版本的一个简化版.
Example 11-10 用"let"命令来作算术操作.
################################Start Script#######################################
1 #!/bin/bash
2
3 echo
4
5 let a=11 # 与 'a=11' 相同
6 let a=a+5 # 等价于let "a = a + 5"
7 # (双引号和空格是这句话更具可读性.)
8 echo "11 + 5 = $a" # 16
9
10 let "a <<= 3" # 等价于let "a = a << 3"
11 echo "\"\$a\" (=16) left-shifted 3 places = $a"
12 # 128
13
14 let "a /= 4" # 等价于let "a = a / 4"
15 echo "128 / 4 = $a" # 32
16
17 let "a -= 5" # 等价于let "a = a - 5"
18 echo "32 - 5 = $a" # 27
19
20 let "a *= 10" # 等价于let "a = a * 10"
21 echo "27 * 10 = $a" # 270
22
23 let "a %= 8" # 等价于let "a = a % 8"
24 echo "270 modulo 8 = $a (270 / 8 = 33, remainder $a)"
25 # 6
26
27 echo
28
29 exit 0
################################End Script#########################################
eval
eval arg1 [arg2] ... [argN]
将表达式中的参数,或者表达式列表,组合起来,并且评估它们.包含在表达式中的任何变量
都将被扩展.结果将会被转化到命令中.这对于从命令行或者脚本中产生代码是很有用的.
bash$ process=xterm
bash$ show_process="eval ps ax | grep $process"
bash$ $show_process
1867 tty1 S 0:02 xterm
2779 tty1 S 0:00 xterm
2886 pts/1 S 0:00 grep xterm
Example 11-11 显示eval命令的效果
################################Start Script#######################################
1 #!/bin/bash
2
3 y=`eval ls -l` # 与 y=`ls -l` 很相似
4 echo $y #+ 但是换行符将被删除,因为echo的变量未被""引用.
5 echo
6 echo "$y" # 用""将变量引用,换行符就不会被空格替换了.
7
8 echo; echo
9
10 y=`eval df` # 与 y=`df` 很相似
11 echo $y #+ 换行符又被空格替换了.
12
13 # 当没有LF(换行符)出现时,对于使用"awk"这样的工具来说,
14 #+ 可能分析输出的结果更容易一些.
15
16 echo
17 echo "==========================================================="
18 echo
19
20 # Now, showing how to "expand" a variable using "eval" . . .
20 # 现在,来看一下怎么用"eval"命令来扩展一个变量. . .
21
22 for i in 1 2 3 4 5; do
23 eval value=$i
24 # value=$i 将具有同样的效果. "eval"并不非得在这里使用.
25 # 一个缺乏特殊含义的变量将被评估为自身 --
26 #+ 也就是说,这个变量除了能够被扩展成自身所表示的字符,不能扩展成任何其他的含义.
27 echo $value
28 done
29
30 echo
31 echo "---"
32 echo
33
34 for i in ls df; do
35 value=eval $i
36 # value=$i has an entirely different effect here.
36 # value=$i 在这里就与上边这句有了本质上的区别.
37 # "eval" 将会评估命令 "ls" 和 "df" . . .
38 # 术语 "ls" 和 "df" 就具有特殊含义,
39 #+ 因为它们被解释成命令,
40 #+ 而不是字符串本身.
41 echo $value
42 done
43
44
45 exit 0
################################End Script#########################################
Example 11-12 强制登出(log-off)
################################Start Script#######################################
1 #!/bin/bash
2 # 结束ppp进程来强制登出log-off.
3
4 # 脚本应该以根用户的身份来运行.
5
6 killppp="eval kill -9 `ps ax | awk '/ppp/ { print $1 }'`"
7 # -------- ppp 的进程ID -------
8
9 $killppp # 这个变量现在成为了一个命令.
10
11
12 # 下边的命令必须以根用户的身份来运行.
13
14 chmod 666 /dev/ttyS3 # 恢复读写权限,否则什么?
15 # 因为在ppp上执行一个SIGKILL将会修改串口的权限,
16 #+ 我们把权限恢复到之前的状态.
17
18 rm /var/lock/LCK..ttyS3 # 删除串口琐文件.为什么?
19
20 exit 0
21
22 # 练习:
23 # -----
24 # 1) 编写一个脚本来验证是否跟用户正在运行它.
25 # 2) 做一个检查,检查一下将要杀掉的进程
26 #+ 再杀掉这个进程之前,它是否正在运行.
27 # 3) 基于'fuser'来编写达到这个目的的另一个版本的脚本
28 #+ if [ fuser -s /dev/modem ]; then . . .
################################End Script#########################################
Example 11-13 另一个"rot13"的版本
################################Start Script#######################################
1 #!/bin/bash
2 # 使用'eval'的一个"rot13"的版本,(译者:rot13就是把26个字母,从中间分为2瓣,各13个)
3 # 与脚本"rot13.sh" 比较一下.
4
5 setvar_rot_13() # "rot13" 函数
6 {
7 local varname=$1 varvalue=$2
8 eval $varname='$(echo "$varvalue" | tr a-z n-za-m)'
9 }
10
11
12 setvar_rot_13 var "foobar" # 用"foobar" 传递到rot13函数中.
13 echo $var # 结果是sbbone
14
15 setvar_rot_13 var "$var" # 传递"sbbone" 到rot13函数中.
16 # 又变成了原始值.
17 echo $var # foobar
18
19 # 这个例子是Segebart Chazelas编写的.
20 # 作者又修改了一下.
21
22 exit 0
################################End Script#########################################
Rory Winston捐献了下编的脚本,关于使用eval命令.
Example 11-14 在Perl脚本中使用eval命令来强制变量替换
################################Start Script#######################################
1 In the Perl script "test.pl":
2 ...
3 my $WEBROOT = ;
4 ...
5
6 To force variable substitution try:
7 $export WEBROOT_PATH=/usr/local/webroot
8 $sed 's//$WEBROOT_PATH/' < test.pl > out
9
10 But this just gives:
11 my $WEBROOT = $WEBROOT_PATH;
12
13 However:
14 $export WEBROOT_PATH=/usr/local/webroot
15 $eval sed 's%\%$WEBROOT_PATH%' < test.pl > out
16 # ====
17
18 That works fine, and gives the expected substitution:
19 my $WEBROOT = /usr/local/webroot;
20
21
22 ### Correction applied to original example by Paulo Marcel Coelho Aragao.
################################End Script#########################################
eval命令是有风险的,如果有更合适的方法来实现功能的话,尽量要避免使用它.
eval命令将执行命令的内容,如果命令中有rm -rf*这种东西,可能就不是你想要的了.
如果在一个不熟悉的人编写的脚本中使用eval命令将是危险的.
set
set命令用来修改内部脚本变量的值.一个作用就是触发选项标志位来帮助决定脚本的行
为.另一个应用就是以一个命令的结果(set `command`)来重新设置脚本的位置参数.脚本
将会从命令的输出中重新分析出位置参数.
Example 11-15 使用set来改变脚本的位置参数
################################Start Script#######################################
1 #!/bin/bash
2
3 # script "set-test"
4
5 # 使用3个命令行参数来调用这个脚本,
6 # 比如, "./set-test one two three".
7
8 echo
9 echo "Positional parameters before set \`uname -a\` :" #uname命令打印操作系统名
10 echo "Command-line argument #1 = $1"
11 echo "Command-line argument #2 = $2"
12 echo "Command-line argument #3 = $3"
13
14
15 set `uname -a` # 把`uname -a`的命令输出设置
16 # 为新的位置参数.
17
18 echo $_ # 这要看你的unmae -a输出了,这句打印出的就是输出的最后一个单词.
19 # 在脚本中设置标志.
20
21 echo "Positional parameters after set \`uname -a\` :"
22 # $1, $2, $3, 等等. 这些位置参数将被重新初始化为`uname -a`的结果
23 echo "Field #1 of 'uname -a' = $1"
24 echo "Field #2 of 'uname -a' = $2"
25 echo "Field #3 of 'uname -a' = $3"
26 echo ---
27 echo $_ # ---
28 echo
29
30 exit 0
################################End Script#########################################
不使用任何选项或参数来调用set命令的话,将会列出所有的环境变量和其他所有的已经
初始化过的命令.
bash$ set
AUTHORCOPY=/home/bozo/posts
BASH=/bin/bash
BASH_VERSION=$'2.05.8(1)-release'
...
XAUTHORITY=/home/bozo/.Xauthority
_=/etc/bashrc
variable22=abc
variable23=xzy
使用参数--来调用set命令的话,将会明确的分配位置参数.如果--选项后边没有跟变量名
的话,那么结果就使所有位置参数都比unset了.
Example 11-16 重新分配位置参数
################################Start Script#######################################
1 #!/bin/bash
2
3 variable="one two three four five"
4
5 set -- $variable
6 # 将位置参数的内容设为变量"$variable"的内容.
7
8 first_param=$1
9 second_param=$2
10 shift; shift # Shift past first two positional params.
11 remaining_params="$*"
12
13 echo
14 echo "first parameter = $first_param" # one
15 echo "second parameter = $second_param" # two
16 echo "remaining parameters = $remaining_params" # three four five
17
18 echo; echo
19
20 # 再来一次.
21 set -- $variable
22 first_param=$1
23 second_param=$2
24 echo "first parameter = $first_param" # one
25 echo "second parameter = $second_param" # two
26
27 # ======================================================
28
29 set --
30 # Unsets positional parameters if no variable specified.
30 # 如果没指定变量,那么将会unset所有的位置参数.
31
32 first_param=$1
33 second_param=$2
34 echo "first parameter = $first_param" # (null value)
35 echo "second parameter = $second_param" # (null value)
36
37 exit 0
################################End Script#########################################
见Example 10-2,和Example 12-51.
unset
unset命令用来删除一个shell变量,效果就是把这个变量设为null.注意:这个命令对位置
参数无效.
bash$ unset PATH
bash$ echo $PATH
bash$
Example 11-17 Unset一个变量
################################Start Script#######################################
1 #!/bin/bash
2 # unset.sh: Unset一个变量.
3
4 variable=hello # 初始化.
5 echo "variable = $variable"
6
7 unset variable # Unset.
8 # 与 variable= 的效果相同.
9 echo "(unset) variable = $variable" # $variable 设为 null.
10
11 exit 0
################################End Script#########################################
export
export命令将会使得被export的变量在运行的脚本(或shell)的所有的子进程中都可用.
不幸的是,没有办法将变量export到父进程(就是调用这个脚本或shell的进程)中.
关于export命令的一个重要的使用就是用在启动文件中,启动文件是用来初始化并且
设置环境变量,让用户进程可以存取环境变量.
Example 11-18 使用export命令传递一个变量到一个内嵌awk的脚本中
################################Start Script#######################################
1 #!/bin/bash
2
3 # 这是"求列的和"脚本的另外一个版本(col-totaler.sh)
4 #+ 那个脚本可以把目标文件中的指定的列上的所有数字全部累加起来,求和.
5 # 这个版本将把一个变量通过export的形式传递到'awk'中 . . .
6 #+ 并且把awk脚本放到一个变量中.
7
8
9 ARGS=2
10 E_WRONGARGS=65
11
12 if [ $# -ne "$ARGS" ] # 检查命令行参数的个数.
13 then
14 echo "Usage: `basename $0` filename column-number"
15 exit $E_WRONGARGS
16 fi
17
18 filename=$1
19 column_number=$2
20
21 #===== 上边的这部分,与原始脚本完全一样 =====#
22
23 export column_number
24 # 将列号通过export出来,这样后边的进程就可用了.
25
26
27 # -----------------------------------------------
28 awkscript='{ total += $ENVIRON["column_number"] }
29 END { print total }'
30 # 是的,一个变量可以保存一个awk脚本.
31 # -----------------------------------------------
32
33 # 现在,运行awk脚本.
34 awk "$awkscript" "$filename"
35
36 # Thanks, Stephane Chazelas.
37
38 exit 0
################################End Script#########################################
注意:可以在一个操作中同时赋值和export变量,如: export var1=xxx.
然而,像Greg Keraunen指出的,在某些情况下使用上边这种形式,将与先设置变量,然后
export变量效果不同.
bash$ export var=(a b); echo ${var[0]}
(a b)
bash$ var=(a b); export var; echo ${var[0]}
a
declare, typeset
declare和typeset命令被用来指定或限制变量的属性.
readonly
与declare -r作用相同,设置变量的只读属性,也可以认为是设置常量.设置了这种属性之后
如果你还要修改它,那么你将得到一个错误消息.这种情况与C语言中的const常量类型的情
况是相同的.
getopts
可以说这是分析传递到脚本的命令行参数的最强力工具.这个命令与getopt外部命令,和
C语言中的库函数getopt的作用是相同的.它允许传递和连接多个选项[2]到脚本中,并能分
配多个参数到脚本中.
getopts结构使用两个隐含变量.$OPTIND是参数指针(选项索引),和$OPTARG(选项参数)
(可选的)可以在选项后边附加一个参数.在声明标签中,选项名后边的冒号用来提示
这个选项名已经分配了一个参数.
getopts结构通常都组成一组放在一个while循环中,循环过程中每次处理一个选项和参数,
然后增加隐含变量$OPTIND的值,再进行下一次的处理.
注意: 1.通过命令行传递到脚本中的参数前边必须加上一个减号(-).这是一个前缀,这样
getopts命令将会认为这个参数是一个选项.事实上,getopts不会处理不带"-"前缀
的参数,如果第一个参数就没有"-",那么将结束选项的处理.
2.使用getopts的while循环模版还是与标准的while循环模版有些不同.没有标准
while循环中的[]判断条件.
3.getopts结构将会取代getopt外部命令.
################################Start Script#######################################
1 while getopts ":abcde:fg" Option
2 # Initial declaration.
2 # 开始的声明.
3 # a, b, c, d, e, f, 和 g 被认为是选项(标志).
4 # e选项后边的:提示,这个选项带一个参数.
5 do
6 case $Option in
7 a ) # Do something with variable 'a'.
7 a ) # 对选项'a'作些操作.
8 b ) # 对选项'b'作些操作.
9 ...
10 e) # Do something with 'e', and also with $OPTARG,
10 e) # 对选项'e'作些操作, 同时处理一下$OPTARG,
11 # which is the associated argument passed with option 'e'.
11 # 这个变量里边将保存传递给选项"e"的参数.
12 ...
13 g ) # 对选项'g'作些操作.
14 esac
15 done
16 shift $(($OPTIND - 1))
17 # 将参数指针向下移动.
18
19 # 所有这些远没有它看起来的那么复杂.<嘿嘿>
20
################################End Script#########################################
Example 11-19 使用getopts命令来读取传递给脚本的选项/参数.
(我测试的结果与说明不同,我使用 ./scriptname -mnp,但是$OPTIND的值居然是1 1 2)
################################Start Script#######################################
1 #!/bin/bash
2 # 练习 getopts 和 OPTIND
3 # 在Bill Gradwohl的建议下,这个脚本于 10/09/03 被修改.
4
5
6 # 这里我们将学习 'getopts'如何处理脚本的命令行参数.
7 # 参数被作为"选项"(标志)被解析,并且分配参数.
8
9 # 试一下通过如下方法来调用这个脚本
10 # 'scriptname -mn'
11 # 'scriptname -oq qOption' (qOption 可以是任意的哪怕有些诡异字符的字符串.)
12 # 'scriptname -qXXX -r'
13 #
14 # 'scriptname -qr' - 意外的结果, "r" 将被看成是选项 "q" 的参数.
15 # 'scriptname -q -r' - 意外的结果, 同上.
16 # 'scriptname -mnop -mnop' - 意外的结果
17 # (OPTIND is unreliable at stating where an option came from).
18 #
19 # 如果一个选项需要一个参数("flag:"),那么它应该
20 #+ 取得在命令行上挨在它后边的任何字符.
21
22 NO_ARGS=0
23 E_OPTERROR=65
24
25 if [ $# -eq "$NO_ARGS" ] # 不带命令行参数就调用脚本?
26 then
27 echo "Usage: `basename $0` options (-mnopqrs)"
28 exit $E_OPTERROR # 如果没有参数传进来,那就退出,并解释用法.
29 fi
30 # 用法: 脚本名 -选项名
31 # 注意: 破折号(-)是必须的
32
33
34 while getopts ":mnopq:rs" Option
35 do
36 case $Option in
37 m ) echo "Scenario #1: option -m- [OPTIND=${OPTIND}]";;
38 n | o ) echo "Scenario #2: option -$Option- [OPTIND=${OPTIND}]";;
39 p ) echo "Scenario #3: option -p- [OPTIND=${OPTIND}]";;
40 q ) echo "Scenario #4: option -q-\
41 with argument \"$OPTARG\" [OPTIND=${OPTIND}]";;
42 # 注意,选项'q'必须分配一个参数,
43 #+ 否则默认将失败.
44 r | s ) echo "Scenario #5: option -$Option-";;
45 * ) echo "Unimplemented option chosen.";; # DEFAULT
46 esac
47 done
48
49 shift $(($OPTIND - 1))
50 # 将参数指针减1,这样它将指向下一个参数.
51 # $1 现在引用的是命令行上的第一个非选项参数
52 #+ 如果有一个这样的参数存在的话.
53
54 exit 0
55
56 # 像 Bill Gradwohl 所说,
57 # "The getopts mechanism allows one to specify: scriptname -mnop -mnop
58 #+ but there is no reliable way to differentiate what came from where
59 #+ by using OPTIND."
################################End Script#########################################
脚本行为
source, . (点命令)
这个命令在命令行上执行的时候,将会执行一个脚本.在一个文件内一个source file-name
将会加载file-name文件.source一个文件(或点命令)将会在脚本中引入代码,并附加到脚
本中(与C语言中的#include指令的效果相同).最终的结果就像是在使用"sourced"行上插
入了相应文件的内容.这在多个脚本需要引用相同的数据,或函数库时非常有用.
Example 11-20 "Including"一个数据文件
################################Start Script#######################################
1 #!/bin/bash
2
3 . data-file # 加载一个数据文件.
4 # 与"source data-file"效果相同,但是更具可移植性.
5
6 # 文件"data-file"必须存在于当前工作目录,
7 #+ 因为这个文件时使用'basename'来引用的.
8
9 # 现在,引用这个数据文件中的一些数据.
10
11 echo "variable1 (from data-file) = $variable1"
12 echo "variable3 (from data-file) = $variable3"
13
14 let "sum = $variable2 + $variable4"
15 echo "Sum of variable2 + variable4 (from data-file) = $sum"
16 echo "message1 (from data-file) is \"$message1\""
17 # 注意 : 将双引号转义
18
19 print_message This is the message-print function in the data-file.
20
21
22 exit 0
################################End Script#########################################
Example 11-20使用的data-file.见上边,这个文件必须和上边的脚本放在同一目录下.
################################Start Script#######################################
1 # 这是需要被脚本加载的data file.
2 # 这种文件可以包含变量,函数,等等.
3 # 在脚本中可以通过'source'或者'.'命令来加载.
4
5 # 让我们初始化一些变量.
6
7 variable1=22
8 variable2=474
9 variable3=5
10 variable4=97
11
12 message1="Hello, how are you?"
13 message2="Enough for now. Goodbye."
14
15 print_message ()
16 {
17 # Echo出传递进来的任何消息.
18
19 if [ -z "$1" ]
20 then
21 return 1
22 # 如果没有参数的话,出错.
23 fi
24
25 echo
26
27 until [ -z "$1" ]
28 do
29 # 循环处理传递到函数中的参数.
30 echo -n "$1"
31 # 每次Echo 一个参数, -n禁止换行.
32 echo -n " "
33 # 在参数间插入空格.
34 shift
35 # 下一个.
36 done
37
38 echo
39
40 return 0
41 }
################################End Script#########################################
如果引入的文件本身就是一个可执行脚本的话,那么它将运行起来,当它return的时候,控制
权又重新回到了引用它的脚本中.一个用source引入的脚本可以使用return 命令来达到这
个目的.
也可以向需要source的脚本中传递参数.这些参数在source脚本中被认为是位置参数.
1 source $filename $arg1 arg2
你甚至可以在脚本文件中source脚本文件自身,虽然看不出有什么实际的应用价值.
Example 11-21 一个没什么用的,source自身的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # self-source.sh: 一个脚本递归的source自身.
3 # 来自于"Stupid Script Tricks," 卷 II.
4
5 MAXPASSCNT=100 # source自身的最大数量.
6
7 echo -n "$pass_count "
8 # 在第一次运行的时候,这句只不过echo出2个空格,
9 #+ 因为$pass_count还没被初始化.
10
11 let "pass_count += 1"
12 # 假定这个为初始化的变量 $pass_count
13 #+ 可以在第一次运行的时候+1.
14 # 这句可以正常工作于Bash和pdksh,但是
15 #+ 它依赖于不可移植(并且可能危险)的行为.
16 # 更好的方法是在使用$pass_count之前,先把这个变量初始化为0.
17
18 while [ "$pass_count" -le $MAXPASSCNT ]
19 do
20 . $0 # 脚本"sources" 自身, 而不是调用自己.
21 # ./$0 (应该能够正常递归) 但是不能在这正常运行. 为什么?
22 done
23
24 # 这里发生的动作并不是真正的递归,
25 #+ 因为脚本成功的展开了自己,换句话说,
26 #+ 在每次循环的过程中
27 #+ 在每个'source'行(第20行)上
28 # 都产生了新的代码.
29 #
30 # 当然,脚本会把每个新'sourced'进来的文件的"#!"行
31 #+ 都解释成注释,而不会把它看成是一个新的脚本.
32
33 echo
34
35 exit 0 # 最终的效果就是从1数到100.
36 # 让人印象深刻.
37
38 # 练习:
39 # -----
40 # 使用这个小技巧编写一些真正能干些事情的脚本.
################################End Script#########################################
exit
绝对的停止一个脚本的运行.exit命令有可以随便找一个整数变量作为退出脚本返回shell
时的退出码.使用exit 0对于退出一个简单脚本来说是种好习惯,表明成功运行.
注意: 如果不带参数的使用exit来退出,那么退出码将是脚本中最后一个命令的退出码.
等价于exit $?.
exec
这个shell内建命令将使用一个特定的命令来取代当前进程.一般的当shell遇到一个命令,
它会fork off一个子进程来真正的运行命令.使用exec内建命令,shell就不会fork了,并
且命令的执行将会替换掉当前shell.因此,当我们在脚本中使用它时,当命令实行完毕,
它就会强制退出脚本.[3]
Example 11-22 exec的效果
################################Start Script#######################################
1 #!/bin/bash
2
3 exec echo "Exiting \"$0\"." # 脚本将在此退出.
4
5 # ----------------------------------
6 # 下边的部分将执行不到.
7
8 echo "This echo will never echo."
9
10 exit 99 # 脚本不会在这退出.
11 # 脚本退出后检查一下退出码
12 #+ 使用'echo $?'命令.
13 # 肯定不是99.
################################End Script#########################################
Example 11-23 一个exec自身的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # self-exec.sh
3
4 echo
5
6 echo "This line appears ONCE in the script, yet it keeps echoing."
7 echo "The PID of this instance of the script is still $$."
8 # 上边这句用来根本没产生子进程.
9
10 echo "==================== Hit Ctl-C to exit ===================="
11
12 sleep 1
13
14 exec $0 # 产生了本脚本的另一个实例,
15 #+ 并且这个实例代替了之前的那个.
16
17 echo "This line will never echo!" # 当然会这样.
18
19 exit 0
################################End Script#########################################
exec命令还能用于重新分配文件描述符.比如: exec stdin.
注意: find命令的 -exec选项与shell内建的exec命令是不同的.
shopt
这个命令允许shell在空闲时修改shell选项(见Example 24-1和Example 24-2).它经常出
现在启动脚本中,但是在一般脚本中也可用.需要Bash 2.0版本以上.
1 shopt -s cdspell
2 # Allows minor misspelling of directory names with 'cd'
2 # 使用'cd'命令时,允许产生少量的拼写错误.
3
4 cd /hpme # 噢! 应该是'/home'.
5 pwd # /home
6 # 拼写错误被纠正了.
caller
将caller命令放到函数中,将会在stdout上打印出函数调用者的信息.
1 #!/bin/bash
2
3 function1 ()
4 {
5 # 在 function1 () 内部.
6 caller 0 # 显示调用者信息.
7 }
8
9 function1 # 脚本的第9行.
10
11 # 9 main test.sh
12 # ^ 函数调用者所在的行号.
13 # ^^^^ 从脚本的"main"部分调用的.
14 # ^^^^^^^ 调用脚本的名字
15
16 caller 0 # 没效果,因为这个命令不再函数中.
caller命令也可以返回在一个脚本中被source的另一个脚本的信息.象函数一样,这是一个
"子例程调用",你会发现这个命令在调试的时候特别有用.
命令类
ture
一个返回成功(就是返回0)退出码的命令,但是除此之外什么事也不做.
1 # 死循环
2 while true # 这里的true可以用":"替换
3 do
4 operation-1
5 operation-2
6 ...
7 operation-n
8 # 需要一种手段从循环中跳出来,或者是让这个脚本挂起.
9 done
flase
一个返回失败(非0)退出码的命令,但是除此之外什么事也不做.
1 # 测试 "false"
2 if false
3 then
4 echo "false evaluates \"true\""
5 else
6 echo "false evaluates \"false\""
7 fi
8 # 失败会显示"false"
9
10
11 # while "false" 循环 (空循环)
12 while false
13 do
14 # 这里边的代码将不会走到.
15 operation-1
16 operation-2
17 ...
18 operation-n
19 # 什么事都没发生!
20 done
type[cmd]
与which扩展命令很相像,type cmd将给出"cmd"的完整路径.与which命令不同的是,type命
令是Bash内建命令.一个很有用的选项是-a选项,使用这个选项可以鉴别所识别的参数是关
键字还是内建命令,也可以定位同名的系统命令.
bash$ type '['
[ is a shell builtin
bash$ type -a '['
[ is a shell builtin
[ is /usr/bin/[
hash[cmds]
在shell的hash表中[4],记录指定命令的路径名,所以在shell或脚本中在调用这个命令的
话,shell或脚本将不需要再在$PATH中重新搜索这个命令了.如果不带参数的调用hash命
令,它将列出所有已经被hash的命令.-r选项会重新设置hash表.
bind
bind内建命令用来显示或修改readline[5]的键绑定.
help
获得shell内建命令的一个小的使用总结.这与whatis命令比较象,但是help是内建命令.
bash$ help exit
exit: exit [n]
Exit the shell with a status of N. If N is omitted, the exit status
is that of the last command executed.
11.1 作业控制命令
-----------------
下边的作业控制命令需要一个"作业标识符"作为参数.见这章结尾的表.
jobs
在后台列出所有正在运行的作业,给出作业号.
注意: 进程和作业的概念太容易混淆了.特定的内建命令,比如kill,disown和wait即可以
接受一个作业号作为参数也可以接受一个作为参数.但是fg,bg和jobs命令只能接受
作业号作为参数.
bash$ sleep 100 &
[1] 1384
bash $ jobs
[1]+ Running sleep 100 &
注意: "1"是作业号(作业是被当前shell所维护的),而"1384"是进程号(进程是被系统
维护的).为了kill掉作业/进程,或者使用 kill %1命令或者使用kill 1384命令,
这两个命令都可以.
感谢,S.C.
disown
从shell的当前作业表中,删除作业.
fg,bg
fg命令可以把一个在后台运行的作业放到前台来运行.而bg命令将会重新启动一个挂起的
作业,并且在后台运行它.如果使用fg或者bg命令的时候没指定作业号,那么默认将对当前
正在运行的作业做操作.
wait
停止脚本的运行,直到后台运行的所有作业都结束为止,或者直到指定作业号或进程号为选
项的作业结束为止.
你可以使用wait命令来防止在后台作业没完成(这会产生一个孤儿进程)之前退出脚本.
Example 11-24 在继续处理之前,等待一个进程的结束
################################Start Script#######################################
1 #!/bin/bash
2
3 ROOT_UID=0 # 只有$UID 为0的用户才拥有root权限.
4 E_NOTROOT=65
5 E_NOPARAMS=66
6
7 if [ "$UID" -ne "$ROOT_UID" ]
8 then
9 echo "Must be root to run this script."
10 # "Run along kid, it's past your bedtime."
11 exit $E_NOTROOT
12 fi
13
14 if [ -z "$1" ]
15 then
16 echo "Usage: `basename $0` find-string"
17 exit $E_NOPARAMS
18 fi
19
20
21 echo "Updating 'locate' database..."
22 echo "This may take a while."
23 updatedb /usr & # 必须使用root身份来运行.
24
25 wait
26 # 将不会继续向下运行,除非 'updatedb'命令执行完成.
27 # 你希望在查找文件名之前更新database.
28
29 locate $1
30
31 # 如果没有'wait'命令的话,而且在比较糟的情况下,
32 #+ 脚本可能在'updatedb'命令还在运行的时候退出,
33 #+ 这将会导致'updatedb'成为一个孤儿进程.
34
35 exit 0
################################End Script#########################################
当然,wait 也可以接受一个作业标识符作为参数,比如,wait %1或wait $PPID.见"作业标识
符表".
注意: 在一个脚本中,使用一个后台运行的命令(使用&)可能会使这个脚本挂起,直到敲
回车,挂起才会被恢复.看起来只有这个命令的结果需要输出到stdout的时候才会发
生这种现象.这会是一个很烦人的现象.
1 #!/bin/bash
2 # test.sh
3
4 ls -l &
5 echo "Done."
bash$ ./test.sh
Done.
[bozo@localhost test-scripts]$ total 1
-rwxr-xr-x 1 bozo bozo 34 Oct 11 15:09 test.sh
_
看起来在这个后台运行命令的后边放上一个wait命令可能会解决这个问题.
1 #!/bin/bash
2 # test.sh
3
4 ls -l &
5 echo "Done."
6 wait
bash$ ./test.sh
Done.
[bozo@localhost test-scripts]$ total 1
-rwxr-xr-x 1 bozo bozo 34 Oct 11 15:09 test.sh
如果把这个后台运行命令的输出重定向到文件中或者重定向到/dev/null中,也能解决
这个问题.
suspend
这个命令的效果与Control-Z很相像,但是它挂起的是这个shell(这个shell的父进程应该
在合适的时候重新恢复它).
logout
退出一个登陆的shell,也可以指定一个退出码.
times
给出执行命令所占的时间,使用如下形式输出:
0m0.020s 0m0.020s
这是一种很有限的能力,因为这不常出现于shell脚本中.
kill
通过发送一个适当的结束信号,来强制结束一个进程(见Example 13-6).
Example 11-25 一个结束自身的脚本.
################################Start Script#######################################
1 #!/bin/bash
2 # self-destruct.sh
3
4 kill $$ # 脚本将在此处结束自己的进程.
5 # Recall that "$$" is the script's PID.
5 # 回忆一下,"$$"就是脚本的PID.
6
7 echo "This line will not echo."
8 # 而且shell将会发送一个"Terminated"消息到stdout.
9
10 exit 0
11
12 # 在脚本结束自身进程之后,
13 #+ 它返回的退出码是什么?
14 #
15 # sh self-destruct.sh
16 # echo $?
17 # 143
18 #
19 # 143 = 128 + 15
20 # 结束信号
################################End Script#########################################
注意: kill -l将列出所有信号. kill -9 是"必杀"命令,这个命令将会结束哪些顽固的
不想被kill掉的进程.有时候kill -15也可以干这个活.一个僵尸进程不能被登陆的
用户kill掉, -- 因为你不能杀掉一些已经死了的东西 -- ,但是init进程迟早会
把它清除干净.僵尸进程就是子进程已经结束掉,而父进程却没kill掉这个子进程,
那么这个子进程就成为僵尸进程.
command
command命令会禁用别名和函数的查找.它只查找内部命令以及搜索路径中找到的脚本或可
执行程序.(译者,只在要执行的命令与函数或别名同名时使用,因为函数的优先级比内建命
令的优先级高)
(译者:注意一下bash执行命令的优先级:
1.别名
2.关键字
3.函数
4.内置命令
5.脚本或可执行程序($PATH)
)
注意: 当象运行的命令或函数与内建命令同名时,由于内建命令比外部命令的优先级高,而
函数比内建命令优先级高,所以bash将总会执行优先级比较高的命令.这样你就没有选
择的余地了.所以Bash提供了3个命令来让你有选择的机会.command命令就是这3个命
令之一.
另外两个是builtin和enable.
builtin
在"builtin"后边的命令将只调用内建命令.暂时的禁用同名的函数或者是同名的扩展命令.
enable
这个命令或者禁用内建命令或者恢复内建命令.如: enable -n kill将禁用kill内建命令,
所以当我们调用kill时,使用的将是/bin/kill外部命令.
-a选项将会恢复相应的内建命令,如果不带参数的话,将会恢复所有的内建命令.
选项-f filename将会从适当的编译过的目标文件[6]中以共享库(DLL)的形式来加载一个
内建命令.
autoload
这是从ksh的autoloader命令移植过来的.一个带有"autoload"声明的函数,在它第一次被
调用的时候才会被加载.[7] 这样做会节省系统资源.
注意: autoload命令并不是Bash安装时候的核心命令的一部分.这个命令需要使用命令
enable -f(见上边enable命令)来加载.
Table 11-1 作业标识符
==========================================================
记法 | 含义
==========================================================
%N | 作业号[N]
==========================================================
%S | 以字符串S开头的被(命令行)调用的作业
==========================================================
%?S | 包含字符串S的被(命令行)调用的作业
==========================================================
%% | 当前作业(前台最后结束的作业,或后台最后启动的作业)
==========================================================
%+ | 当前作业(前台最后结束的作业,或后台最后启动的作业)
==========================================================
%- | 最后的作业
==========================================================
$! | 最后的后台进程
==========================================================
注意事项:
[1] 一个例外就是time命令,Bash官方文档说这个命令是一个关键字.
[2] 一个选项就是一个行为上比较象标志位的参数,可以用来打开或关闭脚本的某些行为.
而和某个特定选项相关的参数就是用来控制这个选项功能是开启还是关闭的.
[3] 除非exec被用来重新分配文件描述符.
[4] hash是一种处理存储在表中数据的方法,这种方法就是为表中的数据建立查找键.
而数据项本身是不规则的,这样就可以通过一个简单的数学算法来产生一个数字,
这个数字被用来作为查找键.
使用hash的一个最有利的地方就是提高了速度.而缺点就是会产生"冲撞" -- 也就是
说,可能会有多个数据元素使用同一个主键.
关于hash的例子见 Example A-21 和 Example A-22.
[5] 在一个交互的shell中,readline库就是Bash用来读取输入的.
(译者: 比如默认的Emacs风格的输入,当然也可以改为vi风格的输入)
[6] 一些可加载的内建命令的C源代码都放在/usr/share/doc/bash-?.??/functions下.
注意: enable命令的-f选项并不是对所有系统都支持的(看移没移植上).
[7] typeset -fu可以达到和autoload命令相同的作用.
第12章 外部过滤器,程序和命令
=============================
标准的 UNIX 命令使得 shell 脚本更加灵活.通过简单的编程结构把shell指令和系统命令结
合起来,这才是脚本能力的所在.
12.1 基本命令
-------------
新手必须要掌握的初级命令
ls
基本的列出所有文件的命令.但是往往就是因为这个命令太简单,所以我们总是低估它.比如
,用 -R 选项,这是递归选项,ls 将会以目录树的形式列出所有文件, 另一个很有用的选项
是 -S ,将会按照文件尺寸列出所有文件, -t, 将会按照修改时间来列出文件,-i 选项会显
示文件的inode(见 Example 12-4).
Example 12-1 使用ls命令来创建一个烧录CDR的内容列表
################################Start Script#######################################
1 #!/bin/bash
2 # ex40.sh (burn-cd.sh)
3 # 自动刻录CDR的脚本.
4
5
6 SPEED=2 # 如果你的硬件支持的话,你可以选用更高的速度.
7 IMAGEFILE=cdimage.iso
8 CONTENTSFILE=contents
9 DEVICE=cdrom
10 # DEVICE="0,0" 为了使用老版本的CDR
11 DEFAULTDIR=/opt # 这是包含需要被刻录内容的目录.
12 # 必须保证目录存在.
13 # 小练习: 测试一下目录是否存在.
14
15 # Uses Joerg Schilling's "cdrecord" package:
15 # 使用 Joerg Schilling 的 "cdrecord"包:
16 #
17
18 # 如果一般用户调用这个脚本的话,可能需要root身份
19 #+ chmod u+s /usr/bin/cdrecord
20 # 当然, 这会产生安全漏洞, 虽然这是一个比较小的安全漏洞.
21
22 if [ -z "$1" ]
23 then
24 IMAGE_DIRECTORY=$DEFAULTDIR
25 # 如果命令行没指定的话, 那么这个就是默认目录.
26 else
27 IMAGE_DIRECTORY=$1
28 fi
29
30 # 创建一个内容列表文件.
31 ls -lRF $IMAGE_DIRECTORY > $IMAGE_DIRECTORY/$CONTENTSFILE
32 # "l" 选项将给出一个"长"文件列表.
33 # "R" 选项将使这个列表递归.
34 # "F" 选项将标记出文件类型 (比如: 目录是以 /结尾, 而可执行文件以 *结尾).
35 echo "Creating table of contents."
36
37 # 在烧录到CDR之前创建一个镜像文件.
38 mkisofs -r -o $IMAGEFILE $IMAGE_DIRECTORY
39 echo "Creating ISO9660 file system image ($IMAGEFILE)."
40
41 # 烧录CDR.
42 echo "Burning the disk."
43 echo "Please be patient, this will take a while."
44 cdrecord -v -isosize speed=$SPEED dev=$DEVICE $IMAGEFILE
45
46 exit $?
################################End Script#########################################
cat, tac
cat, 是单词 concatenate的缩写, 把文件的内容输出到stdout. 当与重定向操作符 (> 或
>>)结合使用时, 一般都是用来将多个文件连接起来.
1 # Uses of 'cat'
2 cat filename # 打印出文件内容.
3
4 cat file.1 file.2 file.3 > file.123 # 把3个文件连接到一个文件中.
cat 命令的 -n 选项是为了在目标文件中的所有行前边插入行号. -b 选项 与 -n 选项一
样, 区别是不对空行进行编号. -v 选项可以使用 ^ 标记法 来echo 出不可打印字符.-s选
项可以把多个空行压缩成一个空行.
见 Example 12-25 和 Example 12-21.
注意: 在一个 管道 中, 可能有一种把stdin 重定向 到一个文件中的更有效的办法, 这
种方法比 cat文件的方法更有效率.
1 cat filename | tr a-z A-Z
2
3 tr a-z A-Z < filename # 效果相同,但是处理更少,
4 #+ 并且连管道都省掉了.
tac 命令, 就是 cat的反转, 将从文件的结尾列出文件.
rev
把每一行中的内容反转, 并且输出到 stdout上. 这个命令与 tac命令的效果是不同的, 因
为它并不反转行序, 而是把每行的内容反转.
bash$ cat file1.txt
This is line 1.
This is line 2.
bash$ tac file1.txt
This is line 2.
This is line 1.
bash$ rev file1.txt
.1 enil si sihT
.2 enil si sihT
cp
这是文件拷贝命令. cp file1 file2 把 file1 拷贝到 file2, 如果存在 file2 的话,那
file2 将被覆盖 (见 Example 12-6).
注意: 特别有用的选项就是 -a 归档 选项 (为了copy一个完整的目录树), -u 是更新选
项, 和 -r 与 -R 递归选项.
1 cp -u source_dir/* dest_dir
2 # "Synchronize" dest_dir to source_dir把源目录"同步"到目标目录上,
3 #+ 也就是拷贝所有更新的文件和之前不存在的文件.
mv
这是文件移动命令. 它等价于 cp 与 rm 命令的组合. 它可以把多个文件移动到目录中,甚
至将目录重命名. 想查看 mv 在脚本中使用的例子, 见 Example 9-18 和 Example A-2.
注意: 当使用非交互脚本时,可以使用 mv 的-f (强制) 选项来避免用户的输入.
当一个目录被移动到一个已存在的目录时,那么它将成为目标目录的子目录.
bash$ mv source_directory target_directory
bash$ ls -lF target_directory
total 1
drwxrwxr-x 2 bozo bozo 1024 May 28 19:20 source_directory/
rm
删除(清除)一个或多个文件. -f 选项将强制删除文件,即使这个文件是只读的.并且可以
用来避免用户输入(在非交互脚本中使用).
注意: rm 将无法删除以破折号开头的文件.
bash$ rm -badname
rm: invalid option -- b
Try `rm --help' for more information.
解决这个问题的一个方法就是在要删除的文件的前边加上"./".
bash$ rm ./-badname
另一种解决的方法是 在文件名前边加上 " -- ".
bash$ rm -- -badname
注意: 当使用递归参数 -r时, rm 命令将会删除整个目录树. 如果不慎使用 rm -rf *那整
个目录树就真的完了.
rmdir
删除目录. 但是只有这个目录中没有文件 -- 当然会包含不可见的 点文件 [1] -- 的
时候这个命令才会成功.
mkdir
生成目录, 创建一个空目录. 比如, mkdir -p project/programs/December 将会创建出
这个指定的目录, 即使project目录和programs目录都不存在. -p 选项将会自动产生必要
的父目录, 这样也就同时创建了多个目录.
chmod
修改一个现存文件的属性 (见 Example 11-12).
1 chmod +x filename
2 # 使得文件filename对所有用户都可执行.
3
4 chmod u+s filename
5 # 设置"filename"文件的"suid"位.
6 # 这样一般用户就可以执行"filename", 他将拥有和文件宿主相同的权限.
7 # (这并不适用于shell 脚本)
1 chmod 644 filename
2 # Makes "filename" readable/writable to owner, readable to
3 # 设置文件宿主的 r/w 权限,并对一般用户
3 # 设置读权限.
4 # (8进制模式).
1 chmod 1777 directory-name
2 # 对这个目录设置r/w 和可执行权限, 并开放给所有人.
3 # 同时设置 "粘贴位".
4 # 这意味着, 只有目录宿主,
5 # 文件宿主, 当然, 还有root
6 # 可以删除这个目录中的任何特定的文件.
chattr
修改文件属性. 这个命令与上边的 chmod 命令相类似, 但是有不同的选项和不同的调用语
法, 并且这个命令只能工作在ext2文件系统中.
chattr 命令的一个特别有趣的选项是i. chattr +i filename 将使得这个文件被标记为
永远不变. 这个文件将不能被修改, 连接, 或删除, 即使是root也不行. 这个文件属性只
能被root设置和删除. 类似的, a 选项将会把文件标记为只能追加数据.
root# chattr +i file1.txt
root# rm file1.txt
rm: remove write-protected regular file `file1.txt'? y
rm: cannot remove `file1.txt': Operation not permitted
如果文件设置了s(安全)属性, 那么当这个文件被删除时,这个文件所在磁盘的块将全部被0
填充.
如果文件设置了u(不可删除)属性, 那么当这个文件被删除后, 这个文件的内容还可以被恢
复(不可删除).
如果文件设置了c(压缩)属性, 那么当这个文件在进行写操作时,它将自动被压缩,并且在
读的时候, 自动解压.
注意: 使用命令chattr do设置的属性, 将不会显示在文件列表中(ls -l).
ln
创建文件链接, 前提是这个文件是存在的. "链接" 就是一个文件的引用, 也就是这个文
件的另一个名字. ln 命令允许对同一个文件引用多个链接,并且是避免混淆的一个很好的
方法 (见 Example 4-6).
ln 对于文件来说只不过是创建了一个引用, 一个指针而已, 因为创建出来的连接文件只有
几个字节.
绝大多数使用ln 命令时使用是 -s 选项, 可以称为符号链接, 或软链接.使用 -s 选项的
一个优点是它可以穿越文件系统来链接目录.
关于使用这个命令的语法还是有点小技巧的. 比如: ln -s oldfile newfile 将对老文件
产生一个新的文件链接.
注意: 如果之前就存在newfile的话, 那么将会产生一个错误消息.
使用链接中的哪种类型?
就像 John Macdonald 解释的那样:
不论是那种类型的链接, 都提供了一种双向引用的手段 -- 也就是说, 不管你用文件
的那个名字对文件内容进行修改, 你修改的效果都即会反映到原始名字的文件, 也会
反映到链接名字的文件.当你工作在更高层次的时候, 才会发生软硬链接的不同. 硬链
接的优点是, 原始文件与链接文件之间是相互独立的 -- 如果你删除或者重命名老文
件, 那么这种操作将不会影响硬链接的文件, 硬链接的文件讲还是原来文件的内容.
然而如果你使用软链接的, 当你把老文件删除或重命名后, 软链接将再也找不到原来
文件的内容了. 而软链接的优点是它可以跨越文件系统(因为它只不过是文件名的一个
引用, 而并不是真正的数据). 与硬链接的另一个不同是, 一个符号链接可以指向一个
目录.
链接给出了一种可以用多个名字来调用脚本的能力(当然这也适用于任何可执行的类型),
并且脚本的行为将依赖于脚本是如何被调用的.
Example 12-2 Hello or Good-bye
################################Start Script#######################################
1 #!/bin/bash
2 # hello.sh: 显示"hello" 还是 "goodbye"
3 #+ 依赖于脚本是如何被调用的.
4
5 # 在当前目录下($PWD)为这个脚本创建一个链接:
6 # ln -s hello.sh goodbye
7 # 现在, 通过如下两种方法来调用这个脚本:
8 # ./hello.sh
9 # ./goodbye
10
11
12 HELLO_CALL=65
13 GOODBYE_CALL=66
14
15 if [ $0 = "./goodbye" ]
16 then
17 echo "Good-bye!"
18 # 当然, 在这里你也可以添加一些其他的 goodbye类型的命令.Some other goodbye-type commands, as appropriate.
19 exit $GOODBYE_CALL
20 fi
21
22 echo "Hello!"
23 # 当然, 在这里你也可以添加一些其他的 hello类型的命令.
24 exit $HELLO_CALL
################################End Script#########################################
man, info
These 这两个命令用来查看系统命令或安装工具的手册和信息.当两者都可用时, info 页
一般比 man也会包含更多的细节描述.
注意事项:
[1] Dotfiles 就是文件名以"."开头的文件, 比如 ~/.Xdefaults. 这样的文件在一般的 l
s 命令使用中将不会被显示出来 (当然 ls -a 将会显示它们), 并且它们也不会被一
个意外的 rm -rf *删除. 在用户的home目录中,Dotfiles 一般被用来当作安装和配置
文件.
12.2 复杂命令
-------------
更高级的用户命令
find
-exec COMMAND \;
在每一个find 匹配到的文件执行 COMMAND 命令. 命令序列以 ; 结束( ";" 是 转义符 以
保证 shell 传递到find命令中的字符不会被解释为其他的特殊字符).
bash$ find ~/ -name '*.txt'
/home/bozo/.kde/share/apps/karm/karmdata.txt
/home/bozo/misc/irmeyc.txt
/home/bozo/test-scripts/1.txt
如果 COMMAND 中包含 {}, 那么 find 命令将会用所有匹配文件的路径名来替换 "{}" .
1 find ~/ -name 'core*' -exec rm {} \;
2 # 从用户的 home 目录中删除所有的 core dump文件.
1 find /home/bozo/projects -mtime 1
2 # 列出最后一天被修改的
3 #+ 在/home/bozo/projects目录树下的所有文件.
4 #
5 # mtime = last modification time of the target file
6 # ctime = last status change time (via 'chmod' or otherwise)
7 # atime = last access time
8
9 DIR=/home/bozo/junk_files
10 find "$DIR" -type f -atime +5 -exec rm {} \;
11 # ^^
12 # 大括号就是"find"命令用来替换目录的地方.
13 #
14 # 删除至少5天内没被存取过的
15 #+ "/home/bozo/junk_files" 中的所有文件.
16 #
17 # "-type filetype", where
18 # f = regular file
19 # d = directory, etc.
20 # ('find' 命令的 man页有完整的选项列表.)
1 find /etc -exec grep '[0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*' {} \;
2
3 # 在/etc 目录中的文件找到所所有包含 IP 地址(xxx.xxx.xxx.xxx) 的文件.
4 # 可能会查找到一些多余的匹配. 我们如何去掉它们呢?
5
6 # 或许可以使用如下方法:
7
8 find /etc -type f -exec cat '{}' \; | tr -c '.[:digit:]' '\n' \
9 | grep '^[^.][^.]*\.[^.][^.]*\.[^.][^.]*\.[^.][^.]*$'
10 #
11 # [:digit:] 是一种字符类.is one of the character classes
12 #+ 关于字符类的介绍见 POSIX 1003.2 标准化文档.
13
14 # Thanks, Stéphane Chazelas.
注意: find 命令的 -exec 选项不应该与shell中的内建命令 exec 相混淆.
Example 12-3 删除当前目录下文件名中包含一些特殊字符(包括空白)的文件..
################################Start Script#######################################
1 #!/bin/bash
2 # badname.sh
3 # 删除当前目录下文件名中包含一些特殊字符的文件.
4
5 for filename in *
6 do
7 badname=`echo "$filename" | sed -n /[\+\{\;\"`
8 # badname=`echo "$filename" | sed -n '/[+{;"\=?~()<>&*|$]/p'` 这句也行.
9 # 删除文件名包含这些字符的文件: + { ; " \ = ? ~ ( ) < > & * | $
10 #
11 rm $badname 2>/dev/null
12 # ^^^^^^^^^^^ 错误消息将被抛弃.
13 done
14
15 # 现在, 处理文件名中以任何方式包含空白的文件.
16 find . -name "* *" -exec rm -f {} \;
17 # "find"命令匹配到的目录名将替换到{}的位置.
18 # '\' 是为了保证 ';'被正确的转义, 并且放到命令的结尾.
19
20 exit 0
21
22 #---------------------------------------------------------------------
23 # 这行下边的命令将不会运行, 因为 "exit" 命令.
24
25 # 这句是上边脚本的一个可选方法:
26 find . -name '*[+{;" ]*' -exec rm -f '{}' \;
27 # (Thanks, S.C.)
################################End Script#########################################
Example 12-4 通过文件的 inode 号来删除文件
################################Start Script#######################################
1 #!/bin/bash
2 # idelete.sh: 通过文件的inode号来删除文件.
3
4 # 当文件名以一个非法字符开头的时候, 这就非常有用了,
5 #+ 比如 ? 或 -.
6
7 ARGCOUNT=1 # 文件名参数必须被传递到脚本中.
8 E_WRONGARGS=70
9 E_FILE_NOT_EXIST=71
10 E_CHANGED_MIND=72
11
12 if [ $# -ne "$ARGCOUNT" ]
13 then
14 echo "Usage: `basename $0` filename"
15 exit $E_WRONGARGS
16 fi
17
18 if [ ! -e "$1" ]
19 then
20 echo "File \""$1"\" does not exist."
21 exit $E_FILE_NOT_EXIST
22 fi
23
24 inum=`ls -i | grep "$1" | awk '{print $1}'`
25 # inum = inode (索引节点) 号.
26 # --------------------------------------------------------
27 # 每个文件都有一个inode号, 这个号用来记录文件物理地址信息.
28 # --------------------------------------------------------
29
30 echo; echo -n "Are you absolutely sure you want to delete \"$1\" (y/n)? "
31 # 'rm' 命令的 '-v' 选项也会问这句话.
32 read answer
33 case "$answer" in
34 [nN]) echo "Changed your mind, huh?"
35 exit $E_CHANGED_MIND
36 ;;
37 *) echo "Deleting file \"$1\".";;
38 esac
39
40 find . -inum $inum -exec rm {} \;
41 # ^^
42 # 大括号就是"find"命令
43 #+ 用来替换文本输出的地方.
44 echo "File "\"$1"\" deleted!"
45
46 exit 0
################################End Script#########################################
见 Example 12-27, Example 3-4, 和 Example 10-9 这些例子展示了使用 find 命令. 对
于这个复杂而有强大的命令来说, 查看man页可以获得更多的细节.
xargs
这是给命令传递参数的一个过滤器, 也是组合多个命令的一个工具.它把一个数据流分割为
一些足够小的块, 以方便过滤器和命令进行处理. 由此这个命令也是后置引用的一个强有
力的替换. 在一般使用过多参数的命令替换失败的时候,用xargs 来替换它一般都能成功.
[1] 通常情况下, xargs 从管道或者stdin中读取数据, 但是它也能够从文件的输出中读取
数据.
xargs的默认命令是 echo. 这意味着通过管道传递给xargs的输入将会包含换行和空白, 不
过通过xargs的处理, 换行和空白将被空格取代.
bash$ ls -l
total 0
-rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file1
-rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file2
bash$ ls -l | xargs
total 0 -rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file1 -rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file2
bash$ find ~/mail -type f | xargs grep "Linux"
./misc:User-Agent: slrn/0.9.8.1 (Linux)
./sent-mail-jul-2005: hosted by the Linux Documentation Project.
./sent-mail-jul-2005: (Linux Documentation Project Site, rtf version)
./sent-mail-jul-2005: Subject: Criticism of Bozo's Windows/Linux article
./sent-mail-jul-2005: while mentioning that the Linux ext2/ext3 filesystem
. . .
ls | xargs -p -l gzip 使用gzips 压缩当前目录下的每个文件, 一次压缩一个, 并且在
每次压缩前都提示用户.
注意: 一个有趣的 xargs 选项是 -n NN, NN 是限制每次传递进来参数的个数.
ls | xargs -n 8 echo 以每行8列的形式列出当前目录下的所有文件.
注意: 另一个有用的选项是 -0, 使用 find -print0 或 grep -lZ 这两种组合方式. 这允
许处理包含空白或引号的参数.
find / -type f -print0 | xargs -0 grep -liwZ GUI | xargs -0 rm -f
grep -rliwZ GUI / | xargs -0 rm -f
上边两行都可用来删除任何包含 "GUI" 的文件. (Thanks, S.C.)
Example 12-5 Logfile: 使用 xargs 来监控系统 log
################################Start Script#######################################
1 #!/bin/bash
2
3 # 从 /var/log/messagesGenerates 的尾部开始
4 # 产生当前目录下的一个lof 文件.
5
6 # 注意: 如果这个脚本被一个一般用户调用的话,
7 # /var/log/messages 必须是全部可读的.
8 # #root chmod 644 /var/log/messages
9
10 LINES=5
11
12 ( date; uname -a ) >>logfile
13 # 时间和机器名
14 echo --------------------------------------------------------------------- >>logfile
15 tail -$LINES /var/log/messages | xargs | fmt -s >>logfile
16 echo >>logfile
17 echo >>logfile
18
19 exit 0
20
21 # 注意:
22 # -----
23 # 像 Frank Wang 所指出,
24 #+ 在原文件中的任何不匹配的引号(包括单引号和双引号)
25 #+ 都会给xargs造成麻烦.
26 #
27 # 他建议使用下边的这行来替换上边的第15行:
28 # tail -$LINES /var/log/messages | tr -d "\"'" | xargs | fmt -s >>logfile
29
30
31
32 # 练习:
33 # -----
34 # 修改这个脚本, 使得这个脚本每个20分钟
35 #+ 就跟踪一下 /var/log/messages 的修改记录.
36 # 提示: 使用 "watch" 命令.
################################End Script#########################################
在find命令中, 一对大括号就一个文本替换的位置.
Example 12-6 把当前目录下的文件拷贝到另一个文件中
################################Start Script#######################################
1 #!/bin/bash
2 # copydir.sh
3
4 # 拷贝 (verbose) 当前目录($PWD)下的所有文件到
5 #+ 命令行中指定的另一个目录下.
6
7 E_NOARGS=65
8
9 if [ -z "$1" ] # 如果没有参数传递进来那就退出.
10 then
11 echo "Usage: `basename $0` directory-to-copy-to"
12 exit $E_NOARGS
13 fi
14
15 ls . | xargs -i -t cp ./{} $1
16 # ^^ ^^ ^^
17 # -t 是 "verbose" (输出命令行到stderr) 选项.
18 # -i 是"替换字符串"选项.
19 # {} 是输出文本的替换点.
20 # 这与在"find"命令中使用{}的情况很相像.
21 #
22 # 列出当前目录下的所有文件(ls .),
23 #+ 将 "ls" 的输出作为参数传递到 "xargs"(-i -t 选项) 中,
24 #+ 然后拷贝(cp)这些参数({})到一个新目录中($1).
25 #
26 # 最终的结果和下边的命令等价,
27 #+ cp * $1
28 #+ 除非有文件名中嵌入了"空白"字符.
29
30 exit 0
################################End Script#########################################
Example 12-7 通过名字Kill进程
################################Start Script#######################################
1 #!/bin/bash
2 # kill-byname.sh: 通过名字kill进程.
3 # 与脚本kill-process.sh相比较.
4
5 # 例如,
6 #+ 试一下 "./kill-byname.sh xterm" --
7 #+ 并且查看你系统上的所有xterm都将消失.
8
9 # 警告:
10 # -----
11 # 这是一个非常危险的脚本.
12 # 运行它的时候一定要小心. (尤其是以root身份运行时)
13 #+ 因为运行这个脚本可能会引起数据丢失或产生其他一些不好的效果.
14
15 E_BADARGS=66
16
17 if test -z "$1" # 没有参数传递进来?
18 then
19 echo "Usage: `basename $0` Process(es)_to_kill"
20 exit $E_BADARGS
21 fi
22
23
24 PROCESS_NAME="$1"
25 ps ax | grep "$PROCESS_NAME" | awk '{print $1}' | xargs -i kill {} 2&>/dev/null
26 # ^^ ^^
27
28 # -----------------------------------------------------------
29 # 注意:
30 # -i 参数是xargs命令的"替换字符串"选项.
31 # 大括号对的地方就是替换点.
32 # 2&>/dev/null 将会丢弃不需要的错误消息.
33 # -----------------------------------------------------------
34
35 exit $?
################################End Script#########################################
Example 12-8 使用xargs分析单词出现的频率
################################Start Script#######################################
1 #!/bin/bash
2 # wf2.sh: Crude word frequency analysis on a text file.
3
4 # 使用 'xargs' 将文本行分解为单词.
5 # 于后边的 "wf.sh" 脚本相比较.
6
7
8 # 检查命令行上输入的文件.
9 ARGS=1
10 E_BADARGS=65
11 E_NOFILE=66
12
13 if [ $# -ne "$ARGS" ]
14 # 纠正传递到脚本中的参数个数?
15 then
16 echo "Usage: `basename $0` filename"
17 exit $E_BADARGS
18 fi
19
20 if [ ! -f "$1" ] # 检查文件是否存在.
21 then
22 echo "File \"$1\" does not exist."
23 exit $E_NOFILE
24 fi
25
26
27
28 #################################################################
29 cat "$1" | xargs -n1 | \
30 # 列出文件, 每行一个单词.
31 tr A-Z a-z | \
32 # 将字符转换为小写.
33 sed -e 's/\.//g' -e 's/\,//g' -e 's/ /\
34 /g' | \
35 # 过滤掉句号和逗号,
36 #+ 并且将单词间的空格修改为换行,
37 sort | uniq -c | sort -nr
38 # 最后统计出现次数,把数字显示在第一列,然后显示单词,并按数字排序.
39 #################################################################
40
41 # 这个例子的作用与"wf.sh"的作用是一样的,
42 #+ 但是这个例子比较臃肿, 并且运行起来更慢一些(为什么?).
43
44 exit 0
################################End Script#########################################
expr
通用求值表达式: 通过给定的操作(参数必须以空格分开)连接参数,并对参数求值.可以使
算术操作, 比较操作, 字符串操作或者是逻辑操作.
expr 3 + 5
返回 8
expr 5 % 3
返回 2
expr 1 / 0
返回错误消息, expr: division by zero
不允许非法的算术操作.
expr 5 \* 3
返回 15
在算术表达式expr中使用乘法操作时, 乘法符号必须被转义.
y=`expr $y + 1`
增加变量的值, 与 let y=y+1 和 y=$(($y+1)) 的效果相同. 这是使用算术表达式的
一个例子.
z=`expr substr $string $position $length`
在位置$position上提取$length长度的子串.
Example 12-9 使用 expr
################################Start Script#######################################
1 #!/bin/bash
2
3 # 展示一些 'expr'的使用
4 # =====================
5
6 echo
7
8 # 算术 操作
9 # ---- ----
10
11 echo "Arithmetic Operators"
12 echo
13 a=`expr 5 + 3`
14 echo "5 + 3 = $a"
15
16 a=`expr $a + 1`
17 echo
18 echo "a + 1 = $a"
19 echo "(incrementing a variable)"
20
21 a=`expr 5 % 3`
22 # 取模操作
23 echo
24 echo "5 mod 3 = $a"
25
26 echo
27 echo
28
29 # 逻辑 操作
30 # ---- ----
31
32 # true返回 1 ,false 返回 0 ,
33 #+ 而Bash的使用惯例则相反.
34
35 echo "Logical Operators"
36 echo
37
38 x=24
39 y=25
40 b=`expr $x = $y` # 测试相等.
41 echo "b = $b" # 0 ( $x -ne $y )
42 echo
43
44 a=3
45 b=`expr $a \> 10`
46 echo 'b=`expr $a \> 10`, therefore...'
47 echo "If a > 10, b = 0 (false)"
48 echo "b = $b" # 0 ( 3 ! -gt 10 )
49 echo
50
51 b=`expr $a \< 10`
52 echo "If a < 10, b = 1 (true)"
53 echo "b = $b" # 1 ( 3 -lt 10 )
54 echo
55 # Note escaping of operators.
56
57 b=`expr $a \<= 3`
58 echo "If a <= 3, b = 1 (true)"
59 echo "b = $b" # 1 ( 3 -le 3 )
60 # 也有 "\>=" 操作 (大于等于).
61
62
63 echo
64 echo
65
66
67
68 # 字符串 操作
69 # ------ ----
70
71 echo "String Operators"
72 echo
73
74 a=1234zipper43231
75 echo "The string being operated upon is \"$a\"."
76
77 # 长度: 字符串长度
78 b=`expr length $a`
79 echo "Length of \"$a\" is $b."
80
81 # 索引: 从字符串的开头查找匹配的子串,
82 # 并取得第一个匹配子串的位置.
83 b=`expr index $a 23`
84 echo "Numerical position of first \"2\" in \"$a\" is \"$b\"."
85
86 # substr: 从指定位置提取指定长度的字串.
87 b=`expr substr $a 2 6`
88 echo "Substring of \"$a\", starting at position 2,\
89 and 6 chars long is \"$b\"."
90
91
92 # 'match' 操作的默认行为就是
93 #+ 从字符串的开始进行搜索,并匹配第一个匹配的字符串.
94 #
95 # 使用正则表达式
96 b=`expr match "$a" '[0-9]*'` # 数字的个数.
97 echo Number of digits at the beginning of \"$a\" is $b.
98 b=`expr match "$a" '\([0-9]*\)'` # 注意需要转义括号
99 # == == + 这样才能触发子串的匹配.
100 echo "The digits at the beginning of \"$a\" are \"$b\"."
101
102 echo
103
104 exit 0
################################End Script#########################################
注意: ":" 操作可以替换 match. 比如, b=`expr $a : [0-9]*`与上边所使用的 b=`expr
match $a [0-9]*` 完全等价.
################################Start Script#######################################
1 #!/bin/bash
2
3 echo
4 echo "String operations using \"expr \$string : \" construct"
5 echo "==================================================="
6 echo
7
8 a=1234zipper5FLIPPER43231
9
10 echo "The string being operated upon is \"`expr "$a" : '\(.*\)'`\"."
11 # 转义括号对操作. == ==
12
13 # ***************************
14 #+ 转移括号对
15 #+ 用来匹配一个子串
16 # ***************************
17
18
19 # 如果不转义括号的话...
20 #+ 那么 'expr' 将把string操作转换为一个整数.
21
22 echo "Length of \"$a\" is `expr "$a" : '.*'`." # 字符串长度
23
24 echo "Number of digits at the beginning of \"$a\" is `expr "$a" : '[0-9]*'`."
25
26 # ------------------------------------------------------------------------- #
27
28 echo
29
30 echo "The digits at the beginning of \"$a\" are `expr "$a" : '\([0-9]*\)'`."
31 # == ==
32 echo "The first 7 characters of \"$a\" are `expr "$a" : '\(.......\)'`."
33 # ===== == ==
34 # 再来一个, 转义括号对强制一个子串匹配.
35 #
36 echo "The last 7 characters of \"$a\" are `expr "$a" : '.*\(.......\)'`."
37 # ==== end of string operator ^^
38 # (最后这个模式的意思是忽略前边的任何字符,直到最后7个字符,
39 #+ 最后7个点就是需要匹配的任意7个字符的字串)
40
41 echo
42
43 exit 0
################################End Script#########################################
上边的脚本展示了expr是如何使用转义的括号对 -- \( ... \) -- 和 正则表达式 一起来分
析和匹配子串. 下边是另外一个例子, 这次的例子是真正的应用用例.
1 # 去掉字符串开头和结尾的空白.
2 LRFDATE=`expr "$LRFDATE" : '[[:space:]]*\(.*\)[[:space:]]*$'`
3
4 # 来自于 Peter Knowle的 "booklistgen.sh" 脚本
5 #+ 用来将文件转换为Sony Librie格式.
6 # ()
Perl, sed, 和 awk 是更强大的字符串分析工具. 在脚本中嵌入一段比较短的 sed 或 awk
操作 (见 Section 33.2) 比使用 expr 更加有吸引力.
见 Section 9.2 将会有更多使用 expr 进行字符串操作的例子.
注意事项:
[1] 即使在不必非得强制使用 xargs 的时候, 使用 xargs 也可以明显地提高多文件批处
理执行命令的速度.