Example 17-12 一个自文档化(self-documenting)的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # self-document.sh: 自文档化(self-documenting)的脚本
3 # Modification of "colm.sh".
4
5 DOC_REQUEST=70
6
7 if [ "$1" = "-h" -o "$1" = "--help" ] # 请求帮助.
8 then
9 echo; echo "Usage: $0 [directory-name]"; echo
10 sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
11 sed -e '/DOCUMENTATIONXX$/d'; exit $DOC_REQUEST; fi
12
13
14 : <<DOCUMENTATIONXX
15 List the statistics of a specified directory in tabular format.
16 ---------------------------------------------------------------
17 The command line parameter gives the directory to be listed.
18 If no directory specified or directory specified cannot be read,
19 then list the current working directory.
20
21 DOCUMENTATIONXX
22
23 if [ -z "$1" -o ! -r "$1" ]
24 then
25 directory=.
26 else
27 directory="$1"
28 fi
29
30 echo "Listing of "$directory":"; echo
31 (printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
32 ; ls -l "$directory" | sed 1d) | column -t
33
34 exit 0
################################End Script#########################################
使用cat 脚本 也能够完成相同的目的.
1 DOC_REQUEST=70
2
3 if [ "$1" = "-h" -o "$1" = "--help" ] # 请求帮助.
4 then # 使用"cat 脚本" . . .
5 cat <<DOCUMENTATIONXX
6 List the statistics of a specified directory in tabular format.
7 ---------------------------------------------------------------
8 The command line parameter gives the directory to be listed.
9 If no directory specified or directory specified cannot be read,
10 then list the current working directory.
11
12 DOCUMENTATIONXX
13 exit $DOC_REQUEST
14 fi
参见 Example A-27 可以了解更多关于自文档化脚本的好例子.
注意: Here document创建临时文件, 但是这些文件将在打开后被删除, 并且不能够被任何其
他进程所存取.
bash$ bash -c 'lsof -a -p $$ -d0' << EOF
> EOF
lsof 1213 bozo 0r REG 3,5 0 30386 /tmp/t1213-0-sh (deleted)
注意: 某些工具是不能工作在here document中的.
警告: 结束的limit string, 就是here document最后一行的limit string, 必须开始于第一
个字符位置. 它的前面不能够有任何前置的空白. 而在这个limit string后边的空白也会
引起异常问题. 空白将会阻止limit string的识别.(译者注: 下边这个脚本由于结束
limit string的问题, 造成脚本无法结束, 所有内容全部被打印出来, 所以注释就不译了,
保持例子脚本的原样.)
1 #!/bin/bash
2
3 echo "----------------------------------------------------------------------"
4
5 cat <<LimitString
6 echo "This is line 1 of the message inside the here document."
7 echo "This is line 2 of the message inside the here document."
8 echo "This is the final line of the message inside the here document."
9 LimitString
10 #^^^^Indented limit string. Error! This script will not behave as expected.
11
12 echo "----------------------------------------------------------------------"
13
14 # These comments are outside the 'here document',
15 #+ and should not echo.
16
17 echo "Outside the here document."
18
19 exit 0
20
21 echo "This line had better not echo." # Follows an 'exit' command.
对于那些使用"here document"得非常复杂的任务, 最好考虑使用expect脚本语言, 这种语言
就是为了达到向交互程序添加输入的目的而量身定做的.
17.1. Here Strings
------------------
here string 可以被认为是here document的一种定制形式. 除了COMMAND <<<$WORD 就什么都
没有了, $WORD将被扩展并且被送入COMMAND的stdin中.
Example 17-13 在一个文件的开头添加文本
################################Start Script#######################################
1 #!/bin/bash
2 # prepend.sh: 在文件的开头添加文本.
3 #
4 # Kenny Stauffer所捐助的脚本例子,
5 #+ 被本文作者作了少量的修改.
6
7
8 E_NOSUCHFILE=65
9
10 read -p "File: " file # 'read'命令的 -p 参数显示提示符.
11 if [ ! -e "$file" ]
12 then # 如果没有这个文件那就进来.
13 echo "File $file not found."
14 exit $E_NOSUCHFILE
15 fi
16
17 read -p "Title: " title
18 cat - $file <<<$title > $file.new
19
20 echo "Modified file is $file.new"
21
22 exit 0
23
24 # 下边是'man bash'中的一段:
25 # Here Strings
26 # here document的一种变形,形式如下:
27 #
28 # <<<word
29 #
30 # word被扩展并且提供到command的标准输入中.
################################End Script#########################################
练习: 找出here string的其他用法.
第18章 休息时间
================
这个神奇的暂停可以给读者一个休息的机会, 可能读者到了这里也会会心一笑吧.
Linux同志们, 向你们致敬! 你正在阅读的这些东西, 将会给你们带来好运. 把这份文档发给你
的10个朋友. 在拷贝这份文档之前, 在信的结尾写上一个100行的Bash脚本发送给列表上的第一
个人. 然后在信的底部删除它们的名字并添加你自己的名字.
不要打断这个链条! 并且在48小时之内完成它.
Brooklyn的Wilfred?P.没有成功的发送他的10个拷贝, 当他第2天早上醒来发现他的工作变成了
"COBOL 程序员". Newport?News的Howard?L.在一个月内才发出了他的10个拷贝, 这个时间足够
建立一个100个节点的Beowulf cluster来玩Tuxracer了. Chicago的Amelia?V.对这封信付之一
笑并且打断了这个链条, 不久之后, 她的终端爆炸了, 她现在花了好多天时间为MS Windows写
文档.
千万不要打断这个链条! 今天就把10个拷贝发出去!
第四部分 高级
++++++++++++++++
到了这儿,我们将要准备深入脚本编程中一些难的,不寻常的话题.随着话题的展开,我们会
以多种方法和检测边界条件的方式来“打开信封”,看个明白.(当我们涉足未知领域时会发
生什么?).
目录
19. Regular Expressions正则表达式
20. 子shell(Subshells)
21. 受限shell(Restricted Shells)
22. 进程替换
23. 函数
24. 别名(Aliases)
25. 列表结构
26. 数组
27. /dev和/proc
28. 关于Zeros和Nulls
29. 调试
30. 选项
31. 检查遗漏(Gotchas)
32. 脚本编程风格
33. 杂项
34. Bash,版本2和3
第19章 正则表达式
==================
为了充分发挥shell编程的威力, 你需要精通正则表达式. 一些命令和软件包普遍在脚本编程中
使用正则表达式,例如grep, expr, sed和awk.
19.1 一个简要的正则表达式介绍
--------------------------------
一个正式表达式是一个字符串.字符串里的字符被称为元字符,它们可能表示了比它们字面上看
起来的意思更丰富的含义.例如,一个引用符号可能表示引用一个人演讲中的话,或者表示下
面将要讲到的引申表示的意思.正则表达式是一个字符或/和元字符组合成的字符集,它们匹配
(或指定)一个模式.
一个正则表达式包含下面一个或多个项:
1. 一个字符集.
这里的字符集里的字符表示的就是它们字面上的意思.正则表达式最简单的情况就是仅
仅由字符集组成,而没有其他的元字符.
2. 锚.
一个锚指明了正则表达式在一行文本中要匹配的位置,例如^和$就是锚.
3. 修饰符
它们用于展开或缩小(即是修改了)正则表达式匹配文本行的范围.修饰符包括了星号.
括号和反斜杠符号.
正则表达是的主要作用是用来文本搜索和字串操作.一个正则表达式匹配一个字符或是一串字
符--完整的一串字符或是另外一个字符串的子串.
星号 -- * -- 匹配前一个字符的任意多次(包括零次).
"1133*"匹配11 + 一个或更多的3 + 可能的其他字符: 113, 1133, 111312, 等等.
点 -- . -- 匹配除了新行符之外的任意一个字符. [1]
"13." 匹配13 + 至少一个任意字符(包括空格): 1133, 11333, 但不匹配 13
(因为少了附加的至少一个任意字符).
脱字符 -- ^ -- 匹配一行的开头,但依赖于上下文环境,可能在正则表达式中表示否定
一个字符集的意思.
美元符 -- $ -- 在正则表达式中匹配行尾.
"^$" 匹配空行.
方括号 -- [...] -- 在正则表达式中表示匹配括号中的一个字符.
"[xyz]" 匹配字符x, y, 或z.
"[c-n]" 匹配从字符c到n之间的任意一个字符.
"[B-Pk-y]" 匹配从B到P 或从k到y的任意一个字符.
"[a-z0-9]" 匹配任意小写字母或数字.
"[^b-d]" 匹配除了从b到d范围内所有的字符. 这是正则表达式中反转意思或取否
的一 个例子.(就好像在别的情形中!字符所扮演的角色).
多个方括号字符集组合使用可以匹配一般的单词和数字模式."[Yy][Ee][Ss]" 匹
配yes, Yes, YES, yEs, 等等.
"[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]"匹配社会安全码
(Social Security number).
反斜杠字符 -- \ -- 转义(escapes) 一个特殊的字符,使这个字符表示原来字面上的意思.
"\$"表示了原来的字面意思"$",而不是在正则表达式中表达的匹配行尾的意思.
同样,"\\"也被解释成了字面上的意思"\".
转义(escape)"尖角号" -- \<...\> -- 用于表示单词的边界.
尖角号必须被转义,因为不这样做的话它们就表示单纯的字面意思
而已.
"\<the\>" 匹配单词"the",但不匹配"them", "there", "other",
等等.
bash$ cat textfile
This is line 1, of which there is only one instance.
This is the only instance of line 2.
This is line 3, another line.
This is line 4.
bash$ grep 'the' textfile
This is line 1, of which there is only one instance.
This is the only instance of line 2.
This is line 3, another line.
bash$ grep '\<the\>' textfile
This is the only instance of line 2.
确定正则表达式能否工作的唯一办法是测试它.
1 TEST FILE: tstfile # 不匹配.
2 # 不匹配.
3 Run grep "1133*" on this file. # 匹配.
4 # 不匹配.
5 # 不匹配.
6 This line contains the number 113. # 匹配.
7 This line contains the number 13. # 不匹配.
8 This line contains the number 133. # 不匹配.
9 This line contains the number 1133. # 匹配.
10 This line contains the number 113312. # 匹配.
11 This line contains the number 1112. # 不匹配.
12 This line contains the number 113312312. # 匹配.
13 This line contains no numbers at all. # 不匹配.
bash$ grep "1133*" tstfile
Run grep "1133*" on this file. # 匹配.
This line contains the number 113. # 匹配.
This line contains the number 1133. # 匹配.
This line contains the number 113312. # 匹配.
This line contains the number 113312312. # 匹配.
扩展的正则表达式. 增加了一些元字符到上面提到的基本的元字符集合里. 它们在egrep,
awk,和Perl中使用.
问号 -- ? -- 匹配零或一个前面的字符. 它一般用于匹配单个字符.
加号 -- + -- 匹配一个或多个前面的字符.它的作用和*很相似,但唯一的区别是它不
匹配零个字符的情况.
1 # GNU 版本的 sed 和 awk 可以使用"+",
2 # 但它应该转义一下.
3
4 echo a111b | sed -ne '/a1\+b/p'
5 echo a111b | grep 'a1\+b'
6 echo a111b | gawk '/a1+b/'
7 # 上面三句都是等价的效果.
8
9 # 多谢, S.C.
转义"大括号" -- \{ \} -- 指示前面正则表达式匹配的次数.
要转义是因为不转义的话大括号只是表示他们字面上的意思.这个用法只是
技巧上的而不是基本正则表达式的内容.
"[0-9]\{5\}" 精确匹配5个数字 (从 0 到 9的数字).
注意: 大括号不能在“经典”(不是POSIX兼容)的正则表达式版本的awk中
使用. 然而, gawk 有一个选项--re-interval来允许使用大括号
(不必转义).
bash$ echo 2222 | gawk --re-interval '/2{3}/'
2222
Perl和一些egrep版本不要求转义大括号.
圆括号 -- ( ) -- 括起一组正则表达式. 它和下面要讲的"|"操作符或在用expr进行子字
符串提取(substring extraction)一起使用很有用.
竖线 -- | -- "或"正则操作符用于匹配一组可选的字符.
bash$ egrep 're(a|e)d' misc.txt
People who read seem to be better informed than those who do not.
The clarinet produces sound by the vibration of its reed.
注意: 一些sed, ed, 和ex的版本像GNU的软件版本一样支持上面描述的扩展正
则表达式的版本.
POSIX字符类. [:class:]
这是另外一个可选的用于指定匹配字符范围的方法.
[:alnum:] 匹配字母和数字.等同于A-Za-z0-9.
[:alpha:] 匹配字母. 等同于A-Za-z.
[:blank:] 匹配一个空格或是一个制表符(tab).
[:cntrl:] 匹配控制字符.
[:digit:] 匹配(十进制)数字. 等同于0-9.
[:graph:] (可打印的图形字符). 匹配 ASCII 码值的33 - 126之间的字符. 这和下面提到的
[:print:]一样,但是不包括空格字符.
[:lower:] 匹配小写字母. 等同于a-z.
[:print:] (可打印字符). 匹配 ASCII码值 32 - 126之间的字符. 这和上面提到的一样
[:graph:],但是增多一个空格字符.
[:space:] 匹配空白字符 (空格符和水平制表符).
[:upper:] 匹配大写字母. 等同于A-Z.
[:xdigit:] 匹配十六进制数字. 等同于0-9A-Fa-f.
注意: POSIX字符类一般都要求用引号或是双方括号double brackets ([[ ]])引起来.
bash$ grep [[:digit:]] test.file
abc=723
这些字符类在一个受限的范围内甚至可能用在能用在通配(globbing)中.
bash$ ls -l ?[[:digit:]][[:digit:]]?
-rw-rw-r-- 1 bozo bozo 0 Aug 21 14:47 a33b
为了理解POSIX字符类在脚本中的使用,请参考例子 12-18 和 例子 12-19.
Sed, awk, 和Perl在脚本中被用作过滤器, "过滤"或转换文件/IO流的时候以正则表达式作为参
数.参考例子 A-12和例子 A-17 来理解这种用法.
在正则表达式这个复杂主题的标准参考是Friedl的Mastering Regular Expressions.由
Dougherty和Robbins写的 Sed & Awk也给出了一个清晰的正则表达式论述. 查看参考书目找
到这个主题更多的信息.
注意事项:
[1] 因为sed, awk, 和 grep 通常处理单行,而不能匹配一个新行符. 在要处理多行的一
个输入时,可以使用点操作符,它可以匹配新行符.
1 #!/bin/bash
2
3 sed -e 'N;s/.*/[&]/' << EOF # Here Document
4 line1
5 line2
6 EOF
7 # 输出:
8 # [line1
9 # line2]
10
11
12
13 echo
14
15 awk '{ $0=$1 "\n" $2; if (/line.1/) {print}}' << EOF
16 line 1
17 line 2
18 EOF
19 # 输出:
20 # line
21 # 1
22
23
24 # 多谢, S.C.
25
26 exit 0
19.1 通配
------------
Bash本身没有正则表达式的功能.在脚本里,使用正则表达式的是命令和软件包 -- 例如sed和
awk -- 它们可以解释正则表达式.
Bash所做的是展开文件名扩展 [1] -- 这就是所谓的通配(globbing) -- 但它不是使用标准的
正则表达式. 而是使用通配符. 通配解释标准的通配符:*和?, 方括号括起来的字符,还有其他
的一些特殊的字符(比如说^用来表示取反匹配).然而通配机制的通配符有很大的局限性. 包含
有*号的字符串将不会匹配以点开头的文件,例如.bashrc. [2] 另外,通配机制的? 字符和正则
表达式中表示的意思不一样.
bash$ ls -l
total 2
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
bash$ ls -l t?.sh
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
bash$ ls -l [ab]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
bash$ ls -l [a-c]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 a.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
bash$ ls -l [^ab]*
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 466 Aug 6 17:48 t2.sh
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
bash$ ls -l {b*,c*,*est*}
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 b.1
-rw-rw-r-- 1 bozo bozo 0 Aug 6 18:42 c.1
-rw-rw-r-- 1 bozo bozo 758 Jul 30 09:02 test1.txt
Bash会对命令行中没有引号引起来的字符尝试文件名扩展. echo 命令可以印证这一点.
bash$ echo *
a.1 b.1 c.1 t2.sh test1.txt
bash$ echo t*
t2.sh test1.txt
注意: 可以改变Bash对通配字符进行解释的行为. set -f 命令可以禁止通配机制, 并且
shopt的选项nocaseglob和nullglob 能改变通配的行为.
参考例子 10-4.
注意事项:
[1] 文件名扩展意思是扩展包含有特殊字符的文件名模式和模板. 例如,example.???可能
扩展成example.001和/或example.txt.
[2] 文件名扩展能匹配点开头的文件,但仅在模式字串明确地包含字面意思的点(.)时才
扩展.
1 ~/[.]bashrc # 不会扩展成 ~/.bashrc
2 ~/?bashrc # 也不会扩展.
3 # 通配机制中的通配符和元字符不会扩展点文件
4 #
5
6 ~/.[b]ashrc # 会扩展成 ~/.bashrc
7 ~/.ba?hrc # 也会.
8 ~/.bashr* # 也会.
9
10 # 可以使用"dotglob"选项把这个特性禁用.
11
12 # 多谢, S.C.
第20章 子shell(Subshells)
==========================
运行一个shell脚本时会启动另一个命令解释器. 就好像你的命令是在命令行提示下被解释的一
样, 类似于批处理文件里的一系列命令.每个shell脚本有效地运行在父shell(parent shell)的
一个子进程里.这个父shell是指在一个控制终端或在一个xterm窗口中给你命令指示符的进程.
shell脚本也能启动他自已的子进程. 这些子shell(即子进程)使脚本因为效率而同时进行多个
子任务执行时能做串行处理.
一般来说,脚本里的一个外部命令(external command)能生成(forks)出一个子进程,然而
Bash内建(builtin)的命令却不这样做,因此,内建命令比起外部的等价命令执行起来更快.
圆括号里的命令列表
( 命令1; 命令2; 命令3; ... )
嵌在圆括号里的一列命令在一个子shell里运行.
注意: 在子shell里的变量不能被这段子shell代码块之外外面的脚本访问.这些变量是不能被
产生这个子shell的父进程(parent process)存取的,实际上它们是局部变量
(local variables).
Example 20-1 子shell中的变量作用域
################################Start Script#######################################
1 #!/bin/bash
2 # subshell.sh
3
4 echo
5
6 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
7 # Bash, 版本 3, 增加了新的 $BASH_SUBSHELL 变量.
8 echo
9
10 outer_variable=Outer
11
12 (
13 echo "Subshell level INSIDE subshell = $BASH_SUBSHELL"
14 inner_variable=Inner
15
16 echo "From subshell, \"inner_variable\" = $inner_variable"
17 echo "From subshell, \"outer\" = $outer_variable"
18 )
19
20 echo
21 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
22 echo
23
24 if [ -z "$inner_variable" ]
25 then
26 echo "inner_variable undefined in main body of shell"
27 else
28 echo "inner_variable defined in main body of shell"
29 fi
30
31 echo "From main body of shell, \"inner_variable\" = $inner_variable"
32 # $inner_variable 会以没有初始化的变量来打印
33 #+ 因为变量是在子shell里定义的"局部变量".
34 # 这个有办法补救的吗?
35
36 echo
37
38 exit 0
################################End Script#########################################
参考例子 31-2.
+
在子shell中的目录更改不会影响到父shell.
Example 20-2 列出用户的配置文件
################################Start Script#######################################
1 #!/bin/bash
2 # allprofs.sh: 打印所有用户的配置文件
3
4 # 由 Heiner Steven编写, 并由本书作者修改.
5
6 FILE=.bashrc # 在一般的脚本里,包含用户配置的文件是".profile".
7 #
8
9 for home in `awk -F: '{print $6}' /etc/passwd`
10 do
11 [ -d "$home" ] || continue # 如果没有家目录,跳过此次循环.
12 [ -r "$home" ] || continue # 如果目录没有读权限,跳过此次循环.
13 (cd $home; [ -e $FILE ] && less $FILE)
14 done
15
16 # 当脚本终止时,不必用'cd'命令返回原来的目录,
17 #+ 因为'cd $home'是在子shell中发生的,不影响父shell.
18
19 exit 0
################################End Script#########################################
子shell可用于为一组命令设定临时的环境变量.
1 COMMAND1
2 COMMAND2
3 COMMAND3
4 (
5 IFS=:
6 PATH=/bin
7 unset TERMINFO
8 set -C
9 shift 5
10 COMMAND4
11 COMMAND5
12 exit 3 # 只是从子shell退出.
13 )
14 # 父shell不受影响,变量值没有更改.
15 COMMAND6
16 COMMAND7
它的一个应用是测试是否一个变量被定义了.
1 if (set -u; : $variable) 2> /dev/null
2 then
3 echo "Variable is set."
4 fi # 变量已经在当前脚本中被设置,
5 #+ 或是Bash的一个内部变量,
6 #+ 或是可见环境变量(指已经被导出的环境变量).
7
8 # 也可以写成 [[ ${variable-x} != x || ${variable-y} != y ]]
9 # 或 [[ ${variable-x} != x$variable ]]
10 # 或 [[ ${variable+x} = x ]]
11 # 或 [[ ${variable-x} != x ]]
另一个应用是检查一个加锁的文件:
1 if (set -C; : > lock_file) 2> /dev/null
2 then
3 : # lock_file 不存在,还没有用户运行这个脚本
4 else
5 echo "Another user is already running that script."
6 exit 65
7 fi
8
9 # 由St閜hane Chazelas编程
10 #+ 由Paulo Marcel Coelho Aragao修改.
进程在不同的子shell中可以串行地执行.这样就允许把一个复杂的任务分成几个小的子问题来
同时地处理.
Example 20-3 在子shell里进行串行处理
################################Start Script#######################################
1 (cat list1 list2 list3 | sort | uniq > list123) &
2 (cat list4 list5 list6 | sort | uniq > list456) &
3 #列表的合并和排序同时进.
4 #放到后台运行可以确保能够串行执行.
5 #
6 #和下面的有相同的作用:
7 # cat list1 list2 list3 | sort | uniq > list123 &
8 # cat list4 list5 list6 | sort | uniq > list456 &
9
10 wait #在所有的子shell执行完成前不再执行后面的命令.
11
12 diff list123 list456
################################End Script#########################################
用"|"管道操作把I/O流重定向到子shell,例如ls -al | (command).
注意: 在一个花括号内的代码块不会运行一个子shell.
{ command1; command2; command3; ... }
第21章 受限shell(Restricted Shells)
====================================
在受限shell中禁用的命令
在受限shell中运行的脚本或脚本的个代码断会禁用一些正常shell中可以执行的命令.这是
限制脚本用户的权限和最小化运行脚本导致的破坏的安全措施.
使用cd 命令更改工作目录.
更改环境变量$PATH, $SHELL, $BASH_ENV,或$ENV 的值.
读或更改shell环境选项变量$SHELLOPTS的值.
输出重定向.
调用的命令路径中包括有一个或更多个/字符.
调用exec来把当前的受限shell替换成另外一个不同的进程.
脚本中许多其他无意中能破坏或捣乱的命令.
在脚本中企图脱离受限shell模式的操作.
Example 21-1 在受限的情况下运行脚本
################################Start Script#######################################
1 #!/bin/bash
2
3 # 脚本开头以"#!/bin/bash -r"来调用
4 #+ 会使整个脚本在受限模式下运行.
5
6 echo
7
8 echo "Changing directory."
9 cd /usr/local
10 echo "Now in `pwd`"
11 echo "Coming back home."
12 cd
13 echo "Now in `pwd`"
14 echo
15
16 # 不受限的模式下,所有操作都能正常成功.
17
18 set -r
19 # set --restricted 也能起相同的作用.
20 echo "==> Now in restricted mode. <=="
21
22 echo
23 echo
24
25 echo "Attempting directory change in restricted mode."
26 cd ..
27 echo "Still in `pwd`"
28
29 echo
30 echo
31
32 echo "\$SHELL = $SHELL"
33 echo "Attempting to change shell in restricted mode."
34 SHELL="/bin/ash"
35 echo
36 echo "\$SHELL= $SHELL"
37
38 echo
39 echo
40
41 echo "Attempting to redirect output in restricted mode."
42 ls -l /usr/bin > bin.files
43 ls -l bin.files # Try to list attempted file creation effort.
44
45 echo
46
47 exit 0
################################End Script#########################################
第22章 进程替换
================
进程替换与命令替换(command substitution)很相似. 命令替换把一个命令的结果赋给一个
变量,例如 dir_contents=`ls -al`或xref=$( grep word datafile). 进程替换则是把一个进
程的输出回馈给另一个进程 (换句话说,它把一个命令的结果发送给另一个命令).
命令替换的一般形式
由圆括号括起的命令
>(command)
<(command)
启动进程替换. 它是用/dev/fd/<n>文件把在圆括号内的进程的处理结果发送给另外一个进
程. [1] (译者注:实际上现代的UNIX类操作系统提供的/dev/fd/n文件是与文件描述相关
的,整数n指的就是在进程运行时对应数字的文件描述符)
注意: 在"<" 或or ">" 与圆括号之间是没有空格的. 如果加了空格将会引起错误信息.
bash$ echo >(true)
/dev/fd/63
bash$ echo <(true)
/dev/fd/63
Bash在两个文件描述符(file descriptors)之间创建了一个管道, --fIn 和 fOut--. true
命令的标准输入被连接到fOut(dup2(fOut, 0)), 然后Bash把/dev/fd/fIn作为参数传给echo.
如果系统的/dev/fd/<n>文件不够时,Bash会使用临时文件. (Thanks, S.C.)
进程替换能比较两个不同命令之间的输出,或者甚至相同命令不同选项的输出.
bash$ comm <(ls -l) <(ls -al)
total 12
-rw-rw-r-- 1 bozo bozo 78 Mar 10 12:58 File0
-rw-rw-r-- 1 bozo bozo 42 Mar 10 12:58 File2
-rw-rw-r-- 1 bozo bozo 103 Mar 10 12:58 t2.sh
total 20
drwxrwxrwx 2 bozo bozo 4096 Mar 10 18:10 .
drwx------ 72 bozo bozo 4096 Mar 10 17:58 ..
-rw-rw-r-- 1 bozo bozo 78 Mar 10 12:58 File0
-rw-rw-r-- 1 bozo bozo 42 Mar 10 12:58 File2
-rw-rw-r-- 1 bozo bozo 103 Mar 10 12:58 t2.sh
用进程替换来比较两个不同目录的内容 (考察哪些文件名是相同的,哪些是不同的):
1 diff <(ls $first_directory) <(ls $second_directory)
其他一些进程替换的用法和技巧:
1 cat <(ls -l)
2 # 等同于 ls -l | cat
3
4 sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)
5 # 列出系统中3个主要的'bin'目录的所有文件,并且按文件名排序.
6 # 注意是三个明显不同的命令输出回馈给'sort'.
7
8
9 diff <(command1) <(command2) # 给出两个命令输出的不同之处.
10
11 tar cf >(bzip2 -c > file.tar.bz2) $directory_name
12 # 调用"tar cf /dev/fd/?? $directory_name",和"bzip2 -c > file.tar.bz2".
13 #
14 # 因为/dev/fd/<n>的系统属性,
15 # 所以两个命令之间的管道不必是命名的.
16 #
17 # 这种效果可以模仿出来.
18 #
19 bzip2 -c < pipe > file.tar.bz2&
20 tar cf pipe $directory_name
21 rm pipe
22 # 或者
23 exec 3>&1
24 tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&-
25 exec 3>&-
26
27
28 # Thanks, St`phane Chazelas
有个读者给我发来下面关于进程替换的有趣例子A.
1 # 摘自SuSE发行版中的代码片断:
2
3 while read des what mask iface; do
4 # 这里省略了一些命令 ...
5 done < <(route -n)
6
7
8 # 为了测试它,我们来做些动作.
9 while read des what mask iface; do
10 echo $des $what $mask $iface
11 done < <(route -n)
12
13 # 输出:
14 # Kernel IP routing table
15 # Destination Gateway Genmask Flags Metric Ref Use Iface
16 # 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
17
18
19
20 # 由 St閜hane Chazelas给出的,一个更容易理解的等价代码是:
21 route -n |
22 while read des what mask iface; do # 管道的输出被赋给了变量.
23 echo $des $what $mask $iface
24 done # 这样就取回了和上面一样的输出.
25 # 但是, Ulrich Gayer指出 . . .
26 #+ 这个简单版本的等价代码在while循环中使用了一个子shell,
27 #+ 因此当管道结束后变量会被毁掉.
28
29
30
31 # 更进一步, Filip Moritz解释了上面两个例子之间有一个细微的不同之处
32 #+ 如下所示.
33
34 (
35 route -n | while read x; do ((y++)); done
36 echo $y # $y 仍然没有被声明或设置
37
38 while read x; do ((y++)); done < <(route -n)
39 echo $y # $y的值为 route -n 输出的行数
40 )
41
42 # 一般来说
43 (
44 : | x=x
45 # 看上去是启动了一个子shell
46 : | ( x=x )
47 # 但
48 x=x < <(:)
49 # 实际上不是
50 )
51
52 # 当解析csv或类似的东西时非常有用.
53 # 事实上,这就是SuSE原本的代码片断所要实现的功能.
注意事项:
[1] 这与命名管道(named pipe)(临时文件)有相同的作用, 事实上命名管道同样在进程
替换中被使用.
第23章 函数
============
和"真正的"编程语言一样, Bash也有函数,虽然在某些实现方面稍有些限制. 一个函数是一个
子程序,用于实现一串操作的代码块(code block),它是完成特定任务的"黑盒子". 当有重复
代码, 当一个任务只需要很少的修改就被重复几次执行时, 这时你应考虑使用函数.
function function_name {
command...
}
或
function_name () {
command...
}
第二种格式的写法更深得C程序员的喜欢(并且也是更可移植的).
因为在C中,函数的左花括号也可以写在下一行中.
function_name ()
{
command...
}
函数被调用或被触发, 只需要简单地用函数名调用.
Example 23-1 简单函数
################################Start Script#######################################
1 #!/bin/bash
2
3 JUST_A_SECOND=1
4
5 funky ()
6 { # 这是一个最简单的函数.
7 echo "This is a funky function."
8 echo "Now exiting funky function."
9 } # 函数必须在调用前声明.
10
11
12 fun ()
13 { # 一个稍复杂的函数.
14 i=0
15 REPEATS=30
16
17 echo
18 echo "And now the fun really begins."
19 echo
20
21 sleep $JUST_A_SECOND # 嘿, 暂停一秒!
22 while [ $i -lt $REPEATS ]
23 do
24 echo "----------FUNCTIONS---------->"
25 echo "<------------ARE-------------"
26 echo "<------------FUN------------>"
27 echo
28 let "i+=1"
29 done
30 }
31
32 # 现在,调用两个函数.
33
34 funky
35 fun
36
37 exit 0
################################End Script#########################################
函数定义必须在第一次调用函数前完成.没有像C中的函数“声明”方法.
1 f1
2 # 因为函数"f1"还没有定义,这会引起错误信息.
3
4 declare -f f1 # 这样也没用.
5 f1 # 仍然会引起错误.
6
7 # 然而...
8
9
10 f1 ()
11 {
12 echo "Calling function \"f2\" from within function \"f1\"."
13 f2
14 }
15
16 f2 ()
17 {
18 echo "Function \"f2\"."
19 }
20
21 f1 # 虽然在它定义前被引用过,
22 #+ 函数"f2"实际到这儿才被调用.
23 # 这样是允许的.
24
25 # Thanks, S.C.
在一个函数内嵌套另一个函数也是可以的,但是不常用.
1 f1 ()
2 {
3
4 f2 () # nested
5 {
6 echo "Function \"f2\", inside \"f1\"."
7 }
8
9 }
10
11 f2 # 引起错误.
12 # 就是你先"declare -f f2"了也没用.
13
14 echo
15
16 f1 # 什么也不做,因为调用"f1"不会自动调用"f2".
17 f2 # 现在,可以正确的调用"f2"了,
18 #+ 因为之前调用"f1"使"f2"在脚本中变得可见了.
19
20 # Thanks, S.C.
函数声明可以出现在看上去不可能出现的地方,那些不可能的地方本该由一个命令出现的地方.
1 ls -l | foo() { echo "foo"; } # 允许,但没什么用.
2
3
4
5 if [ "$USER" = bozo ]
6 then
7 bozo_greet () # 在if/then结构中定义了函数.
8 {
9 echo "Hello, Bozo."
10 }
11 fi
12
13 bozo_greet # 只能由Bozo运行, 其他用户会引起错误.
14
15
16
17 # 在某些上下文,像这样可能会有用.
18 NO_EXIT=1 # 将会打开下面的函数定义.
19
20 [[ $NO_EXIT -eq 1 ]] && exit() { true; } # 在"and-list"(and列表)中定义函数.
21 # 如果 $NO_EXIT 是 1,声明函数"exit ()".
22 # 把"exit"取别名为"true"将会禁用内建的"exit".
23
24 exit # 调用"exit ()"函数, 而不是内建的"exit".
25
26 # Thanks, S.C.
23.1. 复杂函数和函数复杂性
--------------------------
函数可以处理传递给它的参数并且能返回它的退出状态码(exit status)给脚本后续使用.
1 function_name $arg1 $arg2
函数以位置来引用传递过来的参数(就好像他们是位置参数(positional parameters)), 例如
$1, $2,以此类推.
Example 23-2 带着参数的函数
################################Start Script#######################################
1 #!/bin/bash
2 # 函数和参数
3
4 DEFAULT=default # 默认的参数值.
5
6 func2 () {
7 if [ -z "$1" ] # 第一个参数是否长度为零?
8 then
9 echo "-Parameter #1 is zero length.-" # 则没有参数传递进来.
10 else
11 echo "-Param #1 is \"$1\".-"
12 fi
13
14 variable=${1-$DEFAULT} #
15 echo "variable = $variable" # 参数替换会表现出什么?
16 # ---------------------------
17 # 它用于分辨没有参数和一个只有NULL值的参数.
18 #
19
20 if [ "$2" ]
21 then
22 echo "-Parameter #2 is \"$2\".-"
23 fi
24
25 return 0
26 }
27
28 echo
29
30 echo "Nothing passed."
31 func2 # 没有参数来调用
32 echo
33
34
35 echo "Zero-length parameter passed."
36 func2 "" # 以一个长度为零的参数调用
37 echo
38
39 echo "Null parameter passed."
40 func2 "$uninitialized_param" # 以未初始化的参数来调用
41 echo
42
43 echo "One parameter passed."
44 func2 first # 用一个参数来调用
45 echo
46
47 echo "Two parameters passed."
48 func2 first second # 以二个参数来调用
49 echo
50
51 echo "\"\" \"second\" passed."
52 func2 "" second # 以第一个参数为零长度,而第二个参数是一个ASCII码组成的字符串来调用.
53 echo #
54
55 exit 0
################################End Script#########################################
注意: shift命令可以工作在传递给函数的参数 (参考例子 33-15).
但是,传给脚本的命令行参数怎么办?在函数内部可以看到它们吗?好,让我们来弄清楚.
Example 23-3 函数和被传给脚本的命令行参数
################################Start Script#######################################
1 #!/bin/bash
2 # func-cmdlinearg.sh
3 # 以一个命令行参数来调用这个脚本,
4 #+ 类似 $0 arg1来调用.
5
6
7 func ()
8
9 {
10 echo "$1"
11 }
12
13 echo "First call to function: no arg passed."
14 echo "See if command-line arg is seen."
15 func
16 # 不!命令行参数看不到.
17
18 echo "============================================================"
19 echo
20 echo "Second call to function: command-line arg passed explicitly."
21 func $1
22 # 现在可以看到了!
23
24 exit 0
################################End Script#########################################
与别的编程语言相比,shell脚本一般只传递值给函数,变量名(实现上是指针)如果作为参数传递给函数会被看成是字面上字符串的意思.函数解释参数是以字面上的意思来解释的.
间接变量引用(Indirect variable references) (参考例子 34-2)提供了传递变量指针给函数的一个笨拙的机制.
Example 23-4 传递间接引用给函数
################################Start Script#######################################
1 #!/bin/bash
2 # ind-func.sh: 传递间接引用给函数.
3
4 echo_var ()
5 {
6 echo "$1"
7 }
8
9 message=Hello
10 Hello=Goodbye
11
12 echo_var "$message" # Hello
13 # 现在,让我们传递一个间接引用给函数.
14 echo_var "${!message}" # Goodbye
15
16 echo "-------------"
17
18 # 如果我们改变"hello"变量的值会发生什么?
19 Hello="Hello, again!"
20 echo_var "$message" # Hello
21 echo_var "${!message}" # Hello, again!
22
23 exit 0
################################End Script#########################################
下一个逻辑问题是:在传递参数给函数之后是否能解除参数的引用.
Example 23-5 解除传递给函数的参数引用
################################Start Script#######################################
1 #!/bin/bash
2 # dereference.sh
3 # 给函数传递不同的参数.
4 # Bruce W. Clare编写.
5
6 dereference ()
7 {
8 y=\$"$1" # 变量名.
9 echo $y # $Junk
10
11 x=`eval "expr \"$y\" "`
12 echo $1=$x
13 eval "$1=\"Some Different Text \"" # 赋新值.
14 }
15
16 Junk="Some Text"
17 echo $Junk "before" # Some Text before
18
19 dereference Junk
20 echo $Junk "after" # Some Different Text after
21
22 exit 0
################################End Script#########################################
Example 23-6 再次尝试解除传递给函数的参数引用
################################Start Script#######################################
1 #!/bin/bash
2 # ref-params.sh: 解除传递给函数的参数引用.
3 # (复杂例子)
4
5 ITERATIONS=3 # 取得输入的次数.
6 icount=1
7
8 my_read () {
9 # 用my_read varname来调用,
10 #+ 输出用括号括起的先前的值作为默认值,
11 #+ 然后要求输入一个新值.
12
13 local local_var
14
15 echo -n "Enter a value "
16 eval 'echo -n "[$'$1'] "' # 先前的值.
17 # eval echo -n "[\$$1] " # 更好理解,
18 #+ 但会丢失用户输入在尾部的空格.
19 read local_var
20 [ -n "$local_var" ] && eval $1=\$local_var
21
22 # "and列表(And-list)": 如果变量"local_var"测试成功则把变量"$1"的值赋给它.
23 }
24
25 echo
26
27 while [ "$icount" -le "$ITERATIONS" ]
28 do
29 my_read var
30 echo "Entry #$icount = $var"
31 let "icount += 1"
32 echo
33 done
34
35
36 # 多谢Stephane Chazelas提供的示范例子.
37
38 exit 0
################################End Script#########################################
退出和返回
退出状态(exit status)
函数返回一个被称为退出状态的值. 退出状态可以由return来指定statement, 否则函数的
退出状态是函数最后一个执行命令的退出状态(0表示成功,非0表示出错代码). 退出状态
(exit status)可以在脚本中由$? 引用. 这个机制使脚本函数也可以像C函数一样有一个"
返回值".
return
终止一个函数.return 命令[1]可选地带一个整数参数,这个整数作为函数的"返回值"返回
给调用此函数的脚本,并且这个值也被赋给变量$?.
Example 23-7 两个数中的最大者
################################Start Script#######################################
1 #!/bin/bash
2 # max.sh: 两个整数中的最大者.
3
4 E_PARAM_ERR=-198 # 如果传给函数的参数少于2个时的返回值.
5 EQUAL=-199 # 如果两个整数值相等的返回值.
6 # 任一个传给函数的参数值溢出
7 #
8
9 max2 () # 返回两个整数的较大值.
10 { # 注意: 参与比较的数必须小于257.
11 if [ -z "$2" ]
12 then
13 return $E_PARAM_ERR
14 fi
15
16 if [ "$1" -eq "$2" ]
17 then
18 return $EQUAL
19 else
20 if [ "$1" -gt "$2" ]
21 then
22 return $1
23 else
24 return $2
25 fi
26 fi
27 }
28
29 max2 33 34
30 return_val=$?
31
32 if [ "$return_val" -eq $E_PARAM_ERR ]
33 then
34 echo "Need to pass two parameters to the function."
35 elif [ "$return_val" -eq $EQUAL ]
36 then
37 echo "The two numbers are equal."
38 else
39 echo "The larger of the two numbers is $return_val."
40 fi
41
42
43 exit 0
44
45 # 练习 (容易):
46 # ---------------
47 # 把这个脚本转化成交互式的脚本,
48 #+ 也就是说,让脚本可以要求调用者输入两个整数.
################################End Script#########################################
注意: 为了函数可以返回字符串或是数组,用一个可在函数外可见的变量.
1 count_lines_in_etc_passwd()
2 {
3 [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
4 # 如果/etc/passwd可读,则把REPLY设置成文件的行数.
5 # 返回一个参数值和状态信息.
6 # 'echo'好像没有必要,但 . . .
7 #+ 它的作用是删除输出中的多余空白字符.
8 }
9
10 if count_lines_in_etc_passwd
11 then
12 echo "There are $REPLY lines in /etc/passwd."
13 else
14 echo "Cannot count lines in /etc/passwd."
15 fi
16
17 # Thanks, S.C.
Example 23-8 把数字转化成罗马数字
################################Start Script#######################################
1 #!/bin/bash
2
3 # 阿拉伯数字转化为罗马数字
4 # 转化范围: 0 - 200
5 # 这是比较粗糙的,但可以工作.
6
7 # 扩展可接受的范围来作为脚本功能的扩充,这个作为练习完成.
8
9 # 用法: roman number-to-convert
10
11 LIMIT=200
12 E_ARG_ERR=65
13 E_OUT_OF_RANGE=66
14
15 if [ -z "$1" ]
16 then
17 echo "Usage: `basename $0` number-to-convert"
18 exit $E_ARG_ERR
19 fi
20
21 num=$1
22 if [ "$num" -gt $LIMIT ]
23 then
24 echo "Out of range!"
25 exit $E_OUT_OF_RANGE
26 fi
27
28 to_roman () # 在第一次调用函数前必须先定义.
29 {
30 number=$1
31 factor=$2
32 rchar=$3
33 let "remainder = number - factor"
34 while [ "$remainder" -ge 0 ]
35 do
36 echo -n $rchar
37 let "number -= factor"
38 let "remainder = number - factor"
39 done
40
41 return $number
42 # 练习:
43 # --------
44 # 解释这个函数是怎么工作的.
45 # 提示: 靠不断地除来分割数字.
46 }
47
48
49 to_roman $num 100 C
50 num=$?
51 to_roman $num 90 LXXXX
52 num=$?
53 to_roman $num 50 L
54 num=$?
55 to_roman $num 40 XL
56 num=$?
57 to_roman $num 10 X
58 num=$?
59 to_roman $num 9 IX
60 num=$?
61 to_roman $num 5 V
62 num=$?
63 to_roman $num 4 IV
64 num=$?
65 to_roman $num 1 I
66
67 echo
68
69 exit 0
################################End Script#########################################
请参考例子 10-28.
注意: 函数最大可返回的正整数为255. return 命令与退出状态(exit status)的概念联
系很紧密,而退出状态的值受此限制.幸运地是有多种(工作区workarounds)来对
付这种要求函数返回大整数的情况.
Example 23-9 测试函数最大的返回值
################################Start Script#######################################
1 #!/bin/bash
2 # return-test.sh
3
4 # 一个函数最大可能返回的值是255.
5
6 return_test () # 无论传给函数什么都返回它.
7 {
8 return $1
9 }
10
11 return_test 27 # o.k.
12 echo $? # 返回 27.
13
14 return_test 255 # 仍然 o.k.
15 echo $? # 返回 255.
16
17 return_test 257 # 错误!
18 echo $? # 返回 1 (返回代码指示错误).
19
20 # ======================================================
21 return_test -151896 # 能够返回这个非常大的负数么?
22 echo $? # 会返回-151896?
23 # 不! 它将返回168.
24 # 2.05b版本之前的Bash是允许
25 #+ 超大负整数作为返回值的.
26 # 但是比它更新一点的版本修正了这个漏洞.
27 # 这将破坏比较老的脚本.
28 # 慎用!
29 # ======================================================
30
31 exit 0
################################End Script#########################################
如果你非常想使用超大整数作为"返回值"的话, 那么只能通过将你想返回的返回值直接的
传递到一个全局变量中的手段来达到目的.
1 Return_Val= # 全局变量, 用来保存函数中需要返回的超大整数.
2
3 alt_return_test ()
4 {
5 fvar=$1
6 Return_Val=$fvar
7 return # Returns 0 (success).
8 }
9
10 alt_return_test 1
11 echo $? # 0
12 echo "return value = $Return_Val" # 1
13
14 alt_return_test 256
15 echo "return value = $Return_Val" # 256
16
17 alt_return_test 257
18 echo "return value = $Return_Val" # 257
19
20 alt_return_test 25701
21 echo "return value = $Return_Val" #25701
一种更优雅的方法是让函数echo出它的返回值, 输出到stdout上, 然后再通过"命令替换"
的手段来捕获它. 参考Section 33.7关于这个问题的讨论.
Example 23-10 比较两个大整数
################################Start Script#######################################
1 #!/bin/bash
2 # max2.sh: 取两个超大整数中最大的.
3
4 # 这个脚本与前面的"max.sh"例子作用相同,
5 #+ 经过修改可以适用于比较超大整数.
6
7 EQUAL=0 # 如果两个参数相同的返回值.
8 E_PARAM_ERR=-99999 # 没有足够的参数传递到函数中.
9 # ^^^^^^ 也可能是传递到函数中的某个参数超出范围了.
10
11 max2 () # 从这两个数中"返回"更大一些的.
12 {
13 if [ -z "$2" ]
14 then
15 echo $E_PARAM_ERR
16 return
17 fi
18
19 if [ "$1" -eq "$2" ]
20 then
21 echo $EQUAL
22 return
23 else
24 if [ "$1" -gt "$2" ]
25 then
26 retval=$1
27 else
28 retval=$2
29 fi
30 fi
31
32 echo $retval # echo(到stdout), 而不是使用返回值.
33 # 为什么?
34 }
35
36
37 return_val=$(max2 33001 33997)
38 # ^^^^ 函数名
39 # ^^^^^ ^^^^^ 这是传递进来的参数
40 # 这事实上是一个命令替换的形式:
41 #+ 会把这个函数当作一个命令来处理,
42 #+ 并且分配这个函数的stdout到变量"return_val"中.
43
44
45 # ========================= OUTPUT ========================
46 if [ "$return_val" -eq "$E_PARAM_ERR" ]
47 then
48 echo "Error in parameters passed to comparison function!"
49 elif [ "$return_val" -eq "$EQUAL" ]
50 then
51 echo "The two numbers are equal."
52 else
53 echo "The larger of the two numbers is $return_val."
54 fi
55 # =========================================================
56
57 exit 0
58
59 # 练习:
60 # -----
61 # 1) 找出一种更优雅的方法来测试
62 #+ 传递到函数中的参数.
63 # 2) 在"OUTPUT"的时候简化if/then结构.
64 # 3) 重写这个脚本使其能够从命令行参数中来获取输入.
################################End Script#########################################
下边是获得一个函数的"返回值"的另一个例子. 想要了解这个例子需要一些awk的知识.
1 month_length () # 以月份数作为参数.
2 { # 返回这个月有几天.
3 monthD="31 28 31 30 31 30 31 31 30 31 30 31" # 作为局部变量来声明?
4 echo "$monthD" | awk '{ print $'"${1}"' }' # 有技巧的.
5 # ^^^^^^^^^
6 # 先将参数传递到函数中 ($1 -- 月份号), 然后就到awk了.
7 # Awk将会根据传递进来的月份号来决定打印"print $1 . . . print $12"中的哪个 (依赖于月份号)
8 # 传递参数到内嵌awk脚本的模版:
9 # $'"${script_parameter}"'
10
11 # 需要错误检查来修正参数的范围(1-12)
12 #+ 并且要处理闰年的特殊的2月.
13 }
14
15 # ----------------------------------------------
16 # 用例:
17 month=4 # 拿4月来举个例子.
18 days_in=$(month_length $month)
19 echo $days_in # 30
20 # ----------------------------------------------
也参考例子 A-7.
练习: 用我们已经学到的扩展先前罗马数字那个例子脚本能接受任意大的输入.
重定向
重定向函数的标准输入
函数本质上是一个代码块(code block), 这样意思着它的标准输入可以被重定向
(就像在例子 3-1中显示的).
Example 23-11 用户名的真实名
################################Start Script#######################################
1 #!/bin/bash
2 # realname.sh
3 #
4 # 由用户名而从/etc/passwd取得"真实名".
5
6
7 ARGCOUNT=1 # 需要一个参数.
8 E_WRONGARGS=65
9
10 file=/etc/passwd
11 pattern=$1
12
13 if [ $# -ne "$ARGCOUNT" ]
14 then
15 echo "Usage: `basename $0` USERNAME"
16 exit $E_WRONGARGS
17 fi
18
19 file_excerpt () # 以要求的模式来扫描文件,然后打印文件相关的部分.
20 {
21 while read line # "while" does not necessarily need "[ condition ]"
22 do
23 echo "$line" | grep $1 | awk -F":" '{ print $5 }' # awk指定使用":"为界定符.
24 done
25 } <$file # 重定向函数的标准输入.
26
27 file_excerpt $pattern
28
29 # Yes, this entire script could be reduced to
30 # grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
31 # or
32 # awk -F: '/PATTERN/ {print $5}'
33 # or
34 # awk -F: '($1 == "username") { print $5 }' # real name from username
35 # 但是,这些可能起不到示例的作用.
36
37 exit 0
################################End Script#########################################
还有一个办法,可能是更好理解的重定向函数标准输入方法.它为函数内的一个括号内的
代码块调用标准输入重定向.
1 # 用下面的代替:
2 Function ()
3 {
4 ...
5 } < file
6
7 # 也试一下这个:
8 Function ()
9 {
10 {
11 ...
12 } < file
13 }
14
15 # 同样,
16
17 Function () # 可以工作.
18 {
19 {
20 echo $*
21 } | tr a b
22 }
23
24 Function () # 这个不会工作
25 {
26 echo $*
27 } | tr a b # 这儿的内嵌代码块是强制的.
28
29
30 # Thanks, S.C.
注意事项:
[1] return命令是Bash内建(builtin)的.
23.2. 局部变量
--------------
怎么样使一个变量变成局部的?
局部变量
如果变量用local来声明,那么它只能在该变量声明的代码块(block of code)中可见.
这个代码块就是局部"范围". 在一个函数内,局部变量意味着只能在函数代码块内它才
有意义.
Example 23-12 局部变量的可见范围
################################Start Script#######################################
1 #!/bin/bash
2 # 在函数内部的全局和局部变量.
3
4 func ()
5 {
6 local loc_var=23 # 声明为局部变量.
7 echo # 使用内建的'local'关键字.
8 echo "\"loc_var\" in function = $loc_var"
9 global_var=999 # 没有声明为局部变量.
10 # 默认为全局变量.
11 echo "\"global_var\" in function = $global_var"
12 }
13
14 func
15
16 # 现在,来看看是否局部变量"loc_var"能否在函数外面可见.
17
18 echo
19 echo "\"loc_var\" outside function = $loc_var"
20 # $loc_var outside function =
21 # 不, $loc_var不是全局可访问的.
22 echo "\"global_var\" outside function = $global_var"
23 # $global_var outside function = 999
24 # $global_var 是全局可访问的.
25 echo
26
27 exit 0
28 # 与In contrast to C相比, 在函数内声明的Bash变量只有在
29 #+ 它被明确声明成局部的变量时才是局部的.
################################End Script#########################################
注意: 在函数调用之前,所有在函数内声明且没有明确声明为local的变量都可在函数体
外可见.
1 #!/bin/bash
2
3 func ()
4 {
5 global_var=37 # 在函数还没有被调用前
6 #+ 变量只在函数内可见.
7 } # 函数结束
8
9 echo "global_var = $global_var" # global_var =
10 # 函数"func"还没有被调用,
11 #+ 所以变量$global_var还不能被访问.
12
13 func
14 echo "global_var = $global_var" # global_var = 37
15 # 已经在函数调用时设置了值.
23.2.1. 局部变量使递归变得可能.
-------------------------------
局部变量可以递归, [1] 但这个办法会产生大量的计算,因此它在shell脚本中是被明确表明
不推荐的. [2]
Example 23-13 用局部变量来递归
################################Start Script#######################################
1 #!/bin/bash
2
3 # 阶乘
4 # ---------
5
6
7 # bash允许递归吗?
8 # 嗯, 允许, 但是...
9 # 它太慢以致你难以忍受.
10
11
12 MAX_ARG=5
13 E_WRONG_ARGS=65
14 E_RANGE_ERR=66
15
16
17 if [ -z "$1" ]
18 then
19 echo "Usage: `basename $0` number"
20 exit $E_WRONG_ARGS
21 fi
22
23 if [ "$1" -gt $MAX_ARG ]
24 then
25 echo "Out of range (5 is maximum)."
26 # 现在让我们来了解实际情况.
27 # 如果你想求比这个更大的范围的阶乘,
28 #+ 应该重新用一个真正的编程语言来写.
29 exit $E_RANGE_ERR
30 fi
31
32 fact ()
33 {
34 local number=$1
35 # 变量"number"必须声明为局部,
36 #+ 否则它不会工作.
37 if [ "$number" -eq 0 ]
38 then
39 factorial=1 # 0的阶乘为1.
40 else
41 let "decrnum = number - 1"
42 fact $decrnum # 递归调用(函数内部调用自己本身).
43 let "factorial = $number * $?"
44 fi
45
46 return $factorial
47 }
48
49 fact $1
50 echo "Factorial of $1 is $?."
51
52 exit 0
################################End Script#########################################
也请参考例子 A-16的脚本递归的例子. 必须意识到递归也意味着巨大的资源消耗和缓慢的运
行,因此它不适合在脚本中使用.
注意事项:
[1] Herbert Mayer 给递归下的定义是". . . expressing an algorithm by using a
simpler version of that same algorithm(用一个相同算法的版本来表示一个算法)
. . ." 递归函数是调用它自己本身的函数.
[2] 太多层的递归可能会引起脚本段错误而崩溃.
1 #!/bin/bash
2
3 # 警告: 运行这个脚本可能使你的系统失去响应!
4 # 如果你运气不错,在它使用完所有可用内存之前会段错误而退出.
5
6 recursive_function ()
7 {
8 echo "$1" # 使函数做些事情以加速产生段错误.
9 (( $1 < $2 )) && recursive_function $(( $1 + 1 )) $2;
10 # 当第一个参数比第二个参数少时,
11 #+ 把第1个参数增1再次递归.
12 }
13
14 recursive_function 1 50000 # 递归 50,000 次!
15 # 非常可能段错误 (依赖于栈的大小,它由ulimit -m设置).
16
17 # 这种深度的递归甚至可能由于耗尽栈的内存大小而引起C程序的段错误.
18 #
19
20
21 echo "This will probably not print."
22 exit 0 # 这个脚本将不会从这儿正常退出.
23
24 # 多谢, St`phane Chazelas.
23.3. 不使用局部变量的递归
--------------------------
函数甚至可以不使用局部变量来调用自己.
Example 23-14 汉诺塔
################################Start Script#######################################
1 #! /bin/bash
2 #
3 # 汉诺塔(The Towers Of Hanoi)
4 # Bash script
5 # Copyright (C) 2000 Amit Singh. All Rights Reserved.
6 # http://hanoi.kernelthread.com
7 #
8 # 在bash version 2.05b.0(13)-release下测试通过
9 #
10 # 经过作者同意后在"Advanced Bash Scripting Guide"书中使用
11 #
12 # 由ABS的作者做了少许修改.
13
14 #=================================================================#
15 # 汉诺塔是由Edouard Lucas提出的数学谜题 ,
16 #+ 他是19世纪的法国数学家.
17 #
18 # 有三个直立的柱子竖在地面上.
19 # 第一个柱子有一组的盘子套在上面.
20 # 这些盘子是平整的,中间带着孔,
21 #+ 因此它们才能套在柱子上面.
22 # 这组盘子有不同的直径,它们是依照直径从小到大来从高到低放置.
23 #
24 # 最小的盘在最高,最大的盘在最底部.
25 #
26 # 现在的任务是要把这一组的盘子从一个柱子全部地搬到另一个柱子上.
27 #
28 # 你只能一次从一个柱子上移动一个盘子到另一个柱子.
29 # 允许把盘子重新移回到它原来的最初位置.
30 # 你可以把一个小的盘子放在大的盘子上面,
31 #+ 但不能把大的盘子放在小的盘子上面.
32 # 请注意这一点.
33 #
34 # 对于这一组盘子,数量少时,只需要移动很少的次数就能达到要求.
35 #+ 但随着这组盘子的数量的增加,
36 #+ 移动的次数几乎成倍增长的,
37 #+ 而移动的策略变得愈加复杂.
38 #
39 # 想了解更多的信息, 请访问 http://hanoi.kernelthread.com.
40 #
41 #
42 # ... ... ...
43 # | | | | | |
44 # _|_|_ | | | |
45 # |_____| | | | |
46 # |_______| | | | |
47 # |_________| | | | |
48 # |___________| | | | |
49 # | | | | | |
50 # .--------------------------------------------------------------.
51 # |**************************************************************|
52 # #1 #2 #3
53 #
54 #=================================================================#
55
56
57 E_NOPARAM=66 # 没有参数传给脚本.
58 E_BADPARAM=67 # 传给脚本的盘子数不合法.
59 Moves= # 保存移动次数的全局变量.
60 # 这儿修改了原脚本.
61
62 dohanoi() { # 递归函数.
63 case $1 in
64 0)
65 ;;
66 *)
67 dohanoi "$(($1-1))" $2 $4 $3
68 echo move $2 "-->" $3
69 let "Moves += 1" # 这儿修改了原脚本.
70 dohanoi "$(($1-1))" $4 $3 $2
71 ;;
72 esac
73 }
74
75 case $# in
76 1)
77 case $(($1>0)) in # 至少要有一个盘子.
78 1)
79 dohanoi $1 1 3 2
80 echo "Total moves = $Moves"
81 exit 0;
82 ;;
83 *)
84 echo "$0: illegal value for number of disks";
85 exit $E_BADPARAM;
86 ;;
87 esac
88 ;;
89 *)
90 echo "usage: $0 N"
91 echo " Where \"N\" is the number of disks."
92 exit $E_NOPARAM;
93 ;;
94 esac
95
96 # 练习:
97 # ---------
98 # 1) 从现在这个位置以下的命令会不会总是被执行?
99 # 为什么? (容易)
100 # 2) 解释这个可运行的"dohanoi"函数的原理.
101 # (难)
################################End Script#########################################
第24章 别名(Aliases)
=====================
Bash别名本质上是一个简称, 缩写, 这可避免键入过长的命令序列. 例如,如果我们添加
alias lm="ls -l | more" 这一行到文件~/.bashrc file里, 然后每次在命令行键入lm 将会
自动被替换成ls -l | more. 这使用户在命令行不必键冗长的命令序列也避免了记忆复杂的命
令及众多选项. 设置alias rm="rm -i" (交互式删除)可以使你犯下错误时不必过度悲伤,它
能避免你不小心删除重要文件.
在脚本里,别名机制不是非常的有用. 如果把别名机制想像成C预处理器的某些功能将会非常
好,比如宏扩展,但是,不幸的是Bash不能在别名中扩展参数. [1] 而且,别名不能在“混合
型的结构”中使用,比如if/then语句, 循环, 和函数. 还有一个限制是别名不能递归地扩展.
大多数情况Almost invariably, 我们想让别名完成的工作都能被函数更高效地完成.
Example 24-1 脚本中的别名
################################Start Script#######################################
1 #!/bin/bash
2 # alias.sh
3
4 shopt -s expand_aliases
5 # 必须设置这个选项,否则脚本不会扩展别名功能.
6
7
8 # 首先, 来点有趣的.
9 alias Jesse_James='echo "\"Alias Jesse James\" was a 1959 comedy starring Bob Hope."'
10 Jesse_James
11
12 echo; echo; echo;
13
14 alias ll="ls -l"
15 # 可以使用单引号(')或双引号(")来定义一个别名.
16
17 echo "Trying aliased \"ll\":"
18 ll /usr/X11R6/bin/mk* #* 别名工作了.
19
20 echo
21
22 directory=/usr/X11R6/bin/
23 prefix=mk* # 看通配符会不会引起麻烦.
24 echo "Variables \"directory\" + \"prefix\" = $directory$prefix"
25 echo
26
27 alias lll="ls -l $directory$prefix"
28
29 echo "Trying aliased \"lll\":"
30 lll # 详细列出在/usr/X11R6/bin目录下所有以mk开头的文件.
31 # 别名能处理连接变量 -- 包括通配符 -- o.k.
32
33
34
35
36 TRUE=1
37
38 echo
39
40 if [ TRUE ]
41 then
42 alias rr="ls -l"
43 echo "Trying aliased \"rr\" within if/then statement:"
44 rr /usr/X11R6/bin/mk* #* 引起错误信息!
45 # 别名不能在混合结构中使用.
46 echo "However, previously expanded alias still recognized:"
47 ll /usr/X11R6/bin/mk*
48 fi
49
50 echo
51
52 count=0
53 while [ $count -lt 3 ]
54 do
55 alias rrr="ls -l"
56 echo "Trying aliased \"rrr\" within \"while\" loop:"
57 rrr /usr/X11R6/bin/mk* #* 在这儿,别名也不会扩展.
58 # alias.sh: line 57: rrr: command not found
59 let count+=1
60 done
61
62 echo; echo
63
64 alias xyz='cat $0' # 脚本打印自身内容.
65 # 注意是单引号(强引用).
66 xyz
67 # 虽然Bash的文档它是不会工作的,但好像它是可以工作的.
68 #
69 #
70 # 然而,就像 Steve Jacobson指出,
71 #+ 参数"$0"立即扩展成了这个别名的声明.
72
73 exit 0
################################End Script#########################################
unalias 命令删除先前设置的别名.
Example 24-2 unalias: 设置和删除别名
################################Start Script#######################################
1 #!/bin/bash
2 # unalias.sh
3
4 shopt -s expand_aliases # 打开别名功能扩展.
5
6 alias llm='ls -al | more'
7 llm
8
9 echo
10
11 unalias llm # 删除别名.
12 llm
13 # 引起错误信息,因为'llm'已经不再有效了.
14
15 exit 0
################################End Script#########################################
bash$ ./unalias.sh
total 6
drwxrwxr-x 2 bozo bozo 3072 Feb 6 14:04 .
drwxr-xr-x 40 bozo bozo 2048 Feb 6 14:04 ..
-rwxr-xr-x 1 bozo bozo 199 Feb 6 14:04 unalias.sh
./unalias.sh: llm: command not found
注意事项:
[1] 但是, 别名好像能扩展位置参数.
第25章 列表结构
================
"与列表(and list)"和"或列表(or list)" 结构提供一种处理一串连续命令的方法. 它们能有
效地替代复杂的嵌套if/then语句甚至可以代替case语句.
连接命令
与列表(and list)
1 command-1 && command-2 && command-3 && ... command-n
如果每个命令都返回真值(0)将会依次执行下去. 当某个命令返回假值(非零值), 整个命
令链就会结束执行(第一个返回假的命令将会是最后一个执行的命令,后面的都不再执行).
Example 25-1 使用"与列表(and list)"来测试命令行参数
################################Start Script#######################################
1 #!/bin/bash
2 # "and list"
3
4 if [ ! -z "$1" ] && echo "Argument #1 = $1" && [ ! -z "$2" ] && echo "Argument #2 = $2"
5 then
6 echo "At least 2 arguments passed to script."
7 # 所有连接起来的命令都返回真.
8 else
9 echo "Less than 2 arguments passed to script."
10 # 整个命令列表中至少有一个命令返回假值.
11 fi
12 # 注意"if [ ! -z $1 ]" 可以工作,但它是有所假定的等价物,
13 # if [ -n $1 ] 不会工作.
14 # 但是, 加引用可以让它工作.
15 # if [ -n "$1" ] 就可以了.
16 # 小心!
17 # 最好总是引起要测试的变量.
18
19
20 # 这是使用"纯粹"的 if/then 语句完成的同等功能.
21 if [ ! -z "$1" ]
22 then
23 echo "Argument #1 = $1"
24 fi
25 if [ ! -z "$2" ]
26 then
27 echo "Argument #2 = $2"
28 echo "At least 2 arguments passed to script."
29 else
30 echo "Less than 2 arguments passed to script."
31 fi
32 # 这会更长且不如"与列表"精致.
33
34
35 exit 0
################################End Script#########################################
返回顶部