魅力博客

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

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



译者序

毫无疑问,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<rojy bug>
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 <scriptname,因为这禁用了脚本从stdin中读数据的功能.)
更方便的方法是让脚本本身就具有可执行权限,通过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中.


返回顶部

发表评论:

Powered By Z-BlogPHP 1.7.3


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