红联Linux门户
Linux帮助

高级Bash脚本编程指南

发布时间:2006-09-15 10:43:31来源:红联作者:译元
译者序

毫无疑问,UNIX/Linux最重要的软件之一就是shell,目前最流行的shell被称为Bash(Bourne Again Shell),几乎所有的Linux和绝大部分的UNIX都可以使用Bash。作为系统与用户之间的交互接口,shell几乎是你在UNIX工作平台上最亲密的朋友,因此,学好shell,是学习Linux/UNIX的的开始,并且它会始终伴随你的工作学习。

shell是如此地重要,但令人惊奇的是,介绍shell的书没有真正令人满意的。所幸的是,我看到了这本被人称为abs的书,这本书介绍了bash大量的细节和广阔的范围,我遇到的绝大部分的技术问题--无论是我忘记的或是以前没有发现的--都可以在这本书里找到答案。这本使用大量的例子详细地介绍了Bash的语法,各种技巧,调试等等的技术,以循序渐进的学习方式,让你了解Bash的所有特性,在书中还有许多练习可以引导你思考,以得到更深入的知识。无论你是新手还是老手,或是使用其他语言的程序员,我能肯定你能在此书用受益。而本书除了介绍BASH的知识之外,也有许多有用的关于Linux/UNIX的知识和其他shell的介绍。

在看到本书的英文版后,我决定把它翻译出来,在Linuxsir论坛上结识了译者之一杨春敏共同翻译这本书,600多页的书是本大部头的书,我们花了6个月的业余时间才翻译完了。

关于版权的问题,英文版的作者Mendel Cooper对英文版的版权做了详细的约定,请参考:Appendix Q. Copyright。中文版版权由译者杨春敏和黄毅共同所有,在遵守英文版版权相应条款的条件下,欢迎在保留本书译者名字和版权说明以非盈利的方式自由发布此中文版,以盈利目的的所有行为必须联系英文作者和两位中文译者以获得许可。

本书得以成稿,我(黄毅)要多谢我的女朋友,本该给予她的时间我用来了翻译,多谢你的理解,你是一个很棒的女朋友!

译者 杨春敏 黄毅
2006.5.15

Advanced Bash-Scripting Guide
<<高级Bash脚本编程指南>>
一本深入学习shell脚本艺术的书籍

Version 3.7.2
2005/11/16

作者:Mendel Cooper

mail:thegrendel@theriver.com

这本书假定你没有任何脚本或一般程序的编程知识,但是如果你有相关的知识,那么你将很容易
达到中高级的水平...all the while sneaking in little snippets of UNIX? wisdom and
lore(这句不知道怎么译).你可以把本书作为教材,自学手册,或者你获得shell脚本技术的文档.
书中的练习和例子脚本中的注释将会与读者有更好的互动,但是最关键的前提是:
想真正学习脚本编程的唯一途径就是编写脚本.

这本书也可作为教材来讲解一般的编程概念.

下载本书最新版本,http://personal.riverusers.com/~thegrendel/abs-guide-3.7.tar.bz2,
这是一个以tar和bzip2进行打包的,并且是以HTML来发行的.当然,你也可以获得本书的pdf版本
http://www.tldp.org/LDP/abs/abs-guide.pdf.可以在
http://personal.riverusers.com/~thegrendel/Change.log中查看修订历史.

译者:杨春敏,黄毅
mail:chunmin.yang@gmail.com

一直想好好学习一下bash,可惜网上的资料都杂乱不堪,我还是喜欢通过一本书系统的学习.这本
书来得正是时候.本书的作者真是非常的严谨,从例子里的改进人名单就能看出来.可惜我水平真
的是非常有限,好多地方估计译得都有问题.希望阅读的朋友们多多提些修改建议.我会尽我的最
大努力去修正它.

目录
++++
第一部分. 热身

1. 为什么使用shell编程
2. 带着一个Sha-Bang出发(Sha-Bang指的是#!)

2.1. 调用一个脚本
2.2. 初步的练习

第二部分. 基本

3. 特殊字符
4. 变量和参数的介绍

4.1. 变量替换
4.2. 变量赋值
4.3. Bash变量是不分类型的
4.4. 特殊的变量类型

5. 引用(翻译的可能有问题,特指引号)

5.1. 引用变量
5.2. 转义(\)

6. 退出和退出状态
7. Tests

7.1. Test结构
7.2. 文件测试操作
7.3. 其他比较操作
7.4. 嵌套的if/then条件test
7.5. 检查你的test知识

8. 操作符和相关的主题

8.1. 操作符
8.2. 数字常量

第三部分. 超越基本

9. 变量重游

9.1. 内部变量
9.2. 操作字符串
9.3. 参数替换
9.4. 指定类型的变量:declare或者typeset
9.5. 变量的间接引用
9.6. $RANDOM: 产生随机整数
9.7. 双圆括号结构

10. 循环和分支

10.1. 循环
10.2. 嵌套循环
10.3. 循环控制
10.4. 测试与分支(case和select结构)

11. 内部命令与内建

11.1. 作业控制命令

12. 外部过滤器,程序和命令

12.1. 基本命令
12.2. 复杂命令
12.3. 时间/日期 命令
12.4. 文本处理命令
12.5. 文件与归档命令
12.6. 通讯命令
12.7. 终端控制命令
12.8. 数学计算命令
12.9. 混杂命令

13. 系统与管理命令

13.1. 分析一个系统脚本

14. 命令替换
15. 算术扩展
16. I/O 重定向

16.1. 使用exec
16.2. 代码块的重定向
16.3. 应用

17. Here Documents

17.1. Here Strings

18. 休息时间

Part 4. 高级

19. 正则表达式

19.1. 一个简要的正则表达式介绍
19.2. 通配

20. 子shell(Subshells)
21. 受限shell(Restricted Shells)
22. 进程替换
23. 函数

23.1. 复杂函数和函数复杂性
23.2. 局部变量
23.3. 不使用局部变量的递归

24. 别名(Aliases)
25. 列表结构
26. 数组
27. /dev 和 /proc

27.1. /dev
27.2. /proc

28. 关于Zeros和Nulls
29. 调试
30. 选项
31. Gotchas
32. 脚本编程风格

32.1. 非官方的Shell脚本风格

33. 杂项

33.1. 交互式和非交互式的shells和脚本
33.2. Shell 包装
33.3. 测试和比较: 另一种方法
33.4. 递归
33.5. 彩色脚本
33.6. 优化
33.7. 各种小技巧
33.8. 安全话题

33.8.1. 被感染的脚本
33.8.2. 隐藏Shell脚本源码

33.9. 移植话题
33.10. 在Windows下进行Shell编程

34. Bash, 版本 2 和 3

34.1. Bash, 版本2
34.2. Bash, 版本3

35. 后记

35.1. 作者后记
35.2. 关于作者
35.3. 哪里可以取得帮助?
35.4. 制作这本书的工具

35.4.1. 硬件
35.4.2. 软件和排版软件

35.5. Credits

Bibliography
A. Contributed Scripts
B. Reference Cards
C. A Sed and Awk Micro-Primer

C.1. Sed
C.2. Awk

D. Exit Codes With Special Meanings
E. A Detailed Introduction to I/O and I/O Redirection
F. Standard Command-Line Options
G. Important Files
H. Important System Directories
I. Localization
J. History Commands
K. A Sample .bashrc File
L. Converting DOS Batch Files to Shell Scripts
M. Exercises

M.1. Analyzing Scripts
M.2. Writing Scripts

N. Revision History
O. Mirror Sites
P. To Do List
Q. Copyright

表格清单:

11-1. 作业标识符
30-1. Bash 选项
33-1. 转义序列中数值和彩色的对应
B-1. Special Shell Variables
B-2. TEST Operators: Binary Comparison
B-3. TEST Operators: Files
B-4. Parameter Substitution and Expansion
B-5. String Operations
B-6. Miscellaneous Constructs
C-1. Basic sed operators
C-2. Examples of sed operators
D-1. "Reserved" Exit Codes
L-1. Batch file keywords / variables / operators, and their shell equivalents
L-2. DOS commands and their UNIX equivalents
N-1. Revision History

例子清单:

2-1. 清除:清除/var/log下的log文件
2-2. 清除:一个改良的清除脚本
2-3. cleanup:一个增强的和广义的删除logfile的脚本
3-1. 代码块和I/O重定向
3-2. 将一个代码块的结果保存到文件
3-3. 在后台运行一个循环
3-4. 备份最后一天所有修改的文件.
4-1. 变量赋值和替换
4-2. 一般的变量赋值
4-3. 变量赋值,一般的和比较特殊的
4-4. 整型还是string?
4-5. 位置参数
4-6. wh,whois节点名字查询
4-7. 使用shift
5-1. echo一些诡异的变量
5-2. 转义符
6-1. exit/exit状态
6-2. 否定一个条件使用!
7-1. 什么情况下为真?
7-2. 几个等效命令test,/usr/bin/test,[],和/usr/bin/[
7-3. 算数测试使用(( ))
7-4. test死的链接文件
7-5. 数字和字符串比较
7-6. 测试字符串是否为null
7-7. zmore
8-1. 最大公约数
8-2. 使用算术操作符
8-3. 使用&&和||进行混合状态的test
8-4. 数字常量的处理
9-1. $IFS和空白
9-2. 时间输入
9-3. 再来一个时间输入
9-4. Timed read
9-5. 我是root?
9-6. arglist:通过$*和$@列出所有的参数
9-7. 不一致的$*和$@行为
9-8. 当$IFS为空时的$*和$@
9-9. 下划线变量
9-10. 在一个文本文件的段间插入空行
9-11. 利用修改文件名,来转换图片格式
9-12. 模仿getopt命令
9-13. 提取字符串的一种可选的方法
9-14. 使用参数替换和error messages
9-15. 参数替换和"usage"messages
9-16. 变量长度
9-17. 参数替换中的模式匹配
9-18. 重命名文件扩展名
9-19. 使用模式匹配来分析比较特殊的字符串
9-20. 对字符串的前缀或后缀使用匹配模式
9-21. 使用declare来指定变量的类型
9-22. 间接引用
9-23. 传递一个间接引用给awk
9-24. 产生随机数
9-25. 从一副扑克牌中取出一张随机的牌
9-26. 两个指定值之间的随机数
9-27. 使用随机数来摇一个骰子
9-28. 重新分配随机数种子
9-29. 使用awk产生伪随机数
9-30. C风格的变量处理
10-1. 循环的一个简单例子
10-2. 每个[list]元素带两个参数的for循环
10-3. 文件信息:对包含在变量中的文件列表进行操作
10-4. 在for循环中操作文件
10-5. 在for循环中省略[list]
10-6. 使用命令替换来产生for循环的[list]
10-7. 对于二进制文件的一个grep替换
10-8. 列出系统上的所有用户
10-9. 在目录的所有文件中查找源字串
10-10. 列出目录中所有的符号连接文件
10-11. 将目录中的符号连接文件名保存到一个文件中
10-12. 一个C风格的for循环
10-13. 在batch mode中使用efax
10-14. 简单的while循环
10-15. 另一个while循环
10-16. 多条件的while循环
10-17. C风格的while循环
10-18. until循环
10-19. 嵌套循环
10-20. break和continue命令在循环中的效果
10-21. 多层循环的退出
10-22. 多层循环的continue
10-23. 在实际的任务中使用"continue N"
10-24. 使用case
10-25. 使用case来创建菜单
10-26. 使用命令替换来产生case变量
10-27. 简单字符串匹配
10-28. 检查是否是字母输入
10-29. 用select来创建菜单
10-30. 用函数中select结构来创建菜单
11-1. 一个fork出多个自己实例的脚本
11-2. printf
11-3. 使用read,变量分配
11-4. 当使用一个不带变量参数的read命令时,将会发生什么?
11-5. read命令的多行输入
11-6. 检测方向键
11-7. 通过文件重定向来使用read
11-8. 管道输出到read中的问题
11-9. 修改当前的工作目录
11-10. 用"let"命令来作算术操作.
11-11. 显示eval命令的效果
11-12. 强制登出(log-off)
11-13. 另一个"rot13"的版本
11-14. 在Perl脚本中使用eval命令来强制变量替换
11-15. 使用set来改变脚本的位置参数
11-16. 重新分配位置参数
11-17. Unset一个变量
11-18. 使用export命令传递一个变量到一个内嵌awk的脚本中
11-19. 使用getopts命令来读取传递给脚本的选项/参数.
11-20. "Including"一个数据文件
11-21. 一个没什么用的,source自身的脚本
11-22. exec的效果
11-23. 一个exec自身的脚本
11-24. 在继续处理之前,等待一个进程的结束
11-25. 一个结束自身的脚本.
12-1. 使用ls命令来创建一个烧录CDR的内容列表
12-2. Hello or Good-bye
12-3. 删除当前目录下文件名中包含一些特殊字符(包括空白)的文件..
12-4. 通过文件的 inode 号来删除文件
12-5. Logfile: 使用 xargs 来监控系统 log
12-6. 把当前目录下的文件拷贝到另一个文件中
12-7. 通过名字Kill进程
12-8. 使用xargs分析单词出现的频率
12-9. 使用 expr
12-10. 使用 date 命令
12-11. 分析单词出现的频率
12-12. 那个文件是脚本?
12-13. 产生10进制随机数
12-14. 使用 tail 命令来监控系统log
12-15. 在一个脚本中模仿 "grep" 的行为
12-16. 在1913年的韦氏词典中查找定义
12-17. 检查列表中单词的正确性
12-18. 转换大写: 把一个文件的内容全部转换为大写.
12-19. 转换小写: 将当前目录下的所有文全部转换为小写.
12-20. Du: DOS 到 UNIX 文本文件的转换.
12-21. rot13: rot13, 弱智加密.
12-22. Generating "Crypto-Quote" Puzzles
12-23. 格式化文件列表.
12-24. 使用 column 来格式化目录列表
12-25. nl: 一个自己计算行号的脚本.
12-26. manview: 查看格式化的man页
12-27. 使用 cpio 来拷贝一个目录树
12-28. 解包一个 rpm 归档文件
12-29. 从 C 文件中去掉注释
12-30. Exploring /usr/X11R6/bin
12-31. 一个"改进过"的 strings 命令
12-32. 在一个脚本中使用 cmp 来比较2个文件.
12-33. basename 和 dirname
12-34. 检查文件完整性
12-35. Uudecod 编码后的文件
12-36. 查找滥用的连接来报告垃圾邮件发送者
12-37. 分析一个垃圾邮件域
12-38. 获得一份股票报价
12-39. 更新 Fedora Core 4
12-40. 使用 ssh
12-41. 一个可以mail自己的脚本
12-42. 按月偿还贷款
12-43. 数制转换
12-44. 使用 "here document" 来调用 bc
12-45. 计算圆周率
12-46. 将10进制数字转换为16进制数字
12-47. 因子分解
12-48. 计算直角三角形的斜边
12-49. 使用 seq 来产生循环参数
12-50. 字母统计
12-51. 使用getopt来分析命令行选项
12-52. 一个拷贝自身的脚本
12-53. 练习dd
12-54. 记录按键
12-55. 安全的删除一个文件
12-56. 文件名产生器
12-57. 将米转换为英里
12-58. 使用 m4
13-1. 设置一个新密码
13-2. 设置一个擦除字符
13-3. 关掉终端对于密码的echo
13-4. 按键检测
13-5. Checking a remote server for identd
13-6. pidof 帮助杀掉一个进程
13-7. 检查一个CD镜像
13-8. 在一个文件中创建文件系统
13-9. 添加一个新的硬盘驱动器
13-10. 使用umask来将输出文件隐藏起来
13-11. killall, 来自于 /etc/rc.d/init.d
14-1. 愚蠢的脚本策略
14-2. 从循环的输出中产生一个变量
14-3. 找anagram(回文构词法, 可以将一个有意义的单词, 变换为1个或多个有意义的单词, 但是还是原来的子母集合)
16-1. 使用exec重定向标准输入
16-2. 使用exec来重定向stdout
16-3. 使用exec在同一脚本中重定向stdin和stdout
16-4. 避免子shell
16-5. while循环的重定向
16-6. 另一种while循环的重定向
16-7. until循环重定向
16-8. for循环重定向
16-9. for循环重定向 loop (将标准输入和标准输出都重定向了)
16-10. 重定向if/then测试结构
16-11. 用于上面例子的"names.data"数据文件
16-12. 记录日志事件
17-1. 广播: 发送消息给每个登录上的用户
17-2. 仿造文件: 创建一个两行的仿造文件
17-3. 使用cat的多行消息
17-4. 带有抑制tab功能的多行消息
17-5. 使用参数替换的here document
17-6. 上传一个文件对到"Sunsite"的incoming目录
17-7. 关闭参数替换
17-8. 一个产生另外一个脚本的脚本
17-9. Here documents与函数
17-10. "匿名" here Document
17-11. 注释掉一段代码块
17-12. 一个自文档化(self-documenting)的脚本
17-13. 在一个文件的开头添加文本
20-1. 子shell中的变量作用域
20-2. 列出用户的配置文件
20-3. 在子shell里进行串行处理
21-1. 在受限的情况下运行脚本
23-1. 简单函数
23-2. 带着参数的函数
23-3. 函数和被传给脚本的命令行参数
23-4. 传递间接引用给函数
23-5. 解除传递给函数的参数引用
23-6. 再次尝试解除传递给函数的参数引用
23-7. 两个数中的最大者
23-8. 把数字转化成罗马数字
23-9. 测试函数最大的返回值
23-10. 比较两个大整数
23-11. 用户名的真实名
23-12. 局部变量的可见范围
23-13. 用局部变量来递归
23-14. 汉诺塔
24-1. 脚本中的别名
24-2. unalias: 设置和删除别名
25-1. 使用"与列表(and list)"来测试命令行参数
25-2. 用"与列表"的另一个命令行参数测试
25-3. "或列表"和"与列表"的结合使用
26-1. 简单的数组用法
26-2. 格式化一首诗
26-3. 多种数组操作
26-4. 用于数组的字符串操作符
26-5. 将脚本的内容传给数组
26-6. 一些数组专用的工具
26-7. 关于空数组和空数组元素
26-8. 初始化数组
26-9. 复制和连接数组
26-10. 关于连接数组的更多信息
26-11. 一位老朋友: 冒泡排序
26-12. 内嵌数组和间接引用
26-13. 复杂数组应用: 埃拉托色尼素数筛子
26-14. 模拟下推的堆栈
26-15. 复杂的数组应用: 列出一种怪异的数学序列
26-16. 模拟二维数组,并使它倾斜
27-1. 利用/dev/tcp 来检修故障
27-2. 搜索与一个PID相关的进程
27-3. 网络连接状态
28-1. 隐藏cookie而不再使用
28-2. 用/dev/zero创建一个交换临时文件
28-3. 创建ramdisk
29-1. 一个错误的脚本
29-2. 丢失关键字(keyword)
29-3. 另一个错误脚本
29-4. 用"assert"测试条件
29-5. 捕捉 exit
29-6. 在Control-C后清除垃圾
29-7. 跟踪变量
29-8. 运行多进程 (在多处理器的机器里)
31-1. 数字和字符串比较是不相等同的
31-2. 子SHELL缺陷
31-3. 把echo的输出用管道输送给read命令
33-1. shell 包装
33-2. 稍微复杂一些的shell包装
33-3. 写到日志文件的shell包装
33-4. 包装awk的脚本
33-5. 另一个包装awk的脚本
33-6. 把Perl嵌入Bash脚本
33-7. Bash 和 Perl 脚本联合使用
33-8. 递归调用自己本身的(无用)脚本
33-9. 递归调用自己本身的(有用)脚本
33-10. 另一个递归调用自己本身的(有用)脚本
33-11. 一个 "彩色的" 地址资料库
33-12. 画盒子
33-13. 显示彩色文本
33-14. "赛马" 游戏
33-15. 返回值技巧
33-16. 整型还是string?
33-17. 传递和返回数组
33-18. anagrams游戏
33-19. 在shell脚本中调用的窗口部件
34-1. 字符串扩展
34-2. 间接变量引用 - 新方法
34-3. 使用间接变量引用的简单数据库应用
34-4. 用数组和其他的小技巧来处理四人随机打牌

A-1. mailformat: Formatting an e-mail message
A-2. rn: A simple-minded file rename utility
A-3. blank-rename: renames filenames containing blanks
A-4. encryptedpw: Uploading to an ftp site, using a locally encrypted password
A-5. copy-cd: Copying a data CD
A-6. Collatz series
A-7. days-between: Calculate number of days between two dates
A-8. Make a "dictionary"
A-9. Soundex conversion
A-10. "Game of Life"
A-11. Data file for "Game of Life"
A-12. behead: Removing mail and news message headers
A-13. ftpget: Downloading files via ftp
A-14. password: Generating random 8-character passwords
A-15. fifo: Making daily backups, using named pipes
A-16. Generating prime numbers using the modulo operator
A-17. tree: Displaying a directory tree
A-18. string functions: C-like string functions
A-19. Directory information
A-20. Object-oriented database
A-21. Library of hash functions
A-22. Colorizing text using hash functions
A-23. Mounting USB keychain storage devices
A-24. Preserving weblogs
A-25. Protecting literal strings
A-26. Unprotecting literal strings
A-27. Spammer Identification
A-28. Spammer Hunt
A-29. Making wget easier to use
A-30. A "podcasting" script
A-31. Basics Reviewed
A-32. An expanded cd command
C-1. Counting Letter Occurrences
K-1. Sample .bashrc file
L-1. VIEWDATA.BAT: DOS Batch File
L-2. viewdata.sh: Shell Script Conversion of VIEWDATA.BAT
P-1. Print the server environment

第一部分 热身
++++++++++++++++
shell是一个命令解释器.是介于操作系统kernel与用户之间的一个绝缘层.准确地说,它也是一
一种强力的计算机语言.一个shell程序,被称为一个脚本,是一种很容易使用的工具,它可以通过
将系统调用,公共程序,工具,和编译过的二进制程序粘合在一起来建立应用.事实上,所有的UNIX
命令和工具再加上公共程序,对于shell脚本来说,都是可调用的.如果这些你还觉得不够,那么
shell内建命令,比如test与循环结构,也会给脚本添加强力的支持和增加灵活性.Shell脚本对于
管理系统任务和其它的重复工作的例程来说,表现的非常好,根本不需要那些华而不实的成熟
紧凑的程序语言.

第1章 为什么使用shell编程
===========================
没有程序语言是完美的.甚至没有一个唯一最好的语言,只有对于特定目的,比较适合和不适合
的程序语言.
Herbert Mayer

对于任何想适当精通一些系统管理知识的人来说,掌握shell脚本知识都是最基本的,即使这些
人可能并不打算真正的编写一些脚本.想一下Linux机器的启动过程,在这个过程中,必将运行
/etc/rc.d目录下的脚本来存储系统配置和建立服务.详细的理解这些启动脚本对于分析系统的
行为是非常重要的,并且有时候可能必须修改它.

学习如何编写shell脚本并不是一件很困难的事,因为脚本可以分为很小的块,并且相对于shell
特性的操作和选项[1]部分,只需要学习很小的一部分就可以了.语法是简单并且直观的,编写脚
本很像是在命令行上把一些相关命令和工具连接起来,并且只有很少的一部分规则需要学习.
绝大部分脚本第一次就可以正常的工作,而且即使调试一个长一些的脚本也是很直观的.

一个shell脚本是一个类似于小吃店的(quick and dirty)方法,在你使用原型设计一个复杂的
应用的时候.在工程开发的第一阶段,即使从功能中取得很有限的一个子集放到shell脚本中来
完成往往都是非常有用的.使用这种方法,程序的结果可以被测试和尝试运行,并且在处理使用
诸如C/C++,Java或者Perl语言编写的最终代码前,主要的缺陷和陷阱往往就被发现了.

Shell脚本遵循典型的UNIX哲学,就是把大的复杂的工程分成小规模的子任务,并且把这些部件
和工具组合起来.许多人认为这种办法更好一些,至少这种办法比使用那种高\大\全的语言更
美,更愉悦,更适合解决问题.比如Perl就是这种能干任何事能适合任何人的语言,但是代价就是
你需要强迫自己使用这种语言来思考解决问题的办法.

什么时候不使用Shell脚本

资源密集型的任务,尤其在需要考虑效率时(比如,排序,hash等等)

需要处理大任务的数学操作,尤其是浮点运算,精确运算,或者复杂的算术运算
(这种情况一般使用C++或FORTRAN来处理)

有跨平台移植需求(一般使用C或Java)

复杂的应用,在必须使用结构化编程的时候(需要变量的类型检查,函数原型,等等)

对于影响系统全局性的关键任务应用。

对于安全有很高要求的任务,比如你需要一个健壮的系统来防止入侵,破解,恶意破坏等等.

项目由连串的依赖的各个部分组成。

需要大规模的文件操作

需要多维数组的支持

需要数据结构的支持,比如链表或数等数据结构

需要产生或操作图形化界面GUI

需要直接操作系统硬件

需要I/O或socket接口

需要使用库或者遗留下来的老代码的接口

私人的,闭源的应用(shell脚本把代码就放在文本文件中,全世界都能看到)

如果你的应用符合上边的任意一条,那么就考虑一下更强大的语言吧--或许是Perl,Tcl,Python,
Ruby -- 或者是更高层次的编译语言比如C/C++,或者是Java.即使如此,你会发现,使用shell
来原型开发你的应用,在开发步骤中也是非常有用的.

我们将开始使用Bash,Bash是"Bourne-Again shell"首字母的缩写,也是Stephen Bourne的经典
的Bourne shell的一个双关语,(译者:说实话,我一直搞不清这个双关语是什么意思,为什么叫
"Bourn-Again shell",这其中应该有个什么典故吧,哪位好心,告诉我一下^^).Bash已经成为了
所有UNIX中shell脚本的事实上的标准了.同时这本书也覆盖了绝大部分的其他一些shell的原
则,比如Korn Shell,Bash从ksh中继承了一部分特性,[2]C Shell和它的变种.(注意:C Shell
编程是不被推荐的,因为一些特定的内在问题,Tom Christiansen在1993年10月指出了这个问题
请在http://www.etext.org/Quartz/computer/unix/csh.harmful.gz中查看具体内容.)

接下来是脚本的一些说明.在展示shell不同的特征之前,它可以减轻一些阅读书中例子
的负担.本书中的例子脚本,都在尽可能的范围内进行了测试,并且其中的一些将使用在真
实的生活中.读者可以运行这些例子脚本(使用scriptname.sh或者scriptname.bash的形式),
[3]并给这些脚本执行权限(chmod u+rx scriptname),然后执行它们,看看发生了什么.如果存
档的脚本不可用,那么就从本书的HTML,pdf或者text的发行版本中把它们拷贝粘贴出来.考虑到
这些脚本中的内容在我们还没解释它之前就被列在这里,可能会影响读者的理解,这就需要读者
暂时忽略这些内容.

除非特别注明,本书作者编写了本书中的绝大部分例子脚本.

注意事项:
[1] 这些在builtins章节被引用,这些是shell的内部特征.
[2] ksh88的许多特性,甚至是一些ksh93的特性都被合并到Bash中了.
[3] 根据惯例,用户编写的Bourne shell脚本应该在脚本的名字后边加上.sh扩展名.
一些系统脚本,比如那些在/etc/rc.d中的脚本,则不遵循这种命名习惯.



第2章 带着一个Sha-Bang出发(Sha-Bang指的是#!)
==============================================
在一个最简单的例子中,一个shell脚本其实就是将一堆系统命令列在一个文件中.它的最基本的
用处就是,在你每次输入这些特定顺序的命令时可以少敲一些字.

Example 2-1 清除:清除/var/log下的log文件
################################Start Script#######################################
1 # Cleanup
2 # 当然要使用root身份来运行这个脚本
3
4 cd /var/log
5 cat /dev/null > messages
6 cat /dev/null > wtmp
7 echo "Logs cleaned up."
################################End Script#########################################
这根本就没什么稀奇的, 只不过是命令的堆积, 来让从console或者xterm中一个一个的输入命
令更方便一些.好处就是把所有命令都放在一个脚本中,不用每次都敲它们.这样的话,对于特定
的应用来说,这个脚本就很容易被修改或定制.

Example 2-2 清除:一个改良的清除脚本
################################Start Script#######################################
1 #!/bin/bash
2 # 一个Bash脚本的正确的开头部分.
3
4 # Cleanup, 版本 2
5
6 # 当然要使用root身份来运行.
7 # 在此处插入代码,来打印错误消息,并且在不是root身份的时候退出.
8
9 LOG_DIR=/var/log
10 # 如果使用变量,当然比把代码写死的好.
11 cd $LOG_DIR
12
13 cat /dev/null > messages
14 cat /dev/null > wtmp
15
16
17 echo "Logs cleaned up."
18
19 exit # 这个命令是一种正确并且合适的退出脚本的方法.
################################End Script#########################################

现在,让我们看一下一个真正意义的脚本.而且我们可以走得更远...
Example 2-3. cleanup:一个增强的和广义的删除logfile的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # 清除, 版本 3
3
4 # Warning:
5 # -------
6 # 这个脚本有好多特征,这些特征是在后边章节进行解释的,大概是进行到本书的一半的
7 # 时候,
8 # 你就会觉得它没有什么神秘的了.
9 #
10
11
12
13 LOG_DIR=/var/log
14 ROOT_UID=0 # $UID为0的时候,用户才具有根用户的权限
15 LINES=50 # 默认的保存行数
16 E_XCD=66 # 不能修改目录?
17 E_NOTROOT=67 # 非根用户将以error退出
18
19
20 # 当然要使用根用户来运行
21 if [ "$UID" -ne "$ROOT_UID" ]
22 then
23 echo "Must be root to run this script."
24 exit $E_NOTROOT
25 fi
26
27 if [ -n "$1" ]
28 # 测试是否有命令行参数(非空).
29 then
30 lines=$1
31 else
32 lines=$LINES # 默认,如果不在命令行中指定
33 fi
34
35
36 # Stephane Chazelas 建议使用下边
37 #+ 的更好方法来检测命令行参数.
38 #+ 但对于这章来说还是有点超前.
39 #
40 # E_WRONGARGS=65 # 非数值参数(错误的参数格式)
41 #
42 # case "$1" in
43 # "" ) lines=50;;
44 # *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;;
45 # * ) lines=$1;;
46 # esac
47 #
48 #* 直到"Loops"的章节才会对上边的内容进行详细的描述.
49
50
51 cd $LOG_DIR
52
53 if [ `pwd` != "$LOG_DIR" ] # 或者 if[ "$PWD" != "$LOG_DIR" ]
54 # 不在 /var/log中?
55 then
56 echo "Can't change to $LOG_DIR."
57 exit $E_XCD
58 fi # 在处理log file之前,再确认一遍当前目录是否正确.
59
60 # 更有效率的做法是
61 #
62 # cd /var/log || {
63 # echo "Cannot change to necessary directory." >&2
64 # exit $E_XCD;
65 # }
66
67
68
69
70 tail -$lines messages > mesg.temp # 保存log file消息的最后部分.
71 mv mesg.temp messages # 变为新的log目录.
72
73
74 # cat /dev/null > messages
75 #* 不再需要了,使用上边的方法更安全.
76
77 cat /dev/null > wtmp # ': > wtmp' 和 '> wtmp'具有相同的作用
78 echo "Logs cleaned up."
79
80 exit 0
81 # 退出之前返回0,返回0表示成功.
82 #
################################End Script#########################################

因为你可能希望将系统log全部消灭,这个版本留下了log消息最后的部分.你将不断地找到新
的方法来完善这个脚本,并提高效率.

要注意,在每个脚本的开头都使用"#!",这意味着告诉你的系统这个文件的执行需要指定一个解
释器.#!实际上是一个2字节[1]的魔法数字,这是指定一个文件类型的特殊标记, 换句话说, 在
这种情况下,指的就是一个可执行的脚本(键入man magic来获得关于这个迷人话题的更多详细
信息).在#!之后接着是一个路径名.这个路径名指定了一个解释脚本中命令的程序,这个程序可
以是shell,程序语言或者是任意一个通用程序.这个指定的程序从头开始解释并且执行脚本中
的命令(从#!行下边的一行开始),忽略注释.[2]
如:
1 #!/bin/sh
2 #!/bin/bash
3 #!/usr/bin/perl
4 #!/usr/bin/tcl
5 #!/bin/sed -f
6 #!/usr/awk -f

上边每一个脚本头的行都指定了一个不同的命令解释器,如果是/bin/sh,那么就是默认shell
(在Linux系统中默认是Bash).[3]使用#!/bin/sh,在大多数商业发行的UNIX上,默认是Bourne
shell,这将让你的脚本可以正常的运行在非Linux机器上,虽然这将会牺牲Bash一些独特的特征.
脚本将与POSIX[4] 的sh标准相一致.

注意: #! 后边给出的路径名必须是正确的,否则将会出现一个错误消息,通常是
"Command not found",这将是你运行这个脚本时所得到的唯一结果.

当然"#!"也可以被忽略,不过这样你的脚本文件就只能是一些命令的集合,不能够使用shell内建
的指令了,如果不能使用变量的话,当然这也就失去了脚本编程的意义了.

注意:这个例子鼓励你使用模块化的方式来编写脚本,平时也要注意收集一些零碎的代码,
这些零碎的代码可能用在你将来编写的脚本中.这样你就可以通过这些代码片段来构
造一个较大的工程用例. 以下边脚本作为序,来测试脚本被调用的参数是否正确.
################################Start Script#######################################
1 E_WRONG_ARGS=65
2 script_parameters="-a -h -m -z"
3 # -a = all, -h = help, 等等.
4
5 if [ $# -ne $Number_of_expected_args ]
6 then
7 echo "Usage: `basename $0` $script_parameters"
8 # `basename $0`是这个脚本的文件名
9 exit $E_WRONG_ARGS
10 fi
################################End Script#########################################
大多数情况下,你需要编写一个脚本来执行一个特定的任务,在本章中第一个脚本就是一个这样
的例子, 然后你会修改它来完成一个不同的,但比较相似的任务.用变量来代替写死的常量,就是
一个好方法,将重复的代码放到一个函数中,也是一种好习惯.


2.1 调用一个脚本
----------------
编写完脚本之后,你可以使用sh scriptname,[5]或者bash scriptname来调用它.
(不推荐使用sh 更方便的方法是让脚本本身就具有可执行权限,通过chmod命令可以修改.

比如:
chmod 555 scriptname (允许任何人都具有 可读和执行权限) [6]
或:
chmod +rx scriptname (允许任何人都具有 可读和执行权限)
chmod u+rx scriptname (只给脚本的所有者 可读和执行权限)

既然脚本已经具有了可执行权限,现在你可以使用./scriptname.[7]来测试它了.如果这个脚本
以一个"#!"行开头,那么脚本将会调用合适的命令解释器来运行.

最后一步,在脚本被测试和debug之后,你可能想把它移动到/usr/local/bin(当然是以root身份)
,来让你的脚本对所有用户都有用.这样用户就可以直接敲脚本名字来运行了.

注意事项:
[1] 那些具有UNIX味道的脚本(基于4.2BSD)需要一个4字节的魔法数字,在#!后边需要一个
空格#! /bin/sh.
[2] 脚本中的#!行的最重要的任务就是命令解释器(sh或者bash).因为这行是以#开始的,
当命令解释器执行这个脚本的时候,会把它作为一个注释行.当然,在这之前,这行语句
已经完成了它的任务,就是调用命令解释器.

如果在脚本的里边还有一个#!行,那么bash将把它认为是一个一般的注释行.
1 #!/bin/bash
2
3 echo "Part 1 of script."
4 a=1
5
6 #!/bin/bash
7 # 这将不会开始一个新脚本.
8
9 echo "Part 2 of script."
10 echo $a # Value of $a stays at 1.
[3] 这里可以玩一些小技巧.
1 #!/bin/rm
2 # 自删除脚本.
3
4 # 当你运行这个脚本时,基本上什么都不会发生...除非这个文件消失不见.
5
6 WHATEVER=65
7
8 echo "This line will never print (betcha!)."
9
10 exit $WHATEVER # 没关系,脚本是不会在这退出的.
当然,你还可以试试在一个README文件的开头加上#!/bin/more,并让它具有执行权限.
结果将是文档自动列出自己的内容.(一个使用cat命令的here document可能是一个
更好的选则,--见Example 17-3).
[4] 可移植的操作系统接口,标准化类UNIX操作系统的一种尝试.POSIX规范可以在
http://www.opengroup.org/onlinepubs/007904975/toc.htm中查阅.
[5] 小心:使用sh scriptname来调用脚本的时候将会关闭一些Bash特定的扩展,脚本可能
因此而调用失败.
[6] 脚本需要读和执行权限,因为shell需要读这个脚本.
[7] 为什么不直接使用scriptname来调用脚本?如果你当前的目录下($PWD)正好有你想要
执行的脚本,为什么它运行不了呢?失败的原因是,出于安全考虑,当前目录并没有被
加在用户的$PATH变量中.因此,在当前目录下调用脚本必须使用./scriptname这种
形式.


2.2 初步的练习
--------------
1. 系统管理员经常会为了自动化一些常用的任务而编写脚本.举出几个这种有用的脚本的实例.
2. 编写一个脚本,显示时间和日期,列出所有的登录用户,显示系统的更新时间.然后这个脚本
将会把这些内容保存到一个log file中.


第二部分 基本
++++++++++++++++

第3章 特殊字符
================

# 注释,行首以#开头为注释(#!是个例外).

1 # This line is a comment.

注释也可以存在于本行命令的后边.

1 echo "A comment will follow." # 注释在这里
2 # ^ 注意#前边的空白

注释也可以在本行空白的后边.

1 # A tab precedes this comment.

注意:命令是不能跟在同一行上注释的后边的,没有办法,在同一行上,注释的后边想
要再使用命令,只能另起一行.
当然,在echo命令中被转义的#是不能作为注释的.
同样的,#也可以出现在特定的参数替换结构中或者是数字常量表达式中.

1 echo "The # here does not begin a comment."
2 echo 'The # here does not begin a comment.'
3 echo The \# here does not begin a comment.
4 echo The # 这里开始一个注释
5
6 echo ${PATH#*:} # 参数替换,不是一个注释
7 echo $(( 2#101011 )) # 数制转换,不是一个注释
8
9 # Thanks, S.C.

标准的引用和转义字符("'\)可以用来转义#

; 命令分隔符,可以用来在一行中来写多个命令.

1 echo hello; echo there
2
3
4 if [ -x "$filename" ]; then # 注意:"if"和"then"需要分隔
5 # 为啥?
6 echo "File $filename exists."; cp $filename $filename.bak
7 else
8 echo "File $filename not found."; touch $filename
9 fi; echo "File test complete."

有时候需要转义

;; 终止"case"选项.

1 case "$variable" in
2 abc) echo "\$variable = abc" ;;
3 xyz) echo "\$variable = xyz" ;;
4 esac

. .命令等价于source命令(见Example 11-20).这是一个bash的内建命令.

. .作为文件名的一部分.如果作为文件名的前缀的话,那么这个文件将成为隐藏文件.
将不被ls命令列出.

bash$ touch .hidden-file
bash$ ls -l
total 10
-rw-r--r-- 1 bozo 4034 Jul 18 22:04 data1.addressbook
-rw-r--r-- 1 bozo 4602 May 25 13:58 data1.addressbook.bak
-rw-r--r-- 1 bozo 877 Dec 17 2000 employment.addressbook


bash$ ls -al
total 14
drwxrwxr-x 2 bozo bozo 1024 Aug 29 20:54 ./
drwx------ 52 bozo bozo 3072 Aug 29 20:51 ../
-rw-r--r-- 1 bozo bozo 4034 Jul 18 22:04 data1.addressbook
-rw-r--r-- 1 bozo bozo 4602 May 25 13:58 data1.addressbook.bak
-rw-r--r-- 1 bozo bozo 877 Dec 17 2000 employment.addressbook
-rw-rw-r-- 1 bozo bozo 0 Aug 29 20:54 .hidden-file

.命令如果作为目录名的一部分的话,那么.表达的是当前目录.".."表示上一级目录.

bash$ pwd
/home/bozo/projects

bash$ cd .
bash$ pwd
/home/bozo/projects

bash$ cd ..
bash$ pwd
/home/bozo/

.命令经常作为一个文件移动命令的目的地.

bash$ cp /home/bozo/current_work/junk/* .

. .字符匹配,这是作为正则表达是的一部分,用来匹配任何的单个字符.

" 部分引用."STRING"阻止了一部分特殊字符,具体见第5章.

' 全引用. 'STRING' 阻止了全部特殊字符,具体见第5章.

, 逗号链接了一系列的算术操作,虽然里边所有的内容都被运行了,但只有最后一项被
返回.

如:
1 let "t2 = ((a = 9, 15 / 3))" # Set "a = 9" and "t2 = 15 / 3"

\ 转义字符,如\X等价于"X"或'X',具体见第5章.

/ 文件名路径分隔符.或用来做除法操作.

` 后置引用,命令替换,具体见第14章

: 空命令,等价于"NOP"(no op,一个什么也不干的命令).也可以被认为与shell的内建命令(true)作用相同.":"命令是一
个bash的内建命令,它的返回值为0,就是shell返回的true.

如:
1 :
2 echo $? # 0

死循环,如:

1 while :
2 do
3 operation-1
4 operation-2
5 ...
6 operation-n
7 done
8
9 # 与下边相同:
10 # while true
11 # do
12 # ...
13 # done

在if/then中的占位符,如:
1 if condition
2 then : # 什么都不做,引出分支.
3 else
4 take-some-action
5 fi

在一个2元命令中提供一个占位符,具体见Example 8-2,和"默认参数".如:
1 : ${username=`whoami`}
2 # ${username=`whoami`} 如果没有":"的话,将给出一个错误,除非"username"是
3 # 个命令
在here document中提供一个占位符,见Example 17-10.

使用"参数替换"来评估字符串变量(见Example 9-14).如:
1 : ${HOSTNAME?} ${USER?} ${MAIL?}
2 # 如果一个或多个必要的环境变量没被设置的话,
3 #+ 就打印错误信息.

"变量扩展/子串替换"
在和 > (重定向操作符)结合使用时,把一个文件截断到0长度,没有修改它的权限.
如果文件在之前并不存在,那么就创建它.如:
1 : > data.xxx #文件"data.xxx"现在被清空了.
2
3 #与 cat /dev/null >data.xxx 的作用相同
4 #然而,这不会产生一个新的进程,因为":"是一个内建命令.
具体参见Example 12-14.

在和>>重定向操作符结合使用时,将不会对想要附加的文件产生任何影响.
如果文件不存在,将创建.
注意: 这只适用于正规文件,而不是管道,符号连接,和某些特殊文件.

也可能用来作为注释行,虽然我们不推荐这么做.使用#来注释的话,将关闭剩余行的
错误检查,所以可以在注释行中写任何东西.然而,使用:的话将不会这样.如:
1 : This is a comment thar generates an error,(if [ $x -eq 3] ).

":"还用来在/etc/passwd和$PATH变量中用来做分隔符.
bash$ echo $PATH
/usr/local/bin:/bin:/usr/X11R6/bin:/sbin:/usr/sbin:/usr/games
! 取反操作符,将反转"退出状态"结果,(见Example 6-2).也会反转test操作符的意义.比
如修改=为!=.!操作是Bash的一个关键字.

在一个不同的上下文中,!也会出现在"间接变量引用"见Example 9-22.

在另一种上下文中,!还能反转bash的"history mechanism"(见附录J 历史命令)
需要注意的是,在一个脚本中,"history mechanism"是被禁用的.

* 万能匹配字符,用于文件名匹配(这个东西有个专有名词叫file globbing),或者是正则
表达式中.注意:在正则表达式匹配中的作用和在文件名匹配中的作用是不同的.
bash$ echo *
abs-book.sgml add-drive.sh agram.sh alias.sh
* 数学乘法.
**是幂运算.
? 测试操作.在一个确定的表达式中,用?来测试结果.
(())结构可以用来做数学计算或者是写c代码,那?就是c语言的3元操作符的
一个.
在"参数替换"中,?测试一个变量是否被set了.
? 在file globbing中和在正则表达式中一样匹配任意的单个字符.

$ 变量替换
1 var1=5
2 var2=23skidoo
3
4 echo $var1 # 5
5 echo $var2 # 23skidoo
$ 在正则表达式中作为行结束符.
${} 参数替换,见9.3节.
$*,$@ 位置参数
$? 退出状态变量.$?保存一个命令/一个函数或者脚本本身的退出状态.
$$ 进程ID变量.这个$$变量保存运行脚本进程ID
() 命令组.如:
1 (a=hello;echo $a)
注意:在()中的命令列表,将作为一个子shell来运行.
在()中的变量,由于是在子shell中,所以对于脚本剩下的部分是不可用的.
如:
1 a=123
2 ( a=321; )
3
4 echo "a = $a" # a = 123
5 # 在圆括号中a变量,更像是一个局部变量.

用在数组初始化,如:
1 Array=(element1,element2,element3)

{xxx,yyy,zzz...}
大括号扩展,如:
1 cat {file1,file2,file3} > combined_file
2 # 把file1,file2,file3连接在一起,并且重定向到combined_file中.
3
4
5 cp file22.{txt,backup}
6 # 拷贝"file22.txt" 到"file22.backup"中

一个命令可能会对大括号中的以逗号分割的文件列表起作用[1]. file globbing将对
大括号中的文件名作扩展.
注意: 在大括号中,不允许有空白,除非这个空白是有意义的.
echo {file1,file2}\ :{\ A," B",' C'}
file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C
{} 代码块.又被称为内部组.事实上,这个结构创建了一个匿名的函数.但是与函数不同的
是,在其中声明的变量,对于脚本其他部分的代码来说还是可见的.如:
bash$
{
local a;
a= 123;
}
bash中的local申请的变量只能够用在函数中.

1 a=123
2 { a=321; }
3 echo "a = $a" # a = 321 (说明在代码块中对变量a所作的修改,影响了外边的变量a)
4
5 # Thanks, S.C.

下边的代码展示了在{}结构中代码的I/O重定向.

Example 3-1. 代码块和I/O重定向
################################Start Script#######################################
1 #!/bin/bash
2 # 从 /etc/fstab中读行
3
4 File=/etc/fstab
5
6 {
7 read line1
8 read line2
9 } < $File
10
11 echo "First line in $File is:"
12 echo "$line1"
13 echo
14 echo "Second line in $File is:"
15 echo "$line2"
16
17 exit 0
18
19 # 现在,你怎么分析每行的分割域
20 # 暗示: 使用 awk.
################################End Script#########################################

Example 3-2. 将一个代码块的结果保存到文件
################################Start Script#######################################
1 #!/bin/bash
2 # rpm-check.sh
3
4 # 这个脚本的目的是为了描述,列表,和确定是否可以安装一个rpm包.
5 # 在一个文件中保存输出.
6 #
7 # 这个脚本使用一个代码块来展示
8
9 SUCCESS=0
10 E_NOARGS=65
11
12 if [ -z "$1" ]
13 then
14 echo "Usage: `basename $0` rpm-file"
15 exit $E_NOARGS
16 fi
17
18 {
19 echo
20 echo "Archive Description:"
21 rpm -qpi $1 # 查询说明
22 echo
23 echo "Archive Listing:"
24 rpm -qpl $1 # 查询列表
25 echo
26 rpm -i --test $1 # 查询rpm包是否可以被安装
27 if [ "$?" -eq $SUCCESS ]
28 then
29 echo "$1 can be installed."
30 else
31 echo "$1 cannot be installed."
32 fi
33 echo
34 } > "$1.test" # 把代码块中的所有输出都重定向到文件中
35
36 echo "Results of rpm test in file $1.test"
37
38 # 查看rpm的man页来查看rpm的选项
39
40 exit 0
################################End Script#########################################
注意: 与()中的命令不同的是,{}中的代码块将不能正常地开启一个新shell.[2]

{} \; 路径名.一般都在find命令中使用.这不是一个shell内建命令.
注意: ";"用来结束find命令序列的-exec选项.

[] test.
test的表达式将在[]中.
值得注意的是[是shell内建test命令的一部分,并不是/usr/bin/test中的扩展命令
的一个连接.

[[]] test.
test表达式放在[[]]中.(shell关键字)
具体查看[[]]结构的讨论.

[] 数组元素
Array[1]=slot_1
echo ${Array[1]}

[] 字符范围
在正则表达式中使用,作为字符匹配的一个范围

(()) 数学计算的扩展
在(())结构中可以使用一些数字计算.
具体参阅((...))结构.

>&>>&>><
重定向.
scriptname >filename 重定向脚本的输出到文件中.覆盖文件原有内容.
command &>filename 重定向stdout和stderr到文件中
command >&2 重定向command的stdout到stderr
scriptname >>filename 重定向脚本的输出到文件中.添加到文件尾端,如果没有文件,
则创建这个文件.

进程替换,具体见"进程替换部分",跟命令替换极其类似.
(command)>
<(command)

<和> 可用来做字符串比较
<和> 可用在数学计算比较

<< 重定向,用在"here document"

<<< 重定向,用在"here string"

<,> ASCII比较
1 veg1=carrots
2 veg2=tomatoes
3
4 if [[ "$veg1" < "$veg2" ]]
5 then
6 echo "Although $veg1 precede $veg2 in the dictionary,"
7 echo "this implies nothing about my culinary preferences."
8 else
9 echo "What kind of dictionary are you using, anyhow?"
10 fi

\<,\> 正则表达式中的单词边界.如:
bash$grep '\' textfile

| 管道.分析前边命令的输出,并将输出作为后边命令的输入.这是一种产生命令链的
好方法.
1 echo ls -l | sh
2 # 传递"echo ls -l"的输出到shell中,
3 #+ 与一个简单的"ls -l"结果相同.
4
5
6 cat *.lst | sort | uniq
7 # 合并和排序所有的".lst"文件,然后删除所有重复的行.

管道是进程间通讯的一个典型办法,将一个进程的stdout放到另一个进程的stdin中.
标准的方法是将一个一般命令的输出,比如cat或echo,传递到一个过滤命令中(在这个
过滤命令中将处理输入),得到结果,如:
cat $filename1 | $filename2 | grep $search_word

当然输出的命令也可以传递到脚本中.如:
################################Start Script#######################################
1 #!/bin/bash
2 # uppercase.sh : 修改输出,全部转换为大写
3
4 tr 'a-z' 'A-Z'
5 # 字符范围必须被""引用起来
6 #+ 来阻止产生单字符的文件名.
7
8 exit 0
################################End Script#########################################

现在让我们输送ls -l的输出到一个脚本中.
bash$ ls -l | ./uppercase.sh
-RW-RW-R-- 1 BOZO BOZO 109 APR 7 19:49 1.TXT
-RW-RW-R-- 1 BOZO BOZO 109 APR 14 16:48 2.TXT
-RW-R--R-- 1 BOZO BOZO 725 APR 20 20:56 DATA-FILE

注意:管道中的一个进程的stdout必须被下一个进程作为stdin读入.否则,数据流会阻
塞,并且管道将产生非预期的行为.
如:
1 cat file1 file2 | ls -l | sort
2 #从"cat file1 file2"中的输出并没出现

作为子进程的运行的管道,不能够改变脚本的变量.
1 variable="initial_value"
2 echo "new_value" | read variable
3 echo "variable = $variable" #variable = initial_value
如果管道中的某个命令产生了一个异常,并中途失败,那么这个管道将过早的终止.
这种行为被叫做a broken pipe,并且这种状态下将发送一个SIGPIPE信号.

>| 强制重定向(即使设置了noclobber选项--就是-C选项).这将强制的覆盖一个现存文件.

|| 或-逻辑操作.

& 后台运行命令.一个命令后边跟一个&,将表示在后台运行.
bash$sleep 10 &
[1] 850
[1]+ Done sleep 10
在一个脚本中,命令和循环都可能运行在后台.

Example 3-3. 在后台运行一个循环
################################Start Script#######################################
1 #!/bin/bash
2 #background-loop.sh
3
4 for i in 1 2 3 4 5 6 7 8 9 10 #第一个循环
5 do
6 echo -n "$i"
7 done& #在后台运行这个循环
8 #在第2个循环之后,将在某些时候执行.
9
10 echo #这个'echo'某些时候将不会显示.
11
12 for i in 11 12 13 14 15 16 17 18 19 20 #第二个循环
13 do
14 echo -n "$i"
15 done
16
17 echo #这个'echo'某些时候将不会显示.
18
19 #--------------------------------------------------------
20
21 #期望的输出应该是
22 #1 2 3 4 5 6 7 8 9 10
23 #11 12 13 14 15 16 17 18 19 20
24
25 #然而实际的结果有可能是
26 #11 12 13 14 15 16 17 18 19 20
27 #1 2 3 4 5 6 7 8 9 10 bozo $
28 #(第2个'echo'没执行,为什么?)
29
30 #也可能是
31 #1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
32 #(第1个'echo'没执行,为什么?)
33
34 #非常少见的执行结果,也有可能是:
35 #11 12 13 1 2 3 4 5 6 7 8 9 10 14 15 16 17 18 19 20
36 #前台的循环先于后台的执行
37
38 exit 0
39
40 # Nasimuddin Ansari 建议加一句 sleep 1
41 #+ 在 6行和14行的 echo -n "$i"之后加
42 #+ 将看到一些乐趣
################################End Script#########################################
注意:在一个脚本内后台运行一个命令,有可能造成这个脚本的挂起,等待一个按键
响应.幸运的是,我们可以在Example 11-24附近,看到这个问题的解决办法.

&& 与-逻辑操作.

- 选项,前缀.在所有的命令内如果想使用选项参数的话,前边都要加上"-".

COMMAND -[Option1][Option2][...]
ls -al
sort -dfu $filename
set -- $variable

1 if [ $file1 -ot $file2 ]
2 then
3 echo "File $file1 is older than $file2."
4 fi
5
6 if [ "$a" -eq "$b" ]
7 then
8 echo "$a is equal to $b."
9 fi
10
11 if [ "$c" -eq 24 -a "$d" -eq 47 ]
12 then
13 echo "$c equals 24 and $d equals 47."
14 fi

- 用于重定向 stdin 或 stdout.

################################Start Script#######################################
1 (cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)
2 # 从一个目录移动整个目录树到另一个目录
3 # [courtesy Alan Cox <a.cox@swansea.ac.uk>, with a minor change]
4
5 # 1) cd /source/directory 源目录
6 # 2) && 与操作,如果cd命令成功了,那么就执行下边的命令
7 # 3) tar cf - . 'c'创建一个新文档,'f'后边跟'-'指定目标文件作为stdout
8 # '-'后边的'f'(file)选项,指明作为stdout的目标文件.
9 # 并且在当前目录('.')执行.
10 # 4) | 管道...
11 # 5) ( ... ) 一个子shell
12 # 6) cd /dest/directory 改变当前目录到目标目录.
13 # 7) && 与操作,同上.
14 # 8) tar xpvf - 'x'解档,'p'保证所有权和文件属性,
15 # 'v'发完整消息到stdout
16 # 'f'后边跟'-',从stdin读取数据
17 #
18 # 注意:'x' 是一个命令, 'p', 'v', 'f' 是选项.
19 # Whew!
20
21
22
23 # 更优雅的写法应该是
24 # cd source/directory
25 # tar cf - . | (cd ../dest/directory; tar xpvf -)
26 #
27 # 当然也可以这么写:
28 # cp -a /source/directory/* /dest/directory
29 # 或者:
30 # cp -a /source/directory/* /source/directory/.[^.]* /dest/directory
31 # 如果在/source/directory中有隐藏文件的话.
################################End Script#########################################

################################Start Script#######################################
1 bunzip2 linux-2.6.13.tar.bz2 | tar xvf -
2 # --未解压的tar文件-- | --然后把它传递到"tar"中--
3 # 如果 "tar" 没能够正常的处理"bunzip2",
4 # 这就需要使用管道来执行2个单独的步骤来完成它.
5 # 这个练习的目的是解档"bzipped"的kernel源文件.
################################End Script#########################################
注意:在上边这个例子中'-'不太象是bash的操作符,而更像是tar的参数.
bash$echo "whatever" | cat -
whatever

在需要一个文件名的地方,-重定向输出到stdout(如在tar和cf命令中),或者从
stdin中接受输入,而不是从一个文件中接受输入.这是在管道中作为一个过滤
器,来使用文件定位工具的一种办法.
bash$file
用法: file [-bciknvzl] [-f namefile] [-m magicfiles] file...
上边这个例子file将会出错,提示你如何使用file命令.

添加一个"-"将得到一个更有用的结果.这将使得shell等待用户输入.
bash$file -
abc
standard input: ASCII text

bash$file -
#!/bin/bash
standard input: Bourn-Again shell script tesxt executable

现在命令从stdin中接受了输入,并分析它.

"-"常用于管道后边的命令,具体参看33.7节,来看使用技巧.
使用diff命令来和另一个文件的一部分进行比较.
grep Linux file1 | diff file2 -

最后,一个真实世界的使用tar命令的例子.

Example 3-4. 备份最后一天所有修改的文件.
################################Start Script#######################################
1 #!/bin/bash
2
3 # 在一个"tarball"中(经过tar和gzip处理过的文件)
4 #+ 备份最后24小时当前目录下d所有修改的文件.
5
6 BACKUPFILE=backup-$(date +%m-%d-%Y)
7 # 在备份文件中嵌入时间.
8 # Thanks, Joshua Tschida, for the idea.
9 archive=${1:-$BACKUPFILE}
10 # 如果在命令行中没有指定备份文件的文件名,
11 #+ 那么将默认使用"backup-MM-DD-YYYY.tar.gz".
12
13 tar cvf - `find . -mtime -1 -type f -print` > $archive.tar
14 gzip $archive.tar
15 echo "Directory $PWD backed up in archive file \"$archive.tar.gz\"."
16
17
18 # Stephane Chazelas指出上边代码,
19 #+ 如果在发现太多的文件的时候,或者是如果文件
20 #+ 名包括空格的时候,将执行失败.
21
22 # Stephane Chazelas建议使用下边的两种代码之一
23 # -------------------------------------------------------------------
24 # find . -mtime -1 -type f -print0 | xargs -0 tar rvf "$archive.tar"
25 # 使用gnu版本的find.
26
27
28 # find . -mtime -1 -type f -exec tar rvf "$archive.tar" '{}' \;
29 # 对于其他风格的UNIX便于移植,但是比较慢.
30 # -------------------------------------------------------------------
31
32
33 exit 0
################################End Script#########################################

注意:以"-"开头的文件名在使用"-"作为重定向操作符的时候,可能会产生问题.
应该写一个脚本来检查这个问题,并给这个文件加上合适的前缀.如:
./-FILENAME, $PWD/-FILENAME,或$PATHNAME/-FILENAME.

如果变量的值以"-"开头,可能也会引起问题.
1 var="-n"
2 echo $var
3 #具有"echo -n"的效果了,这样什么都不会输出的.

- 之前工作的目录."cd -"将回到之前的工作目录,具体请参考"$OLDPWD"环境变量.
注意:一定要和之前讨论的重定向功能分开,但是只能依赖上下文区分.

- 算术减号.

= 算术等号,有时也用来比较字符串.
1 a=28
2 echo $a # 28

+ 算术加号,也用在正则表达式中.
+ 选项,对于特定的命令来说使用"+"来打开特定的选项,用"-"来关闭特定的选项.

% 算术取模运算.也用在正则表达式中.

~ home目录.相当于$HOME变量.~bozo是bozo的home目录,并且ls ~bozo将列出其中的
内容. ~/就是当前用户的home目录,并且ls ~/将列出其中的内容,如:
bash$ echo ~bozo
/home/bozo

bash$ echo ~
/home/bozo

bash$ echo ~/
/home/bozo/

bash$ echo ~:
/home/bozo:

bash$ echo ~nonexistent-user
~nonexistent-user

~+ 当前工作目录,相当于$PWD变量.

~- 之前的工作目录,相当于$OLDPWD内部变量.

=~ 用于正则表达式,这个操作将在正则表达式匹配部分讲解,只有version3才支持.

^ 行首,正则表达式中表示行首."^"定位到行首.


控制字符
修改终端或文本显示的行为.控制字符以CONTROL + key组合.
控制字符在脚本中不能正常使用.
Ctl-B 光标后退,这应该依赖于bash输入的风格,默认是emacs风格的.
Ctl-C Break,终止前台工作.
Ctl-D 从当前shell登出(和exit很像)
"EOF"(文件结束符).这也能从stdin中终止输入.
在console或者在xterm window中输入的时候,Ctl-D将删除光标下字符.
当没有字符时,Ctrl-D将退出当前会话.在xterm window也有关闭窗口
的效果.
Ctl-G beep.在一些老的终端,将响铃.
Ctl-H backspace,删除光标前边的字符.如:
1 #!/bin/bash
2 # 在一个变量中插入Ctl-H
3
4 a="^H^H" # 两个 Ctl-H (backspaces).
5 echo "abcdef" # abcdef
6 echo -n "abcdef$a " # abcd f
7 # 注意结尾的空格 ^ ^ 两个 twice.
8 echo -n "abcdef$a" # abcdef
9 # 结尾没有空格 没有 backspace 的效果了(why?).
10 # 结果并不像期望的那样
11 echo; echo
Ctl-I 就是tab键.
Ctl-J 新行.
Ctl-K 垂直tab.(垂直tab?新颖,没听过)
作用就是删除光标到行尾的字符.
Ctl-L clear,清屏.
Ctl-M 回车
################################Start Script#######################################
1 #!/bin/bash
2 # Thank you, Lee Maschmeyer, for this example.
3
4 read -n 1 -s -p $'Control-M leaves cursor at beginning of this line. Press Enter. \x0d'
5 #当然,'0d'就是二进制的回车.
6 echo >&2 # '-s'参数使得任何输入都不将回显出来
7 #+ 所以,明确的重起一行是必要的.
8
9 read -n 1 -s -p $'Control-J leaves cursor on next line. \x0a'
10 echo >&2 # Control-J 是换行.
11
12 ###
13
14 read -n 1 -s -p $'And Control-K\x0bgoes straight down.'
15 echo >&2 # Control-K 是垂直制表符.
16
17 # 关于垂直制表符效果的一个更好的例子见下边:
18
19 var=$'\x0aThis is the bottom line\x0bThis is the top line\x0a'
20 echo "$var"
21 # 这句与上边的例子使用的是同样的办法,然而:
22 echo "$var" | col
23 # 这将造成垂直制表符右边的部分在左边部分的上边.
24 # 这也解释了为什么我们要在行首和行尾加上一个换行符--
25 #+ 来避免一个混乱的屏幕输出.
26
27 # Lee Maschmeyer的解释:
28 # ---------------------
29 # In the [first vertical tab example] . . . the vertical tab
29 # 在这里[第一个垂直制表符的例子中] . . . 这个垂直制表符
30 #+ makes the printing go straight down without a carriage return.
31 # This is true only on devices, such as the Linux console,
32 #+ that can't go "backward."
33 # The real purpose of VT is to go straight UP, not down.
34 # It can be used to print superscripts on a printer.
34 # 它可以用来在一个打印机上打印上标.
35 # col的作用,可以用来模仿VT的合适的行为.
36
37 exit 0
################################End Script#########################################
Ctl-Q 继续(等价于XON字符),这个继续的标准输入在一个终端里
Ctl-S 挂起(等价于XOFF字符),这个被挂起的stdin在一个终端里,用Ctl-Q恢复
Ctl-U 删除光标到行首的所有字符,在某些设置下,删除全行.
Ctl-V 当输入字符时,Ctl-V允许插入控制字符.比如,下边2个例子是等价的
echo -e '\x0a'
echo
Ctl-V在文本编辑器中十分有用,在vim中一样.
Ctl-W 删除当前光标到前边的最近一个空格之间的字符.
在某些设置下,删除到第一个非字母或数字的字符.
Ctl-Z 终止前台工作.

空白部分
分割命令或者是变量.包括空格,tab,空行,或任何它们的组合.
在一些特殊情况下,空白是不允许的,如变量赋值时,会引起语法错误.
空白行在脚本中没有效果.
"$IFS",对于某些命令输入的特殊变量分割域,默认使用的是空白.
如果想保留空白,使用引用.

注意事项:
[1] shell做大括号的命令扩展.但是命令本身需要对扩展的结果作处理.
[2] 例外:在pipe中的一个大括号中的代码段可能运行在一个子shell中.
1 ls | { read firstline; read secondline; }
2 # 错误,在打括号中的代码段,将运行到子shell中.
3 #+ 所以ls的输出将不能传递到代码块中.
4 echo "First line is $firstline; second line is $secondline" # 不能工作
5
6 # Thanks, S.C.
[3] 换行符也被认为是空白.这也解释了为什么一个空行也会被认为是空白.



第4章 变量和参数的介绍
======================

4.1 变量替换
------------
$ 变量替换操作符
只有在变量被声明,赋值,unset或exported或者是在变量代表一个signal的时候,
变量才会是以本来的面目出现在脚本里.变量在被赋值的时候,可能需要使用"=",
read状态或者是在循环的头部.
在""中还是会发生变量替换,这被叫做部分引用,或叫弱引用.而在''中就不会发生变
量替换,这叫做全引用,也叫强引用.具体见第5章的讨论.

注意:$var与${var}的区别,不加{},在某些上下文将引起错误,为了安全,使用2.
具体见9.3节 参数替换.

Example 4-1. 变量赋值和替换
################################Start Script#######################################
1 #!/bin/bash
2
3 # 变量赋值和替换
4
5 a=375
6 hello=$a
7
8 #-------------------------------------------------------------------------
9 # 强烈注意,在赋值的前后一定不要有空格.
10 # 如果有空格会发生什么?
11
12 # 如果"VARIABLE =value",
13 # ^
14 #+ 脚本将尝试运行一个"VARIABLE"的命令,带着一个"=value"参数.
15
16 # 如果"VARIABLE= value",
17 # ^
18 #+ script tries to run "value" command with
18 #+ 脚本将尝试运行一个"value"的命令,带着
19 #+ the environmental variable "VARIABLE" set to "".
19 #+ 一个被赋成""值的环境变量"VARIABLE".
20 #-------------------------------------------------------------------------
21
22
23 echo hello # 没有变量引用,不过是个hello字符串
24
25 echo $hello
26 echo ${hello} # 同上
27
28 echo "$hello"
29 echo "${hello}"
30
31 echo
32
33 hello="A B C D"
34 echo $hello # A B C D
35 echo "$hello" # A B C D
36 # 就象你看到的echo $hello 和 echo "$hello" 将给出不同的结果.
37 # ^ ^
38 # Quoting a variable preserves whitespace.
38 # 引用一个变量将保留其中的空白,当然,如果是变量替换就不会保留了.
39
40 echo
41
42 echo '$hello' # $hello
43 # ^ ^
44 # 全引用的作用
45 #+ 将导致"$"变成一个单独的字符.
46
47 # 注意两种引用不同的效果
48
49
50 hello= # 设置为空值
51 echo "\$hello (null value) = $hello"
52 # 注意设置一个变量为空,与unset它,不是一回事,虽然看起来一样
53 #
54
55 # --------------------------------------------------------------
56
57 # 可以在同一行上设置多个变量.
58 #+ 要以空白分隔
59 # 小心,这会降低可读性,和可移植性.
60
61 var1=21 var2=22 var3=$V3
62 echo
63 echo "var1=$var1 var2=$var2 var3=$var3"
64
65 # 在老版本的"sh"上,可能会有问题.
66
67 # --------------------------------------------------------------
68
69 echo; echo
70
71 numbers="one two three"
72 # ^ ^
73 other_numbers="1 2 3"
74 # ^ ^
75 # 如果变量中有空白,那么引用就必要了.
76 #
77 echo "numbers = $numbers"
78 echo "other_numbers = $other_numbers" # other_numbers = 1 2 3
79 echo
80
81 echo "uninitialized_variable = $uninitialized_variable"
82 # Uninitialized变量为空值(根本就没赋值).
83 uninitialized_variable= # 声明,但是没被初始化
84 #+ 其实和前边设置为空值得作用是一样的.
85 echo "uninitialized_variable = $uninitialized_variable"
86 # 还是一个空值
87
88 uninitialized_variable=23 # 赋值
89 unset uninitialized_variable # Unset it.
90 echo "uninitialized_variable = $uninitialized_variable"
91 # 还是空值
92 echo
93
94 exit 0
################################End Script#########################################
注意: 一个空值变量,或者是根本就没声明的变量,在赋值之前使用它可能会引起问题.
但是还是可以用来做算术运算
################################Start Script#######################################
1 echo "$uninitialized" # (blank line)
2 let "uninitialized += 5" # Add 5 to it.
3 echo "$uninitialized" # 5
4
5 # 结论:
6 # 对于一个空值变量在做算术操作的时候,就好像它的值为0一样.
8 # This is undocumented (and probably non-portable) behavior.
7 # 这并没被文档化(可能是不可移植)的行为.
################################End Script#########################################
具体参考 Example 11-21


4.2 变量赋值
------------
= 赋值操作符(前后都不能有空白)
不要与-eq混淆,那个是test,并不是赋值.
注意,=也可被用来做test操作,这依赖于上下文.

Example 4-2. 一般的变量赋值
################################Start Script#######################################
1 #!/bin/bash
2 # "裸体"变量
3
4 echo
5
6 # 变量什么时候是"裸体"的,比如前边少了$的时候.
7 # 当它被赋值的时候,而不是被引用的时候.
8
9 # 赋值
10 a=879
11 echo "The value of \"a\" is $a."
12
13 # 使用let赋值
14 let a=16+5
15 echo "The value of \"a\" is now $a."
16
17 echo
18
19 # 在for循环中
20 echo -n "Values of \"a\" in the loop are: "
21 for a in 7 8 9 11
22 do
23 echo -n "$a "
24 done
25
26 echo
27 echo
28
29 # 在read命令状态中
30 echo -n "Enter \"a\" "
31 read a
32 echo "The value of \"a\" is now $a."
33
34 echo
35
36 exit 0
################################End Script#########################################

Example 4-3. 变量赋值,一般的和比较特殊的
################################Start Script#######################################
1 #!/bin/bash
2
3 a=23 # Simple case
4 echo $a
5 b=$a
6 echo $b
7
8 # 现在让我们来点小变化
9
10 a=`echo Hello!` # 把echo命令的结果传给变量a
11 echo $a
12 # 注意,如果在命令扩展结构中使用一个(!)的话,在命令行中将不能工作
13 #+ 因为这触发了Bash的"历史机制".
14 # 但是,在校本里边使用的话,历史功能是被关闭的,所以就能够正常运行.
15
16
17 a=`ls -l` # 把ls -l的结果给a
18 echo $a # 别忘了,这么引用的话,ls的结果中的所有空白部分都没了(包括换行)
19 echo
20 echo "$a" # 这么引用就正常了,保留了空白
21 # (具体参阅章节"引用")
22
23 exit 0
################################End Script#########################################
使用$(...)机制进行的变量赋值(除去使用``来赋值的另外一种新方法).事实上这两种方法都是
命令替换的一种形式.
# 来自于/ect/rc.d/rc.local
R=$(cat /ect/redhat-release)
arch=$(uname -m)


4.3 Bash变量是不分类型的
------------------------
不像其他程序语言一样,Bash并不对变量区分"类型".本质上,Bash变量都是字符串.
但是依赖于上下文,Bash也允许比较操作和算术操作.决定这些的关键因素就是,变量中的值
是否只有数字.

Example 4-4 整型还是string?
################################Start Script#######################################
1 #!/bin/bash
2 # int-or-string.sh: 整形还是string?
3
4 a=2334 # 整型
5 let "a += 1"
6 echo "a = $a " # a = 2335
7 echo # 还是整型
8
9
10 b=${a/23/BB} # 将23替换成BB
11 # 这将把b变量从整型变为string
12 echo "b = $b" # b = BB35
13 declare -i b # 即使使用declare命令也不会对此有任何帮助,9.4节有解释
14 echo "b = $b" # b = BB35
15
16 let "b += 1" # BB35 + 1 =
17 echo "b = $b" # b = 1
18 echo
19
20 c=BB34
21 echo "c = $c" # c = BB34
22 d=${c/BB/23} # S将BB替换成23
23 # 这使得$d变为一个整形
24 echo "d = $d" # d = 2334
25 let "d += 1" # 2334 + 1 =
26 echo "d = $d" # d = 2335
27 echo
28
29 # 关于空变量怎么样?
30 e=""
31 echo "e = $e" # e =
32 let "e += 1" # 算术操作允许一个空变量?
33 echo "e = $e" # e = 1
34 echo # 空变量将转换成一个整型变量
35
36 # 关于未声明的变量怎么样?
37 echo "f = $f" # f =
38 let "f += 1" # 算术操作允许么?
39 echo "f = $f" # f = 1
40 echo # 未声明的变量将转换成一个整型变量
41
42
43
44 # 所以说Bash中的变量都是无类型的.
45
46 exit 0
################################End Script#########################################


4.4 特殊的变量类型
------------------
local variables
这种变量只有在代码块或者是函数中才可见(具体见23.2和23章)
environmental variables
这种变量将改变用户接口和shell的行为.

在一般的上下文中,每个进程都有自己的环境,就是一组保持进程可能引用的信息的
变量.这种情况下,shell于一个一般进程是相同的.

每次当shell启动时,它都将创建自己的环境变量.更新或者添加新的环境变量,将导
致shell更新它的环境,同时也会影响所有继承自这个环境的所有子进程(由这个命令
导致的).

注意:分配给环境变量的空间是受限的.创建太多的环境变量将引起空间溢出,这会引
起问题.
关于eval命令,具体见第11章
bash$ eval "`seq 10000 | sed -e 's/.*/export var&=ZZZZZZZZZZZZZZ/'`"
bash$ du
bash: /usr/bin/du: Argument list too long

如果一个脚本设置了环境变量,需要export它,来通知本脚本的环境,这是export
命令的功能,关于export命令,具体见11章.

脚本只能对它产生的子进程export变量.一个从命令行被调用的脚本export的变量,将
不能影响调用这个脚本的那个命令行shell的环境.

positional parameters
就是从命令行中传进来的参数,$0, $1, $2, $3...

$0就是脚本文件的名字,$1是第一个参数,$2为第2个...,参见[1](有$0的说明),$9
以后就需要打括号了,如${10},${11},${12}...
两个值得注意的变量$*和$@(第9章有具体的描述),表示所有的位置参数.

Example 4-5 位置参数
################################Start Script#######################################
1 #!/bin/bash
2
3 # 作为用例,调用这个脚本至少需要10个参数,如
4 # ./scriptname 1 2 3 4 5 6 7 8 9 10
5 MINPARAMS=10
6
7 echo
8
9 echo "The name of this script is \"$0\"."
10 # 添加./是为了当前目录
11 echo "The name of this script is \"`basename $0`\"."
12 # 去掉目录信息,具体见'basename'命令
13
14 echo
15
16 if [ -n "$1" ] # 测试变量被被引用
17 then
18 echo "Parameter #1 is $1" # "#"没被转义
19 fi
20
21 if [ -n "$2" ]
22 then
23 echo "Parameter #2 is $2"
24 fi
25
26 if [ -n "$3" ]
27 then
28 echo "Parameter #3 is $3"
29 fi
30
31 # ...
32
33
34 if [ -n "${10}" ] # 大于9的参数必须出现在{}中.
35 then
36 echo "Parameter #10 is ${10}"
37 fi
38
39 echo "-----------------------------------"
40 echo "All the command-line parameters are: "$*""
41
42 if [ $# -lt "$MINPARAMS" ] #$#是传到脚本里的位置参数的个数
43 then
44 echo
45 echo "This script needs at least $MINPARAMS command-line arguments!"
46 fi
47
48 echo
49
50 exit 0
################################End Script#########################################
{}标记法是一种很好的使用位置参数的方法.这也需要间接引用(见Example 34-2)
1 args=$# # 位置参数的个数
2 lastarg=${!args}
3 # 或: lastarg=${!#}
4 # 注意 lastarg=${!$#} 将报错

一些脚本可能会依赖于使用不同的调用名字,而表现出不同的行为,这样一般都需要
判断$0,而其他的名字都是通过ln命令产生的链接.(具体参见Example 12-2)

如果脚本需要一个命令行参数,而调用的时候,没用这个参数,这就有可能造成分配一个
空变量,这样估计就会引起问题.一种解决办法就是在这个位置参数,和相关的变量后
边,都添加一个额外的字符.具体见下边的例子.
################################Start Script#######################################
1 variable1_=$1_ # 而不是 variable1=$1
2 # 这将阻止一个错误,即使在调用时没使用这个位置参数.
3
4 critical_argument01=$variable1_
5
6 # 这个扩展的字符是可以被消除掉的,就像这样.
7 variable1=${variable1_/_/}
8 # 副作用就是$variable1_多了一个下划线
9 # 这里使用了一个参数替换模版(后边会有具体的讨论)
10 # (Leaving out the replacement pattern results in a deletion.)
10 # (在一个删除动作中,节省了一个替换模式)
11
12
13 # 一个解决这种问题的更简单的做法就是,判断一下这个位置参数是否传递下来了
14 if [ -z $1 ]
15 then
16 exit $E_MISSING_POS_PARAM
17 fi
18
19
20 # 但是上边的方法将可能产生一个意外的副作用
21 # 参数替换的更好的办法应该是:
22 # ${1:-$DefaultVal}
23 # 具体察看"Parameter Substition"节
24 #+ 在第9章
################################End Script#########################################


Example 4-6 wh,whois节点名字查询
################################Start Script#######################################
1 #!/bin/bash
2 # ex18.sh
3
4 # Does a 'whois domain-name' lookup on any of 3 alternate servers:
5 # ripe.net, cw.net, radb.net
6
7 # 把这个脚本重命名为'wh',然后放到/usr/local/bin下
8
9 # 需要3个符号链接
10 # ln -s /usr/local/bin/wh /usr/local/bin/wh-ripe
11 # ln -s /usr/local/bin/wh /usr/local/bin/wh-cw
12 # ln -s /usr/local/bin/wh /usr/local/bin/wh-radb
13
14 E_NOARGS=65
15
16
17 if [ -z "$1" ]
18 then
19 echo "Usage: `basename $0` [domain-name]"
20 exit $E_NOARGS
21 fi
22
23 # Check script name and call proper server.
23 # 检查脚本名字,然后调用合适的服务器
24 case `basename $0` in # Or: case ${0##*/} in
25 "wh" ) whois $1@whois.ripe.net;;
26 "wh-ripe") whois $1@whois.ripe.net;;
27 "wh-radb") whois $1@whois.radb.net;;
28 "wh-cw" ) whois $1@whois.cw.net;;
29 * ) echo "Usage: `basename $0` [domain-name]";;
30 esac
31
32 exit $?
################################End Script#########################################

shift shift命令重新分配位置参数,其实就是向左移动一个位置.
$1 <--- $2, $2 <--- $3, $3 <--- $4, 等等.
老的$1将消失,但是$0(脚本名)是不会改变的.如果你使用了大量的位置参数,那么
shift命令允许你存取超过10个参数.虽然{}表示法也允许这样.


Example 4-7 使用shift
################################Start Script#######################################
1 #!/bin/bash
2 # 使用'shift'来穿过所有的位置参数.
3
4 # 把这个脚本命名为shft,
5 #+ 并且使用一些参数来调用它,如:
6 # ./shft a b c def 23 skidoo
7
8 until [ -z "$1" ] # 知道所有参数都用光
9 do
10 echo -n "$1 "
11 shift
12 done
13
14 echo # 额外的换行.
15
16 exit 0
################################End Script#########################################
在将参数传递到函数中时,shift的工作方式也基本差不多.具体见Example 33-15

注意事项:
[1] 进程调用设置$0参数的脚本.一般的,这个参数就是脚本名字.具体察看execv的man页.



第5章 引用(翻译的可能有问题,特指引号)
======================================
引号的特殊效果就是,保护字符串中的特殊字符不被shell或者是shell脚本重新解释或者扩展.
(我们这里所说的"特殊"指的是一些字符在shell中具有的特殊意义,比如*)
如:
bash$ ls -l [Vv]*
-rw-rw-r-- 1 bozo bozo 324 Apr 2 15:05 VIEWDATA.BAT
-rw-rw-r-- 1 bozo bozo 507 May 4 14:25 vartrace.sh
-rw-rw-r-- 1 bozo bozo 539 Apr 14 17:11 viewdata.sh

bash$ ls -l '[Vv]*'
ls: [Vv]*: No such file or directory

在我们一般的生活中,引号内的内容往往有特殊的含义,而在Bash中,当我们引用一个字符串,
我们是保护它的字面含义.

特定的程序和工具能够重新解释或扩展特殊的字符.引用的一个重要的作用就是保护命令行中
的参数,但还是允许正在调用的程序来扩展它.
bash$ grep '[Ff]irst' *.txt
file1.txt:This is the first line of file1.txt.
file2.txt:This is the First line of file2.txt.

注意 grep [Ff]irst *.txt在Bash下的行为(其实就是正则表达式么),[1]

引用还可以抑制echo命令的换行作用.

bash$ echo $(ls -l)
total 8 -rw-rw-r-- 1 bozo bozo 130 Aug 21 12:57 t222.sh -rw-rw-r-- 1 bozo bozo 78 Aug 21 12:57 t71.sh

bash$ echo "$(ls -l)"
total 8
-rw-rw-r-- 1 bozo bozo 130 Aug 21 12:57 t222.sh
-rw-rw-r-- 1 bozo bozo 78 Aug 21 12:57 t71.sh


5.1 引用变量
------------
在一个双引号中直接使用变量名,一般都是没有问题的.它阻止了所有在引号中的特殊字符的
重新解释--包括变量名[2]--但是$,`和\除外.[3]保留$,作为特殊字符的意义,是为了能够在双
引号中也能够正常地引用变量("$var").这样在""中可以使用变量所表达的值(Example 4-1).

使用""来防止单词分割.[4]如果在参数列表中使用双引号,将使得双引号中的参数作为一个参
数.即使双引号中的字符串包含多个单词(也就是包含空白部分),也不会变为多个参数,如:
1 variable1="a variable containing five words"
2 COMMAND This is $variable1 # COMMAND将以7个参数来执行
3 # "This" "is" "a" "variable" "containing" "five" "words"
4
5 COMMAND "This is $variable1" # COMMAND将以1个参数来执行
6 # "This is a variable containing five words"
7
8
9 variable2="" # 空值
10
11 COMMAND $variable2 $variable2 $variable2 # COMMAND将不带参数执行
12 COMMAND "$variable2" "$variable2" "$variable2" # COMMAND将以3个空参数来执行
13 COMMAND "$variable2 $variable2 $variable2" # COMMAND将以1个参数来执行(2空格)
用双引号把参数封到echo中是很有必要的,只有在单词分隔或时保留空白时的时候可能
有些问题.

Example 5-1 echo一些诡异的变量
################################Start Script#######################################
1 #!/bin/bash
2 # weirdvars.sh: echo诡异的变量
3
4 var="'(]\\{}\$\""
5 echo $var # '(]\{}$"
6 echo "$var" # '(]\{}$" 并没有什么不同
7
8 echo
9
10 IFS='\'
11 echo $var # '(] {}$" \ 转换成空格了?明显和IFS有关系么!又不傻!
12 echo "$var" # '(]\{}$"
13
14 exit 0
################################End Script#########################################

单引号操作总体上和""很像,但不允许引用变量.因为$的特殊含义被关闭了.在''中除了',其他
字符都没有特殊的含义了.所以单引号比双引号严格.
因为即使是\,在''中都被关闭了,所以你想在''中显示'的含义,将得不到预期的效果.
1 echo "Why can't I write 's between single quotes"
2
3 echo
4
5 # 一种绕弯的方法
6 echo 'Why can'\''t I write '"'"'s between single quotes'
7 # |-------| |----------| |-----------------------|
8 # 包含了2个单引号字符,原书好像有错误

注意事项:
[1] 除非当前目录下,正好有个叫first的文件.
[2] 即使是变量的值也是有副作用的(见下边)
[3] 如果在""中包含"!"的话,在命令行中将会出现错误.因为这个"!"被当作历史命令来解释了.
在一个脚本中,这种情况是不会发生的,因为在脚本中,Bash历史记录被关闭了.

下边是一些关于"\"一些不协调的行为.
bash$ echo hello\!
hello!

bash$ echo "hello\!"
hello\!

bash$ echo -e x\ty
xty

bash$ echo -e "x\ty"
x y

[4] "单词分隔",在这个上下文中意味着,将一个字符串分隔为一些分离的参数.


5.2 转义(\)
-----------
转义是一种引用单个字符的方法.一个具有特殊含义的字符前边放上一个转义符(\)就告诉shell
这个字符失去了特殊的含义.
值得注意的是,在某些特定的命令和工具中,比如echo和sed,转义符往往会起到相反的效果,
它反倒有可能引发出这个字符特殊的含义.

对于特定的转义符的特殊的含义
在echo和sed中所使用的
\n 意味着新的一行
\r 回车
\t tab键
\v vertical tab(垂直tab),查前边的Ctl-K
\b backspace,查前边的Ctl-H
\a "alert"(如beep或flash)
\0xx 转换成8进制ASCII解码,等价于oxx

Example 5-2 转义符
################################Start Script#######################################
1 #!/bin/bash
2 # escaped.sh: 转义符
3
4 echo; echo
5
6 echo "\v\v\v\v" # 逐字的打印\v\v\v\v .
7 # 使用-e选项的echo命令来打印转义符
8 echo "============="
9 echo "VERTICAL TABS"
10 echo -e "\v\v\v\v" # Prints 4 vertical tabs.
11 echo "=============="
12
13 echo "QUOTATION MARK"
14 echo -e "\042" # 打印" (引号, 8进制的ASCII 码就是42).
15 echo "=============="
16
17 # The $'\X' construct makes the -e option unnecessary.
17 # 如果使用$'\X'结构,那-e选项就不必要了
18 echo; echo "NEWLINE AND BEEP"
19 echo $'\n' # 新行.
20 echo $'\a' # Alert (beep).
21
22 echo "==============="
23 echo "QUOTATION MARKS"
24 # 版本2以后Bash允许使用$'\nnn'结构
25 # 注意这种情况,'\nnn\是8进制
26 echo $'\t \042 \t' # Quote (") framed by tabs.
27
28 # 当然,也可以使用16进制的值,使用$'\xhhh' 结构
29 echo $'\t \x22 \t' # Quote (") framed by tabs.
30
31 # 早一点的Bash版本允许'\x022'这种形式
32 echo "==============="
33 echo
34
35
36 # 分配ASCII字符到变量中
37 # ---------------------
38 quote=$'\042' # \042是",分配到变量中
39 echo "$quote This is a quoted string, $quote and this lies outside the quotes."
40
41 echo
42
43 # Concatenating ASCII chars in a variable.
43 # 变量中的连续的ASCII char.
44 triple_underline=$'\137\137\137' # 137 是8进制的ASCII 码'_'.
45 echo "$triple_underline UNDERLINE $triple_underline"
46
47 echo
48
49 ABC=$'\101\102\103\010' # 101, 102, 103 是8进制的码A, B, C.
50 echo $ABC
51
52 echo; echo
53
54 escape=$'\033' # 033 是8进制码for escape.
55 echo "\"escape\" echoes as $escape"
56 #"escape" echoes as 没有变量被输出
57
58 echo; echo
59
60 exit 0
################################End Script#########################################
另一个关于$''字符串扩展结果的例子见Example 34-1

\" 表达引号本身
1 echo "Hello" # Hello
2 echo "\"Hello\", he said." # "Hello", he said.

\$ $号本身,跟在\$后的变量名,将不能扩展
1 echo "\$variable01" # 结果是$variable01

\\ \号本身.
1 echo "\\" # 结果是\
2
3 # 相反的 . . .
4
5 echo "\" # 这会出现第2个命令提示符,说白了就是提示你命令不全,你再补个"就
6 # 好了.如果是在脚本里,就会给出一个错误.

注意:\的行为依赖于它是否被转义,被"",或者是否在"命令替换"和"here document"中.
################################Start Script#######################################
1 # 简单的转义和""
2 echo \z # z
3 echo \\z # \z
4 echo '\z' # \z
5 echo '\\z' # \\z
6 echo "\z" # \z
7 echo "\\z" # \z
8
9 # 命令替换
10 echo `echo \z` # z
11 echo `echo \\z` # z
12 echo `echo \\\z` # \z
13 echo `echo \\\\z` # \z
14 echo `echo \\\\\\z` # \z
15 echo `echo \\\\\\\z` # \\z
16 echo `echo "\z"` # \z
17 echo `echo "\\z"` # \z
18
19 # Here document
20 cat <21 \z
22 EOF # \z
23
24 cat <25 \\z
26 EOF # \z
################################End Script#########################################

分配给变量的字符串的元素也会被转义,但是只把一个转义符分配给变量将会报错.
################################Start Script#######################################
1 variable=\
2 echo "$variable"
3 # Will not work - gives an error message:
3 # 将不能正常工作- 将给出一个错误消息:
4 # test.sh: : command not found
5 # 一个"裸体的" 转义符将不能够安全的分配给变量.
6 #
7 # What actually happens here is that the "\" escapes the newline and
7 # 这里其实真正发生的是variable=\,这句被shell认为是没有完成,\被认为是一个续行符
8 #+ 这样,下边的这句echo,也被认为是上一行的补充.所以,总的来说就是一个非法变量分配
9
10 variable=\
11 23skidoo
12 echo "$variable" # 23skidoo
13 # 这句就可以使用,因为这是一个合法的变量分配
14
15 variable=\
16 # \^ 转义一个空格
17 echo "$variable" # 显示空格
18
19 variable=\\
20 echo "$variable" # \
21
22 variable=\\\
23 echo "$variable"
24 # 不能正常工作,给出一个错误
25 # test.sh: \: command not found
26 #
27 # 第一个转义符把第2个\转义了,但是第3个又变成"裸体的"了,
28 #+ 与上边的例子的原因相同
29
30 variable=\\\\
31 echo "$variable" # \\
32 # 转了两个\
33 # 没问题
################################End Script#########################################

转义一个空格,在命令行参数列表中将会阻止单词分隔问题.
################################Start Script#######################################
1 file_list="/bin/cat /bin/gzip /bin/more /usr/bin/less /usr/bin/emacs-20.7"
2 # 列出的文件都作为命令的参数.
3
4 # Add two files to the list, and list all.
4 # 加2个文件到list中,并且列出全部.
5 ls -l /usr/X11R6/bin/xsetroot /sbin/dump $file_list
6
7 echo "-------------------------------------------------------------------------"
8
9 # 如果我们转义2个空格,会发生什么?
10 ls -l /usr/X11R6/bin/xsetroot\ /sbin/dump\ $file_list
11 # 错误: 因为前3个路径名被合并成一个参数传给了'ls -l'
12 # 因为2个转义符阻止了参数(单词)分离
################################End Script#########################################

转义符也提供续行功能.一般,每一行都包含一个不同的命令,但如果在行尾加上\,那就会接受
新行的输入,作为这一行的补充.
1 (cd /source/directory && tar cf - . ) | \
2 (cd /dest/directory && tar xpvf -)
3 # 重复了 Alan Cox的目录树拷贝命令
4 # 为了增加可读性分成2行.
5
6 # 也可以使用如下方式:
7 tar cf - -C /source/directory . |
8 tar xpvf - -C /dest/directory
9 # 察看下边的注意事项

注意:如果一个脚本以|(管道字符)结束.那么一个\(转义符),就不用非加上不可了.
但是一个好的shell脚本编写风格,还是应该在行尾加上\,以增加可读性.
################################Start Script#######################################
1 echo "foo
2 bar"
3 #foo
4 #bar
5
6 echo
7
8 echo 'foo
9 bar' # 没区别
10 #foo
11 #bar
12
13 echo
14
15 echo foo\
16 bar # 续行
17 #foobar
18
19 echo
20
21 echo "foo\
22 bar" # 与上边一样,\还是作为续行符
23 #foobar
24
25 echo
26
27 echo 'foo\
28 bar' # 由于是强引用,所以\没被解释成续行符
29 #foo\
30 #bar
################################End Script#########################################



第6章 退出和退出状态
====================
exit命令被用来结束脚本,就像C语言一样.他也会返回一个值来传给父进程,父进程会判断是否
可用.

每个命令都会返回一个exit状态(有时候也叫return状态).成功返回0,如果返回一个非0值,通
常情况下都会被认为是一个错误码.一个编写良好的UNIX命令,程序,和工具都会返回一个0作为
退出码来表示成功,虽然偶尔也会有例外.

同样的,脚本中的函数和脚本本身都会返回退出状态.在脚本或者是脚本函数中执行的最后的命
令会决定退出状态.在脚本中,exit nnn命令将会把nnn退出码传递给shell(nnn必须是10进制数
0-255).

当一个脚本以不带参数exit来结束时,脚本的退出状态就由脚本中最后执行命令来决定.
1 #!/bin/bash
2
3 COMMAND_1
4
5 . . .
6
7 # 将以最后的命令来决定退出状态
8 COMMAND_LAST
9
10 exit $?

1 #!/bin/bash
2
3 COMMAND1
4
5 . . .
6
7 # 将以最后的命令来决定退出状态
8 COMMAND_LAST

$?读取最后执行命令的退出码.函数返回后,$?给出函数最后执行的那条命令的退出码.这种给
函数返回值的方法是Bash的方法.对于脚本来说也一样.总之,一般情况下,0为成功,非0失败W.
Example 6-1 exit/exit状态
################################Start Script#######################################
1 #!/bin/bash
2
3 echo hello
4 echo $? # 返回0,因为执行成功
5
6 lskdf # 不认识的命令.
7 echo $? # 返回非0值,因为失败了.
8
9 echo
10
11 exit 113 # 将返回113给shell.
12 # To verify this, type "echo $?" after script terminates.
12 # 为了验证这个,在脚本结束的地方使用"echo $?"
################################End Script#########################################

$?对于测试脚本中的命令的结果特别有用(见Example 12-32和Example 12-17).
注意: !逻辑非操作,将会反转test命令的结果,并且这会影响exit状态.
Example 6-2 否定一个条件使用!
################################Start Script#######################################
1 true # true是shell内建命令,什么事都不做,就是shell返回0
2 echo "exit status of \"true\" = $?" # 0
3
4 ! true
5 echo "exit status of \"! true\" = $?" # 1
6 # 注意:"!"需要一个空格
7 # !true 将导致一个"command not found"错误
8 #
9 # 如果一个命令以'!'开头,那么将使用Bash的历史机制.就是显示这个命令被使用的历史.
10
11 true
12 !true
13 # 这次就没有错误了.
14 # 他不过是重复了之前的命令(true).
################################End Script#########################################

注意事项:
特定的退出码都有预定的含义(见附录D),用户不应该在自己的脚本中指定他.



第7章 Tests
===========
每个完整的合理的编程语言都具有条件判断的功能.Bash具有test命令,不同的[]和()操作,和
if/then结构.


7.1 Test结构
------------
一个if/then结构可以测试命令的返回值是否为0(因为0表示成功),如果是的话,执行更多命令.

有一个专用命令"["(左中括号,特殊字符).这个命令与test命令等价,但是出于效率上的考虑,
它是一个内建命令.这个命令把它的参数作为比较表达式或是文件测试,并且根据比较的结果,
返回一个退出码.

在版本2.02的Bash中,推出了一个新的[[...]]扩展test命令.因为这种表现形式可能对某些语
言的程序员来说更加熟悉.注意"[["是一个关键字,并不是一个命令.

Bash把[[ $a -lt $b ]]看作一个单独的元素,并且返回一个退出码.

((...))和let...结果也能够返回一个退出码,当它们所测试的算术表达式的结果为非0的时候,
他们的退出码将返回0.这些算术扩展(见第15章)结构被用来做算术比较.
1 let "1<2" returns 0 (as "1<2" expands to "1")
2 (( 0 && 1 )) returns 1 (as "0 && 1" expands to "0")

if命令可以测试任何命令,不仅仅是括号中的条件.
1 if cmp a b &> /dev/null # 阻止输出.
2 then echo "Files a and b are identical."
3 else echo "Files a and b differ."
4 fi
5
6 # 非常有用的"if-grep" 结构:
7 # ------------------------
8 if grep -q Bash file
9 then echo "File contains at least one occurrence of Bash."
10 fi
11
12 word=Linux
13 letter_sequence=inu
14 if echo "$word" | grep -q "$letter_sequence"
15 # "-q"选项是用来阻止输出
16 then
17 echo "$letter_sequence found in $word"
18 else
19 echo "$letter_sequence not found in $word"
20 fi
21
22
23 if COMMAND_WHOSE_EXIT_STATUS_IS_0_UNLESS_ERROR_OCCURRED
24 then echo "Command succeeded."
25 else echo "Command failed."
26 fi

一个if/then结构可以包含多级比较和tests.

1 if echo "Next *if* is part of the comparison for the first *if*."
2
3 if [[ $comparison = "integer" ]]
4 then (( a < b ))
5 else
6 [[ $a < $b ]]
7 fi
8
9 then
10 echo '$a is less than $b'
11 fi

Example 7-1 什么情况下为真?
################################Start Script#######################################
1 #!/bin/bash
2
3 # 技巧:
4 # 如果你不确定一个特定的条件如何判断.
5 #+ 在一个if-test结构中测试它.
6
7 echo
8
9 echo "Testing \"0\""
10 if [ 0 ] # zero
11 then
12 echo "0 is true."
13 else
14 echo "0 is false."
15 fi # 0 is true.
16
17 echo
18
19 echo "Testing \"1\""
20 if [ 1 ] # one
21 then
22 echo "1 is true."
23 else
24 echo "1 is false."
25 fi # 1 is true.
26
27 echo
28
29 echo "Testing \"-1\""
30 if [ -1 ] # -1
31 then
32 echo "-1 is true."
33 else
34 echo "-1 is false."
35 fi # -1 is true.
36
37 echo
38
39 echo "Testing \"NULL\""
40 if [ ] # NULL (控状态)
41 then
42 echo "NULL is true."
43 else
44 echo "NULL is false."
45 fi # NULL is false.
46
47 echo
48
49 echo "Testing \"xyz\""
50 if [ xyz ] # 字符串
51 then
52 echo "Random string is true."
53 else
54 echo "Random string is false."
55 fi # Random string is true.
56
57 echo
58
59 echo "Testing \"\$xyz\""
60 if [ $xyz ] # 测试$xyz是否为null,但是...(明显没人定义么!)
61 # 只不过是一个未定义的变量
62 then
63 echo "Uninitialized variable is true."
64 else
65 echo "Uninitialized variable is false."
66 fi # Uninitialized variable is false.
67
68 echo
69
70 echo "Testing \"-n \$xyz\""
71 if [ -n "$xyz" ] # 更学究的的检查
72 then
73 echo "Uninitialized variable is true."
74 else
75 echo "Uninitialized variable is false."
76 fi # Uninitialized variable is false.
77
78 echo
79
80
81 xyz= # 初始化了,但是将其设为空值
82
83 echo "Testing \"-n \$xyz\""
84 if [ -n "$xyz" ]
85 then
86 echo "Null variable is true."
87 else
88 echo "Null variable is false."
89 fi # Null variable is false.
90
91
92 echo
93
94
95 # 什么时候"flase"为true?
96
97 echo "Testing \"false\""
98 if [ "false" ] # 看起来"false"只不过是个字符串而已.
99 then
100 echo "\"false\" is true." #+ 并且它test的结果就是true.
101 else
102 echo "\"false\" is false."
103 fi # "false" is true.
104
105 echo
106
107 echo "Testing \"\$false\"" # 再来一个,未声明的变量
108 if [ "$false" ]
109 then
110 echo "\"\$false\" is true."
111 else
112 echo "\"\$false\" is false."
113 fi # "$false" is false.
114 # 现在我们终于得到了期望的结果
115
116 # 如果我们test这个变量"$true"会发生什么结果?答案是和"$flase"一样,都为空,因为我
117 #+ 们并没有定义它.
118 echo
119
120 exit 0
################################End Script#########################################
练习.解释上边例子的行为(我想我解释的已经够清楚了)

1 if [ condition-true ]
2 then
3 command 1
4 command 2
5 ...
6 else
7 # 可选的(如果不需要可以省去)
8 # 如果原始的条件测试结果是false,那么添加默认的代码来执行.
9 command 3
10 command 4
11 ...
12 fi

注意:当if和then在一个条件测试的同一行中的话,必须使用";"来终止if表达式.if和then都是
关键字.关键字(或者命令)作为一个表达式的开头,并且在一个新的表达式开始之前,必须
结束上一个表达式.
1 if [ -x "$filename" ]; then

Else if和elif

elif
elif是else if的缩减形式.
1 if [ condition1 ]
2 then
3 command1
4 command2
5 command3
6 elif [ condition2 ]
7 # Same as else if
8 then
9 command4
10 command5
11 else
12 default-command
13 fi

使用if test condition-true这种形式和if[condition-true]这种形式是等价的.向我们前边
所说的"["是test的标记.并且以"]"结束.在if/test中并不应该这么严厉,但是新版本的Bash
需要它.

注意:test命令是Bash的内建命令,用来测试文件类型和比较字符串.因此,在Bash脚本中,test
并不调用/usr/bin/test的二进制版本(这是sh-utils工具包的一部分).同样的,[并不调用
/usr/bin/[,被连接到/usr/bin/test.
bash$ type test
test is a shell builtin
bash$ type '['
[ is a shell builtin
bash$ type '[['
[[ is a shell keyword
bash$ type ']]'
]] is a shell keyword
bash$ type ']'
bash: type: ]: not found

Example 7-2 几个等效命令test,/usr/bin/test,[],和/usr/bin/[
################################Start Script#######################################
1 #!/bin/bash
2
3 echo
4
5 if test -z "$1"
6 then
7 echo "No command-line arguments."
8 else
9 echo "First command-line argument is $1."
10 fi
11
12 echo
13
14 if /usr/bin/test -z "$1" # 与内建的test结果相同
15 then
16 echo "No command-line arguments."
17 else
18 echo "First command-line argument is $1."
19 fi
20
21 echo
22
23 if [ -z "$1" ] # 与上边代码的作用相同
24 # if [ -z "$1" 应该工作,但是...
25 #+ Bash相应一个缺少关闭中括号的错误消息.
26 then
27 echo "No command-line arguments."
28 else
29 echo "First command-line argument is $1."
30 fi
31
32 echo
33
34
35 if /usr/bin/[ -z "$1" ] # 再来一个,与上边代码的作用相同
36 # if /usr/bin/[ -z "$1" # 工作,但是给个错误消息
37 # # 注意:
38 # This has been fixed in Bash, version 3.x.
38 # 在ver 3.x上,这个bug已经被Bash修正了.
39 then
40 echo "No command-line arguments."
41 else
42 echo "First command-line argument is $1."
43 fi
44
45 echo
46
47 exit 0
###############################End Script#########################################

[[]]结构比Bash的[]更加灵活,这是一个扩展的test命令,从ksh88继承过来的.
注意:在[[]]结构中,将没有文件扩展或者是单词分离,但是会发生参数扩展和命令替换.
1 file=/etc/passwd
2
3 if [[ -e $file ]]
4 then
5 echo "Password file exists."
6 fi
注意:使用[[]],而不是[],能够阻止脚本中的许多逻辑错误.比如,尽管在[]中将给出一个错误,
但是&&,||,<>操作还是能够工作在一个[[]]test之中.
注意:在if后边,test命令和[]或[[]]都不是必须的.如下:
1 dir=/home/bozo
2
3 if cd "$dir" 2>/dev/null; then # "2>/dev/null" hides error message.
4 echo "Now in $dir."
5 else
6 echo "Can't change to $dir."
7 fi
if命令将返回if后边的命令的退出码.

与此相似,当在一个在使用与或列表结构的时候,test或中括号的使用,也并不一定非的有if不可
1 var1=20
2 var2=22
3 [ "$var1" -ne "$var2" ] && echo "$var1 is not equal to $var2"
4
5 home=/home/bozo
6 [ -d "$home" ] || echo "$home directory does not exist."

(())结构扩展并计算一个算术表达式的结果.如果表达式的结果为0,它将返回1作为退出码,或
者是"false".而一个非0表达式的结果将返回0作为退出码,或者是"true".

Example 7-3 算数测试使用(( ))
################################Start Script#######################################
1 #!/bin/bash
2 # 算数测试
3
4 # The (( ... )) construct evaluates and tests numerical expressions.
4 # (( ... ))结构计算并测试算数表达式的结果.
5 # 退出码将与[ ... ]结构相反!
6
7 (( 0 ))
8 echo "Exit status of \"(( 0 ))\" is $?." # 1
9
10 (( 1 ))
11 echo "Exit status of \"(( 1 ))\" is $?." # 0
12
13 (( 5 > 4 )) # true
14 echo "Exit status of \"(( 5 > 4 ))\" is $?." # 0
15
16 (( 5 > 9 )) # false
17 echo "Exit status of \"(( 5 > 9 ))\" is $?." # 1
18
19 (( 5 - 5 )) # 0
20 echo "Exit status of \"(( 5 - 5 ))\" is $?." # 1
21
22 (( 5 / 4 )) # 除法也行
23 echo "Exit status of \"(( 5 / 4 ))\" is $?." # 0
24
25 (( 1 / 2 )) # 出发结果<1
26 echo "Exit status of \"(( 1 / 2 ))\" is $?." # 结果将为0
27 # 1
28
29 (( 1 / 0 )) 2>/dev/null # 除数为0的错误
30 # ^^^^^^^^^^^
31 echo "Exit status of \"(( 1 / 0 ))\" is $?." # 1
32
33 # What effect does the "2>/dev/null" have?
33 # "2>/dev/null"的作用是什么?
34 # 如果删除"2>dev/null"将会发生什么?
35 # Try removing it, then rerunning the script.
35 # 尝试删除它,然后再运行脚本.
36
37 exit 0
################################End Script#########################################


7.2 文件测试操作
----------------
返回true如果...

-e 文件存在
-a 文件存在
这个选项的效果与-e相同.但是它已经被弃用了,并且不鼓励使用
-f file是一个regular文件(不是目录或者设备文件)
-s 文件长度不为0
-d 文件是个目录
-b 文件是个块设备(软盘,cdrom等等)
-c 文件是个字符设备(键盘,modem,声卡等等)
-p 文件是个管道
-h 文件是个符号链接
-L 文件是个符号链接
-S 文件是个socket
-t 关联到一个终端设备的文件描述符
这个选项一般都用来检测是否在一个给定脚本中的stdin[-t0]或[-t1]是一个终端
-r 文件具有读权限(对于用户运行这个test)
-w 文件具有写权限(对于用户运行这个test)
-x 文件具有执行权限(对于用户运行这个test)
-g set-group-id(sgid)标志到文件或目录上
如果一个目录具有sgid标志,那么一个被创建在这个目录里的文件,这个目录属于创建
这个目录的用户组,并不一定与创建这个文件的用户的组相同.对于workgroup的目录
共享来说,这非常有用.见<>第58页.
-u set-user-id(suid)标志到文件上
如果运行一个具有root权限的文件,那么运行进程将取得root权限,即使你是一个普通
用户.[1]这对于需要存取系统硬件的执行操作(比如pppd和cdrecord)非常有用.如果
没有suid标志的话,那么普通用户(没有root权限)将无法运行这种程序.
见<>第58页.
-rwsr-xr-t 1 root 178236 Oct 2 2000 /usr/sbin/pppd
对于设置了suid的文件,在它的权限标志中有"s".
-k 设置粘贴位,见<>第65页.
对于"sticky bit",save-text-mode标志是一个文件权限的特殊类型.如果设置了这
个标志,那么这个文件将被保存在交换区,为了达到快速存取的目的.如果设置在目录
中,它将限制写权限.对于设置了sticky bit位的文件或目录,权限标志中有"t".
drwxrwxrwt 7 root 1024 May 19 21:26 tmp/
如果一个用户并不时具有stick bit位的目录的拥有者,但是具有写权限,那么用户只
能在这个目录下删除自己所拥有的文件.这将防止用户在一个公开的目录中不慎覆盖
或者删除别人的文件,比如/tmp(当然root或者是目录的所有者可以随便删除或重命名
其中的文件).
-O 你是文件的所有者.
-G 文件的group-id和你的相同.
-N 从文件最后被阅读到现在,是否被修改.

f1 -nt f2
文件f1比f2新
f1 -ot f2
f1比f2老
f1 -ef f2
f1和f2都硬连接到同一个文件.

! 非--反转上边测试的结果(如果条件缺席,将返回true)

Example 7-4 test死的链接文件
################################Start Script#######################################
1 #!/bin/bash
2 # broken-link.sh
3 # Written by Lee bigelow <ligelowbee@yahoo.com>
4 # Used with permission.
5
6 #一个真正有用的shell脚本来找出死链接文件并且输出它们的引用
7 #以便于它们可以被输入到xargs命令中进行处理 :)
8 #比如: broken-link.sh /somedir /someotherdir|xargs rm
9 #
10 #这里,不管怎么说,是一种更好的方法
11 #
12 #find "somedir" -type l -print0|\
13 #xargs -r0 file|\
14 #grep "broken symbolic"|
15 #sed -e 's/^\|: *broken symbolic.*$/"/g'
16 #
17 #但这不是一个纯粹的bash,最起码现在不是.
18 #小心:小心/proc文件系统和任何的循环链接文件.
19 ##############################################################
20
21
22 #如果没对这个脚本传递参数,那么就使用当前目录.
23 #否则就使用传递进来的参数作为目录来搜索.
24 #
25 ####################
26 [ $# -eq 0 ] && directorys=`pwd` || directorys=$@
27
28 #建立函数linkchk来检查传进来的目录或文件是否是链接和是否存在,
29 #并且打印出它们的引用
30 #如果传进来的目录有子目录,
31 #那么把子目录也发送到linkchk函数中处理,就是递归目录.
32 ##########
33 linkchk () {
34 for element in $1/*; do
35 [ -h "$element" -a ! -e "$element" ] && echo \"$element\"
36 [ -d "$element" ] && linkchk $element
37 # Of course, '-h' tests for symbolic link, '-d' for directory.
37 # 当然'-h'是测试链接,'-d'是测试目录.
38 done
39 }
40
41 #如果是个可用目录,那就把每个从脚本传递进来的参数都送到linkche函数中.
42 #如果不是,那就打印出错误消息和使用信息.
43 #
44 ################
45 for directory in $directorys; do
46 if [ -d $directory ]
47 then linkchk $directory
48 else
49 echo "$directory is not a directory"
50 echo "Usage: $0 dir1 dir2 ..."
51 fi
52 done
53
54 exit 0
################################End Script#########################################
Example 28-1, Example 10-7, Example 10-3, Example 28-3, 和Example A-1 也会说明文件
测试操作的使用过程.

注意事项:
[1] 小心suid,可能引起安全漏洞,但是不会影响shell脚本.
[2] 在当代UNIX系统中,已经不使用sticky bit了,只在目录中使用.


7.3 其他比较操作
----------------
二元比较操作符,比较变量或者比较数字.注意数字与字符串的区别.

整数比较

-eq 等于,如:if [ "$a" -eq "$b" ]
-ne 不等于,如:if [ "$a" -ne "$b" ]
-gt 大于,如:if [ "$a" -gt "$b" ]
-ge 大于等于,如:if [ "$a" -ge "$b" ]
-lt 小于,如:if [ "$a" -lt "$b" ]
-le 小于等于,如:if [ "$a" -le "$b" ]
< 小于(需要双括号),如:(("$a" < "$b"))
<= 小于等于(需要双括号),如:(("$a" <= "$b"))
> 大于(需要双括号),如:(("$a" > "$b"))
>= 大于等于(需要双括号),如:(("$a" >= "$b"))

字符串比较
= 等于,如:if [ "$a" = "$b" ]
== 等于,如:if [ "$a" == "$b" ],与=等价
注意:==的功能在[[]]和[]中的行为是不同的,如下:
1 [[ $a == z* ]] # 如果$a以"z"开头(模式匹配)那么将为true
2 [[ $a == "z*" ]] # 如果$a等于z*(字符匹配),那么结果为true
3
4 [ $a == z* ] # File globbing 和word splitting将会发生
5 [ "$a" == "z*" ] # 如果$a等于z*(字符匹配),那么结果为true
一点解释,关于File globbing是一种关于文件的速记法,比如"*.c"就是,再如~也是.
但是file globbing并不是严格的正则表达式,虽然绝大多数情况下结构比较像.
!= 不等于,如:if [ "$a" != "$b" ]
这个操作符将在[[]]结构中使用模式匹配.
< 小于,在ASCII字母顺序下.如:
if [[ "$a" < "$b" ]]
if [ "$a" \< "$b" ]
注意:在[]结构中"<"需要被转义.
> 大于,在ASCII字母顺序下.如:
if [[ "$a" > "$b" ]]
if [ "$a" \> "$b" ]
注意:在[]结构中">"需要被转义.
具体参考Example 26-11来查看这个操作符应用的例子.
-z 字符串为"null".就是长度为0.
-n 字符串不为"null"
注意:
使用-n在[]结构中测试必须要用""把变量引起来.使用一个未被""的字符串来使用! -z
或者就是未用""引用的字符串本身,放到[]结构中(见Example 7-6)虽然一般情况下可
以工作,但这是不安全的.习惯于使用""来测试字符串是一种好习惯.[1]

Example 7-5 数字和字符串比较
################################Start Script#######################################
1 #!/bin/bash
2
3 a=4
4 b=5
5
6 # 这里的变量a和b既可以当作整型也可以当作是字符串.
7 # 这里在算术比较和字符串比较之间有些混淆,
8 #+ 因为Bash变量并不是强类型的.
9
10 # Bash允许对整型变量操作和比较
11 #+ 当然变量中只包含数字字符.
12 # 但是还是要考虑清楚再做.
13
14 echo
15
16 if [ "$a" -ne "$b" ]
17 then
18 echo "$a is not equal to $b"
19 echo "(arithmetic comparison)"
20 fi
21
22 echo
23
24 if [ "$a" != "$b" ]
25 then
26 echo "$a is not equal to $b."
27 echo "(string comparison)"
28 # "4" != "5"
29 # ASCII 52 != ASCII 53
30 fi
31
32 # 在这个特定的例子中,"-ne"和"!="都可以.
33
34 echo
35
36 exit 0
################################End Script#########################################

Example 7-6 测试字符串是否为null
################################Start Script#######################################
1 #!/bin/bash
2 # str-test.sh: 测试null字符串和非引用字符串,
3 #+ but not strings and sealing wax, not to mention cabbages and kings . . .
4 #+ 上边这句没看懂
5 # Using if [ ... ]
6
7
8 # 如果一个字符串没被初始化,那么它就没有定义的值(像这种话,总感觉像屁话)
9 # 这种状态叫做"null"(与zero不同)
10
11 if [ -n $string1 ] # $string1 没被声明和初始化
12 then
13 echo "String \"string1\" is not null."
14 else
15 echo "String \"string1\" is null."
16 fi
17 # 错误的结果.
18 # 显示$string1为非空,虽然他没被初始化.
19
20
21 echo
22
23
24 # 让我们再试一下.
25
26 if [ -n "$string1" ] # 这次$string1被引用了.
27 then
28 echo "String \"string1\" is not null."
29 else
30 echo "String \"string1\" is null."
31 fi # ""的字符串在[]结构中
32
33
34 echo
35
36
37 if [ $string1 ] # 这次$string1变成"裸体"的了
38 then
39 echo "String \"string1\" is not null."
40 else
41 echo "String \"string1\" is null."
42 fi
43 # 这工作得很好.
44 # 这个[]test操作检测string是否为null.
45 # 然而,使用("$string1")是一种很好的习惯
46 #
47 # As Stephane Chazelas points out,
48 # if [ $string1 ] 有1个参数 "]"
49 # if [ "$string1" ] 有2个参数,空的"$string1"和"]"
50
51
52
53 echo
54
55
56
57 string1=initialized
58
59 if [ $string1 ] # 再来,$string1"裸体了"
60 then
61 echo "String \"string1\" is not null."
62 else
63 echo "String \"string1\" is null."
64 fi
65 # 再来,给出了正确的结果.
66 # 不过怎么说("$string1")还是好很多,因为. . .
67
68
69 string1="a = b"
70
71 if [ $string1 ] # 再来,$string1 再次裸体了.
72 then
73 echo "String \"string1\" is not null."
74 else
75 echo "String \"string1\" is null."
76 fi
77 # 非引用的"$string1"现在给出了一个错误的结果!
78
79 exit 0
80 # Thank you, also, Florian Wisser, for the "heads-up".
################################End Script#########################################

Example 7-7 zmore
################################Start Script#######################################
1 #!/bin/bash
2 # zmore
3
4 #使用'more'来查看gzip文件
5
6 NOARGS=65
7 NOTFOUND=66
8 NOTGZIP=67
9
10 if [ $# -eq 0 ] # 与 if [ -z "$1" ]同样的效果
11 # 应该是说前边的那句注释有问题,$1是可以存在的,比如:zmore "" arg2 arg3
12 then
13 echo "Usage: `basename $0` filename" >&2
14 # 错误消息到stderr
15 exit $NOARGS
16 # 脚本返回65作为退出码.
17 fi
18
19 filename=$1
20
21 if [ ! -f "$filename" ] # 将$filename ""起来,来允许可能的空白
22 then
23 echo "File $filename not found!" >&2
24 # 错误消息到stderr
25 exit $NOTFOUND
26 fi
27
28 if [ ${filename##*.} != "gz" ]
29 # 在变量替换中使用中括号
30 then
31 echo "File $1 is not a gzipped file!"
32 exit $NOTGZIP
33 fi
34
35 zcat $1 | more
36
37 # 使用过滤命令'more'
38 # 如果你想的话也可使用'less'
39
40
41 exit $? # 脚本将返回pipe的结果作为退出码
42 # 事实上,不用非的有"exit $?",但是不管怎么说,有了这句,能正规一些
43 # 将最后一句命令的执行状态作为退出码返回
################################End Script#########################################

混合比较

-a 逻辑与
exp1 -a exp2 如果exp1和exp2都为true的话,这个表达式将返回true

-o 逻辑或
exp1 -o exp2 如果exp1和exp2中有一个为true的话,那么这个表达式就返回true

这与Bash的比较操作符&&和||很相像.在[[]]中使用它.
1 [[ condition1 && condition2 ]]
-o和-a一般都是和test命令或者是[]一起工作.
1 if [ "$exp1" -a "$exp2" ]

请参考Example 8-3,Example 26-16和Example A-28来查看混合比较操作的行为.

注意事项:
[1] S.C.(这家伙是个人名)指出,在使用混合比较的时候即使"$var"也可能会产生问题.
如果$string为空的话,[ -n "$string" -o "$a" = "$b" ]可能在某些版本的Bash中
会有问题.为了附加一个额外的字符到可能的空变量中的一种安全的办法是,
[ "x$string" != x -o "x$a" = "x$b" ](the "x's" cancel out)(没看懂).
cancel out是抵消的意思.


7.4 嵌套的if/then条件test
-------------------------
可以使用if/then来进行嵌套的条件test.最终的结果和上边的使用&&混合比较操作是相同的.
1 if [ condition1 ]
2 then
3 if [ condition2 ]
4 then
5 do-something # 这里只有在condition1和condition2都可用的时候才行.
6 fi
7 fi
具体请查看Example 34-4.


7.5 检查你的test知识
--------------------
系统范围的xinitrc文件可以用来启动X server.这个文件中包含了相当多的if/then test,
就像下边的节选一样:
1 if [ -f $HOME/.Xclients ]; then
2 exec $HOME/.Xclients
3 elif [ -f /etc/X11/xinit/Xclients ]; then
4 exec /etc/X11/xinit/Xclients
5 else
6 # 故障保险设置,虽然我们永远都不会走到这来.
7 # (我们在Xclients中也提供了相同的机制)它不会受伤的.
8 xclock -geometry 100x100-5+5 &
9 xterm -geometry 80x50-50+150 &
10 if [ -f /usr/bin/netscape -a -f /usr/share/doc/HTML/index.html ]; then
11 netscape /usr/share/doc/HTML/index.html &
12 fi
13 fi

对上边的"test"结构进行解释,然后检查整个文件,/etc/X11/xinit/xinitrc,并分析if/then
test结构.你可能需要查看一下后边才能讲解到的grep,sed和正则表达式的知识.



第8章 操作符和相关的主题
========================

8.1 操作符
----------

等号操作符

变量赋值
初始化或者修改变量的值
=
无论在算术运算还是字符串运算中,都是赋值语句.
1 var=27
2 category=minerals # No spaces allowed after the "=".

注意:不要和"="test操作符混淆.
1 # = as a test operator
2
3 if [ "$string1" = "$string2" ]
4 # if [ "X$string1" = "X$string2" ] is safer,
5 # to prevent an error message should one of the variables be empty.
6 # (The prepended "X" characters cancel out.)
7 then
8 command
9 fi

算术操作符

+ 加法
- 减法
* 乘法
/ 除法
** 幂运算
1 # Bash, version 2.02, introduced the "**" exponentiation operator.
2
3 let "z=5**3"
4 echo "z = $z" # z = 125
% 取模
bash$ expr 5 % 3
2

5/3=1余2
模运算经常用在其它的事情中,比如产生特定的范围的数字(Example 9-24,
Example 9-27)和格式化程序的输出(Example 26-15,Example A-6).它甚至可以用来
产生质数,(Example A-16).事实上取模运算在算术运算中使用的频率惊人的高.

Example 8-1 最大公约数
################################Start Script#######################################
1 #!/bin/bash
2 # gcd.sh: 最大公约数
3 # 使用Euclid's 算法
4
5 # 最大公约数,就是2个数能够同时整除的最大的数.
6 #
7
8 # Euclid's算法采用连续除法.
9 # 在每个循环中
10 #+ 被除数 <--- 除数
11 #+ 除数 <--- 余数
12 #+ 直到余数= 0.
13 #+ 在最后的循环中The gcd = 被除数
14 #
15 # 关于这个算法更精彩的讨论
16 # 见Jim Loy's site, http://www.jimloy.com/number/euclids.htm.
17
18
19 # ------------------------------------------------------
20 # 参数检查
21 ARGS=2
22 E_BADARGS=65
23
24 if [ $# -ne "$ARGS" ]
25 then
26 echo "Usage: `basename $0` first-number second-number"
27 exit $E_BADARGS
28 fi
29 # ------------------------------------------------------
30
31
32 gcd ()
33 {
34
35 dividend=$1 # 随便给值
36 divisor=$2 #+ 即使$2大,也没关系.
37 # Why not?
38
39 remainder=1 # 如果再循环中使用为初始化的变量.
40 #+ 那将在第一次循环中产生一个错误消息.
41
42
43 until [ "$remainder" -eq 0 ]
44 do
45 let "remainder = $dividend % $divisor"
46 dividend=$divisor # 现在使用2个最小的数重复.
47 divisor=$remainder
48 done # Euclid's algorithm
49
50 } # Last $dividend is the gcd.
50 } # 最后的$dividend就是gcd.
51
52
53 gcd $1 $2
54
55 echo; echo "GCD of $1 and $2 = $dividend"; echo
56
57
58 # 练习:
59 # --------
60 # 检查命令行参数来确定它们都是整数,
61 #+ and exit the script with an appropriate error message if not.
61 #+ 否则就选择合适的错误消息退出.
62
63 exit 0
################################End Script#########################################

+= 加等于(通过常量增加变量)
let "var += 5" #var将在本身值的基础上增加5
-= 减等于
*= 乘等于
let "var *= 4"
/= 除等于
%= 取模赋值,算术操作经常使用expr或者let表达式.

Example 8-2 使用算术操作符
################################Start Script#######################################
1 #!/bin/bash
2 # Counting to 11 in 10 different ways.
3
4 n=1; echo -n "$n "
5
6 let "n = $n + 1" # let "n = n + 1" 这么写也行
7 echo -n "$n "
8
9
10 : $((n = $n + 1))
11 # ":" 是必须的,这是因为,如果没有":"的话,Bash将
12 #+ 尝试把"$((n = $n + 1))"解释成一个命令
13 echo -n "$n "
14
15 (( n = n + 1 ))
16 # 对于上边的方法的一个更简单的选则.
17 # Thanks, David Lombard, for pointing this out.
18 echo -n "$n "
19
20 n=$(($n + 1))
21 echo -n "$n "
22
23 : $[ n = $n + 1 ]
24 # ":" 是必须的,这是因为,如果没有":"的话,Bash将
25 #+ 尝试把"$[ n = $n + 1 ]" 解释成一个命令
26 # 即使"n"被初始化成为一个字符串,这句也能工作.
27 echo -n "$n "
28
29 n=$[ $n + 1 ]
30 # 即使"n"被初始化成为一个字符串,这句也能工作.
31 #* Avoid this type of construct, since it is obsolete and nonportable.
31 #* 尽量避免这种类型的结果,因为这已经被废弃了,并且不具可移植性.
32 # Thanks, Stephane Chazelas.
33 echo -n "$n "
34
35 # 现在来个C风格的增量操作.
36 # Thanks, Frank Wang, for pointing this out.
37
38 let "n++" # let "++n" also works.
39 echo -n "$n "
40
41 (( n++ )) # (( ++n ) also works.
42 echo -n "$n "
43
44 : $(( n++ )) # : $(( ++n )) also works.
45 echo -n "$n "
46
47 : $[ n++ ] # : $[ ++n ]] also works
48 echo -n "$n "
49
50 echo
51
52 exit 0
################################End Script#########################################

注意:在Bash中的整型变量事实上是32位的,范围是 -2147483648 到2147483647.如果超过这个
范围进行算术操作,将不会得到你期望的结果(就是溢出么).
1 a=2147483646
2 echo "a = $a" # a = 2147483646
3 let "a+=1" # 加1 "a".
4 echo "a = $a" # a = 2147483647
5 let "a+=1" # 再加1 "a" ,将超过上限了.
6 echo "a = $a" # a = -2147483648
7 # 错误 (溢出了)
在Bash 2.05b版本中,Bash支持64位整型了.

注意:Bash并不能理解浮点运算.它把包含的小数点看作字符串.
1 a=1.5
2
3 let "b = $a + 1.3" # 错误.
4 # t2.sh: let: b = 1.5 + 1.3: 表达式的语义错误(错误标志为".5 + 1.3")
5
6 echo "b = $b" # b=1
如果真想做浮点运算的话,使用bc(见12.8节),bc可以进行浮点运算或调用数学库函数.

位操作符.
(晕,有点强大过分了吧,位级操作都支持.)
位操作符在shell脚本中极少使用.它们最主要的用途看起来就是操作和test从sockets中
读出的变量."Bit flipping"与编译语言的联系很紧密,比如c/c++,在这种语言中它可以
运行得足够快.(原文有处on the fly,我查了一下,好像是没事干的意思,没理解)

<< 左移1位(每次左移都将乘2)

<<= 左移几位,=号后边将给出左移几位
let "var <<= 2"就是左移2位(就是乘4)

>> 右移1位(每次右移都将除2)

>>= 右移几位

& 按位与

&= 按位与赋值

| 按位或

|= 按位或赋值

~ 按位非

! 按位否?(没理解和上边的~有什么区别?),感觉是应该放到下边的逻辑操作中

^ 按位异或XOR

^= 异或赋值


逻辑操作:

&& 逻辑与
1 if [ $condition1 ] && [ $condition2 ]
2 # 与: if [ $condition1 -a $condition2 ] 相同
3 # 如果condition1和condition2都为true,那结果就为true.
4
5 if [[ $condition1 && $condition2 ]] # 也可以.
6 # 注意&&不允许出现在[ ... ]中.
注意:&&也可以用在and list中(见25章),但是使用的时候需要依赖上下文.

|| 逻辑或
1 if [ $condition1 ] || [ $condition2 ]
2 # 与: if [ $condition1 -o $condition2 ] 相同
3 # 如果condition1或condition2为true,那结果就为true.
4
5 if [[ $condition1 || $condition2 ]] # 也可以
6 # 注意||不允许出现在[ ... ]中.
注意:Bash将test每个连接到逻辑操作的状态的退出状态(见第6章).


Example 8-3 使用&&和||进行混合状态的test
################################Start Script#######################################
1 #!/bin/bash
2
3 a=24
4 b=47
5
6 if [ "$a" -eq 24 ] && [ "$b" -eq 47 ]
7 then
8 echo "Test #1 succeeds."
9 else
10 echo "Test #1 fails."
11 fi
12
13 # 错误: if [ "$a" -eq 24 && "$b" -eq 47 ]
14 #+ 尝试执行' [ "$a" -eq 24 '
15 #+ 因为没找到']'所以失败了.
16 #
17 # 注意: 如果 [[ $a -eq 24 && $b -eq 24 ]] 能够工作.
18 # 那这个[[]]的test结构就比[]结构更灵活了.
19 #
20 # (在17行的"&&"与第6行的"&&"意义不同)
21 # Thanks, Stephane Chazelas, for pointing this out.
22
23
24 if [ "$a" -eq 98 ] || [ "$b" -eq 47 ]
25 then
26 echo "Test #2 succeeds."
27 else
28 echo "Test #2 fails."
29 fi
30
31
32 # -a和-o选项提供了
33 #+ 一种可选的混合test方法.
34 # Thanks to Patrick Callahan for pointing this out.
35
36
37 if [ "$a" -eq 24 -a "$b" -eq 47 ]
38 then
39 echo "Test #3 succeeds."
40 else
41 echo "Test #3 fails."
42 fi
43
44
45 if [ "$a" -eq 98 -o "$b" -eq 47 ]
46 then
47 echo "Test #4 succeeds."
48 else
49 echo "Test #4 fails."
50 fi
51
52
53 a=rhino
54 b=crocodile
55 if [ "$a" = rhino ] && [ "$b" = crocodile ]
56 then
57 echo "Test #5 succeeds."
58 else
59 echo "Test #5 fails."
60 fi
61
62 exit 0
################################End Script#########################################
&&和||操作也能在算术运算的上下文中找到.
bash$ echo $(( 1 && 2 )) $((3 && 0)) $((4 || 0)) $((0 || 0))
1 0 1 0

混杂操作:
, 逗号操作符
逗号操作符可以连接2个或多个算术运算.所有的操作都会被执行,但是只有最后一个
操作作为结果.
1 let "t1 = ((5 + 3, 7 - 1, 15 - 4))"
2 echo "t1 = $t1" # t1 = 11
3
4 let "t2 = ((a = 9, 15 / 3))" # Set "a" and calculate "t2".
5 echo "t2 = $t2 a = $a" # t2 = 5 a = 9
","主要用在for循环中,具体见Example 10-12.


8.2 数字常量
------------
shell脚本默认都是将数字作为10进制数处理,除非这个数字某种特殊的标记法或前缀开头.
以0开头就是8进制.以0x开头就是16进制数.使用BASE#NUMBER这种形式可以表示其它进制
表示法

Example 8-4 数字常量的处理
################################Start Script#######################################
1 #!/bin/bash
2 # numbers.sh: 数字常量的几种不同的表示法
3
4 # 10进制: 默认
5 let "dec = 32"
6 echo "decimal number = $dec" # 32
7 # 一切都很正常
8
9
10 # 8进制: 以'0'(零)开头
11 let "oct = 032"
12 echo "octal number = $oct" # 26
13 # 表达式的结果用10进制表示.
14 #
15
16 # 16进制表示:数字以'0x'或者'0X'开头
17 let "hex = 0x32"
18 echo "hexadecimal number = $hex" # 50
19 # 表达式的结果用10进制表示.
20
21 # 其它进制: BASE#NUMBER
22 # BASE between 2 and 64.
22 # 2到64进制都可以.
23 # NUMBER必须在BASE的范围内,具体见下边.
24
25
26 let "bin = 2#111100111001101"
27 echo "binary number = $bin" # 31181
28
29 let "b32 = 32#77"
30 echo "base-32 number = $b32" # 231
31
32 let "b64 = 64#@_"
33 echo "base-64 number = $b64" # 4031
34 # 这种64进制的表示法中的每位数字都必须在64进制表示法的限制字符内.
35 # 10 个数字+ 26 个小写字母+ 26 个大写字母+ @ + _
36
37
38 echo
39
40 echo $((36#zz)) $((2#10101010)) $((16#AF16)) $((53#1aA))
41 # 1295 170 44822 3375
42
43
44 # 重要的注意事项:
45 # ---------------
46 # 如果使用的每位数字超出了这个进制表示法规定字符的范围的话,
47 #+ 将给出一个错误消息.
48
49 let "bad_oct = 081"
50 # (部分的) 错误消息输出:
51 # bad_oct = 081: too great for base (error token is "081")
52 # Octal numbers use only digits in the range 0 - 7.
53
54 exit 0 # Thanks, Rich Bartell and Stephane Chazelas, for clarification.
################################End Script#########################################



第三部分 超越基本
++++++++++++++++++++


第9章 变量重游
================
如果变量使用恰当,将会增加脚本的能量和灵活性.但是前提是这需要仔细学习变量的细节知识.


9.1 内部变量
------------
Builtin variable
这些内建的变量,将影响bash脚本的行为.

$BASH
这个变量将指向Bash的二进制执行文件的位置.
bash$ echo $BASH
/bin/bash

$BASH_ENV
这个环境变量将指向一个Bash启动文件,这个启动文件将在调用一个脚本时被读取.

$BASH_SUBSHELL
这个变量将提醒subshell的层次,这是一个在version3才被添加到Bash中的新特性.
见Example 20-1.

$BASH_VERSINFO[n]
记录Bash安装信息的一个6元素的数组.与下边的$BASH_VERSION很像,但这个更加详细.
1 # Bash version info:
2
3 for n in 0 1 2 3 4 5
4 do
5 echo "BASH_VERSINFO[$n] = ${BASH_VERSINFO[$n]}"
6 done
7
8 # BASH_VERSINFO[0] = 3 # 主版本号
9 # BASH_VERSINFO[1] = 00 # 次版本号
10 # BASH_VERSINFO[2] = 14 # Patch 次数.
11 # BASH_VERSINFO[3] = 1 # Build version.
12 # BASH_VERSINFO[4] = release # Release status.
13 # BASH_VERSINFO[5] = i386-redhat-linux-gnu # Architecture

$BASH_VERSION
安装在系统上的Bash的版本号.
bash$ echo $BASH_VERSION
3.00.14(1)-release
tcsh% echo $BASH_VERSION
BASH_VERSION: Undefined variable.
使用这个变量对于判断系统上到底运行的是那个shll来说是一种非常好的办法.$SHELL
有时将不能给出正确的答案.

$DIRSTACK
在目录栈中最上边的值(将受到pushd和popd的影响).
这个内建的变量与dirs命令是保持一致的,但是dirs命令将显示目录栈的整个内容.

$EDITOR
脚本调用的默认编辑器,一般是vi或者是emacs.

$EUID
"effective"用户ID号.
当前用户被假定的任何id号.可能在su命令中使用.
注意:$EUID并不一定与$UID相同.

$FUNCNAME
当前函数的名字.
1 xyz23 ()
2 {
3 echo "$FUNCNAME now executing." # xyz23 现在正在被执行.
4 }
5
6 xyz23
7
8 echo "FUNCNAME = $FUNCNAME" # FUNCNAME =
9 # 出了函数就变为Null值了.

$GLOBIGNORE
一个文件名的模式匹配列表,如果在file globbing中匹配到的文件包含这个列表中的
某个文件,那么这个文件将被从匹配到的文件中去掉.

$GROUPS
当前用户属于的组.
这是一个当前用户的组id列表(数组),就像在/etc/passwd中记录的一样.
root# echo $GROUPS
0

root# echo ${GROUPS[1]}
1

root# echo ${GROUPS[5]}
6

$HOME
用户的home目录,一般都是/home/username(见Example 9-14)

$HOSTNAME
hostname命令将在一个init脚本中,在启动的时候分配一个系统名字.
gethostname()函数将用来设置这个$HOSTNAME内部变量.(见Example 9-14)

$HOSTTYPE
主机类型
就像$MACHTYPE,识别系统的硬件.
bash$ echo $HOSTTYPE
i686

$IFS
内部域分隔符.
这个变量用来决定Bash在解释字符串时如何识别域,或者单词边界.
$IFS默认为空白(空格,tab,和新行),但可以修改,比如在分析逗号分隔的数据文件时.
注意:$*使用$IFS中的第一个字符,具体见Example 5-1.
bash$ echo $IFS | cat -vte
$

bash$ bash -c 'set w x y z; IFS=":-;"; echo "$*"'
w:x:y:o

注意:$IFS并不像它处理其它字符一样处理空白.

Example 9-1 $IFS和空白
################################Start Script#######################################
1 #!/bin/bash
2 # $IFS 处理空白的方法,与处理其它字符不同.
3
4 output_args_one_per_line()
5 {
6 for arg
7 do echo "[$arg]"
8 done
9 }
10
11 echo; echo "IFS=\" \""
12 echo "-------"
13
14 IFS=" "
15 var=" a b c "
16 output_args_one_per_line $var # output_args_one_per_line `echo " a b c "`
17 #
18 # [a]
19 # [b]
20 # [c]
21
22
23 echo; echo "IFS=:"
24 echo "-----"
25
26 IFS=:
27 var=":a::b:c:::" # 与上边的一样,但是用" "替换了":"
28 output_args_one_per_line $var
29 #
30 # []
31 # [a]
32 # []
33 # [b]
34 # [c]
35 # []
36 # []
37 # []
38
39 # 同样的事情也会发生在awk中的"FS"域分隔符.
40
41 # Thank you, Stephane Chazelas.
42
43 echo
44
45 exit 0
################################End Script#########################################
Example 12-37也是使用$IFS的另一个启发性的例子.

$IGNOREEOF
忽略EOF: 告诉shell在log out之前要忽略多少文件结束符(control-D).

$LC_COLLATE
常在.bashrc或/etc/profile中设置,这个变量用来在文件名扩展和模式匹配校对顺序.
如果$LC_COLLATE被错误的设置,那么将会在filename globbing中引起错误的结果.

注意:在2.05以后的Bash版本中,filename globbing将不在对[]中的字符区分大小写.
比如:ls [A-M]* 将即匹配File1.txt也会匹配file1.txt.为了恢复[]的习惯用法,
设置$LC_COLLATE的值为c,使用export LC_COLLATE=c 在/etc/profile或者是
~/.bashrc中.

$LC_CTYPE
这个内部变量用来控制globbing和模式匹配的字符串解释.

$LINENO
这个变量记录它所在的shell脚本中它所在行的行号.这个变量一般用于调试目的.
1 # *** BEGIN DEBUG BLOCK ***
2 last_cmd_arg=$_ # Save it.
3
4 echo "At line number $LINENO, variable \"v1\" = $v1"
5 echo "Last command argument processed = $last_cmd_arg"
6 # *** END DEBUG BLOCK ***

$MACHTYPE
系统类型
提示系统硬件
bash$ echo $MACHTYPE
i686

$OLDPWD
老的工作目录("OLD-print-working-directory",你所在的之前的目录)

$OSTYPE
操作系统类型.
bash$ echo $OSTYPE
linux

$PATH
指向Bash外部命令所在的位置,一般为/usr/bin,/usr/X11R6/bin,/usr/local/bin等.
当给出一个命令时,Bash将自动对$PATH中的目录做一张hash表.$PATH中以":"分隔的
目录列表将被存储在环境变量中.一般的,系统存储的$PATH定义在/ect/processed或
~/.bashrc中(见Appendix G).

bash$ echo $PATH
/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin:/sbin:/usr/sbin

PATH=${PATH}:/opt/bin将把/opt/bin目录附加到$PATH变量中.在脚本中,这是一个
添加目录到$PATH中的便捷方法.这样在这个脚本退出的时候,$PATH将会恢复(因为这个
shell是个子进程,像这样的一个脚本是不会将它的父进程的环境变量修改的)

注意:当前的工作目录"./"一般都在$PATH中被省去.

$PIPESTATUS
数组变量将保存最后一个运行的前台管道的退出码.有趣的是,这个退出码和最后一个命令
运行的退出码并不一定相同.
bash$ echo $PIPESTATUS
0

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo $PIPESTATUS
141

bash$ ls -al | bogus_command
bash: bogus_command: command not found
bash$ echo $?
127

$PIPESTATUS数组的每个成员都会保存一个管道命令的退出码,$PIPESTATUS[0]保存第
一个管道命令的退出码,$PIPESTATUS[1]保存第2个,以此类推.

注意:$PIPESTATUS变量在一个login shell中可能会包含一个错误的0值(3.0以下版本)
tcsh% bash

bash$ who | grep nobody | sort
bash$ echo ${PIPESTATUS[*]}
0
包含在脚本中的上边这行将会产生一个期望的输出0 1 0.

注意:在某些上下文$PIPESTATUS可能不会给出正确的结果.
bash$ echo $BASH_VERSION
3.00.14(1)-release

bash$ $ ls | bogus_command | wc
bash: bogus_command: command not found
0 0 0

bash$ echo ${PIPESTATUS[@]}
141 127 0

Chet Ramey把上边输出不成确原因归咎于ls的行为.因为如果把ls的结果放到管道上,
并且这个输出没被读取,那么SIGPIPE将会kill掉它,并且退出码变为141,而不是我们期
望的0.这种情况也会发生在tr命令中.

注意:$PIPESTATUS是一个"volatile"变量.在任何命令插入之前,并且在pipe询问之后,
这个变量需要立即被捕捉.
bash$ $ ls | bogus_command | wc
bash: bogus_command: command not found
0 0 0

bash$ echo ${PIPESTATUS[@]}
0 127 0

bash$ echo ${PIPESTATUS[@]}
0

$PPID
一个进程的$PPID就是它的父进程的进程id(pid).[1]
使用pidof命令对比一下.

$PROMPT_COMMAND
这个变量保存一个在主提示符($PS1)显示之前需要执行的命令.

$PS1
主提示符,具体见命令行上的显示.

$PS2
第2提示符,当你需要额外的输入的时候将会显示,默认为">".

$PS3
第3提示符,在一个select循环中显示(见Example 10-29).

$PS4
第4提示符,当使用-x选项调用脚本时,这个提示符将出现在每行的输出前边.
默认为"+".

$PWD
工作目录(你当前所在的目录).
与pwd内建命令作用相同.
################################Start Script#######################################
1 #!/bin/bash
2
3 E_WRONG_DIRECTORY=73
4
5 clear # 清屏.
6
7 TargetDirectory=/home/bozo/projects/GreatAmericanNovel
8
9 cd $TargetDirectory
10 echo "Deleting stale files in $TargetDirectory."
11
12 if [ "$PWD" != "$TargetDirectory" ]
13 then # 防止偶然删除错误的目录
14 echo "Wrong directory!"
15 echo "In $PWD, rather than $TargetDirectory!"
16 echo "Bailing out!"
17 exit $E_WRONG_DIRECTORY
18 fi
19
20 rm -rf *
21 rm .[A-Za-z0-9]* # Delete dotfiles.
21 rm .[A-Za-z0-9]* # 删除"."文件(隐含文件).
22 # rm -f .[^.]* ..?* 为了删除以多个"."开头的文件.
23 # (shopt -s dotglob; rm -f *) 也行.
24 # Thanks, S.C. for pointing this out.
25
26 # 文件名能够包含0-255范围的所有字符,除了"/".
27 # 删除以各种诡异字符开头的文件将作为一个练习留给大家.
28
29 # 这里预留给其他的必要操作.
30
31 echo
32 echo "Done."
33 echo "Old files deleted in $TargetDirectory."
34 echo
35
36
37 exit 0
################################End Script#########################################

$REPLY
read命令如果没有给变量,那么输入将保存在$REPLY中.在select菜单中也可用,但是只
提供选择的变量的项数,而不是变量本身的值.
################################Start Script#######################################
1 #!/bin/bash
2 # reply.sh
3
4 # REPLY是'read'命令结果保存的默认变量.
5
6 echo
7 echo -n "What is your favorite vegetable? "
8 read
9
10 echo "Your favorite vegetable is $REPLY."
11 # 当且仅当在没有变量提供给"read"命令时,
12 #+ REPLY才保存最后一个"read"命令读入的值.
13
14 echo
15 echo -n "What is your favorite fruit? "
16 read fruit
17 echo "Your favorite fruit is $fruit."
18 echo "but..."
19 echo "Value of \$REPLY is still $REPLY."
20 # $REPLY还是保存着上一个read命令的值,
21 #+ 因为变量$fruit被传入到了这个新的"read"命令中.
22
23 echo
24
25 exit 0
################################End Script#########################################

$SECONDS
这个脚本已经运行的时间(单位为秒).
################################Start Script#######################################
1 #!/bin/bash
2
3 TIME_LIMIT=10
4 INTERVAL=1
5
6 echo
7 echo "Hit Control-C to exit before $TIME_LIMIT seconds."
8 echo
9
10 while [ "$SECONDS" -le "$TIME_LIMIT" ]
11 do
12 if [ "$SECONDS" -eq 1 ]
13 then
14 units=second
15 else
16 units=seconds
17 fi
18
19 echo "This script has been running $SECONDS $units."
20 # 在一台比较慢的或者是负载很大的机器上,这个脚本可能会跳过几次循环
21 #+ 在一个while循环中.
22 sleep $INTERVAL
23 done
24
25 echo -e "\a" # Beep!
26
27 exit 0
################################End Script#########################################

$SHELLOPTS
这个变量里保存shell允许的选项,这个变量是只读的.
bash$ echo $SHELLOPTS
braceexpand:hashall:histexpand:monitor:history:interactive-comments:emacs

$SHLVL
Shell层次,就是shell层叠的层次,如果是命令行那$SHLVL就是1,如果命令行执行的脚
本中,$SHLVL就是2,以此类推.

$TMOUT
如果$TMOUT环境变量被设置为一个非零的时间值,那么在过了这个指定的时间之后,
shell提示符将会超时,这会引起一个logout.

在2.05b版本的Bash中,已经支持在一个带有read命令的脚本中使用$TMOUT变量.
1 # 需要使用Bash v2.05b或者以后的版本上
2
3 TMOUT=3 # Prompt times out at three seconds.
3 TMOUT=3 # 设置超时的时间为3秒
4
5 echo "What is your favorite song?"
6 echo "Quickly now, you only have $TMOUT seconds to answer!"
7 read song
8
9 if [ -z "$song" ]
10 then
11 song="(no answer)"
12 # 默认响应.
13 fi
14
15 echo "Your favorite song is $song."

这里有一个更复杂的方法来在一个脚本中实现超时功能.一种办法就是建立一个时间循
环,在超时的时候通知脚本.不过,这也需要一个信号处理机制,在超时的时候来产生中
断.
(参见Example 29-5)

Example 9-2 时间输入
################################Start Script#######################################
1 #!/bin/bash
2 # timed-input.sh
3
4 # TMOUT=3 在新版本的Bash上也能工作.
5
6
7 TIMELIMIT=3 # 在这个例子上是3秒,也可以设其他的值.
8
9 PrintAnswer()
10 {
11 if [ "$answer" = TIMEOUT ]
12 then
13 echo $answer
14 else # 别想混合着两个例子.
15 echo "Your favorite veggie is $answer"
16 kill $! # kill将不再需要TimerOn函数运行在后台.
17 # $! 是运行在后台的最后一个工作的PID.
18 fi
19
20 }
21
22
23
24 TimerOn()
25 {
26 sleep $TIMELIMIT && kill -s 14 $$ &
27 # 等待3秒,然后发送一个信号给脚本.
28 }
29
30 Int14Vector()
31 {
32 answer="TIMEOUT"
33 PrintAnswer
34 exit 14
35 }
36
37 trap Int14Vector 14 # 为了我们的目的,时间中断(14)被破坏了.
38
39 echo "What is your favorite vegetable "
40 TimerOn
41 read answer
42 PrintAnswer
43
44
45 # 很明显的,这是一个拼凑的实现.
46 #+ 然而使用"-t"选项来"read"的话,将会简化这个任务.
47 # 见"t-out.sh",在下边.
48
49 # 如果你需要一个真正的幽雅的写法...
50 #+ 建议你使用c/c++来写这个应用,
51 #+ 使用合适的库来完成这个任务,比如'alarm'和'setitimer'.
52
53 exit 0
################################End Script#########################################
使用stty也是一种选择.

Example 9-3 再来一个时间输入
################################Start Script#######################################
1 #!/bin/bash
2 # timeout.sh
3
4 # Stephane Chazelas编写,
5 #+ 本书作者进行了一些修改.
6
7 INTERVAL=5 # timeout间隔
8
9 timedout_read() {
10 timeout=$1
11 varname=$2
12 old_tty_settings=`stty -g`
13 stty -icanon min 0 time ${timeout}0
14 eval read $varname # 或者就是 read $varname
15 stty "$old_tty_settings"
16 # 察看"stty"的man页.
17 }
18
19 echo; echo -n "What's your name? Quick! "
20 timedout_read $INTERVAL your_name
21
22 # 这种方法可能不是每个终端类型都可以正常使用的.
23 # 最大的timeout依赖于具体的终端.
24 #+ (一般都是25.5秒).
25
26 echo
27
28 if [ ! -z "$your_name" ] # If name input before timeout...
29 then
30 echo "Your name is $your_name."
31 else
32 echo "Timed out."
33 fi
34
35 echo
36
37 # 这个脚本的行为可能与"timed-input.sh"有点不同.
38 # 在每次按键的时候,计数器都会重置.
39
40 exit 0
################################End Script#########################################
或许,最简单的办法就是使用-t选项来read了.

Example 9-4 Timed read
################################Start Script#######################################
1 #!/bin/bash
2 # t-out.sh
3 # "syngin seven"的一个很好的提议 (thanks).
4
5
6 TIMELIMIT=4 # 4 seconds
7
8 read -t $TIMELIMIT variable <&1
9 # ^^^
10 # 在这个例子中,对于Bash 1.x和2.x就需要使用"<&1"
11 # 但对于Bash 3.x就不需要.
12
13 echo
14
15 if [ -z "$variable" ] # Is null?
16 then
17 echo "Timed out, variable still unset."
18 else
19 echo "variable = $variable"
20 fi
21
22 exit 0
################################End Script#########################################

$UID
用户ID号.
当前用户的id号,在/etc/passwd中记录.
这个值不会因为用户使用了su命令而改变.$UID是只读变量,不容易在命令行或者是脚
本中被修改,并且和内建的id命令很相像.
Example 9-5 我是root?
################################Start Script#######################################
1 #!/bin/bash
2 # am-i-root.sh: 我是不是root用户?
3
4 ROOT_UID=0 # Root的$UID是0.
5
6 if [ "$UID" -eq "$ROOT_UID" ] # 是否是root用户,请站出来.
7 then
8 echo "You are root."
9 else
10 echo "You are just an ordinary user (but mom loves you just the same)."
11 fi
12
13 exit 0
14
15
16 # ============================================================= #
17 # 下边的代码将不被执行,因为脚本已经退出了.
18
19 # 检验是root用户的一种可选方法:
20
21 ROOTUSER_NAME=root
22
23 username=`id -nu` # Or... username=`whoami`
24 if [ "$username" = "$ROOTUSER_NAME" ]
25 then
26 echo "Rooty, toot, toot. You are root."
27 else
28 echo "You are just a regular fella."
29 fi
################################End Script#########################################
见例子Example 2-3
注意:变量$ENV,$LOGNAME,$MAIL,$TERM,$USER,和$USERNAME并不是Bash的内建变量.它
们经常被设置成环境变量,它们一般都放在Bash的安装文件中.$SHELL,用户登录的
shell的名字,可能是从/etc/passwd设置的,也可能是在一个"init"脚本中设置的,同样
的,它也不是Bash的内建变量.
tcsh% echo $LOGNAME
bozo
tcsh% echo $SHELL
/bin/tcsh
tcsh% echo $TERM
rxvt

bash$ echo $LOGNAME
bozo
bash$ echo $SHELL
/bin/tcsh
bash$ echo $TERM
rxvt

位置参数
$0, $1, $2,等等...
位置参数,从命令行传递给脚本,或者是传递给函数.或者赋职给一个变量.
(具体见Example 4-5和Example 11-15)

$#
命令行或者是位置参数的个数.(见Example 33-2)

$*
所有的位置参数,被作为一个单词.
注意:"$*"必须被""引用.

$@
与$*同义,但是每个参数都是一个独立的""引用字串,这就意味着参数被完整地传递,
并没有被解释和扩展.这也意味着,每个参数列表中的每个参数都被当成一个独立的
单词.
注意:"$@"必须被引用.

Example 9-6 arglist:通过$*和$@列出所有的参数
################################Start Script#######################################
1 #!/bin/bash
2 # arglist.sh
3 # 多使用几个参数来调用这个脚本,比如"one tow three".
4
5 E_BADARGS=65
6
7 if [ ! -n "$1" ]
8 then
9 echo "Usage: `basename $0` argument1 argument2 etc."
10 exit $E_BADARGS
11 fi
12
13 echo
14
15 index=1 # 初始化数量.
16
17 echo "Listing args with \"\$*\":"
18 for arg in "$*" # 如果"$*"不被""引用,那么将不能正常地工作
19 do
20 echo "Arg #$index = $arg"
21 let "index+=1"
22 done # $* sees all arguments as single word.
22 done # $* 认为所有的参数为一个单词
23 echo "Entire arg list seen as single word."
24
25 echo
26
27 index=1 # 重置数量.
28 # 如果你忘了这句会发生什么?
29
30 echo "Listing args with \"\$@\":"
31 for arg in "$@"
32 do
33 echo "Arg #$index = $arg"
34 let "index+=1"
35 done # $@ 认为每个参数都一个单独的单词.
36 echo "Arg list seen as separate words."
37
38 echo
39
40 index=1 # 重置数量.
41
42 echo "Listing args with \$* (unquoted):"
43 for arg in $*
44 do
45 echo "Arg #$index = $arg"
46 let "index+=1"
47 done # 未""引用的$*把参数作为独立的单词.
48 echo "Arg list seen as separate words."
49
50 exit 0
################################End Script#########################################

在shift命令后边,$@将保存命令行中剩余的参数,而$1被丢掉了.
1 #!/bin/bash
2 # 使用 ./scriptname 1 2 3 4 5 来调用这个脚本
3
4 echo "$@" # 1 2 3 4 5
5 shift
6 echo "$@" # 2 3 4 5
7 shift
8 echo "$@" # 3 4 5
9
10 # 每个"shift"都丢弃$1.
11 # "$@" 将包含剩下的参数.
$@也作为为工具使用,用来过滤传给脚本的输入.
cat "$@"结构接受从stdin传来的输入,也接受从参数中指定的文件传来的输入.
具体见Example 12-21和Example 12-22.

注意:$*和$@的参数有时会不一致,发生令人迷惑的行为,这依赖于$IFS的设置.

Example 9-7 不一致的$*和$@行为
################################Start Script#######################################
1 #!/bin/bash
2
3 # "$*"和"$@"的古怪行为,
4 #+ 依赖于它们是否被""引用.
5 # 单词拆分和换行的不一致处理.
6
7
8 set -- "First one" "second" "third:one" "" "Fifth: :one"
9 # 设置这个脚本参数,$1,$2,等等.
10
11 echo
12
13 echo 'IFS unchanged, using "$*"'
14 c=0
15 for i in "$*" # 引用
16 do echo "$((c+=1)): [$i]" # 这行在下边的每个例子中都一样.
17 # Echo参数.
18 done
19 echo ---
20
21 echo 'IFS unchanged, using $*'
22 c=0
23 for i in $* # 未引用
24 do echo "$((c+=1)): [$i]"
25 done
26 echo ---
27
28 echo 'IFS unchanged, using "$@"'
29 c=0
30 for i in "$@"
31 do echo "$((c+=1)): [$i]"
32 done
33 echo ---
34
35 echo 'IFS unchanged, using $@'
36 c=0
37 for i in $@
38 do echo "$((c+=1)): [$i]"
39 done
40 echo ---
41
42 IFS=:
43 echo 'IFS=":", using "$*"'
44 c=0
45 for i in "$*"
46 do echo "$((c+=1)): [$i]"
47 done
48 echo ---
49
50 echo 'IFS=":", using $*'
51 c=0
52 for i in $*
53 do echo "$((c+=1)): [$i]"
54 done
55 echo ---
56
57 var=$*
58 echo 'IFS=":", using "$var" (var=$*)'
59 c=0
60 for i in "$var"
61 do echo "$((c+=1)): [$i]"
62 done
63 echo ---
64
65 echo 'IFS=":", using $var (var=$*)'
66 c=0
67 for i in $var
68 do echo "$((c+=1)): [$i]"
69 done
70 echo ---
71
72 var="$*"
73 echo 'IFS=":", using $var (var="$*")'
74 c=0
75 for i in $var
76 do echo "$((c+=1)): [$i]"
77 done
78 echo ---
79
80 echo 'IFS=":", using "$var" (var="$*")'
81 c=0
82 for i in "$var"
83 do echo "$((c+=1)): [$i]"
84 done
85 echo ---
86
87 echo 'IFS=":", using "$@"'
88 c=0
89 for i in "$@"
90 do echo "$((c+=1)): [$i]"
91 done
92 echo ---
93
94 echo 'IFS=":", using $@'
95 c=0
96 for i in $@
97 do echo "$((c+=1)): [$i]"
98 done
99 echo ---
100
101 var=$@
102 echo 'IFS=":", using $var (var=$@)'
103 c=0
104 for i in $var
105 do echo "$((c+=1)): [$i]"
106 done
107 echo ---
108
109 echo 'IFS=":", using "$var" (var=$@)'
110 c=0
111 for i in "$var"
112 do echo "$((c+=1)): [$i]"
113 done
114 echo ---
115
116 var="$@"
117 echo 'IFS=":", using "$var" (var="$@")'
118 c=0
119 for i in "$var"
120 do echo "$((c+=1)): [$i]"
121 done
122 echo ---
123
124 echo 'IFS=":", using $var (var="$@")'
125 c=0
126 for i in $var
127 do echo "$((c+=1)): [$i]"
128 done
129
130 echo
131
132 # 用ksh或者zsh -y来试试这个脚本.
133
134 exit 0
135
136 # This example script by Stephane Chazelas,
137 # and slightly modified by the document author.
################################End Script#########################################
注意:$@和$*中的参数只有在""中才会不同.

Example 9-8 当$IFS为空时的$*和$@
################################Start Script#######################################
1 #!/bin/bash
2
3 # 如果$IFS被设置为空时,
4 #+ 那么"$*" 和"$@" 将不会象期望那样echo出位置参数.
5
6 mecho () # Echo 位置参数.
7 {
8 echo "$1,$2,$3";
9 }
10
11
12 IFS="" # 设置为空.
13 set a b c # 位置参数.
14
15 mecho "$*" # abc,,
16 mecho $* # a,b,c
17
18 mecho $@ # a,b,c
19 mecho "$@" # a,b,c
20
21 # 当$IFS设置为空时,$* 和$@ 的行为依赖于
22 #+ 正在运行的Bash或者sh的版本.
23 # 所以在脚本中使用这种"feature"不是明智的行为.
24
25
26 # Thanks, Stephane Chazelas.
27
28 exit 0
################################End Script#########################################

其他的特殊参数

$-
传递给脚本的falg(使用set命令).参考Example 11-15.

注意:这起初是ksh的特征,后来被引进到Bash中,但不幸的是,在Bash中它看上去也不
能可靠的工作.使用它的一个可能的方法就是让这个脚本进行自我测试(查看是否是交
互的).

$!
在后台运行的最后的工作的PID(进程ID).
1 LOG=$0.log
2
3 COMMAND1="sleep 100"
4
5 echo "Logging PIDs background commands for script: $0" >> "$LOG"
6 # 所以它们可以被监控,并且在必要的时候kill掉.
7 echo >> "$LOG"
8
9 # Logging 命令.
10
11 echo -n "PID of \"$COMMAND1\": " >> "$LOG"
12 ${COMMAND1} &
13 echo $! >> "$LOG"
14 # PID of "sleep 100": 1506
15
16 # Thank you, Jacques Lederer, for suggesting this.


1 possibly_hanging_job & { sleep ${TIMEOUT}; eval 'kill -9 $!' &> /dev/null; }
2 # 强制结束一个品行不良的程序.
3 # 很有用,比如在init脚本中.
4
5 # Thank you,Sylvain Fourmanoit,for this creative use of the "!" variable.

$_
保存之前执行的命令的最后一个参数.

Example 9-9 下划线变量
################################Start Script#######################################
1 #!/bin/bash
2
3 echo $_ # /bin/bash
4 # 只是调用/bin/bash来运行这个脚本.
5
6 du >/dev/null # 将没有命令的输出
7 echo $_ # du
8
9 ls -al >/dev/null # 没有命令输出
10 echo $_ # -al (最后的参数)
11
12 :
13 echo $_ # :
################################End Script#########################################

$?
命令,函数或者脚本本身的退出状态(见Example 23-7)

$$
脚本自身的进程ID.这个变量经常用来构造一个"unique"的临时文件名.
(参考Example A-13,Example 29-6,Example 12-28和Example 11-25).
这通常比调用mktemp来得简单.

注意事项:
[1] 当前运行的脚本的PID为$$.
[2] "argument"和"parameter"这两个单词经常不加区分的使用.在这整本书中,这两个
单词的意思完全相同.(在翻译的时候就未加区分,统统翻译成参数)


9.2 操作字符串
--------------
Bash支持超多的字符串操作,操作的种类和数量令人惊异.但不幸的是,这些工具缺乏集中性.
一些是参数替换的子集,但是另一些则属于UNIX的expr命令.这就导致了命令语法的不一致和
功能的重叠,当然也会引起混乱.

字符串长度

${#string}
expr length $string
expr "$string" : '.*'

1 stringZ=abcABC123ABCabc
2
3 echo ${#stringZ} # 15
4 echo `expr length $stringZ` # 15
5 echo `expr "$stringZ" : '.*'` # 15

Example 9-10 在一个文本文件的段间插入空行
################################Start Script#######################################
1 #!/bin/bash
2 # paragraph-space.sh
3
4 # 在一个不空行的文本文件的段间插入空行.
5 # Usage: $0 6
7 MINLEN=45 # 可能需要修改这个值.
8 # 假定行的长度小于$MINLEN指定的长度
9 #+ $MINLEN中的值用来描述多少个字符结束一个段.
10
11 while read line # 对于需要多行输入的文件基本都是这个样子
12 do
13 echo "$line" # 输出line.
14
15 len=${#line}
16 if [ "$len" -lt "$MINLEN" ]
17 then echo # 在短行后边添加一个空行
18 fi
19 done
20
21 exit 0
################################End Script#########################################

从字符串开始的位置匹配子串的长度

expr match "$string" '$substring'
$substring是一个正则表达式

expr "$string" : '$substring'
$substring是一个正则表达式

1 stringZ=abcABC123ABCabc
2 # |------|
3
4 echo `expr match "$stringZ" 'abc[A-Z]*.2'` # 8
5 echo `expr "$stringZ" : 'abc[A-Z]*.2'` # 8

索引

expr index $string $substring
匹配到子串的第一个字符的位置.

1 stringZ=abcABC123ABCabc
2 echo `expr index "$stringZ" C12` # 6
3 # C position.
4
5 echo `expr index "$stringZ" 1c` # 3
6 # 'c' (in #3 position) matches before '1'.

在C语言中最近的等价函数为strchr().

提取子串

${string:position}
在string中从位置$position开始提取子串.
如果$string为"*"或"@",那么将提取从位置$position开始的位置参数,[1]

${string:position:length}
在string中从位置$position开始提取$length长度的子串.

################################Start Script#######################################
1 stringZ=abcABC123ABCabc
2 # 0123456789.....
3 # 0-based indexing.
4
5 echo ${stringZ:0} # abcABC123ABCabc
6 echo ${stringZ:1} # bcABC123ABCabc
7 echo ${stringZ:7} # 23ABCabc
8
9 echo ${stringZ:7:3} # 23A
10 # 3个字符长度的子串.
11
12
13
14 # 有没有可能从字符结尾开始,反向提取子串?
15
16 echo ${stringZ:-4} # abcABC123ABCabc
17 # 以${parameter:-default}方式,默认是提取完整地字符串.
18 # 然而 . . .
19
20 echo ${stringZ:(-4)} # Cabc
21 echo ${stringZ: -4} # Cabc
22 # 现在,它可以工作了.
23 # 使用圆括号或者添加一个空格来转义这个位置参数.
24
25 # Thank you, Dan Jacobson, for pointing this out.
################################End Script#########################################
如果$string参数为"*"或"@",那将最大的提取从$position开始的$length个位置参数.
1 echo ${*:2} # Echo出第2个和后边所有的位置参数.
2 echo ${@:2} # 与前边相同.
3
4 echo ${*:2:3} # 从第2个开始,Echo出后边3个位置参数.


expr substr $string $position $length
在string中从位置$position开始提取$length长度的子串.
1 stringZ=abcABC123ABCabc
2 # 123456789......
3 # 1-based indexing.
4
5 echo `expr substr $stringZ 1 2` # ab
6 echo `expr substr $stringZ 4 3` # ABC

expr match "$string" '\($substring\)'
从$string的开始位置提取$substring,$substring是一个正则表达式.

expr "$string" : '\($substring\)'
从$string的开始位置提取$substring,$substring是一个正则表达式.
1 stringZ=abcABC123ABCabc
2 # =======
3
4 echo `expr match "$stringZ" '\(.[b-c]*[A-Z]..[0-9]\)'` # abcABC1
5 echo `expr "$stringZ" : '\(.[b-c]*[A-Z]..[0-9]\)'` # abcABC1
6 echo `expr "$stringZ" : '\(.......\)'` # abcABC1
7 # All of the above forms give an identical result.

子串削除

${string#substring}
从$string的左边截掉第一个匹配的$substring
${string##substring}
从$string的左边截掉最后一个个匹配的$substring

1 stringZ=abcABC123ABCabc
2 # |----|
3 # |----------|
4
5 echo ${stringZ#a*C} # 123ABCabc
6 # 截掉'a'和'C'之间最近的匹配.
7
8 echo ${stringZ##a*C} # abc
9 # 截掉'a'和'C'之间最远的匹配.


${string%substring}
从$string的右边截掉第一个匹配的$substring
${string%%substring}
从$string的右边截掉最后一个匹配的$substring

1 stringZ=abcABC123ABCabc
2 # ||
3 # |------------|
4
5 echo ${stringZ%b*c} # abcABC123ABCa
6 # 从$stringZ的后边开始截掉'b'和'c'之间的最近的匹配
7
8 echo ${stringZ%%b*c} # a
9 # 从$stringZ的后边开始截掉'b'和'c'之间的最远的匹配

Example 9-11 利用修改文件名,来转换图片格式
################################Start Script#######################################
1 #!/bin/bash
2 # cvt.sh:
3 # 把一个目录下的所有MacPaint格式的图片文件都转换为"pbm"格式的图片文件.
4
5 # 使用来自"netpbm"包的"macptopbm"程序,
6 #+ 这个程序主要是由Brian Henderson(bryanh@giraffe-data.com)来维护的.
7 # Netpbm是大多数Linux发行版的标准部分.
8
9 OPERATION=macptopbm
10 SUFFIX=pbm # 新的文件名后缀
11
12 if [ -n "$1" ]
13 then
14 directory=$1 # 如果目录名作为第1个参数给出...
15 else
16 directory=
文章评论

共有 1 条评论

  1. qxfsy 于 2014-01-05 00:34:48发表:

    可惜下不到