免费注册 登录
»专业资料»IT/计算机»计算机软件及应用»高级Bash脚本编程指南中文版.pdf
收起/展开

高级Bash脚本编程指南中文版

文档类型:pdf 上传时间:2018-06-25 文档页数:325页 文档大小:1.49 M 文档浏览:10839次 文档下载:3次 所需积分:0 学币 文档评分:3.0星

高级Bash脚本编程指南中文版内容摘要: 1.11.21.2.11.2.21.2.2.11.2.2.21.31.3.11.3.21.3.2.11.3.2.21.3.2.31.3.2.41.3.31.3.3.11.3.3.21.3.41.3.51.3.5.11.3.5.21.3.5.31.3.5.41.3.5.51.3.61.3.6.11.3.6.21.3.6.3目錄Introduction第一部分 初见shell1. 为什么使用shell编程2. 和Sha-Bang(#!)一起出发2.1 调用一个脚本2.2 牛刀小试第二部分 shell基础3. 特殊字符4. 变量与参数4.1 变量替换4.2 变量赋值4.3 Bash弱类型变量4.4 特殊变量类型5. 引用5.1 引用变量5.2 转义6. 退出与退出状态7. 测试7.1 测试结构7.2 文件测试操作7.3 其他比较操作7.4 嵌套 if/then 条件测试7.5 牛刀小试8. 运算符相关话题8.1 运算符8.2 数字常量8.3 双圆括号结构11.3.6.41.41.4.11.4.1.11.4.1.21.4.1.31.4.21.4.2.11.4.2.1.11.4.2.1.21.4.2.21.4.31.4.3.11.4.3.21.4.3.31.4.3.41.4.41.4.51.4.61.51.5.11.5.21.5.2.11.5.2.21.5.2.31.5.2.41.5.2.51.5.2.61.5.38.4 运算符优先级第三部分 shell进阶9. 换个角度看变量9.1 内部变量9.2 指定变量属性: 或9.3 :随机产生整数10. 变量处理10.1 字符串处理10.1.1 使用 awk 处理字符串10.1.2 参考资料10.2 参数替换11. 循环与分支11.1 循环11.2 嵌套循环11.3 循环控制11.4 测试与分支12. 命令替换13. 算术扩展14. 休息时间第五部分 进阶话题19. 嵌入文档20. I/O 重定向20.1 使用 exec20.2 重定向代码块20.3 应用程序22. 限制模式的Shell23. 进程替换26. 列表结构25. 别名23《Advanced Bash-Scripting Guide》 inChinese《高级Bash脚本编程指南》Revision 10中文版联系/加入我们邮箱:absguide#linuxstory.org(将#替换为@)QQ群:535442421原著及早期翻译作品原著原著链接:http://tldp.org/LDP/abs/html/原作:Mendel Cooper原著版本:Revision 10, 10 Mar 2014译著早期译著连接:http://www.linuxsir.org/bbs/thread256887.html译者:杨春敏 黄毅译著版本:Revision 3.7, 23 Oct 2005最新 Revision 10 由 Linux Story 社区的 imcmy 同学发起并组织翻译Linux Story 通告地址 :http://www.linuxstory.org/asdvanced-bash-scripting-guide-in-chinese/翻译作品翻译作品放在GitBook上,欢迎阅读!翻译进度Introduction4第一部分 初见Shell[@imcmy][@zihengcat]1. 为什么使用shell编程[@imcmy][@zihengcat]2. Sha-Bang(#!)一起出发[@imcmy][@zihengcat]第二部分 Shell基础[@imcmy][@zihengcat]3. 特殊字符[@imcmy][@zihengcat]4. 变量与参数[@imcmy][@zihengcat]5. 引用[@mr253727942][@zihengcat]6. 退出与退出状态[@samita2030][@zihengcat]7. 测试[@imcmy][@zihengcat]8. 运算符相关话题[@samita2030][@zihengcat]第三部分 Shell进阶[@imcmy]9. Another Look at Variables[@Ninestd]10. 变量处理[@imcmy]11. 循环与分支[@imcmy]12. 命令替换[@imcmy]13. 算术扩展[@imcmy]14. 休息时间[@imcmy]第四部分. 命令[@zhaozq]15. 内建命令[@zhaozq]16. 外部过滤器,程序与命令[@zhaozq]17. 系统与高级命令[@zhaozq]第五章. Advanced Topics18. 正则表达式[@Zjie]18.1 正则表达式简介[@Zjie]18.2 文件名替换[@Zjie]19. 嵌入文档[@mingmings]20. I/O 重定向[@mingmings]21. Subshells[@mingmings]22. Restricted Shells[@panblack]23. Process Substitution[@panblack]24. Functions[@zy416548283]25. 别名[@mingmings]26. List Constructs[@panblack]27. Arrays[@zy416548283]28. Indirect References[@panblack]29. /dev and /proc[@panblack]Introduction530. Network Programming[@Zjie]31. Of Zeros and Nulls[@panblack]32. Debugging[@wuqichao]33. Options[@zy416548283]34. Gotchas[@liuburn]35. Scripting With Style[@chuchingkai]36. Miscellany[@richard-ma]37. Bash, versions 2, 3, and 438. Endnotes[@zy416548283]38.1 Author's Note38.2 About the Author38.3 Where to Go For Help38.4 Tools Used to Produce This Book38.5 Credits38.6 DisclaimerBibliographyAppendixA. Contributed ScriptsB. Reference CardsC. A Sed and Awk Micro-Primer[@wuqichao]C.1 Sed[@wuqichao]C.2 Awk[@wuqichao]D. Parsing and Managing PathnamesE. Exit Codes With Special MeaningsF. A Detailed Introduction to I/O and I/O RedirectionG. Command-Line OptionsG.1 Standard Command-Line OptionsG.2 Bash Command-Line OptionsH. Important FilesI. Important System DirectoriesJ. An Introduction to Programmable CompletionK. LocalizationL. History CommandsM. Sample .bashrc and .bash_profile FilesN. Converting DOS Batch Files to Shell ScriptsO. ExercisesIntroduction6O.1 Analyzing ScriptsO.2 Writing ScriptsP. Revision HistoryQ. Download and Mirror SitesR. To Do ListS. CopyrightT. ASCII TableIndexList of TablesList of Examples翻译校审流程初始化1. 首先fork项目2. 把fork过去的项目clone到本地3. 命令行下运行 git checkout -b dev 创建一个新分支4. 运行 git remote add upstreamhttps://github.com/LinuxStory/Advanced-Bash-Scripting-Guide-in-Chinese.git 添加远端库5. 运行 git remote update 更新6. 运行 git fetch upstream master 拉取更新到本地7. 运行 git rebase upstream/master 将更新合并到你的分支初始化只需要做一遍,之后请在dev分支进行修改。如果修改过程中项目有更新,请重复5、6、7步。翻译校审流程1. 保证在dev分支中2. 打开README.md,在翻译进度后加上你自己的github名1. Shell Programming! [@翻译人][@校审人]3. 本地提交修改,写明提交信息4. push到你fork的项目中,然后登录GitHubIntroduction75. 在你fork的项目的首页可以看到一个 pull request 按钮,点击它,填写说明信息,然后提交即可为了不重复工作,请等待我们确认了你的pull request(即你的名字出现在项目中时),再进行翻译校审工作6. 进行翻译校审,重复3-5步提交翻译校审的作品新手可以参阅针对github小白的《翻译流程详解》,妹子写的呦~翻译校审建议1. 使用markdown进行翻译校审,文件名必须使用英文2. 翻译校审后的文档请放到source文件夹下的对应章节中,然后pull request即可3. 有任何问题随时欢迎发issue4. 术语尽量保证和已翻译的一致,也可以查询微软术语搜索或Linux中国术语词典5. 你可以将你认为是术语的词汇加入术语表 TERM.md 中关于版权根据原著作者的要求,翻译成果属于公有领域(CC0),翻译参与人员及原著作者Mendel Cooper享有署名权Introduction8第一部分 初见Shell脚本:文章;书面文档——韦伯斯特字典1913年版Shell是一种命令解释器,它不仅分离了用户层与操作系统内核,更是一门强大的编程语言。我们称为shell编写的程序为脚本(script)。脚本是一种易于使用的工具,它能够将系统调用、工具软件、实用程序(utility)和已编译的二进制文件联系在一起构建程序。实际上,shell脚本可以调用所有的UNIX命令、实用程序以及工具软件。如果你觉得这还不够,使用像 test 命令和循环结构这样的shell内建命令能够让脚本更加灵活强大。Shell脚本特别适合完成系统管理任务和那些不需要复杂结构性语言实现的重复工作。内容目录1. 为什么使用shell编程2. 和Sha-Bang(#!)一起出发2.1 调用一个脚本2.2 牛刀小试第一部分 初见shell9第一章 为什么使用shell编程没有任何一种程序设计语言是完美的,甚至没有一个最好的语言。只有在特定环境下适合的语言。—— Herbert Mayer无论你是否打算真正编写shell脚本,只要你想要在一定程度上熟悉系统管理,了解掌握shell脚本的相关知识都是非常有必要的。例如Linux系统在启动的时候会执行 /etc/rc.d 目录下的shell脚本来恢复系统配置和准备服务。详细了解这些启动脚本对分析系统行为大有益处,何况,你很有可能会去修改它们呢。编写shell脚本并不困难,shell脚本由许多小的部分组成,而其中只有数量相当少的与shell本身特性,操作和选项 有关的部分才需要去学习。Shell语法非常简单朴素,很像是在命令行中调用和连接工具,你只需遵循很少一部分的"规则"就可以了。大部分短小的脚本通常在第一次就可以正常工作,即使是一个稍长一些的脚本,调试起来也十分简单。在个人计算机发展的早期,BASIC语言让计算机专业人士能够在早期的微机上编写程序。几十年后,Bash脚本可以让所有仅对Linux或UNIX系统有初步了解的用户在现代计算机上做同样的事。我们现在已经可以做出一些又小又快的单板机,比如树莓派。Bash脚本提供了一种发掘这些有趣设备潜力的方式。使用shell脚本构建一个复杂应用原型(prototype),不失为是一种虽有缺陷但非常快速的方式。在项目开发初期,使用脚本实现部分功能往往显得十分有用。在使用C/C++,Java,Perl或Python编写最终代码前,可以使用shell脚本测试,修补应用结构,提前发现重大缺陷。Shell脚本与经典的UINX哲学相似,将复杂的任务划分为简单的子任务,将组件与工具连接起来。许多人认为比起新一代功能强大、高度集成的语言,例如Perl,shell脚本至少是一种在美学上更加令人愉悦的解决问题的方式,Perl试图做到面面俱到,但你必须强迫自己改变思维方式适应它。Herbert Mayer曾说:“有用的语言需要数组、指针以及构建数据结构的通用机制”。如果依据这些标准,那shell脚本距“有用”还差得很远,甚至是“无用”的。11. 为什么使用shell编程10什么时候不应该使用shell脚本资源密集型的任务,尤其是对速度有要求(如排序、散列、递归 等)需要做大量的数学运算,例如浮点数运算,高精度运算或者复数运算(使用C++或FORTRAN代替)有跨平台需求(使用C或者Java代替)必须使用结构化编程的复杂应用(如变量类型检查、函数原型等)影响系统全局的关键性任务对安全性有高要求,需要保证系统的完整性以及阻止入侵、破解、恶意破坏项目包含有连锁依赖关系的组件需要大量的文件操作(Bash只能访问连续的文件,并且是以一种非常笨拙且低效的逐行访问的方式进行的)需要使用多维数组需要使用如链表、树等数据结构需要产生或操作图像和图形用户接口(GUI)需要直接访问系统硬件或外部设备需要使用端口或套接字输入输出端口(Socket I/O)需要使用库或旧程序的接口私有或闭源的项目(Shell脚本直接将源代码公开,所有人都可以看到)如果你的应用满足上述任意一条,你可以考虑使用更加强大的脚本语言,如Perl,Tcl,Python,Ruby等,或考虑使用编译型语言,如C,C++或Java等。即使如此,在开发阶段使用shell脚本建立应用原型也是十分有用的。我们接下来将使用Bash。Bash是"Bourne-Again shell"的首字母缩略词 ,Bash来源于Stephen Bourne开发的Bourne shell(sh)。如今Bash已成为了大部分UNIX衍生版中shell脚本事实上的标准。本书所涉及的大部分概念在其他shell中也是适用的,例如Korn Shell,Bash从它当中继承了一部分的特性 ;又如C Shell及其变体(需要注意的是,1993年10月Tom Christiansen在Usenet帖子中指出,因C Shell内部固有的问题,不推荐使用C Shell编程)接下来的部分将是一些编写shell脚本的指导。这些指导很大程度上依赖于实例来阐述shell的特性。本书所有的例子都能够正常工作,并在尽可能的范围内进行过测试,其中的一部分已经运用在实际生产生活中。读者们可以使用这些在存档中的例子(文件名为 scriptname.sh 或 scriptname.bash ) ,赋予它们可执行权限( chmod u+rx scriptname ),然后执行它们看看会发生什么。如果存档不可23451. 为什么使用shell编程11用,读者朋友也可以从本书的HTML或者PDF版本中复制粘贴代码出来。需要注意的是,在部分例子中使用了一些暂时还未被解释的特性,这需要读者暂时跳过它们。除特别说明,本书所有例子均由本书作者编写。His countenance was bold and bashed not.—— Edmund Spenser. 这些操作和选项被称为内建命令(builtin),是shell的内部特征。 ↩. 虽然递归可以在shell脚本中实现,但是它的效率很低且实现起来很复杂、不具有美感。 ↩. 首字母缩略词是由每一个单词的首字母拼接而成的易读的代替短语。这不是一个好习惯,通常会引起一些不必要的麻烦。 ↩. ksh88中的许多特性,甚至一些ksh93的特性都被合并到Bash中了。 ↩. 按照惯例,用户编写的Bourne shell脚本应该在文件名后加上 .sh 的扩展名。而那些系统脚本,比如在 /etc/rc.d 中的脚本通常不遵循这种规范。 ↩123451. 为什么使用shell编程12第二章 和Sha-Bang(#!)一起出发Shell编程声名显赫—— Larry Wall本章目录2.1 调用一个脚本2.2 牛刀小试一个最简单的脚本其实就是将一连串系统命令存储在一个文件中。最起码,它能帮你省下重复输入这一连串命令的功夫。样例 2-1. cleanup:清理 /var/log 目录下的日志文件# Cleanup# 请使用root权限执行cd /var/logcat /dev/null > messagescat /dev/null > wtmpecho "Log files cleaned up."这支脚本仅仅是一些可以很容易从终端或控制台输入的命令的集合罢了,没什么特殊的地方。将命令放在脚本中的好处是,你不用再一遍遍重复输入这些命令啦。脚本成了一支程序、一款工具,它可以很容易的被修改或为特殊需求定制。样例 2-2. cleanup:改进的清理脚本2. 和Sha-Bang(#!)一起出发13#!/bin/bash# Bash脚本标准起始行。# Cleanup, version 2# 请使用root权限执行。# 这里可以插入代码来打印错误信息,并在未使用root权限时退出。LOG_DIR=/var/log# 使用变量比硬编码(hard-coded)更合适cd $LOG_DIRcat /dev/null > messagescat /dev/null > wtmpecho "Logs cleaned up."exit # 正确终止脚本的方式。# 不带参数的exit返回上一条指令的运行结果。现在我们看到了一个真正意义上的脚本! 让我们继续前进...样例 2-3. cleanup:改良、通用版#!/bin/bash# Cleanup, version 3# 注意:# --------# 此脚本涉及到许多后边才会解释的特性。# 当你阅读完整本书的一半以后,理解它们就没有任何困难了。LOG_DIR=/var/logROOT_UID=0 # UID为0的用户才拥有root权限。LINES=50 # 默认保存messages日志文件行数。E_XCD=86 # 无法切换工作目录的错误码。E_NOTROOT=87 # 非root权限用户执行的错误码。2. 和Sha-Bang(#!)一起出发14# 请使用root权限运行。if [ "$UID" -ne "$ROOT_UID" ]thenecho "Must be root to run this script."exit $E_NOTROOTfiif [ -n "$1" ]# 测试命令行参数(保存行数)是否为空thenlines=$1elselines=$LINES # 如果为空则使用默认设置fi# Stephane Chazelas 建议使用如下方法检查命令行参数,# 但是这已经超出了此阶段教程的范围。## E_WRONGARGS=85 # Non-numerical argument (bad argument format).# case "$1" in# "" ) lines=50;;# *[!0-9]*) echo "Usage: `basename $0` lines-to-cleanup";# exit $E_WRONGARGS;;# * ) lines=$1;;# esac##* 在第十一章“循环与分支”中会对此作详细的阐述。cd $LOG_DIRif [ `pwd` != "$LOG_DIR" ] # 也可以这样写 if [ "$PWD" != "$LOG_DIR" ]# 检查工作目录是否为 /var/log ?then2. 和Sha-Bang(#!)一起出发15echo "Can't change to $LOG_DIR"exit $E_XCDfi # 在清理日志前,二次确认是否在正确的工作目录下。# 更高效的写法:## cd /var/log || {# echo "Cannot change to necessary directory." >&2# exit $E_XCD;# }tail -n $lines messages > mesg.temp # 保存messages日志文件最后一部分mv mesg.temp messages # 替换系统日志文件以达到清理目的# cat /dev/null > messages#* 我们不需要使用这个方法了,上面的方法更安全cat /dev/null > wtmp # ': > wtmp' 与 '> wtmp' 有同样的效果echo "Log files cleaned up."# 注意在/var/log目录下的其他日志文件不会被这个脚本清除exit 0# 返回0表示脚本运行成功也许你并不希望清空全部的系统日志,这个脚本保留了messages日志的最后一部分。随着学习的深入,你将明白更多提高脚本运行效率的方法。脚本起始行sha-bang(#!) 告诉系统这个脚本文件需要使用指定的命令解释器来执行。#!实际上是一个占两字节 的幻数(magic number),幻数可以用来标识特殊的文件类型,在这里则是标记可执行shell脚本(你可以在终端中输入 manmagic 了解更多信息)。紧随#!的是一个路径名。此路径指向用来解释此脚本的程序,它可以是shell,可以是程序设计语言,也可以是实用程序。这个解释器从头(#!的下一行)开始执行整个脚本的命令,同时忽略注释。1232. 和Sha-Bang(#!)一起出发16#!/bin/sh#!/bin/bash#!/usr/bin/perl#!/usr/bin/tcl#!/bin/sed -f#!/bin/awk -f上面每一条脚本起始行都调用了不同的解释器,比如 /bin/sh 调用了系统默认shell(Linux系统中默认是bash) 。大部分UNIX商业发行版中默认的是Bourneshell,即 #!/bin/sh 。你可以以牺牲Bash特性为代价,在非Linux的机器上运行sh脚本。当然,脚本得遵循POSIX sh标准。需要注意的是 #! 后的路径必须正确,否则当你运行脚本时只会得到一条错误信息,通常是"Command not found."当脚本仅包含一些通用的系统命令而不使用shell内部指令时,可以省略 #! 。第三个例子需要 #! 是因为当对变量赋值时,例如 lines=50 ,使用了与shell特性相关的结构 。再重复一次, #!/bin/sh 调用的是系统默认shell解释器,在Linux系统中默认为 /bin/bash 。这个例子鼓励读者使用模块化的方式编写脚本,并在平时记录和收集一些在以后可能会用到的代码模板。最终你将拥有一个相当丰富易用的代码库。以下的代码可以用来测试脚本被调用时的参数数量是否正确。E_WRONG_ARGS=85script_parameters="-a -h -m -z"# -a = all, -h = help 等等if [ $# -ne $Number_of_expected_args ]thenecho "Usage: `basename $0` $script_parameters"# `basename $0` 是脚本的文件名exit $E_WRONG_ARGSfi大多数情况下,你会针对特定的任务编写脚本。本章的第一个脚本就是这样。然后你也许会泛化(generalize)脚本使其能够适应更多相似的任务,比如用变量代替硬编码,用函数代替重复代码。456712. 和Sha-Bang(#!)一起出发17. 在文献中更常见的形式是she-bang或者sh-bang。它们都来源于词汇sharp(#)和bang(!)的连接。 ↩. 一些UNIX的衍生版(基于4.2 BSD)声称他们使用四字节的幻数,在#!后增加一个空格,即 #! /bin/sh 。而Sven Mascheck指出这是虚构的。 ↩.命令解释器首先将会解释#!这一行,而因为#!以#打头,因此解释器将其视作注释。起始行作为调用解释器的作用已经完成了。事实上即使脚本中含有不止一个#!,bash也会将除第一个`#!`以外的解释为注释。#!/bin/bashecho "Part 1 of script."a=1#!/bin/bash# 这并不会启动新的脚本echo "Part 2 of script."echo $a # $a的值仍旧为1↩12342. 和Sha-Bang(#!)一起出发18.这里允许使用一些技巧。#!/bin/rm# 自我删除的脚本# 当你运行这个脚本,除了这个脚本本身消失以外并不会发生什么。WHATEVER=85echo "This line will never print (betcha!)."exit $WHATEVER # 这没有任何关系。脚本将不会从这里退出。# 尝试在脚本终止后打印echo $a。# 得到的值将会是0而不是85.当然你也可以建立一个起始行是 #!/bin/more 的README文件,并且使它可以执行。结果就是这个文件成为了一个可以打印本身的文件。(查看样例 19-3,使用 cat 命令的here document也许是一个更好的选择) ↩. 可移植操作系统接口(POSIX)尝试标准化类UNIX操作系统。POSIX规范可以在Open Group site中查看。 ↩. 为了避免这种情况的发生,可以使用 #!/bin/env bash 作为起始行。这在bash不在 /bin 的UNIX系统中会有效果。 ↩. 如果bash是系统默认shell,那么脚本并不一定需要#!作为起始行。但是当你在其他的shell中运行脚本,例如tcsh,则需要使用#!。 ↩45672. 和Sha-Bang(#!)一起出发192.1 调用一个脚本写完一个脚本以后,你可以通过 sh scriptname 或 bash scriptname 来调用它(不推荐使用 sh 重定向操作符结合,可以在不改变文件权限的情况下清空文件。如果文件不存在,那么将创建这个文件。: > data.xxx # 文件 "data.xxx" 已被清空# 与 cat /dev/null >data.xxx 作用相同# 但是此操作不会产生一个新进程,因为 ":" 是shell内建命令。也可查看样例 16-15。与 >> 重定向操作符结合,将不会清空任何已存在的文件( : >>target_file )。如果文件不存在,将创建这个文件。以上操作仅适用于普通文件,不适用于管道、符号链接和特殊文件。空命令可以用来作为一行注释的开头,尽管我们并不推荐这么做。使用 # 可以使解释器关闭该行的错误检测,所以几乎所有的内容都可以出现在注释#中。使用空命令却不是这样的:: 这一行注释将会产生一个错误,( if [ $x -eq 3] )。3. 特殊字符30:也可以作为一个域分隔符,比如在 /etc/passwd 和 $PATH 变量中。bash$ echo $PATH/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/sbin:/usr/sbin:/usr/games将冒号作为函数名也是可以的。:(){echo "The name of this function is "$FUNCNAME" "# 为什么要使用冒号作函数名?# 这是一种混淆代码的方法......}:# 函数名是 :这种写法并不具有可移植性,也不推荐使用。事实上,在Bash的最近的版本更新中已经禁用了这种用法。但我们还可以使用下划线 _来替代。冒号也可以作为非空函数的占位符。not_empty (){:} # 含有空指令,这并不是一个空函数。!取反(或否定)操作符[感叹号]。! 操作符反转已执行的命令的返回状态(查看样例6-2)。它同时可以反转测试操作符的意义,例如可以将相等(=)反转成不等(!=)。它是一个Bash关键词。在一些特殊场景下,它也会出现在间接变量引用中。3. 特殊字符31在另外一些特殊场景下,即在命令行下可以使用 ! 调用Bash的历史记录(附录L)。需要注意的是,在脚本中,这个机制是被禁用的。*通配符[星号]。在文件匹配(globbing)操作时扩展文件名。如果它独立出现,则匹配该目录下的所有文件。bash$ echo *abs-book.sgml add-drive.sh agram.sh alias.sh在正则表达式中表示匹配任意多个(包括0)前个字符。*算术运算符。在进行算术运算时,表示乘法运算。** 双星号可以表示乘方运算或扩展文件匹配。?测试操作符[问号]。在一些特定的语句中,? 表示一个条件测试。在一个双圆括号结构中,? 可以表示一个类似C语言风格的三元(trinary)运算符的一个组成部分。condition?result-if-true:result-if-false23. 特殊字符32(( var0 = var1 combined_file# 将 file1, file2 与 file3 拼接在一起后写入 combined_file 中。cp file22.{txt,backup}# 将 "file22.txt" 拷贝为 "file22.backup"这个命令可以作用于花括号内由逗号分隔的文件描述列表。 文件名扩展(匹配)作用于大括号间的各个文件。除非被引用或被转义,否则空白符不应在花括号中出现。echo {file1,file2}\ :{\ A," B",' C'}file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C{a..z}扩展的花括号扩展结构。53. 特殊字符35echo {a..z} # a b c d e f g h i j k l m n o p q r s t u v w x yz# 输出 a 到 z 之间所有的字母。echo {0..3} # 0 1 2 3# 输出 0 到 3 之间所有的数字。base64_charset=( {A..Z} {a..z} {0..9} + / = )# 使用扩展花括号初始化一个数组。# 摘自 vladz 编写的样例脚本 "base64.sh"。Bash第三版中引入了 {a..z} 扩展的花括号扩展结构。{}代码块[花括号],又被称作内联组(inline group)。它实际上创建了一个匿名函数(anonymous function),即没有名字的函数。但是,不同于那些“标准”函数,代码块内的变量在脚本的其他部分仍旧是可见的。bash$ { local a;a=123; }bash: local: can only be used in afunctiona=123{ a=321; }echo "a = $a" # a = 321 (代码块内赋值)# 感谢S.C.代码块可以经由I/O重定向进行输入或输出。样例 3-1. 代码块及I/O重定向3. 特殊字符36#!/bin/bash# 读取文件 /etc/fstabFile=/etc/fstab{read line1read line2} "$1.test" # 输出重定向至文件。echo "Results of rpm test in file $1.test"# rpm各项参数的具体含义可查看man文档exit 0与由圆括号包裹起来的命令组不同,由花括号包裹起来的代码块不产生子进程。也可以使用非标准的 for 循环语句来遍历代码块。{}文本占位符。在 xargs -i 后作为输出的占位符使用。63. 特殊字符38ls . | xargs -i -t cp ./{} $1# ^^ ^^# 摘自 "ex42.sh" (copydir.sh){} \;路径名。通常在 find 命令中使用,但这不是shell的内建命令。定义:路径名是包含完整路径的文件名,例如 /home/bozo/Notes/Thursday/schedule.txt 。我们通常又称之为绝对路径。在执行 find -exec 时最后需要加上 ; ,但是分号需要被转义以保证其不会被shell解释。[ ]测试。在 [ ] 之间填写测试表达式。值得注意的是,[ 是shell内建命令 test 的一个组成部分,而不是外部命令 /usr/bin/test 的链接。[[ ]]测试。在 [[ ]] 之间填写测试表达式。相比起单括号测试 ([ ]),它更加的灵活。它是一个shell的关键字。详情查看关于 [[ ]] 结构的讨论。[ ]数组元素。在数组中,可以使用中括号的偏移量来用来访问数组中的每一个元素。Array[1]=slot_1echo ${Array[1]}[ ]3. 特殊字符39字符集、字符范围。 在正则表达式中,中括号用来匹配指定字符集或字符范围内的任意字符。$[ ... ]整数扩展符。在 $[ ] 中可以计算整数的算术表达式。a=3b=7echo $[$a+$b] # 10echo $[$a*$b] # 21(( ))整数扩展符。在 (( )) 中可以计算整数的算术表达式。详情查看关于 (( ... )) 结构的讨论。> &> >& >> < 重定向。scriptname >filename 将脚本 scriptname 的输出重定向到 filename 中。如果文件存在,那么覆盖掉文件内容。command &>filename 将命令 command 的标准输出(stdout) 和标准错误输出(stderr) 重定向到 filename。重定向在用于清除测试条件的输出时特别有效。例如测试一个特定的命令是否存在。bash$ type bogus_command &>/dev/nullbash$ echo $?13. 特殊字符40或写在脚本中:command_test () { type "$1" &>/dev/null; }# ^cmd=rmdir # 存在的命令。command_test $cmd; echo $? # 返回0cmd=bogus_command # 不存在的命令。command_test $cmd; echo $? # 返回1command >&2 将命令的标准输出重定向至标准错误输出。scriptname >>filename 将脚本 scriptname 的输出追加到 filename 文件末尾。如果文件不存在,那么将创建这个文件。[i]filename 打开文件 filename 用来读写,并且分配一个文件描述符i指向它。如果文件不存在,那么将创建这个文件。进程替换: (command)> <(command)在某些情况下, "" 将用作字符串比较。在另外一些情况下, "" 将用作数字比较。详情查看样例 16-9。<<在here document中进行重定向。<<<在here string中进行重定向。ASCII码比较。3. 特殊字符41veg1=carrotsveg2=tomatoesif [[ "veg1" < "veg2" ]]thenecho "Although $veg1 precede $veg2 in the dictionary,"echo -n "this does not necessarily imply anything "echo "about my culinary preferences."elseecho "What kind of dictionary are you using, anyhow?"fi\正则表达式中的单词边界(word boundary)。bash$ grep '\' textfile|管道(pipe)。管道可以将上一个命令的输出作为下一个命令的输入,或者直接输出到shell中。管道是一种可以将一系列命令连接在一起的绝妙方式。echo ls -l | sh# 将 "echo ls -l" 的结果输出到shell中,# 与直接输入 "ls -l" 的结果相同。cat *.lst | sort | uniq# 将所有后缀名为 lst 的文件合并后排序,接着删掉所有重复行。3. 特殊字符42管道是一种在进程间通信的典型方法。它将一个进程的输出作为另一个进程的输入。举一个经典的例子,像 cat 或者 echo 这样的命令,可以通过管道将它们产生的数据流导入到过滤器(filter)中。过滤器是可以用来处理输入流的命令。cat $filename1 $filename2 | grep $search_word查看UNIX FAQ第三章获取更多关于使用UNIX管道的信息。命令的输出同样可以通过管道输入到脚本中。#!/bin/bash# uppercase.sh : 将所有输入变成大写tr 'a-z' 'A-Z'# 为了防止产生单字符文件名,# 必须使用单引号引用字符范围。exit 0现在,让我们将 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在管道中,每一个进程的输出必须作为下个进程的输入被正确读入,如果不这样,数据流会被阻塞(block),管道就无法按照预期正常工作。cat file1 file2 | ls -l | sort# "cat file1 file2" 的输出会消失。管道是在一个子进程中运行的,因此它并不能修改父进程脚本中的变量。73. 特殊字符43variable="initial_value"echo "new_value" | read variableecho "variable = $variable" # variable = initial_value如果管道中的任意一个命令意外中止了,管道将会提前中断,我们称其为管道破裂(Broken Pipe)。出现这种情况,系统将发送一个 SIGPIPE 信号。>|强制重定向。即使在 noclobber 选项被设置的情况下,重定向也会覆盖已存在的文件。||或(OR)逻辑运算符。在测试结构中,任意一个测试条件为真,整个表达式为真。返回 0(成功标志位)。&后台运行操作符。如果命令后带&,那么此命令将转至后台运行。bash$ sleep 10 &[1] 850[1]+ Done sleep 10在脚本中,命令甚至循环都可以在后台运行。样例 3-3. 在后台运行的循环#!/bin/bash# background-loop.shfor i in 1 2 3 4 5 6 7 8 9 10 # 第一个循环doecho -n "$i "done & # 这个循环在后台运行。3. 特殊字符44# 有时会在第二个循环结之后才执行此后台循环。echo # 此'echo' 有时不显示for i in 11 12 13 14 15 16 17 18 19 20 # 第二个循环doecho -n "$i "doneecho # 此'echo' 有时不显示# ======================================================# 脚本期望输出结果:# 1 2 3 4 5 6 7 8 9 10# 11 12 13 14 15 16 17 18 19 20# 一些情况下可能会输出:# 11 12 13 14 15 16 17 18 19 20# 1 2 3 4 5 6 7 8 9 10 bozo $# 第二个 'echo' 没有被执行,为什么?# 另外一些情况下可能会输出:# 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20# 第一个 'echo' 没有被执行,为什么?# 非常罕见的情况下,可能会输出:# 11 12 13 1 2 3 4 5 6 7 8 9 10 14 15 16 17 18 19 20# 前台循环抢占(preempt)了后台循环。exit 0# Nasimuddin Ansari 建议:在第6行和第14行的# echo -n "$i " 后增加 sleep 1,# 会得到许多有趣的结果。脚本在后台执行命令时可能因为等待键盘事件被挂起。幸运的是,有一套方案可以解决这个问题。3. 特殊字符45&&与(AND)逻辑操作符。在测试结构中,所有测试条件都为真,表达式才为真,返回 0(成功标志位)。-选项与前缀。它可以作为命令的选项标志,也可以作为一个操作符的前缀,也可以作为在参数代换中作为默认参数的前缀。COMMAND -[Option1][Option2][..]ls -alsort -dfu $filenameif [ $file1 -ot $file2 ]then # ^echo "File $file1 is older than $file2."fiif [ "$a" -eq "$b" ]then # ^echo "$a is equal to $b."fiif [ "$c" -eq 24 -a "$d" -eq 47 ]then # ^ ^echo "$c equals 24 and $d equals 47."fiparam2=${param1:-$DEFAULTVAL}# ^--双横线一般作为命令长选项的前缀。3. 特殊字符46sort --ignore-leading-blanks双横线与Bash内建命令一起使用时,意味着该命令选项的结束。下面提供了一种删除文件名以横线开头文件的简单方法。bash$ ls -l-rw-r--r-- 1 bozo bozo 0 Nov 25 12:29 -badnamebash$ rm -- -badnamebash$ ls -ltotal 0双横线通常也和 set 连用。set -- $variable (查看样例 15-18)。-重定向输入输出[短横线]。bash$ cat -abcabc...Ctl-D在这个例子中, cat - 输出由键盘读入的标准输入(stdin) 到 标准输出(stdout)。但是在真实应用的 I/O 重定向中是否有使用 '-'?(cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)# 将整个文件树从一个目录移动到另一个目录。3. 特殊字符47# 感谢 Alan Cox 所作出的部分改动# 1) cd /source/directory# 工作目录定位到文件所属的源目录# 2) &&# "与链":如果 'cd' 命令操作成功,那么执行下一条命令# 3) tar cf - .# 'tar c' (create 创建) 创建一份新的档案# 'tar f -' (file 指定文件) 在 '-' 后指定一个目标文件作为输出# '.' 代表当前目录# 4) |# 通过管道进行重定向# 5) ( ... )# 在建立的子进程中执行命令# 6) cd /dest/directory# 工作目录定位到目标目录# 7) &&# 与 2) 相同# 8) tar xpvf -# 'tar x' 解压档案# 'tar p' (preserve 保留) 保留档案内文件的所有权及文件权限# 'tar v' (verbose 冗余) 发送全部信息到标准输出# 'tar f -' (file 指定文件) 在 '-' 后指定一个目标文件作为输入## 注意 'x' 是一个命令,而 'p', 'v', 'f' 是选项。# 干的漂亮!# 更加优雅的写法是:# cd source/directory# tar cf - . | (cd ../dest/directory; tar xpvf -)## 同样可以写成:# cp -a /source/directory/* /dest/directory# 或:# cp -a /source/directory/* /source/directory/.[^.]* /dest/directory# 可以在源目录中有隐藏文件时使用3. 特殊字符48bunzip2 -c linux-2.6.16.tar.bz2 | tar xvf -# --未解压的 tar 文件-- | --将解压出的 tar 传递给 "tar"--# 如果不使用管道让 "tar" 处理 "bunzip2" 得到的文件,# 那么就需要使用单独的两步来完成。# 目的是为了解压 "bzipped" 压缩的内核源代码。下面的例子中,"-" 并不是一个Bash的操作符,它仅仅是 tar , cat 等一些特定UNIX命令中将结果输出到标准输出的选项。bash$ echo "whatever" | cat -whatever当需要文件名的时候,- 可以用来代替某个文件而重定向到标准输出(通常出现在tar cf 中)或从 stdin 中接受数据。这是一种在管道中使用面向文件(file-oriented)工具作为过滤器的方法。bash$ fileUsage: file [-bciknvzL] [-f namefile] [-m magicfiles] file...单独执行 file 命令,将会得到一条错误信息。在命令后增加一个 "-" 可以得到一个更加有用的结果。它会使得shell暂停等待用户输入。bash$ file -abcstandard input: ASCII textbash$ file -#!/bin/bashstandard input: Bourne-Again shell script text executable现在命令能够接受标准输入并且处理它们了。3. 特殊字符49"-" 能够通过管道将标准输出重定向到其他命令中。这就可以做到像在某个文件前添加几行这样的事情。使用 diff 比较两个文件的部分内容:grep Linux file1 | diff file2 -最后介绍一个使用 - 的 tar 命令的实际案例。样例 3-4. 备份最近一天修改过的所有文件3. 特殊字符50#!/bin/bash# 将当前目录下24小时之内修改过的所有文件备份成一个# "tarball" (经 tar 打包`与 gzip 压缩) 文件BACKUPFILE=backup-$(date +%m-%d-%Y)# 在备份文件中嵌入时间# 感谢 Joshua Tschida 提供的建议archive=${1:-$BACKUPFILE}# 如果没有在命令行中特别制定备份格式,# 那么将会默认设置为 "backup-MM-DD-YYYY.tar.gz"。tar cvf - `find . -mtime -1 -type f -print` > $archive.targzip $archive.tarecho "Directory $PWD backed up in archive file \"$archive.tar.gz\"."# Stephane Chazeles 指出如果目录中有非常多的文件,# 或文件名中包含空白符时,上面的代码会运行失败。# 他建议使用以下的任意一种方法:# -------------------------------------------------------------------# find . -mtime -1 -type f -print0 | xargs -0 tar rvf "$archive.tar"# 使用了 GNU 版本的 "find" 命令。# find . -mtime -1 -type f -exec tar rvf "$archive.tar" '{}' \;# 兼容其他的 UNIX 发行版,但是速度会比较慢# -------------------------------------------------------------------exit 03. 特殊字符51以 "-" 开头的文件在和"-" 重定向操作符一起使用时可能会导致一些问题。因此合格的脚本必须首先检查这种情况。如果遇到,就需要给文件名加一个合适的前缀,比如 ./-FILENAME, $PWD/-FILENAME 或者 $PATHNAME/-FILENAME 。如果变量的值以 '-' 开头,也可能会造成类似问题。var='-n'echo $var# 等同于 "echo -n",不会输出任何东西。-先前的工作目录。使用 cd - 命令可以返回先前的工作目录。它实际上是使用了$OLDPWD 环境变量。不要将这里的 "-" 与先前的 "-" 重定位操作符混淆。"-" 的具体含义需要根据上下文来解释。-减号。算术运算符中的减法标志。=等号。赋值操作符。a=28echo $a # 28在一些情况下,"=" 可以作为字符串比较操作符。+加号。加法算术运算。在一些情况下,+ 是作为正则表达式中的一个操作符。3. 特殊字符52+选项操作符。作为一个命令或过滤器的选项标记。特定的一些指令和内建命令使用 + 启用特定的选项,使用 - 禁用特定的选项。在参数代换中,+ 是作为变量扩展的备用值(alternate value)的前缀。%取模。取模操作运算符。let "z = 5 % 3"echo $z # 2在另外一些情况下,% 是一种模式匹配的操作符。~主目录[波浪号]。它相当于内部变量 $HOME 。 ~bozo 是 bozo 的主目录,执行ls ~bozo 将会列出他的主目录中内容。 ~/ 是当前用户的主目录,执行 ls~/ 将会列出其中所有的内容。bash$ echo ~bozo/home/bozobash$ echo ~/home/bozobash$ echo ~//home/bozo/bash$ echo ~:/home/bozo:bash$ echo ~nonexistent-user~nonexistent-user3. 特殊字符53~+当前工作目录。它等同于内部变量 $PWD 。~-先前的工作目录。它等同于内部变量 $OLDPWD 。=~正则表达式匹配。将在 Bash version 3 章节中介绍。^行起始符。在正则表达式中,"^" 代表一行文本的开始。^, ^^参数替换中的大写转换符(在Bash第4版新增)。控制字符改变终端或文件显示的一些行为。一个控制符是由 CONTRL + key 组成的(同时按下)。控制字符同样可以通过转义以八进制或十六进制的方式显示。控制符不能在脚本中使用。Ctrl-A移动光标至行首。Ctrl-B非破坏性退格(即不删除字符)。Ctrl-C3. 特殊字符54中断指令。终止当前运行的任务。Ctrl-D登出shell(类似 exit )键入 EOF (end-of-file,文件终止标记),中断 stdin 的输入。当你在终端或 xterm 窗口中输入字符时, Ctl-D 将会删除光标上的字符。当没有字符时, Crl-D 将会登出shell。在 xterm 中,将会关闭整个窗口。Ctrl-E移动光标至行末。Ctrl-F光标向前移动一个字符。Ctrl-G响铃 BEL 。在一些老式打字机终端上,将会响铃。而在 xterm 中,将会产生“哔”声。Ctrl-H抹除(破坏性退格)。退格删除前面的字符。3. 特殊字符55#!/bin/bash# 在字符串中嵌入 Ctrl-Ha="^H^H" # 两个退格符 Ctrl-H# 在 vi/vim 中使用 Ctrl-V Ctrl-H 来键入echo "abcdef" # abcdefechoecho -n "abcdef$a " # abcd f# ^ ^ 末尾有空格退格两次的结果echoecho -n "abcdef$a" # abcdef# ^ 末尾没有空格时为什么退格无效了?# 并不是我们期望的结果。echo; echo# Constantin Hagemeier 建议尝试一下:# a=$'\010\010'# a=$'\b\b'# a=$'\x08\x08'# 但是这些并不会改变结果。######################################### 现在来试试这个。rubout="^H^H^H^H^H" # 5个 Ctrl-Hecho -n "12345678"sleep 2echo -n "$rubout"sleep 2Ctrl-I水平制表符。Ctrl-J3. 特殊字符56另起一行(换行)。在脚本中,你也可使用八进制 '\012' 或者十六进制 '\x0a' 来表示。Ctrl-K垂直制表符。当你在终端或 xterm 窗口中输入字符时, Ctrl-K 将会删除光标上及其后的所有字符。而在脚本中, Ctrl-K 的作用有些不同。具体查看下方 Lee LeeMaschmeyer 写的样例。Ctrl-L清屏、走纸。在终端中等同于 clear 命令。在打印时, Ctrl-L 将会使纸张移动到底部。Ctrl-M回车(CR)。3. 特殊字符57#!/bin/bash# 感谢 Lee Maschmeyer 提供的样例。read -n 1 -s -p \$'Control-M leaves cursor at beginning of this line. Press Enter. \x0d'# '0d' 是 Control-M 的十六进制的值echo >&2 # '-s' 参数禁用了回显,所以需要显式的另起一行。read -n 1 -s -p $'Control-J leaves cursor on next line. \x0a'# '0a' 是 Control-J 换行符的十六进制的值echo >&2###read -n 1 -s -p $'And Control-K\x0bgoes straight down.'echo >&2 # Control-K 是垂直制表符。# 一个更好的垂直制表符例子是:var=$'\x0aThis is the bottom line\x0bThis is the top line\x0a'echo "$var"# 这将会产生与上面的例子类似的结果。但是echo "$var" | col# 这却会使得右侧行高于左侧行。# 这也解释了为什么我们需要在行首和行尾加上换行符# 来避免显示的混乱。# Lee Maschmeyer 的解释:# --------------------------# 在第一个垂直制表符的例子中,垂直制表符使其# 在没有回车的情况下向下打印。# 这在那些不能回退的设备上,例如 Linux 的终端才可以。# 而垂直制表符的真正目的是向上而非向下。# 它可以用来在打印机中用来打印上标。# col 工具可以用来模拟真实的垂直制表符行为。exit 03. 特殊字符58Ctrl-N在命令行历史记录中调用下一条历史命令 。Ctrl-O在命令行中另起一行。Ctrl-P在命令行历史记录中调用上一条历史命令。Ctrl-Q恢复(XON)。终端恢复读入 stdin。Ctrl-R在命令行历史记录中进行搜索。Ctrl-S挂起(XOFF)。终端冻结 stdin。(可以使用 Ctrl-Q 恢复)Ctrl-T交换光标所在字符与其前一个字符。Ctrl-U删除光标所在字符之前的所有字符。 在一些情况下,不管光标在哪个位置, Ctrl-U 都会删除整行文字。Ctrl-V输入时,使用 Ctrl-V 允许插入控制字符。例如,下面两条语句是等价的:83. 特殊字符59echo -e '\x0a'echo Ctrl-V 在文本编辑器中特别有用。Ctrl-W当你在终端或 xterm 窗口中输入字符时, Ctrl-W 将会删除光标所在字符之前到其最近的空白符之间的所有字符。 在一些情况下, Ctrl-W 会删除到之前最近的非字母或数字的字符。Ctrl-X在一些特定的文本处理程序中,剪切高亮文本并复制到剪贴板(clipboard)。Ctrl-Y粘贴之前使用 Ctrl-U 或 Ctrl-W 删除的文字。Ctrl-Z暂停当前运行的任务。在一些特定的文本处理程序中是替代操作。在 MSDOS 文件系统中作为 EOF (end-of-file,文件终止标记)。空白符作为命令或变量之间的分隔符。空白符包含空格、制表符、换行符或它们的任意组合。 在一些地方,比如变量赋值时,空白符不应该出现,否则会造成语法错误。空白行在脚本中不会有任何实际作用,但是可以划分代码,使代码更具可读性。特殊变量 $IFS 是作为一些特定命令的输入域(field)分隔符,默认值为空白符。93. 特殊字符60定义:域是字符串中离散的数据块。使用空白符或者指定的字符(通常由$IFS 决定)来分隔临近域。在一些情况下,域也可以被称作记录(record)。如果想在字符串或者变量中保留空白符,请引用。UNIX 过滤器可以使用 POSIX 字符类 [:space:] 来寻找和操作空白符。. 操作符(operator)用来执行表达式(operation)。最常见的例子就是算术运算符+ - * /。在Bash中,操作符和关键字的概念有一些重叠。 ↩. 它更被人熟知的名字是三元(ternary)操作符。但是读起来不清晰,而且容易令人混淆。trinary 是一种更加优雅的写法。 ↩. 美国信息交换标准代码(American Standard Code for InformationInterchange)。这是一套可以由计算机存储和处理的7位(bit)字符(包含字母、数字和一系列有限的符号)编码系统。 ↩. 进程标识符(PID),是分配给正在运行进程的唯一数字标识。可以使用ps 命令查看进程的 PID。定义:进程是正在执行的命令或程序,通常也称作任务。 ↩. 由shell来执行大括号扩展操作。命令本身是在扩展的基础上进行操作的。 ↩. 例外:作为管道的一部分的大括号中的代码块可能会运行在子进程中。ls | { read firstline; read secondline; }# 错误。大括号中的代码块在子进程中运行,#+ 因此 "ls" 命令输出的结果不能传递到代码块中。echo "First line is $firstline; second line is$secondline" # 无效。# 感谢 S.C.↩. 正如在古代催情剂(philtre)被认为是一种能引发神奇变化的药剂一样,UNIX 中的过滤器(filter)也是有类似的作用的。(如果一个程序员做出了一个能够在 Linux 设备上运行的 "love philtre",那么他将会获得巨大的荣誉。) ↩123456783. 特殊字符61. Bash将之前在命令行中执行过的命令存储在缓存(buffer)中,或者一块内存区域里。可以使用内建命令 history 来查看。 ↩. 换行符本身也是一个空白符。因此这就是为什么仅仅包含一个换行符的空行也被认为是空白符。 ↩893. 特殊字符62第四章 变量与参数本章目录4.1 变量替换4.2 变量赋值4.3 Bash变量弱类型4.4 特殊变量类型变量(variable)在编程语言中用来表示数据。它本身只是一个标记,指向数据在计算机内存中的一个或一组地址。变量通常出现在算术运算,数量操作及字符串解析中。4. 变量与参数634.1 变量替换变量名是其所指向值的一个占位符(placeholder)。引用变量值的过程我们称之为变量替换(variable substitution)。$接下来我们仔细区分一下变量名与变量值。如果变量名是 variable1 , 那么$variable1 就是对变量值的引用。bash$ variable1=23bash$ echo variable1variable1bash$ echo $variable123变量仅仅在声明时、赋值时、被删除时( unset )、被导出时( export ),算术运算中使用双括号结构((...))时或在代表信号时(signal,查看样例 32-5)才不需要有 $ 前缀。赋值可以是使用 =(比如 var1=27 ),可以是在 read 语句中,也可以是在循环的头部( for var2 in 1 2 3 )。在双引号 "" 字符串中可以使用变量替换。我们称之为部分引用,有时候也称弱引用。而使用单引号 '' 引用时,变量只会作为字符串显示,变量替换不会发生。我们称之为全引用,有时也称强引用。更多细节将在第五章讲解。实际上, $variable 这种写法是 ${variable} 的简化形式。在某些特殊情况下,使用 $variable 写法会造成语法错误,使用完整形式会更好(查看章节10.2)。样例 4-1. 变量赋值与替换#!/bin/bash# ex9.sh14.1 变量替换64# 变量赋值与替换a=375hello=$a# ^ ^#----------------------------------------------------# 初始化变量时,赋值号 = 的两侧绝不允许有空格出现。# 如果有空格会发生什么?# "VARIABLE =value"# ^#% 脚本将会尝试运行带参数 "=value" 的 "VARIABLE " 命令。# "VARIABLE= value"# ^#% 脚本将会尝试运行 "value" 命令,#+ 同时设置环境变量 "VARIABLE" 为 ""。#----------------------------------------------------echo hello # hello# 没有引用变量,"hello" 只是一个字符串...echo $hello # 375# ^ 这是变量引用。echo ${hello} # 375# 与上面的类似,变量引用。# 字符串内引用变量echo "$hello" # 375echo "${hello}" # 375echohello="A B C D"echo $hello # A B C Decho "$hello" # A B C D4.1 变量替换65# 正如我们所见,echo $hello 与 echo "$hello" 的结果不同。# ====================================# 字符串内引用变量将会保留变量的空白符。# ====================================echoecho '$hello' # $hello# ^ ^# 单引号会禁用掉(转义)变量引用,这导致 "$" 将以普通字符形式被解析。# 注意单双引号字符串引用效果的不同。hello= # 将其设置为空值echo "\$hello (null value) = $hello" # $hello (null value) =# 注意# 将一个变量设置为空与删除(unset)它不同,尽管它们的表现形式相同。# -----------------------------------------------# 使用空白符分隔,可以在一行内对多个变量进行赋值。# 但是这会降低程序的可读性,并且可能会导致部分程序不兼容的问题。var1=21 var2=22 var3=$V3echoecho "var1=$var1 var2=$var2 var3=$var3"# 在一些老版本的 shell 中这样写可能会有问题。# -----------------------------------------------echo; echonumbers="one two three"# ^ ^other_numbers="1 2 3"# ^ ^# 如果变量中有空白符号,那么必须用引号进行引用。# other_numbers=1 2 3 # 出错4.1 变量替换66echo "numbers = $numbers"echo "other_numbers = $other_numbers" # other_numbers = 1 2 3# 也可以转义空白符。mixed_bag=2\ ---\ Whatever# ^ ^ 使用 \ 转义空格echo "$mixed_bag" # 2 --- Whateverecho; echoecho "uninitialized_variable = $uninitialized_variable"# 未初始化的变量是空值(null表示不含有任何值)。uninitialized_variable= # 只声明而不初始化,等同于设为空值。echo "uninitialized_variable = $uninitialized_variable" # 仍旧为空uninitialized_variable=23 # 设置变量unset uninitialized_variable # 删除变量echo "uninitialized_variable = $uninitialized_variable"# uninitialized_variable =# 变量值为空echoexit 04.1 变量替换67一个未被赋值或未初始化的变量拥有空值(null value)。注意:null值不等同于0。if [ -z "$unassigned" ]thenecho "\$unassigned is NULL."fi # $unassigned is NULL.在赋值前使用变量可能会导致错误。但在算术运算中使用未赋值变量是可行的。echo "$uninitialized" # 空行let "uninitialized += 5" # 加5echo "$uninitialized" # 5# 结论:# 一个未初始化的变量不含值(null),但在算术运算中会被作为0处理。也可参考样例 15-23。. 实际上,变量名是被称作左值(lvalue),意思是出现在赋值表达式的左侧的值,比如 VARIABLE=23 。变量值被称作右值(rvalue),意思是出现在赋值表达式右侧的值,比如 VAR2=$VARIABLE 。事实上,变量名只是一个引用,一枚指针,指向实际存储数据内存地址的指针。 ↩14.1 变量替换684.2 变量赋值=赋值操作符(在其前后没有空白符)。不要混淆 = 与 -eq,后者用来进行比较而非赋值。同时也要注意 = 根据使用场景既可作赋值操作符,也可作比较操作符。样例 4-2. 变量赋值4.2 变量赋值69#!/bin/bash# 非引用形式变量echo# 什么时候变量是非引用形式,即变量名前没有 '$' 符号的呢?# 当变量在被赋值而不是被引用时。# 赋值a=879echo "The value of \"a\" is $a."# 使用 'let' 进行赋值let a=16+5echo "The value of \"a\" is now $a."echo# 在 'for' 循环中赋值(隐式赋值)echo -n "Values of \"a\" in the loop are: "for a in 7 8 9 11doecho -n "$a "doneechoecho# 在 'read' 表达式中(另一种赋值形式)echo -n "Enter \"a\" "read aecho "The value of \"a\" is now $a."echoexit 0样例 4-3. 奇妙的变量赋值4.2 变量赋值70#!/bin/basha=23 # 简单形式echo $ab=$aecho $b# 来我们玩点炫的(命令替换)。a=`echo Hello!` # 将 'echo' 命令的结果赋值给 'a'echo $a# 注意在命令替换结构中包含感叹号(!)在命令行中使用将会失效,#+ 因为它将会触发 Bash 的历史(history)机制。# 在shell脚本内,Bash 的历史机制默认关闭。a=`ls -l` # 将 'ls -l' 命令的结果赋值给 'a'echo $a # 不带引号引用,将会移除所有的制表符与分行符echoecho "$a" # 引号引用变量将会保留空白符# 查看 "引用" 章节。exit 0使用 $(...) 形式进行赋值(与反引号不同的新形式),与命令替换形式相似。# 摘自 /etc/rc.d/rc.localR=$(cat /etc/redhat-release)arch=$(uname -m)4.2 变量赋值714.3 Bash变量是弱类型的不同于许多其他编程语言,Bash 并不区分变量的类型。本质上说,Bash 变量是字符串,但在某些情况下,Bash 允许对变量进行算术运算和比较。决定因素则是变量值是否只含有数字。样例 4-4. 整数还是字符串?#!/bin/bash# int-or-string.sha=2334 # 整数。let "a += 1"echo "a = $a " # a = 2335echo # 依旧是整数。b=${a/23/BB} # 将 "23" 替换为 "BB"。# $b 变成了字符串。echo "b = $b" # b = BB35declare -i b # 将其声明为整数并没有什么卵用。echo "b = $b" # b = BB35let "b += 1" # BB35 + 1echo "b = $b" # b = 1echo # Bash 认为字符串的"整数值"为0。c=BB34echo "c = $c" # c = BB34d=${c/BB/23} # 将 "BB" 替换为 "23"。# $d 变为了一个整数。echo "d = $d" # d = 2334let "d += 1" # 2334 + 1echo "d = $d" # d = 2335echo# 如果是空值会怎样呢?4.3 Bash弱类型变量72e='' # ...也可以是 e="" 或 e=echo "e = $e" # e =let "e += 1" # 空值是否允许进行算术运算?echo "e = $e" # e = 1echo # 空值变为了一个整数。# 如果时未声明的变量呢?echo "f = $f" # f =let "f += 1" # 是否允许进行算术运算?echo "f = $f" # f = 1echo # 未声明变量变为了一个整数。## 然而……let "f /= $undecl_var" # 可以除以0么?# let: f /= : syntax error: operand expected (error token is "")# 语法错误!在这里 $undecl_var 并没有被设置为0!## 但是,仍旧……let "f /= 0"# let: f /= 0: division by 0 (error token is "0")# 预期之中。# 在执行算术运算时,Bash 通常将其空值的整数值设为0。# 但是不要做这种事情!# 因为这可能会导致一些意外的后果。# 结论:上面的结果都表明 Bash 中的变量是弱类型的。exit $?弱类型变量有利有弊。它可以使编程更加灵活、更加容易(给与你足够的想象空间)。但它也同样的容易造成一些小错误,容易养成粗心大意的编程习惯。为了减轻脚本持续跟踪变量类型的负担,Bash 不允许变量声明。4.3 Bash弱类型变量734.3 Bash弱类型变量744.4 特殊的变量类型局部变量仅在代码块或函数中才可见的变量(参考函数章节的局部变量部分)。环境变量会影响用户及shell行为的变量。一般情况下,每一个进程都有自己的“环境”(environment),也就是一组该进程可以访问到的变量。从这个意义上来说,shell表现出与其他进程一样的行为。每当shell启动时,都会创建出与其环境对应的shell环境变量。改变或增加shell环境变量会使shell更新其自身的环境。子进程(由父进程执行产生)会继承父进程的环境变量。分配给环境变量的空间是有限的。创建过多环境变量或占用空间过大的环境变量有可能会造成问题。bash$ eval "`seq 10000 | sed -e 's/.*/export var&=ZZZZZZZZZZZZZZ/'`"bash$ dubash: /usr/bin/du: Argument list too long注意,上面的"错误"已经在Linux内核版本号为2.6.23的系统中修复了。(感谢 Stéphane Chazelas 对此问题的解释并提供了上面的例子。)如果在脚本中设置了环境变量,那么这些环境变量需要被“导出”,也就是通知脚本所在的环境做出相应的更新。这个“导出”操作就是 export 命令。4.4 特殊变量类型75脚本只能将变量导出到子进程,即在这个脚本中所调用的命令或程序。在命令行中调用的脚本不能够将变量回传给命令行环境,即子进程不能将变量回传给父进程。定义: 子进程(child process)是由另一个进程,即其父进程(parentprocess)所启动的子程序。位置参数从命令行中传递给脚本的参数 : $0, $1, $2, $3 ... 即命令行参数。$0 代表脚本名称, $1 代表第一个参数, $2 代表第二个, $3 代表第三个,以此类推 。在 $9 之后的参数必须被包含在大括号中,如 ${10}, ${11},${12} 。特殊变量 $* 与 $@ 代表所有位置参数。样例 4-5. 位置参数#!/bin/bash# 调用脚本时使用至少10个参数,例如# ./scriptname 1 2 3 4 5 6 7 8 9 10MINPARAMS=10echoecho "The name of this script is \"$0\"."# 附带 ./ 代表当前目录echo "The name of this script is \"`basename $0`\"."# 除去路径信息(查看 'basename')echoif [ -n "$1" ] # 测试变量是否存在thenecho "Parameter #1 is $1" # 使用引号转义#fiif [ -n "$2" ]124.4 特殊变量类型76thenecho "Parameter #2 is $2"fiif [ -n "$3" ]thenecho "Parameter #3 is $3"fi# ...if [ -n "${10}" ] # 大于 $9 的参数必须被放在大括号中thenecho "Parameter #10 is ${10}"fiecho "-----------------------------------"echo "All the command-line parameters are: "$*""if [ $# -lt "$MINPARAMS" ]thenechoecho "This script needs at least $MINPARAMS command-line arguments!"fiechoexit 0在位置参数中使用大括号助记符提供了一种非常简单的方式来访问传入脚本的最后一个参数。在其中会使用到间接引用。4.4 特殊变量类型77args=$# # 传入参数的个数lastarg=${!args}# 这是 $args 的一种间接引用方式# 也可以使用: lastarg=${!#} (感谢 Chris Monson.)# 这是 $# 的一种间接引用方式。# 注意 lastarg=${!$#} 是无效的。一些脚本能够根据调用时文件名的不同来执行不同的操作。要达到这样的效果,脚本需要检测 $0 ,也就是调用时的文件名 。同时,也必须存在指向这个脚本所有别名的符号链接文件(symbolic links)。详情查看样例 16-2。如果一个脚本需要一个命令行参数但是在调用的时候却没有传入,那么这将会造成一个空变量赋值。这通常不是我们想要的。一种避免的方法是,在使用期望的位置参数时候,在赋值语句两侧添加一个额外的字符。34.4 特殊变量类型78variable1_=$1_ # 而不是 variable1=$1# 使用这种方法可以在没有位置参数的情况下避免产生错误。critical_argument01=$variable1_# 多余的字符可以被去掉,就像下面这样:variable1=${variable1_/_/}# 仅仅当 $variable1_ 是以下划线开头时候才会有一些副作用。# 这里使用了我们稍后会介绍的参数替换模板中的一种。# (将替换模式设为空等价于删除。)# 更直接的处理方法就是先检测预期的位置参数是否被传入。if [ -z $1 ]thenexit $E_MISSING_POS_PARAMfi# 但是,正如 Fabin Kreutz 指出的,#+ 上面的方法会有一些意想不到的副作用。# 更好的方法是使用参数替换:# ${1:-$DefaultVal}# 详情查看第十章“操作变量”的第二节“变量替换”。样例 4-6. wh, whois 域名查询4.4 特殊变量类型79#!/bin/bash# ex18.sh# 在下面三个可选的服务器中进行 whois 域名查询:# ripe.net, cw.net, radb.net# 将这个脚本重命名为 'wh' 后放在 /usr/local/bin 目录下# 这个脚本需要进行符号链接:# ln -s /usr/local/bin/wh /usr/local/bin/wh-ripe# ln -s /usr/local/bin/wh /usr/local/bin/wh-apnic# ln -s /usr/local/bin/wh /usr/local/bin/wh-tucowsE_NOARGS=75if [ -z "$1" ]thenecho "Usage: `basename $0` [domain-name]"exit $E_NOARGSfi# 检查脚本名,访问对应服务器进行查询。case `basename $0` in # 也可以写: case ${0##*/} in"wh" ) whois $1@whois.tucows.com;;"wh-ripe" ) whois $1@whois.ripe.net;;"wh-apnic" ) whois $1@whois.apnic.net;;"wh-cw" ) whois $1@whois.cw.net;;* ) echo "Usage: `basename $0` [domain-name]";;esacexit $?使用 shift 命令可以将全体位置参数向左移一位, 重新赋值。$1 <--- $2 , $2 <--- $3 , $3 bash$ echo "\\">bash$ echo \aabash$ echo "\a"\abash$ echo x\tyxtybash$ echo "x\ty"x\tybash$ echo -e x\tyxtybash$ echo -e "x\ty"x y在 echo 后的双引号中一般会转义 \ 。并且 echo -e 会将 "\t" 解释成制表符。(感谢 Wayne Pollock 提出这些;感谢Geoff Lee 与 Daniel Barclay 对此做出的解释。) ↩125.1 引用变量90. 字符分割(word splitting)在本文中的意思是指将一个字符串分割成独立的、离散的变量。 ↩25.1 引用变量915.2 转义转义是一种引用单字符的方法。通过在特殊字符前加上转义符 \ 来告诉shell按照字面意思去解释这个字符。需要注意的是,在一些特定的命令和工具,比如 echo 和 sed 中,转义字符通常会起到相反的效果,即可能会使得那些字符产生特殊含义。在 echo 与 sed 命令中,转义字符的特殊含义\n换行(line feed)。\r回车(carriage return)。\t水平制表符。\v垂直制表符。\b退格。\a警报、响铃或闪烁。5.2 转义92\0xxASCII码的八进制形式,等价于 0nn ,其中 nn 是数字。在 $' ... ' 字符串扩展结构中可以通过转义八进制或十六进制的ASCII码形式给变量赋值,比如 quote=$'\042' 。样例 5-2. 转义字符#!/bin/bash# escaped.sh: 转义字符################################################# 首先让我们先看一下转义字符的基本用法。 ################################################## 转义新的一行。# ------------echo ""echo "This will printas two lines."# This will print# as two lines.echo "This will print \as one line."# This will print as one line.echo; echoecho "============="echo "\v\v\v\v" # 按字面意思打印 \v\v\v\v# 使用 echo 命令的 -e 选项来打印转义字符。echo "============="echo "VERTICAL TABS"echo -e "\v\v\v\v" # 打印四个垂直制表符。5.2 转义93echo "=============="echo "QUOTATION MARK"echo -e "\042" # 打印 " (引号,八进制ASCII码为42)。echo "=============="# 使用 $'\X' 这样的形式后可以不需要加 -e 选项。echo; echo "NEWLINE and (maybe) BEEP"echo $'\n' # 新的一行。echo $'\a' # 警报(响铃)。# 根据不同的终端版本,也可能是闪屏。# 我们之前介绍了 $'\nnn' 字符串扩展,而现在我们要看到的是...# ============================================ ## 自 Bash 第二个版本开始的 $'\nnn' 字符串扩展结构。# ============================================ #echo "Introducing the \$\' ... \' string-expansion construct . .. "echo ". . . featuring more quotation marks."echo $'\t \042 \t' # 在制表符之间的引号。# 需要注意的是 '\nnn' 是一个八进制的值。# 字符串扩展同样适用于十六进制的值,格式是 $'\xhhh'。echo $'\t \x22 \t' # 在制表符之间的引号。# 感谢 Greg Keraunen 指出这些。# 在早期的 Bash 版本中允许使用 '\x022' 这样的形式。echo# 将 ASCII 码字符赋值给变量。# -----------------------quote=$'\042' # 将 " 赋值给变量。echo "$quote Quoted string $quote and this lies outside the quot5.2 转义94es."echo# 连接多个 ASCII 码字符给变量。triple_underline=$'\137\137\137' # 137是 '_' ASCII码的八进制形式echo "$triple_underline UNDERLINE $triple_underline"echoABC=$'\101\102\103\010' # 101,102,103是 A, B, C# ASCII码的八进制形式。echo $ABCechoescape=$'\033' # 033 是 ESC 的八进制形式echo "\"escape\" echoes an $escape"# 没有可见输出echoexit 0下面是一个更加复杂的例子:样例 5-3. 检测键盘输入#!/bin/bash# 作者:Sigurd Solaas,作于2011年4月20日# 授权在《高级Bash脚本编程指南》中使用。# 需要 Bash 版本高于4.2。key="no value yet"while true; doclearecho "Bash Extra Keys Demo. Keys to try:"echoecho "* Insert, Delete, Home, End, Page_Up and Page_Down"echo "* The four arrow keys"5.2 转义95echo "* Tab, enter, escape, and space key"echo "* The letter and number keys, etc."echoecho " d = show date/time"echo " q = quit"echo "================================"echo# 将独立的Home键值转换为数字7上的Home键值:if [ "$key" = $'\x1b\x4f\x48' ]; thenkey=$'\x1b\x5b\x31\x7e'# 引用字符扩展结构。fi# 将独立的End键值转换为数字1上的End键值:if [ "$key" = $'\x1b\x4f\x46' ]; thenkey=$'\x1b\x5b\x34\x7e'ficase "$key" in$'\x1b\x5b\x32\x7e') # 插入echo Insert Key;;$'\x1b\x5b\x33\x7e') # 删除echo Delete Key;;$'\x1b\x5b\x31\x7e') # 数字7上的Home键echo Home Key;;$'\x1b\x5b\x34\x7e') # 数字1上的End键echo End Key;;$'\x1b\x5b\x35\x7e') # 上翻页echo Page_Up;;$'\x1b\x5b\x36\x7e') # 下翻页echo Page_Down;;$'\x1b\x5b\x41') # 上箭头echo Up arrow5.2 转义96;;$'\x1b\x5b\x42') # 下箭头echo Down arrow;;$'\x1b\x5b\x43') # 右箭头echo Right arrow;;$'\x1b\x5b\x44') # 左箭头echo Left arrow;;$'\x09') # 制表符echo Tab Key;;$'\x0a') # 回车echo Enter Key;;$'\x1b') # ESCecho Escape Key;;$'\x20') # 空格echo Space Key;;d)date;;q)echo Time to quit...echoexit 0;;*)echo Your pressed: \'"$key"\';;esacechoecho "================================"unset K1 K2 K3read -s -N1 -p "Press a key: "5.2 转义97K1="$REPLY"read -s -N2 -t 0.001K2="$REPLY"read -s -N1 -t 0.001K3="$REPLY"key="$K1$K2$K3"doneexit $?还可以查看样例 37-1。\"转义引号,指代自身。echo "Hello" # Helloecho "\"Hello\" ... he said." # "Hello" ... he said.\$转义美元符号(跟在 \\$ 后的变量名将不会被引用)。echo "\$variable01" # $variable01echo "The book cost \$7.98." # The book cost $7.98.\\转义反斜杠,指代自身。5.2 转义98echo "\\" # 结果是 \# 然而...echo "\" # 在命令行中会出现第二行并提示输入。# 在脚本中会出错。# 但是...echo '\' # 结果是 \根据转义符所在的上下文(强引用、弱引用,命令替换或者在 heredocument)的不同,它的行为也会有所不同。5.2 转义99# 简单转义与引用echo \z # zecho \\z # \zecho '\z' # \zehco '\\z' # \\zecho "\z" # \zecho "\\z" # \z# 命令替换echo `echo \z` # zecho `echo \\z` # zecho `echo \\\z` # \zecho `echo \\\\z` # \zecho `echo \\\\\\z` # \zecho `echo \\\\\\\z` # \\zecho `echo "\z"` # \zecho `echo "\\z"` # \z# Here Documentcat <# 感谢 Stéphane Chazelas 和 Kristopher Newsome。某些特定的退出码具有一些特定的保留含义,用户不应该在自己的脚本中重新定义它们。. 在函数没有用return来结束这个函数的情况下。 ↩16. 退出与退出状态107第七章 测试本章目录7.1 测试结构7.2 文件测试操作7.3 其他比较操作7.4 嵌套 if/then 条件测试7.5 牛刀小试每一个完备的程序设计语言都可以对一个条件进行判断,然后根据判断结果执行相应的指令。Bash 拥有 test 命令,双方括号、双圆括号 测试操作符以及if/then 测试结构。7. 测试1087.1 测试结构if/then 结构是用来检测一系列命令的 退出状态 是否为0(按 UNIX 惯例,退出码 0 表示命令执行成功),如果为0,则执行接下来的一个或多个命令。测试结构会使用一个特殊的命令 [ (参看特殊字符章节 左方括号)。等同于test 命令,它是一个内建命令,写法更加简洁高效。该命令将其参数视为比较表达式或文件测试,以比较结果作为其退出状态码返回(0 为真,1 为假)。Bash 在 2.02 版本中引入了扩展测试命令 [[...]] ,它提供了一种与其他语言语法更为相似的方式进行比较操作。注意, [[ 是一个 关键字 而非一个命令。Bash 将 [[ $a -lt $b ]] 视为一整条语句,执行并返回退出状态。结构 (( ... )) 和 let ... 根据其执行的算术表达式的结果决定退出状态码。这样的 算术扩展 结构可以用来进行 数值比较。7.1 测试结构109(( 0 && 1 )) # 逻辑与echo $? # 1 ***# 然后 ...let "num = (( 0 && 1 ))"echo $num # 0# 然而 ...let "num = (( 0 && 1 ))"echo $? # 1 ***(( 200 || 11 )) # 逻辑或echo $? # 0 ***# ...let "num = (( 200 || 11 ))"echo $num # 1let "num = (( 200 || 11 ))"echo $? # 0 ***(( 200 | 11 )) # 按位或echo $? # 0 ***# ...let "num = (( 200 | 11 ))"echo $num # 203let "num = (( 200 | 11 ))"echo $? # 0 ***# "let" 结构的退出状态与双括号算术扩展的退出状态是相同的。注意,双括号算术扩展表达式的退出状态码不是一个错误的值。算术表达式为0,返回1;算术表达式不为0,返回0。var=-2 && (( var+=2 ))echo $? # 1var=-2 && (( var+=2 )) && echo $var# 并不会输出 $var, 因为((var+=2))的状态码为17.1 测试结构110if 不仅可以用来测试括号内的条件表达式,还可以用来测试其他任何命令。if cmp a b &> /dev/null # 消去输出结果then echo "Files a and b are identical."else echo "Files a and b differ."fi# 下面介绍一个非常实用的 “if-grep" 结构:# -----------------------------------if grep -q Bash filethen echo "File contains at least one occurrence of Bash."fiword=Linuxletter_sequence=inuif echo "$word" | grep -q "$letter_sequence"# 使用 -q 选项消去 grep 的输出结果thenecho "$letter_sequence found in "$word"elseecho "$letter_sequence not found in $word"fiif COMMAND_WHOSE_EXIT_STATUS_IS_0_UNLESS_ERROR_OCCURREDthen echo "Command succeed."else echo "Command failed."fi感谢 Stéphane Chazelas 提供了后两个例子。样例 7-1. 什么才是真?#!/bin/bash# 提示:# 如果你不确定某个表达式的布尔值,可以用 if 结构进行测试。7.1 测试结构111echoecho "Testing \"0\""if [ 0 ]thenecho "0 is true."elseecho "0 is false."fi # 0 为真。echoecho "Testing \"1\""if [ 1 ]thenecho "1 is true."elseecho "1 is false."fi # 1 为真。echoecho "Testing \"-1\""if [ -1 ]thenecho "-1 is true."elseecho "-1 is false."fi # -1 为真。echoecho "Testing \"NULL\""if [ ] # NULL, 空thenecho "NULL is true."elseecho "NULL is false."fi # NULL 为假。7.1 测试结构112echoecho "Testing \"xyz\""if [ xyz ] # 字符串thenecho "Random string is true."elseecho "Random string is false."fi # 随机字符串为真。echoecho "Testing \"$xyz\""if [ $xyz ] # 原意是测试 $xyz 是否为空,但是# 现在 $xyz 只是一个没有初始化的变量。thenecho "Uninitialized variable is true."elseecho "Uninitialized variable is flase."fi # 未初始化变量含有null空值,为假。echoecho "Testing \"-n \$xyz\""if [ -n "$xyz" ] # 更加准确的写法。thenecho "Uninitialized variable is true."elseecho "Uninitialized variable is false."fi # 未初始化变量为假。echoxyz= # 初始化为空。echo "Testing \"-n \$xyz\""if [ -n "$xyz" ]thenecho "Null variable is true."7.1 测试结构113elseecho "Null variable is false."fi # 空变量为假。echo# 什么时候 "false" 为真?echo "Testing \"false\""if [ "false" ] # 看起来 "false" 只是一个字符串thenecho "\"false\" is true." #+ 测试结果为真。elseecho "\"false\" is false."fi # "false" 为真。echoecho "Testing \"\$false\"" # 未初始化的变量。if [ "$false" ]thenecho "\"\$false\" is true."elseecho "\"\$false\" is false."fi # "$false" 为假。# 得到了我们想要的结果。# 如果测试空变量 "$true" 会有什么样的结果?echoexit 0练习:理解 样例 7-17.1 测试结构114if [ condition-true ]thencommand 1command 2...else # 如果测试条件为假,则执行 else 后面的代码段command 3command 4...fi如果把 if 和 then 写在同一行时,则必须在 if 语句后加上一个分号来结束语句。因为 if 和 then 都是 关键字。以关键字(或者命令)开头的语句,必须先结束该语句(分号;),才能执行下一条语句。if [ -x "$filename" ]; thenElse if 与 elifelifelif 是 else if 的缩写。可以把多个 if/then 语句连到外边去,更加简洁明了。if [ condition1 ]thencommand1command2command3elif [condition2 ]# 等价于 else ifthencommand4command5elsedefault-commandfi7.1 测试结构115if test condition-true 完全等价于 if [ condition-true ] 。当语句开始执行时,左括号 [ 是作为调用 test 命令的标记 ,而右括号则不严格要求,但在新版本的 Bash 里,右括号必须补上。test 命令是 Bash 的 内建命令,可以用来检测文件类型和比较字符串。在Bash 脚本中, test 不调用 sh-utils 包下的文件 /usr/bin/test 。同样, [ 也不会调用链接到 /usr/bin/test 的 /usr/bin/[ 文件。bash$ type testtest is a shell builtinbash$ type '['[ is a shell builtinbash$ type '[['[[ is a shell keywordbash$ type ']]']] is a shell keywordbash$ type ']'bash: type: ]: not found如果你想在 Bash 脚本中使用 /usr/bin/test ,那你必须把路径写全。样例 7-2. test , /usr/bin/test , [] 和 /usr/bin/[ 的等价性#!/bin/bashechoif test -z "$1"thenecho "No command-line arguments."elseecho "First command-line argument is $1."fiechoif /usr/bin/test -z "$1" # 等价于内建命令 "test"# ^^^^^^^^^^^^^ # 指定全路径then17.1 测试结构116echo "No command-line arguments."elseecho "First command-line argument is $1."fiechoif [ -z "$1" ] # 功能和上面的代码相同。# if [ -z "$1" 理论上可行,但是 Bash 会提示缺失右括号thenecho "No command-line arguments."elseecho "First command-line argument is $1."fiechoif /usr/bin/[ -z "$1" ] # 功能和上面的代码相同。# if /usr/bin/[ -z "$1" # 理论上可行,但是会报错# # 已经在 Bash 3.x 版本被修复了thenecho "No command-line arguments."elseecho "First command-line argument is $1."fiechoexit 0在 Bash 里, [[ ]] 是比 [ ] 更加通用的写法。其作为扩展 test 命令从ksh88 中被继承了过来。在 [[ 和 ]] 中不会进行文件名扩展或字符串分割,但是可以进行参数扩展和命令替换。7.1 测试结构117file=/etc/passwdif [[ -e $file ]]thenecho "Password file exists."fi使用 [[...]] 代替 [...] 可以避免很多逻辑错误。比如可以在 [[]] 中使用&& , || , 操作符,而在 [] 中使用则会报错。在 [[]] 中会自动执行八进制和十六进制的进制转换操作。7.1 测试结构118# [[ 八进制和十六进制进制转换 ]]# 感谢 Moritz Gronbach 提出。decimal=15octal=017 # = 15 (十进制)hex=0x0f # = 15 (十进制)if [ "$decimal" -eq "$octal" ]thenecho "$decimal equals $octal"elseecho "$decimal is not equal to $octal" # 15 不等于 017fi # 在单括号 [ ] 之间不会进行进制转换。if [[ "$decimal" -eq "$octal" ]]thenecho "$decimal equals $octal" # 15 等于 017elseecho "$decimal is not equal to $octal"fi # 在双括号 [[ ]] 之间会进行进制转换。if [[ "$decimal" -eq "$hex" ]]thenecho "$decimal equals $hex" # 15 等于 0x0felseecho "$decimal is not equal to $hex"fi # 十六进制也可以进行转换。语法上并不严格要求在 if 之后一定要写 test 命令或者测试结构( []或 [[]] )。7.1 测试结构119dir=/home/bozoif cd "$dir" 2>/dev/null; then # "2>/dev/null" 重定向消去错误输出。echo "Now in $dir."elseecho "Can't change to $dir."fiif COMMAND 的退出状态就是 COMMAND 的退出状态。同样的,测试括号也不一定需要与 if 一起使用。其可以同 列表结构 结合而不需要 if 。var1=20var2=22[ "$var1" -ne "$var2" ] && echo "$var1 is not equal to $var2"home=/home/bozo[ -d "$home" ] || echo "$home directory does not exist."(( )) 结构 扩展和执行算术表达式。如果执行结果为0,其返回的 退出状态码为1(假)。非0表达式返回的退出状态为0(真)。这与上述所使用的 test 和[ ] 结构形成鲜明的对比。样例 7-3. 使用 (( )) 进行算术测试#!/bin/bash# arith-tests.sh# 算术测试。# (( ... )) 结构执行并测试算术表达式。# 与 [ ... ] 结构的退出状态正好相反。(( 0 ))echo "Exit status of \"(( 0 ))\" is $?." # 1(( 1 ))7.1 测试结构120echo "Exit status of \"(( 1 ))\" is $?." # 0(( 5 > 4 )) # 真echo "Exit status of \"(( 5 > 4 ))\" is $?." # 0(( 5 > 9 )) # 假echo "Exit status of \"(( 5 > 9 ))\" is $?." # 1(( 5 == 5 )) # 真echo "Exit status of \"(( 5 == 5 ))\" is $?." # 0# (( 5 = 5 )) 会报错。(( 5 - 5 )) # 0echo "Exit status of \"(( 5 - 5 ))\" is $?." # 1(( 5 / 4 )) # 合法echo "Exit status of \"(( 5 / 4 ))\" is $?." # 0(( 1 / 2 )) # 结果小于1echo "Exit status of \"(( 1 / 2 ))\" is $?." # 舍入至0。# 1(( 1 / 0 )) 2>/dev/null # 除0,非法# ^^^^^^^^^^^echo "Exit status of \"(( 1 / 0 ))\" is $?." # 1# "2>/dev/null" 的作用是什么?# 如果将其移除会发生什么?# 尝试移除这条语句并重新执行脚本。# ======================================= ## (( ... )) 在 if-then 中也非常有用var1=5var2=4if (( var1 > var2 ))then #^ ^ 注意不是 $var1 和 $var2,为什么?echo "$var1 is greater then $var2"7.1 测试结构121fi # 5 大于 4exit 0. 标记是一个具有特殊意义(元语义)的符号或者短字符串。在 Bash 里像[ 和 .(点命令) 这样的标记可以扩展成关键字和命令。 ↩17.1 测试结构1227.2 文件测试操作下列每一个 test 选项在满足条件时,返回0(真)。-e检测文件是否存在-a检测文件是否存在等价于 -e 。不推荐使用,已被弃用 。-f文件是常规文件(regular file),而非目录或 设备文件-s文件大小不为0-d文件是一个目录-b文件是一个 块设备-c文件是一个 字符设备17.2 文件测试操作123device0="/dev/sda2" # / (根目录)if [ -b "$device0" ]thenecho "$device0 is a block device."fi# /dev/sda2 是一个块设备。device1="/dev/ttyS1" # PCMCIA 调制解调卡if [ -c "$device1" ]thenecho "$device1 is a character device."fi# /dev/ttyS1 是一个字符设备。-p文件是一个 管道设备function show_input_type(){[ -p /dev/fd/0 ] && echo PIPE || echo STDIN}show_input_type "Input" # STDINecho "Input" | show_input_type # PIPE# 这个例子由 Carl Anderson 提供。-h文件是一个 符号链接7.2 文件测试操作124-L文件是一个符号链接-S文件是一个 套接字-t文件(文件描述符)与终端设备关联该选项通常被用于 测试 脚本中的 stdin [ -t 0 ] 或 stdout [ -t 1 ] 是否为终端设备。-r该文件对执行测试的用户可读-w该文件对执行测试的用户可写-x该文件可被执行测试的用户所执行-g文件或目录设置了 set-group-id sgid 标志如果一个目录设置了 sgid 标志,那么在该目录中所有的新建文件的权限组都归属于该目录的权限组,而非文件创建者的权限组。该标志对共享文件夹很有用。-u7.2 文件测试操作125文件设置了 set-user-id suid 标志。一个属于 root 的可执行文件设置了 suid 标志后,即使是一个普通用户执行也拥有 root 权限 。对需要访问硬件设备的可执行文件(例如 pppd 和 cdrecord )很有用。如果没有 suid 标志,这些可执行文件就不能被非 root 用户所调用了。-rwsr-xr-t 1 root 178236 Oct 2 2000 /usr/sbin/pppd设置了 suid 标志后,在权限中会显示 s 。-k设置了粘滞位(sticky bit)。标志粘滞位是一种特殊的文件权限。如果文件设置了粘滞位,那么该文件将会被存储在高速缓存中以便快速访问 。如果目录设置了该标记,那么它将会对目录的写权限进行限制,目录中只有文件的拥有者可以修改或删除文件。设置标记后你可以在权限中看到 t 。drwxrwxrwt 7 root 1024 May 19 21:26 tmp/如果一个用户不是设置了粘滞位目录的拥有者,但对该目录有写权限,那么他仅仅可以删除目录中他所拥有的文件。这可以防止用户不经意间删除或修改其他人的文件,例如 /tmp 文件夹。(当然目录的所有者可以删除或修改该目录下的所有文件)-O执行用户是文件的拥有者-G文件的组与执行用户的组相同-N237.2 文件测试操作126文件在在上次访问后被修改过了f1 -nt f2文件 f1 比文件 f2 新f1 -ot f2文件 f1 比文件 f2 旧f1 -ef f2文件 f1 和文件 f2 硬链接到同一个文件!取反——对测试结果取反(如果条件缺失则返回真)。样例 7-4. 检测链接是否损坏#!/bin/bash# broken-link.sh# Lee bigelow 编写。# ABS Guide 经许可可以使用。# 该脚本用来发现输出损坏的链接。输出的结果是被引用的,#+ 所以可以直接导到 xargs 中进行处理 :)# 例如:sh broken-link.sh /somedir /someotherdir|xargs rm## 更加优雅的方式:## find "somedir" -type 1 -print0|\# xargs -r0 file|\# grep "broken symbolic"|# sed -e 's/^\|: *broken symbolic.*$/"/g'## 但是这种方法不是纯 Bash 写法。# 警告:小心 /proc 文件下的文件和任意循环链接!7.2 文件测试操作127############################################# 如果不给脚本传任何参数,那么 directories-to-search 设置为当前目录#+ 否则设置为传进的参数#####################[ $# -eq 0 ] && directory=`pwd` || directory=$@# 函数 linkchk 是用来检测传入的文件夹中是否包含损坏的链接文件,#+ 并引用输出他们。# 如果文件夹中包含子文件夹,那么将子文件夹继续传给 linkchk 函数进行检测。#################linkchk () {for element in $1/*; do[ -h "$element" -a ! -e "$element" ] && echo \"$element\"[ -d "$element" ] && linkchk $element# -h 用来检测是否是链接,-d 用来检测是否是文件夹。done}# 检测传递给 linkchk() 函数的参数是否是一个存在的文件夹,#+ 如果不是则报错。################for directory in $direcotrys; doif [ -d $directory ]then linkchk $directoryelseecho "$directory is not a directory"echo "Usage $0 dir1 dir2 ..."fidoneexit $?样例 31-1,样例 11-8,样例 11-3,样例 31-3和样例 A-1 也包含了文件测试操作符的使用。17.2 文件测试操作128. 摘自1913年版本的韦氏词典Deprecate...To pray against, as an evil;to seek to avert by prayer;to desire the removal of;to seek deliverance from;to express deep regret for;to disapprove of strongly.↩. 注意使用 suid 的可执行文件可能会带来安全问题。suid 标记对 shell 脚本没有影响。 ↩. 在 Linux 系统中,文件已经不使用粘滞位了, 粘滞位只作用于目录。 ↩1237.2 文件测试操作1297.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" ]7.3 其他比较操作130-le小于等于if [ "$a" -le "$b" ]<小于(使用 双圆括号)(("$a" < "$b"))<=小于等于(使用双圆括号)(("$a" 大于(使用双圆括号)(("$a" > "$b"))>=大于等于(使用双圆括号)(("$a" >= "$b"))字符串比较=等于if [ "$a" = "$b" ]注意在 = 前后要加上空格7.3 其他比较操作131if [ "$a"="$b" ] 和上面不等价。==等于if [ "$a" == "$b" ]和 = 同义== 操作符在 双方括号 和单方括号里的功能是不同的。[[ $a == z* ]] # $a 以 "z" 开头时为真(模式匹配)[[ $a == "z*" ]] # $a 等于 z* 时为真(字符匹配)[ $a == z* ] # 发生文件匹配和字符分割。[ "$a" == "z*" ] # $a 等于 z* 时为真(字符匹配)# 感谢 Stéphane Chazelas!=不等于if [ "$a" != "$b" ]在 [[ ... ]] 结构中会进行模式匹配。<小于,按照 ASCII码 排序。if [[ "$a" < "$b" ]]if [ "$a" \< "$b" ]注意在 [] 结构里 7.3 其他比较操作132大于,按照 ASCII 码排序。if [[ "$a" > "$b" ]]if [ "$a" \> "$b" ]注意在 [] 结构里 > 需要被转义。样例 27-11 包含了比较操作符。-z字符串为空,即字符串长度为0。String='' # 长度为0的字符串变量。if [ -z "$String" ]thenecho "\$String is null."elseecho "\$String is NOT null."fi # $String is null.-n字符串非空( null )。使用 -n 时字符串必须是在括号中且被引用的。使用 ! -z 判断未引用的字符串或者直接判断(样例 7-6)通常可行,但是非常危险。判断字符串时一定要引用 。样例 7-5. 算术比较和字符串比较17.3 其他比较操作133#!/bin/basha=4b=5# 这里的 "a" 和 "b" 可以是整数也可以是字符串。# 因为 Bash 的变量是弱类型的,因此字符串和整数比较有很多相同之处。# 在 Bash 中可以用处理整数的方式来处理全是数字的字符串。# 但是谨慎使用。echoif [ "$a" -ne "$b" ]thenecho "$a is not equal to $b"echo "(arithmetic comparison)"fiechoif [ "$a" != "$b" ]thenecho "$a is not equal to $b."echo "(string comparison)"# "4" != "5"# ASCII 52 != ASCIII 53fi# 在这个例子里 "-ne" 和 "!=" 都可以。echoexit 0样例 7-6. 测试字符串是否为空( null )#!/bin/bash# str-test.sh: 测试是否为空字符串或是未引用的字符串。7.3 其他比较操作134# 使用 if [ ... ] 结构# 如果字符串未被初始化,则其值是未定义的。# 这种状态就是空 "null"(并不是 0)。if [ -n $string1 ] # 并未声明或是初始化 string1。thenecho "String \"string1\" is not null."elseecho "String \"string1\" is null."fi# 尽管没有初始化 string1,但是结果显示其非空。echo# 再试一次。if [ -n "$string1" ] # 这次引用了 $string1。thenecho "String \"string1\" is not null."elseecho "String \"string1\" is null."fi # 在测试括号内引用字符串得到了正确的结果。echoif [ $string1 ] # 这次只有一个 $string1。thenecho "String \"string1\" is not null."elseecho "String \"string1\" is null."fi # 结果正确。# 独立的 [ ... ] 测试操作符可以用来检测字符串是否为空。# 最好将字符串进行引用(if [ "$string1" ])。## Stephane Chazelas 指出:# if [ $string1 ] 只有一个参数 "]"# if [ "$string1" ] 则有两个参数,空的 "$string1" 和 "]"7.3 其他比较操作135echostring1=initializedif [ $string1 ] # $string1 这次仍然没有被引用。thenecho "String \"string1\" is not null."elseecho "String \"string1\" is null."fi # 这次的结果仍然是正确的。# 最好将字符串引用("$string1")string1="a = b"if [ $string1 ] # $string1 这次仍然没有被引用。thenecho "String \"string1\" is not null."elseecho "String \"string1\" is null."fi # 这次没有引用就错了。exit 0 # 同时感谢 Florian Wisser 的提示。样例 7-7. zmore#!/bin/bash# zmore# 使用筛选器 'more' 查看 gzipped 文件。E_NOARGS=85E_NOTFOUND=86E_NOTGZIP=87if [ $# -eq 0 ] # 作用和 if [ -z "$1" ] 相同。# $1 可以为空: zmore "" arg2 arg37.3 其他比较操作136thenecho "Usage: `basename $0` filename" >&2# 将错误信息通过标准错误 stderr 进行输出。exit $E_NOARGS# 脚本的退出状态为 85.fifilename=$1if [ ! -f "$filename" ] # 引用字符串以防字符串中带有空格。thenecho "File $filename not found!" >&2 # 通过标准错误 stderr 进行输出。exit $E_NOTFOUNDfiif [ ${filename##*.} != "gz" ]# 在括号内使用变量代换。thenecho "File $1 is not a gzipped file!"exit $E_NOTGZIPfizcat $1 | more# 使用筛选器 'more'# 也可以用 'less' 替代exit $? # 脚本的退出状态由管道 pipe 的退出状态决定。# 实际上 "exit $?" 不一定要写出来,#+ 因为无论如何脚本都会返回最后执行命令的退出状态。复合比较-a逻辑与7.3 其他比较操作137exp1 -a exp2 返回真当且仅当 exp1 和 exp2 均为真。-o逻辑或如果 exp1 或 exp2 为真,则 exp1 -o exp2 返回真。以上两个操作和 双方括号 结构中的 Bash 比较操作符号 && 和 || 类似。[[ condition1 && condition2 ]]测试操作 -o 和 -a 可以在 test 命令或在测试括号中进行。if [ "$expr1" -a "$expr2" ]thenecho "Both expr1 and expr2 are true."elseecho "Either expr1 or expr2 is false."firihad 指出:[ 1 -eq 1 ] && [ -n "`echo true 1>&2`" ] # 真[ 1 -eq 2 ] && [ -n "`echo true 1>&2`" ] # 没有输出# ^^^^^^^ 条件为假。到这里为止,一切都按预期执行。# 但是[ 1 -eq 2 -a -n "`echo true 1>&2`" ] # 真# ^^^^^^^ 条件为假。但是为什么结果为真?# 是因为括号内的两个条件子句都执行了么?[[ 1 -eq 2 && -n "`echo true 1>&2`" ]] # 没有输出# 并不是。# 所以显然 && 和 || 具备“短路”机制,#+ 例如对于 &&,若第一个表达式为假,则不执行第二个表达式直接返回假,#+ 而 -a 和 -o 则不是。7.3 其他比较操作138复合比较操作的例子可以参考 样例 8-3,样例 27-17 和 样例 A-29。. S.C. 指出在复合测试中,仅仅引用字符串可能还不够。比如表达式 [ -n"$string" -o "$a" = "$b" ] 在某些 Bash 版本下,如果 $string 为空可能会出错。更加安全的方式是,对于可能为空的字符串,添加一个额外的字符,例如 [ "x$string" != x -o "x$a" = "x$b" ] (其中的 x 互相抵消)。 ↩17.3 其他比较操作1397.4 嵌套 if/then 条件测试可以嵌套 if/then 条件测试结构。嵌套的结果等价于使用 && 复合比较操作符。a=3if [ "$a" -gt 0 ]thenif [ "$a" -lt 5 ]thenecho "The value of \"a\" lies somewhere between 0 and 5."fifi# 和下面的结果相同if [ "$a" -gt 0 ] && [ "$a" -lt 5 ]thenecho "The value of \"a\" lies somewhere between 0 and 5."fi在 样例 37-4 和 样例 17-11 中展示了嵌套 if/then 条件测试结构。7.4 嵌套 if/then 条件测试1407.5 牛刀小试系统文件 xinitrc 可以用来启动软件 X Server。该文件包含了许多 if/then测试结构。下面的代码摘录自较早版本的 xinitrc (大约在 Red Hat 7.1 版本)。if [ -f $HOME/.Xclients ]; thenexec $HOME/.Xclientselif [ -f /etc/X11/xinit/Xclients ]; thenexec /etc/X11/xinit/Xclientselse# 安全分支。尽管程序不会执行这个分支。# (我们在 Xclients 中也提供了相同的机制)增强程序可靠性。xclock -geometry 100x100-5+5 &xterm -geometry 80x50-50+150 &if [ -f /usr/bin/netscape -a -f /usr/share/doc/HTML/index.html ]; thennetscape /usr/share/doc/HTML/index.htmlfifi试着解释代码片段中的条件测试结构, 然后试着在 /etc/X11/xinit/xinitrc 查看最新版本,并且分析其中的 if/then 条件测试结构。为了更好的进行分析,你可能需要继续阅读后面章节中对 grep , sed 和 正则表达式 的讨论。7.5 牛刀小试141第八章 运算符相关话题本章目录8.1 运算符8.2 数字常量8.3 双圆括号结构8.4 运算符优先级8. 运算符相关话题1428.1. 运算符赋值运算符变量赋值,初始化或改变一个变量的值。=等号 = 赋值运算符,既可用于算术赋值,也可用于字符串赋值。var=27category=minerals # "="左右不允许有空格注意,不要混淆 = 赋值运算符与 = 测试操作符。# = 作为测试操作符if [ "$string1" = "$string2" ]thencommandfi# [ "X$string1" = "X$string2" ] 这样写是安全的,# 这样写可以避免任意一个变量为空时的报错。# (变量前加的"X"字符规避了变量为空的情况)算术运算符+加-8.1 运算符143减*乘/除**幂运算# Bash, 2.02版本,推出了"**"幂运算操作符。let "z=5**3" # 5 * 5 * 5echo "z = $z" # z = 125%取余(返回整数除法的余数)bash$ expr 5 % 325/3=1,余2 取余运算符经常被用于生成一定范围内的数( 案例9-11, 案例9-15),以及格式化程序输出(案例 27-16,案例 A-6)。 取余运算符还可以用来产生素数(案例A-15),取余的出现大大扩展了整数的算术运算。样例 8-1. 最大公约数#!/bin/bash# gcd.sh: 最大公约数# 使用欧几里得算法8.1 运算符144# 两个整数的最大公约数(gcd)# 是两数能同时整除的最大数# 欧几里得算法使用辗转相除法# In each pass,# dividend <--- divisor# divisor = 2.05b, Bash支持了64-bit整型数。8.1 运算符148注意,Bash并不支持浮点运算,Bash会将带小数点的数看做字符串。a=1.5let "b = $a + 1.3" # 报错# t2.sh: let: b = 1.5 + 1.3: syntax error in expression# (error token is ".5 + 1.3")echo "b = $b" # b=1如果你想在脚本中使用浮点数运算,借助bc或外部数学函数库吧。位运算位运算很少出现在shell脚本中,在bash中加入位运算的初衷似乎是为了操控和检测来自 ports 或 sockets 的数据。位运算在编译型语言中能发挥更大的作用,比如C/C++,位运算提供了直接访问系统硬件的能力。然而,聪明的vladz在他的base64.sh(案例 A-54)脚本中也用到了位运算。 下面介绍位运算符。<<左移运算符(左移1位相当于乘2)<<=左移赋值let "var <>右移运算符(右移1位相当于除2)>>=右移赋值8.1 运算符149&按位与(AND)&=按位与等(AND-equal)|按位或(OR)|=按位或等(OR-equal)~按位取反^按位异或(XOR)^=按位异或等(XOR-equal)逻辑(布尔)运算符!非(NOT)8.1 运算符150if [ ! -f $FILENAME ]then...&&与(AND)if [ $condition1 ] && [ $condition2 ]# 等同于: if [ $condition1 -a $condition2 ]# 返回true如果 condition1 和 condition2 同时为真...if [[ $condition1 && $condition2 ]] # 可行# 注意,&& 运算符不能用在[ ... ]结构里。&&也可以被用在 list 结构中连接命令。||或(OR)if [ $condition1 ] || [ $condition2 ]# 等同于: if [ $condition1 -a $condition2 ]# 返回true如果 condition1 和 condition2 任意一个为真...if [[ $condition1 || $condition2 ]] # 可行# 注意,|| 运算符不能用在[ ... ]结构里。小结样例 8-3. 在条件测试中使用 && 和 ||#!/bin/bash8.1 运算符151a=24b=47if [ "$a" -eq 24 ] && [ "$b" -eq 47 ]thenecho "Test #1 succeeds."elseecho "Test #1 fails."fi# 错误: if [ "$a" -eq 24 && "$b" -eq 47 ]# 这样写的话,bash会先执行'[ "$a" -eq 24'# 然后就找不到右括号']'了...## 注意: if [[ $a -eq 24 && $b -eq 24 ]] 这样写是可以的# 双方括号测试结构比单方括号更加灵活。# (双方括号中的"&&"与单方括号中的"&&"意义不同)# 感谢 Stephane Chazelas 指出。if [ "$a" -eq 98 ] || [ "$b" -eq 47 ]thenecho "Test #2 succeeds."elseecho "Test #2 fails."fi# 使用 -a 和 -o 选项也具有同样的效果。# 感谢 Patrick Callahan 指出。if [ "$a" -eq 24 -a "$b" -eq 47 ]thenecho "Test #3 succeeds."elseecho "Test #3 fails."fi8.1 运算符152if [ "$a" -eq 98 -o "$b" -eq 47 ]thenecho "Test #4 succeeds."elseecho "Test #4 fails."fia=rhinob=crocodileif [ "$a" = rhino ] && [ "$b" = crocodile ]thenecho "Test #5 succeeds."elseecho "Test #5 fails."fiexit 0&& 和 || 运算符也可以用在算术运算中。bash$ echo $(( 1 && 2 )) $((3 && 0)) $((4 || 0)) $((0 || 0))1 0 1 0其他运算符,逗号运算符 逗号运算符用于连接两个或多个算术操作,所有的操作会被依次求值(可能会有副作用)。28.1 运算符153let "t1 = ((5 + 3, 7 - 1, 15 - 4))"echo "t1 = $t1" ^^^^^^ # t1 = 11# 这里的t1 被赋值了11,为什么?let "t2 = ((a = 9, 15 / 3))" # 对"a"赋值并对"t2"求值。echo "t2 = $t2 a = $a" # t2 = 5 a = 9逗号运算符常被用在 for 循环中。参看案例 11-13。. 取决与不同的上下文,+= 也可能作为字符串连接符。它可以很方便地修改环境变量。 ↩. 副作用,顾名思义,就是预料之外的结果。 ↩128.1 运算符1548.2. 数字常量通常情况下,shell脚本会把数字以十进制整数看待(base 10),除非数字加了特殊的前缀或标记。 带前缀0的数字是八进制数(base 8);带前缀0x的数字是十六进制数(base 16)。 内嵌 # 的数字会以 BASE#NUMBER 的方式进行求值(不能超出当前shell支持整数的范围)。样例 8-4. 数字常量的表示#!/bin/bash# numbers.sh: 不同进制数的表示# 十进制数: 默认let "dec = 32"echo "decimal number = $dec" # 32# 一切正常。# 八进制数: 带前导'0'的数let "oct = 032"echo "octal number = $oct" # 26# 结果以 十进制 打印输出了。# ------ ------ -----------# 十六进制数: 带前导'0x'或'0X'的数let "hex = 0x32"echo "hexadecimal number = $hex" # 50echo $((0x9abc)) # 39612# ^^ ^^ 双圆括号进行表达式求值# 结果以十进制打印输出。# 其他进制数: BASE#NUMBER# BASE 范围: 2 - 64# NUMBER 必须以 BASE 规定的正确形式书写,如下:8.2 数字常量155let "bin = 2#111100111001101"echo "binary number = $bin" # 31181let "b32 = 32#77"echo "base-32 number = $b32" # 231let "b64 = 64#@_"echo "base-64 number = $b64" # 4031# 这种表示法只对进制范围(2 - 64)内的 ASCII 字符有效。# 10 数字 + 26 小写字母 + 26 大写字母 + @ + _echoecho $((36#zz)) $((2#10101010)) $((16#AF16)) $((53#1aA))# 1295 170 44822 3375# 重要提醒:# ---------# 使用超出进制范围以外的符号会报错。let "bad_oct = 081"# (可能的) 报错信息:# bad_oct = 081: value too great for base (error token is "081")# Octal numbers use only digits in the range 0 - 7.exit $? # 退出码 = 1 (错误)# 感谢 Rich Bartell 和 Stephane Chazelas 的说明。8.2 数字常量156双圆括号结构与 let 命令类似, (( ... )) 结构允许对算术表达式的扩展和求值。它是 let 命令的简化形式。例如,a=$(( 5 + 3 )) 会将变量a赋值成 5 + 3,也就是8。在Bash中,双圆括号结构也允许以C风格的方式操作变量。例如,(( var++ ))。样例 8-5. 以C风格的方式操作变量#!/bin/bash# c-vars.sh# 以C风格的方式操作变量,使用(( ... ))结构echo(( a = 23 )) # C风格的变量赋值,注意"="等号前后都有空格echo "a (initial value) = $a" # 23(( a++ )) # 后缀自增'a',C-style.echo "a (after a++) = $a" # 24(( a-- )) # 后缀自减'a', C-style.echo "a (after a--) = $a" # 23(( ++a )) # 前缀自增'a', C-style.echo "a (after ++a) = $a" # 24(( --a )) # 前缀自减'a', C-style.echo "a (after --a) = $a" # 23echo######################################################### 注意,C风格的++,--运算符,前缀形式与后缀形式有不同的#+ 副作用。8.3 双圆括号结构157n=1; let --n && echo "True" || echo "False" # Falsen=1; let n-- && echo "True" || echo "False" # True# 感谢 Jeroen Domburg。########################################################echo(( t = a<45?7:11 )) # C风格三目运算符。# ^ ^ ^echo "If a < 45, then t = 7, else t = 11." # a = 23echo "t = $t " # t = 7echo# -----------# 复活节彩蛋!# -----------# Chet Ramey 偷偷往Bash里加入了C风格的语句结构,# 还没写文档说明 (实际上很多是从ksh中继承过来的)。# 在Bash 文档中,Ramey把 (( ... ))结构称为shell 算术运算,# 但是这种表述并不准确...# 抱歉啊,Chet,把你的秘密抖出来了。# 参看 "for" 和 "while" 循环章节关于 (( ... )) 结构的部分。# (( ... )) 结构在Bash 2.04版本之后才能正常工作。exit还可以参看 样例 11-13 与 样例 8-4。8.3 双圆括号结构158运算符优先级在脚本中,运算执行的顺序被称为优先级: 高优先级的操作会比低优先级的操作先执行。表 8-1. 运算符优先级(从高到低)运算符 含义 注解var++ var--后缀自增/自减C风格运算符++var --var前缀自增/自减! ~按位取反/逻辑取反对每一比特位取反/对逻辑判断的结果取反** 幂运算 算数运算符* / %乘, 除, 取余算数运算符+ - 加, 减 算数运算符<>左移, 右移比特位运算符-z -n 一元比较 字符串是/否为空-e -f -t -x, etc 一元比较 文件测试-lt -gt -le -ge =复合比较 字符串/整数比较-nt -ot -ef 复合比较 文件测试&AND(按位与)按位与操作^XOR(按位异或)按位异或操作\ OR(按位或)按位或操作18.4 运算符优先级159&& -aAND(逻辑与)逻辑与, 复合比较\ \ -oOR(逻辑或)逻辑或,复合比较? :if/else三目运算符C风格运算符= 赋值 不要与test中的等号混淆*= /= %= += -=<>= &=赋值运算 先运算后赋值,逗号运算符连接一系列语句实际上,你只需要记住以下规则就可以了:先乘除取余,后加减,与算数运算相似复合逻辑运算符,&&, ||, -a, -o 优先级较低优先级相同的操作按从左至右顺序求值现在,让我们利用运算符优先级的知识来分析一下Fedora Core Linux中的 /etc/init.d/functions 文件。while [ -n "$remaining" -a "$retry" -gt 0 ]; do# 初看之下很恐怖...# 分开来分析while [ -n "$remaining" -a "$retry" -gt 0 ]; do# --condition 1-- ^^ --condition 2-# 如果变量"$remaining" 长度不为0#+ 并且AND (-a)#+ 变量 "$retry" 大于0#+ 那么#+ [ 方括号表达式 ] 返回成功(0)#+ while-loop 开始迭代执行语句。# =============================================================8.4 运算符优先级160=# "condition 1" 和 "condition 2" 在 AND之前执行,为什么?# 因为AND(-a)优先级比-n,-gt来得低,逻辑与会在最后求值。#################################################################if [ -f /etc/sysconfig/i18n -a -z "${NOLOCALE:-}" ] ; then# 同样,分开来分析if [ -f /etc/sysconfig/i18n -a -z "${NOLOCALE:-}" ] ; then# --condition 1--------- ^^ --condition 2-----# 如果文件"/etc/sysconfig/i18n" 存在#+ 并且AND (-a)#+ 变量 $NOLOCALE 长度不为0#+ 那么#+ [ 方括号表达式 ] 返回成功(0)#+ 执行接下来的语句。## 和之前的情况一样,逻辑与AND(-a)最后求值。# 因为在方括号测试结构中,逻辑运算的优先级是最低的。# ==============================================================# 注意:# ${NOLOCALE:-} 是一个参数扩展式,看起来有点多余。# 但是, 如果 $NOLOCALE 没有提前声明, 它会被设成null,# 在某些情况下,这会有点问题。为了避免在复杂比较运算中的错误,可以把运算分散到几个括号结构中。if [ "$v1" -gt "$v2" -o "$v1" -lt "$v2" -a -e "$filename" ]# 这样写不清晰...if [[ "$v1" -gt "$v2" ]] || [[ "$v1" -lt "$v2" ]] && [[ -e "$filename" ]]# 好多了 -- 把逻辑判断分散到多个组之中18.4 运算符优先级161. Precedence(优先级),根据上下文,与priority含义相近。 ↩18.4 运算符优先级162第三部分 shell进阶目录9. 换个角度看变量9.1 内部变量9.2 指定变量属性: decalre 或 typeset9.3 $RANDOM :随机产生整数10. 变量处理10.1 字符串处理10.1.1 使用 awk 处理字符串10.1.2 参考资料10.2 参数替换11. 循环与分支11.1 循环11.2 嵌套循环11.3 循环控制11.4 测试与分支12. 命令替换13. 算术扩展14. 休息时间第三部分 shell进阶163第十章 变量处理本章目录10.1 字符串处理10.1.1 使用 awk 处理字符串10.1.2 参考资料10.2 参数替换10. 变量处理16410.1 字符串处理Bash 支持的字符串操作数量达到了一个惊人的数目。但可惜的是,这些操作工具缺乏一个统一的核心。他们中的一些是参数代换的子集,另外一些则是 UNIX 下expr 函数的子集。这将会导致语法前后不一致或者功能上出现重叠,更不用说那些可能导致的混乱了。字符串长度$expr length $string上面两个表达式等价于C语言中的 strlen() 函数。expr "$string" : '.*'stringZ=abcABC123ABCabcecho ${#stringZ} # 15echo `expr length $stringz` # 15echo `expr "$stringZ" : '.*'` # 15样例 10-1. 在文本的段落之间插入空行10.1 字符串处理165#!/bin/bash# paragraph-space.sh# 版本 2.1,发布日期 2012年7月29日# 在无空行的文本文件的段落之间插入空行。# 像这样使用: $0 "$filename.$SUFFIX"# 将转换结果重定向到新的文件。rm -f $file # 在转换后删除原文件。echo "$filename.$SUFFIX" # 将记录输出到 stdout 中。doneexit 0# 练习:# -----# 这个脚本会将当前工作目录下的所有文件进行转换。# 修改脚本,使得它仅转换 ".mac" 后缀的文件。# *** 还可以使用另外一种方法。 *** ##!/bin/bash10.1 字符串处理174# 将图像批处理转换成不同的格式。# 假设已经安装了 imagemagick。(在大部分 Linux 发行版中都有)INFMT=png # 可以是 tif, jpg, gif 等等。OUTFMT=pdf # 可以是 tif, jpg, gif, pdf 等等。for pic in *"$INFMT"dop2=$(ls "$pic" | sed -e s/\.$INFMT//)# echo $p2convert "$pic" $p2.$OUTFMTdoneexit $?样例 10-4. 将流音频格式转换成 ogg 格式#!/bin/bash# ra2ogg.sh: 将流音频文件 (*.ra) 转换成 ogg 格式。# 使用 "mplayer" 媒体播放器程序:# http://www.mplayerhq.hu/homepage# 使用 "ogg" 库与 "oggenc":# http://www.xiph.org/## 脚本同时需要安装一些解码器,例如 sipr.so 等等一些。# 这些解码器可以在 compat-libstdc++ 包中找到。OFILEPREF=${1%%ra} # 删除 "ra" 后缀。OFILESUFF=wav # wav 文件后缀。OUTFILE="$OFILEPREF""$OFILESUFF"E_NOARGS=85if [ -z "$1" ] # 必须指定一个文件进行转换。thenecho "Usage: `basename $0` [filename]"exit $E_NOAGRSfi10.1 字符串处理175######################################################mplayer "$1" -ao pcm:file=$OUTFILEoggenc "$OUTFILE" # 由 oggenc 自动加上正确的文件后缀名。######################################################rm "$OUTFILE" # 立即删除 *.wav 文件。# 如果你仍需保留原文件,注释掉上面这一行即可。exit $?# 注意:# -----# 在网站上,点击一个 *.ram 的流媒体音频文件#+ 通常只会下载到 *.ra 音频文件的 URL。# 你可以使用 "wget" 或者类似的工具下载 *.ra 文件本身。# 练习:# -----# 这个脚本仅仅转换 *.ra 文件。# 修改脚本增加适应性,使其可以转换 *.ram 或其他文件格式。## 如果你非常有热情,你可以扩展这个脚本使其#+ 可以自动下载并且转换流媒体音频文件。# 给定一个 URL,自动下载流媒体音频文件 (使用 "wget"),#+ 然后转换它。下面是使用字符串截取结构对 getopt 的一个简单模拟。样例 10-5. 模拟 getopt#!/bin/bash# getopt-simple.sh# 作者: Chris Morgan# 允许在高级脚本编程指南中使用。getopt_simple()10.1 字符串处理176{echo "getopt_simple()"echo "Parameters are '$*'"until [ -z "$1" ]doecho "Processing parameter of: '$1'"if [ ${1:0:1} = '/' ]thentmp=${1:1} # 删除开头的 '/'parameter=${tmp%%=*} # 取出名称。value=${tmp##*=} # 取出值。echo "Parameter: '$parameter', value: '$value'"eval $parameter=$valuefishiftdone}# 将所有参数传递给 getopt_simple()。getopt_simple $*echo "test is '$test'"echo "test2 is '$test2'"exit 0 # 可以查看该脚本的修改版 UseGetOpt.sh。---sh getopt_example.sh /test=value1 /test2=value2Parameters are '/test=value1 /test2=value2'Processing parameter of: '/test=value1'Parameter: 'test', value: 'value1'Processing parameter of: '/test2=value2'Parameter: 'test2', value: 'value2'test is 'value1'test2 is 'value2'子串替换10.1 字符串处理177${string/substring/replacement}替换匹配到的第一个 $substring 为 $replacement 。${string//substring/replacement}替换匹配到的所有 $substring 为 $replacement 。stringZ=abcABC123ABCabcecho ${stringZ/abc/xyz} # xyzABC123ABCabc# 将匹配到的第一个 'abc' 替换为 'xyz'。echo ${stringZ//abc/xyz} # xyzABC123ABCxyz# 将匹配到的所有 'abc' 替换为 'xyz'。echo ---------------echo "$stringZ" # abcABC123ABCabcecho ---------------# 字符串本身并不会被修改!# 匹配以及替换的字符串可以是参数么?match=abcrepl=000echo ${stringZ/$match/$repl} # 000ABC123ABCabc# ^ ^ ^^^echo ${stringZ//$match/$repl} # 000ABC123ABC000# Yes! ^ ^ ^^^ ^^^echo# 如果没有给定 $replacement 字符串会怎样?echo ${stringZ/abc} # ABC123ABCabcecho ${stringZ//abc} # ABC123ABC# 仅仅是将其删除而已。${string/#substring/replacement}210.1 字符串处理178替换 $string 中最前端匹配到的 $substring 为 $replacement 。${string/%substring/replacement}替换 $string 中最末端匹配到的 $substring 为 $replacement 。stringZ=abcABC123ABCabcecho ${stringZ/#abc/XYZ} # XYZABC123ABCabc# 将前端的 'abc' 替换为 'XYZ'echo ${stringZ/%abc/XYZ} # abcABC123ABCXYZ# 将末端的 'abc' 替换为 'XYZ'. 这种情况同时适用于命令行参数和传入函数的参数。 ↩. 注意根据使用时上下文的不同, $substring 和 $replacement 可以是文本字符串也可以是变量。可以参考第一个样例。 ↩1210.1 字符串处理17910.1.1 使用 awk 处理字符串在 Bash 脚本中可以调用字符串处理工具 awk 来替换内置的字符串处理操作。样例 10-6. 使用另一种方式来截取和定位子字符串10.1 字符串处理180#!/bin/bash# substring-extraction.shString=23skidoo1# 012345678 Bash# 123456789 awk# 注意不同字符串索引系统:# Bash 中第一个字符的位置为0。# Awk 中第一个字符的位置为1。echo ${String:2:4} # 从第3位开始(0-1-2),4个字符的长度# skid# Awk 中与 ${string:pos:length} 等价的是 substr(string,pos,length)。echo | awk '{ print substr("'"${String}"'",3,4) # skid}'# 将空的 "echo" 通过管道传递给 awk 作为一个模拟输入,#+ 这样就不需要提供一个文件名来操作 awk 了。echo "----"# 同样的:echo | awk '{ print index("'"${String}"'", "skid") # 3} # (skid 从第3位开始)' # 这里使用 awk 等价于 "expr index"。exit 010.1 字符串处理18110.1.2 参考资料更多关于脚本中处理字符串的资料,可以查看 章节 10.2 以及 expr 命令的相关章节。脚本样例:1. 样例 16-92. 样例 10-93. 样例 10-104. 样例 10-115. 样例 10-136. 样例 A-367. 样例 A-4110.1 字符串处理18210.2 参数替换参数替换用来处理或扩展变量。${parameter}等同于 $parameter ,是变量 parameter 的值。在一些特定的环境下,只允许使用不易混淆的 ${parameter} 形式。可以用于连接变量与字符串。your_id=${USER}-on-${HOSTNAME}echo "$your_id"#echo "Old \$PATH = $PATH"PATH=${PATH}:/opt/bin # 在脚本执行过程中临时在 $PATH 中加入 /opt/bin。echo "New \$PATH = $PATH"${parameter-default}, ${parameter:-default}在没有设置变量的情况下使用缺省值。var1=1var2=2# 没有设置 var3。echo ${var1-$var2} # 1echo ${var3-$var2} # 2# ^ 注意前面的 $ 前缀。echo ${username-`whoami`}# 如果变量 $username 没有被设置,输出 `whoami` 的结果。10.2 参数替换183${parameter-default} 与 ${parameter:-default} 的作用几乎相同,唯一不同的情况就是当变量 parameter 已经被声明但值为空时。#!/bin/bash# param-sub.sh# 无论变量的值是否为空,其是否已被声明决定了缺省设置的触发。username0=echo "username0 has been declared, but is set to null."echo "username0 = ${username0-`whoami`}"# 将不会输出 `whoami` 的结果。echoecho username1 has not been declared.echo "username1 = ${username1-`whoami`}"# 将会输出 `whoami` 的结果。username2=echo "username2 has been declared, but is set to null."echo "username2 = ${username2:-`whoami`}"# ^# 因为这里是 :- 而不是 -,所以将会输出 `whoami` 的结果。# 与上面的 username0 比较。## 再来一次:variable=# 变量已被声明,但其值为空。echo "${varibale-0}" # 没有输出。echo "${variable:-1}" # 1# ^unser variable10.2 参数替换184echo "${variable-2}" # 2echo "${variable:-3}" # 3exit 0当传入的命令行参数的数量不足时,可以使用这种缺省参数结构。DEFAULT_FILENAME=generic.datafilename=${1:-$DEFAULT_FILENAME}# 如果没有其他特殊情况,下面的代码块将会操作文件 "generic.data"。# 代码块开始# ...# ...# ...# 代码块结束# 摘自样例 "hanoi2.bash":DISKS=${1:-E_NOPARAM} # 必须指定碟子的个数。# 将 $DISKS 设置为传入的第一个命令行参数,#+ 如果没有传入第一个参数,则设置为 $E_NOPARAM。可以查看 样例 3-4,样例 31-2 和 样例 A-6。可以同 使用与链设置缺省命令行参数 做比较。${parameter=default}, ${parameter:=default}在没有设置变量的情况下,将其设置为缺省值。两种形式的作用几乎相同,唯一不同的情况与上面类似,就是当变量 parameter 已经被声明但值为空时。echo ${var=abc} # abcecho ${vat=xyz} # abc# $var 已经在第一条语句中被赋值为 abc,因此第二条语句将不会改变它的值。110.2 参数替换185${parameter+alt_value},${parameter:+alt_value}如果变量已被设置,使用 alt_value,否则使用空值。两种形式的作用几乎相同,唯一不同的情况就是当变量 parameter 已经被声明但值为空时,看下面的例子。echo "###### \${parameter+alt_value} ########"echoa=${param1+xyz}echo "a = $a" # a =param2=a=${param2+xyz}echo "a = $a" # a = xyzparam3=123a=${param3+xyz}echo "a = $a" # a = xyzechoecho "###### \${parameter:+alt_value} ########"echoa=${param4:+xyz}echo "a = $a" # a =param5=a=${param5:+xyz}echo "a = $a" # a =# 不同于 a=${param5+xyz}param6=123a=${param6:+xyz}echo "a = $a" # a = xyz${parameter?err_msg}, ${parameter:?err_msg}10.2 参数替换186如果变量已被设置,那么使用原值,否则输出 err_msg 并且终止脚本,返回 错误码 1。两种形式的作用几乎相同,唯一不同的情况与上面类似,就是当变量 parameter 已经被声明但值为空时。样例 10-7. 如何使用变量替换和错误信息#!/bin/bash# 检查系统环境变量。# 这是一种良好的预防性维护措施。# 如果控制台用户的名称 $USER 没有被设置,那么主机将不能够识别用户。: ${HOSTNAME?} ${USER?} ${HOME?} ${MAIL?}echoecho "Name of the machine is $HOSTNAME."echo "You are $USER."echo "Your home directory is $HOME."echo "Your mail INBOX is located in $MAIL."echoecho "If you are reading this message,"echo "critcial environmental variables have been set."echoecho# ------------------------------------------------------# ${variablename?} 结构统一可以检查脚本中的变量是否被设置。ThisVariable=Value-of-ThisVariable# 顺带一提,这个字符串的值可以被设置成名称中不可以使用的禁用字符。: ${ThisVariable?}echo "Value of ThisVariable is $ThisVariable."echo; echo: ${ZZXy23AB?"ZZXy23AB has not been set."}# 因为 ZZXy23AB 没有被设置,所以脚本会终止同时显示错误消息。10.2 参数替换187# 你可以指定错误消息。# : ${variablename?"ERROR MESSAGE"}# 与这些结果相同: dummy_variable=${ZZXy23AB?}# dummy_variable=${ZZXy23AB?"ZZXy23AB has not been set."}## echo ${ZZXy23AB?} >/dev/null# 将上面这些检查变量是否被设置的方法同 "set -u" 作比较。echo "You will not see this message, because script already terminated."HERE=0exit $HERE # 将不会从这里退出。# 事实上,这个脚本将会返回退出码(echo $?)1。样例 10-8. 参数替换与 "usage" 消息10.2 参数替换188#!/bin/bash# usage-message.sh: ${1?"Usage: $0 ARGUMENT"}# 如果命令行参数缺失,脚本将会在这里结束,并且返回下面的错误信息。# usage-message.sh: 1: Usage: usage-message.sh ARGUMENTecho "These two lines echo only if command-line parameter given."echo "command-line parameter = \"$1\""exit 0 # 仅当命令行参数存在是才会从这里退出。# 在传入和未传入命令行参数的情况下查看退出状态。# 如果传入了命令行参数,那么 "$?" 的结果是0。# 如果没有,那么 "$?" 的结果是1。参数替换用来处理或扩展变量。下面的表达式是对 expr 处理字符串的操作的补足(查看样例 16-9)。这些特殊的表达式通常养来解析文件的路径名。变量长度 / 删除子串$字符串的长度( $var 中字符的个数)。对任意 数组 array, ${#array} 返回数组中第一个元素的长度。以下情况例外:${#*} 和 ${#@} 返回位置参数的个数。任意数组 array, ${#array[*]} 和 ${#array[@]} 返回数组中元素的个数。样例 10-9. 变量长度10.2 参数替换189#!/bin/bash# length.shE_NO_ARGS=65if [ $# -eq 0 ] # 脚本必须传入参数。thenecho "Please invoke this script with one or more command-linearguments."exit $E_NO_ARGSfivar01=abcdEFGH28ijecho "var01 = ${var01}"echo "Length of var01 = ${#var01}"# 现在我们尝试加入空格。var02="abcd EFGH28ij"echo "var02 = ${var02}"echo "Length of var02 = ${#var02}"echo "Number of command-line arguments passed to script = ${#@}"echo "Number of command-line arguments passed to script = ${#*}"exit 0${var#Pattern}, ${var##Pattern}${var#Pattern} 删除 $var 前缀部分匹配到的最短长度的 $Pattern 。${var##Pattern} 删除 $var 前缀部分匹配到的最长长度的 $Pattern 。摘自 样例 A-7 的例子:10.2 参数替换190# 函数摘自样例 "day-between.sh"。# 删除传入的参数中的前缀0。strip_leading_zero () # 删除传入参数中可能存在的{ #+ 前缀0。return=${1#0} # "1" 代表 "$1",即传入的参数。} # 从 "$1" 中删除 "0"。下面是由 Manfred Schwarb 提供的上述函数的改进版本:strip_leading_zero2 () # 删除前缀0,{ # 否则 Bash 会将其解释为8进制数。shopt -s extglob # 启用扩展通配特性。local val=${1##+(0)} # 使用本地变量,匹配前缀中所有的0。shopt -u extglob # 禁用扩展通配特性。_strip_leading_zero2=${var:-0}# 如果输入的为0,那么返回 0 而不是 ""。另外一个样例:echo `basename $PWD` # 当前工作目录的目录名。echo "${PWD##*/}" # 当前工作目录的目录名。echoecho `basename $0` # 脚本名。echo $0 # 脚本名。echo "${0##*/}" # 脚本名。echofilename=test.dataecho "${filename##*.}" # data# 文件扩展名。${var%Pattern}, ${var%%Pattern}${var%Pattern} 删除 $var 后缀部分匹配到的最短长度的 $Pattern 。${var%%Pattern} 删除 $var 后缀部分匹配到的最长长度的 $Pattern 。在 Bash 的 第二个版本 中增加了一些额外的选择。10.2 参数替换191样例 10-10. 参数替换中的模式匹配#!/bin/bash# patt-matching.sh# 使用 # ## % %% 参数替换操作符进行模式匹配var1=abcd12345abc6789pattern1=a*c # 通配符 * 可以匹配 a 与 c 之间的任意字符echoecho "var1 = $var1" # abcd12345abc6789echo "var1 = ${var1}" # abcd12345abc6789# (另一种形式)echo "Number of characters in ${var1} = ${#var1}"echoecho "pattern1 = $pattern1" # a*c (匹配 'a' 与 'c' 之间的一切)echo "--------------"echo '${var1#$pattern1} =' "${var1#$pattern1}" # d12345abc6789# 匹配到首部最短的3个字符 abcd12345abc6789# ^ |-|echo '${var1##$pattern1} =' "${var1##$pattern1}" #6789# 匹配到首部最长的12个字符 abcd12345abc6789# ^ |----------|echo; echo; echopattern2=b*9 # 匹配 'b' 与 '9' 之间的任意字符echo "var1 = $var1" # 仍旧是 abcd12345abc6789echoecho "pattern2 = $pattern2"echo "--------------"echo '${var1%pattern2} =' "${var1%$pattern2}" # abcd12345a10.2 参数替换192# 匹配到尾部最短的6个字符 abcd12345abc6789# ^|----|echo '${var1%%pattern2} =' "${var1%%$pattern2}" # a# 匹配到尾部最长的12个字符 abcd12345abc6789# ^ |-------------|# 牢记 # 与 ## 是从字符串左侧开始,# % 与 %% 是从右侧开始。echoexit 0样例 10-11. 更改文件扩展名:10.2 参数替换193#!/bin/bash# rfe.sh: 更改文件扩展名。## rfe old_extension new_extension## 如:# 将当前目录下所有 *.gif 文件重命名为 *.jpg,# rfe gif jpgE_BADARGS=65case $# in0|1) # 竖线 | 在这里表示逻辑或关系。echo "Usage: `basename $0` old_file_suffix new_file_suffix"exit $E_BADARGS # 如果只有0个或1个参数,那么退出脚本。;;esacfor filename in *.$1# 遍历以第一个参数作为后缀名的文件列表。domv $filename ${filename%$1}$2# 删除文件后缀名,增加第二个参数作为后缀名。doneexit 0变量扩展 / 替换子串下面这些结构采用自 ksh。${var:pos}扩展为从偏移量 pos 处截取的变量 var。${var:pos:len}10.2 参数替换194扩展为从偏移量 pos 处截取变量 var 最大长度为 len 的字符串。${var/Pattern/Replacement}替换 var 中第一个匹配到的 Pattern 为 Replacement。如果 Replacement 被省略,那么匹配到的第一个 Pattern 将被替换为空,即删除。${var//Pattern/Replacement}全局替换。替换 var 中所有匹配到的 Pattern 为 Replacement。跟上面一样,如果 Replacement 被省略,那么匹配到的所有 Pattern 将被替换为空,即删除。样例 10-12. 使用模式匹配解析任意字符串#!/bin/bashvar1=abcd-1234-defgecho "var1 = $var1"t=${var1#*-*}echo "var1 (with everything, up to and including first - stripped out) = $t"# t=${var1#*-} 效果相同,#+ 因为 # 只匹配最短的字符串,#+ 并且 * 可以任意匹配,其中也包括空字符串。# (感谢 Stephane Chazelas 指出这一点。)t=${var##*-*}echo "If var1 contains a \"-\", returns empty string... var1 =$t"t=${var1%*-*}echo "var1 (with everything from the last - on stripped out) = $t"echo10.2 参数替换195# -------------------------------------------path_name=/home/bozo/ideas/thoughts/for.today# -------------------------------------------echo "path_name = $path_name"t=${path_name##/*/}echo "path_name, stripped of prefixes = $t"# 在这里与 t=`basename $path_name` 效果相同。# t=${path_name%/}; t=${t##*/} 是更加通用的方法,#+ 但有时仍旧也会出现问题。# 如果 $path_name 以换行结束,那么 `basename $path_name` 将会失效,#+ 但是上面这种表达式却可以。# (感谢 S.C.)t=${path_name%/*.*}# 同 t=`dirname $path_name` 效果相同。echo "path_name, stripped of suffixes = $t"# 在一些情况下会失效,比如 "../", "/foo////", # "foo/", "/"。# 在删除后缀时,尤其是当文件名没有后缀,目录名却有后缀时,#+ 事情会变的非常复杂。# (感谢 S.C.)echot=${path_name:11}echo "$path_name, with first 11 chars stripped off = $t"t=${path_name:11:5}echo "$path_name, with first 11 chars stripped off, length 5 = $t"echot=${path_name/bozo/clown}echo "$path_name with \"bozo\" replaced by \"clown\" = $t"t=${path_name/today/}echo "$path_name with \"today\" deleted = $t"t=${path_name//o/O}echo "$path_name with all o's capitalized = $t"t=${path_name//o/}echo "$path_name with all o's deleted = $t"10.2 参数替换196exit 0${var/#Pattern/Replacement}替换 var 前缀部分匹配到的 Pattern 为 Replacement。${var/%Pattern/Replacement}替换 var 后缀部分匹配到的 Pattern 为 Replacement。样例 10-13. 在字符串首部或尾部进行模式匹配10.2 参数替换197#!/bin/bash# var-match.sh:# 演示在字符串首部或尾部进行模式替换。v0=abc1234zip1234abc # 初始值。echo "v0 = $v0" # abc1234zip1234abcecho# 在字符串首部进行匹配v1=${v0/#abc/ABCDEF} # abc1234zip123abc# |-|echo "v1 = $v1" # ABCDEF1234zip1234abc# |----|# 在字符串尾部进行匹配v2=${v0/%abc/ABCDEF} # abc1234zip123abc# |-|echo "v2 = $v2" # abc1234zip1234ABCDEF# |----|echo# --------------------------------------------# 必须在字符串的最开始或者最末尾的地方进行匹配,#+ 否则将不会发生替换。# --------------------------------------------v3=${v0/#123/000} # 虽然匹配到了,但是不在最开始的地方。echo "v3 = $v3" # abc1234zip1234abc# 没有替换。v4=${v0/%123/000} # 虽然匹配到了,但是不在最末尾的地方。echo "v4 = $v4" # abc1234zip1234abc# 没有替换。exit 0${!varprefix*}, ${!varprefix@}匹配先前声明过所有以 varprefix 作为变量名前缀的变量。10.2 参数替换198# 这是带 * 或 @ 的间接引用的一种变换形式。# 在 Bash 2.04 版本中加入了这个特性。xyz23=whateverxyz23=a=${!xyz*} # 扩展为声明变量中以 "xyz"# ^ ^ ^ + 开头变量名。echo "a = $a" # a = xyz23 xyz24a=${!xyz@} # 同上。echo "a = $a" # a = xyz23 xyz24echo "---"abc23=something_elseb=${!abc*}echo "b = $b" # b = abc23c=${!b} # 这是我们熟悉的间接引用的形式。echo $c # something_else. 如果在非交互的脚本中, $parameter 为空,那么程序将会终止,并且返回 错误码 127(意为“找不到命令”)。 ↩110.2 参数替换199第十一章 循环与分支奥赛罗夫人,您为什么把这句话说了又说呢?—— 《奥赛罗》,莎士比亚本章目录11.1 循环11.2 嵌套循环11.3 循环控制11.4 测试与分支对代码块的处理是结构化和构建 shell 脚本的关键。循环与分支结构恰好提供了这样一种对代码块处理的工具。11. 循环与分支20011.1 循环循环是当循环控制条件为真时,一系列命令迭代 执行的代码块。for 循环for arg in [list]这是 shell 中最基本的循环结构,它与C语言形式的循环有着明显的不同。for arg in [list]docommand(s)...done在循环的过程中, arg 会从 list 中连续获得每一个变量的值。for arg in "$var1" "$var2" "$var3" ... "$varN"# 第一次循环中,arg = $var1# 第二次循环中,arg = $var2# 第三次循环中,arg = $var3# ...# 第 N 次循环中,arg = $varN# 为了防止可能的字符分割问题,[list] 中的参数都需要被引用。参数 list 中允许含有 通配符。如果 do 和 for 写在同一行时,需要在 list 之后加上一个分号。for arg in [list] ; do样例 11-1. 简单的 for 循环111.1 循环201#!/bin/bash# 列出太阳系的所有行星。for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Plutodoecho $planet # 每一行输出一个行星。doneecho; echofor planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"# 所有的行星都输出在一行上。# 整个 'list' 被包裹在引号中时是作为一个单一的变量。# 为什么?因为空格也是变量的一部分。doecho $planetdoneecho; echo "Whoops! Pluto is no longer a planet!"exit 0[list] 中的每一个元素中都可能含有多个参数。这在处理参数组中非常有用。在这种情况下,使用 set 命令(查看 样例 15-16)强制解析 [list] 中的每一个元素,并将元素的每一个部分分配给位置参数。样例 11-2. for 循环 [list] 中的每一个变量有两个参数的情况11.1 循环202#!/bin/bash# 让行星再躺次枪。# 将每个行星与其到太阳的距离放在一起。for planet in "Mercury 36" "Venus 67" "Earth 93" "Mars 142" "Jupiter 483"doset -- $planet # 解析变量 "planet"#+ 并将其每个部分赋值给位置参数。# "--" 防止一些极端情况,比如 $planet 为空或者以破折号开头。# 因为位置参数会被覆盖掉,因此需要先保存原先的位置参数。# 你可以使用数组来保存# original_params=("$@")echo "$1 $2,000,000 miles from the sum"#-------两个制表符---将后面的一系列 0 连到参数 $2 上。done# (感谢 S.C. 做出的额外注释。)exit 0一个单一变量也可以成为 for 循环中的 [list]。样例 11-3. 文件信息:查看一个单一变量中含有的文件列表的文件信息11.1 循环203#!/bin/bash# fileinfo.shFILES="/usr/sbin/accept/usr/sbin/pwck/usr/sbin/chroot/usr/bin/fakefile/sbin/badblocks/sbin/ypbind" # 你可能会感兴趣的一系列文件。# 包含一个不存在的文件,/usr/bin/fakefile。echofor file in $FILESdoif [ ! -e "$file" ] # 检查文件是否存在。thenecho "$file does not exist."; echocontinue # 继续判断下一个文件。fils -l $file | awk '{ print $8 " file size: " $5 }' #输出其中的两个域。whatis `basename $file` # 文件信息。# 脚本正常运行需要注意提前设置好 whatis 的数据。# 使用 root 权限运行 /usr/bin/makewhatis 可以完成。echodoneexit 0for 循环中的 [list] 可以是一个参数。样例 11-4. 操作含有一系列文件的参数11.1 循环204#!/bin/bashfilename="*txt"for file in $filenamedoecho "Contents of $file"echo "---"cat "$file"echodone如果在匹配文件扩展名的 for 循环中的 [list] 含有通配符(* 和 ?),那么将会进行文件名扩展。样例 11-5. 在 for 循环中操作文件11.1 循环205#!/bin/bash# list-glob.sh: 通过文件名扩展在 for 循环中产生 [list]。# 通配 = 文件名扩展。echofor file in *# ^ Bash 在检测到通配表达式时,#+ 会进行文件名扩展。dols -l "$file" # 列出 $PWD(当前工作目录)下的所有文件。# 回忆一下,通配符 "*" 会匹配所有的文件名,#+ 但是,在文件名扩展中,他将不会匹配以点开头的文件。# 如果没有匹配到文件,那么它将会扩展为它自身。# 为了防止出现这种情况,需要设置 nullglob 选项。#+ (shopt -s nullglob)。# 感谢 S.C.doneecho; echofor file in [jx]*dorm -f $file # 删除当前目录下所有以 "j" 或 "x" 开头的文件。echo "Removed file \"$file\"".doneechoexit 0如果在 for 循环中省略 in [list] 部分,那么循环将会遍历位置参数( $@ )。样例 A-15 中使用到了这一点。也可以查看 样例 15-17。样例 11-6. 缺少 in [list] 的 for 循环11.1 循环206#!/bin/bash# 尝试在带参数和不带参数两种情况下调用这个脚本,观察发生了什么。for adoecho -n "$a "done# 缺失 'in list' 的情况下,循环会遍历 '$@'#+(命令行参数列表,包括空格)。echoexit 0可以在 for 循环中使用 命令代换 生成 [list]。查看 样例 16-54,样例 11-11 和 样例 16-48。样例 11-7. 在 for 循环中使用命令代换生成 [list]#!/bin/bash# for-loopcmd.sh: 带命令代换所生成 [list] 的 for 循环NUMBERS="9 7 3 8 37.53"for number in `echo $NUMBERS` # for number in 9 7 3 8 37.53doecho -n "$number "doneechoexit 0下面是使用命令代换生成 [list] 的更加复杂的例子。样例 11-8. 一种替代 grep 搜索二进制文件的方法11.1 循环207#!/bin/bash# bin-grep.sh: 在二进制文件中定位匹配的字符串。# 一种替代 `grep` 搜索二进制文件的方法# 与 "grep -a" 的效果类似E_BADARGS=65E_NOFILE=66if [ $# -ne 2 ]thenecho "Usage: `basename $0` search_string filename"exit $E_BADARGSfiif [ ! -f "$2" ]thenecho "File \"$2\" does not exist."exit $E_NOFILEfiIFS=$'\012' # 按照 Anton Filippov 的意见应该是# IFS="\n"for word in $( strings "$2" | grep "$1" )# "strings" 命令列出二进制文件中的所有字符串。# 将结果通过管道输出到 "grep" 中,检查是不是匹配的字符串。doecho $worddone# 就像 S.C. 指出的那样,第 23-30 行可以换成下面的形式:# strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'# 尝试运行脚本 "./bin-grep.sh mem /bin/ls"exit 011.1 循环208下面的例子同样展示了如何使用命令代换生成 [list]。样例 11-9. 列出系统中的所有用户#!/bin/bash# userlist.shPASSWORD_FILE=/etc/passwdn=1 # 用户数量for name in $(awk 'BEGIN{fs=":"}{print $1}' "$OUTFILE"echo "---------------------------" >> "$OUTFILE"for file in "$( find $directory -type 1 )" # -type 1 = 符号链接doecho "$file"done | sort >> "$OUTFILE" # 将 stdout 的循环结果# ^^^^^^^^^^^^^ 重定向到文件。# echo "Output file = $OUTFILE"exit $?还有另外一种看起来非常像C语言中循环那样的语法。你需要使用到 双圆括号 语法。样例 11-13. C语言风格的循环#!/bin/bash# 用多种方式数到10。echo# 基础版for a in 1 2 3 4 5 6 7 8 9 10doecho -n "$a "11.1 循环213doneecho; echo# +==========================================+# 使用 "seq"for a in `seq 10`doecho -n "$a "doneecho; echo# +==========================================+# 使用大括号扩展语法# Bash 3+ 版本有效。for a in {1..10}doecho -n "$a "doneecho; echo# +==========================================+# 现在用类似C语言的语法再实现一次。LIMIT=10for ((a=1; a <= LIMIT ; a++)) # 双圆括号语法,不带 $ 的 LIMITdoecho -n "$a "done # 从 ksh93 中学习到的特性。echo; echo# +==========================================+11.1 循环214# 我们现在使用C语言中的逗号运算符来使得两个变量同时增加。for ((a=1, b=1; a <= LIMIT ; a++, b++))do # 逗号连接操作。echo -n "$a-$b "doneecho; echoexit 0还可以查看 样例 27-16,样例 27-17 和 样例 A-6。---接下来,我们将展示在真实环境中应用的循环。样例 11-14. 在批处理模式下使用 efax#!/bin/bash# 传真(必须提前安装了 'efax' 模块)。EXPECTED_ARGS=2E_BADARGS=85MODEM_PORT="/dev/ttyS2" # 你的电脑可能会不一样。# ^^^^^ PCMCIA 调制解调卡缺省端口。if [ $# -ne $EXPECTED_ARGS ]# 检查是不是传入了适当数量的命令行参数。thenecho "Usage: `basename $0` phone# text-file"exit $E_BADARGSfiif [ ! -f "$2" ]thenecho "File $2 is not a text file."# File 不是一个正常文件或者文件不存在。exit $E_BADARGS11.1 循环215fifax make $2 # 根据文本文件创建传真格式文件。for file in $(ls $2.0*) # 连接转换后的文件。# 在参数列表中使用通配符(文件名通配)。dofil="$fil $file"doneefax -d "$MODEM_PORT" -t "T$1" $fil # 最后使用 efax。# 如果上面一行执行失败,尝试添加 -o1。# S.C. 指出,上面的 for 循环可以被压缩为# efax -d /dev/ttyS2 -o1 -t "T$1" $2.0*#+ 但是这并不是一个好主意。exit $? # efax 同时也会将诊断信息传递给标准输出。11.1 循环216关键字 do 和 done 圈定了 for 循环代码块的范围。但是在一些特殊的情况下,也可以被 大括号 取代。for((n=1; n<=10; n++))# 没有 do!{echo -n "* $n *"}# 没有 done!# 输出:# * 1 ** 2 ** 3 ** 4 ** 5 ** 6 ** 7 ** 8 ** 9 ** 10 *# 并且 echo $? 返回 0,因此 Bash 并不认为这是一个错误。echo# 但是注意在典型的 for 循环 for n in [list] ... 中,#+ 需要在结尾加一个分号。for n in 1 2 3{ echo -n "$n "; }# ^# 感谢 Yongye 指出这一点。while 循环while 循环结构会在循环顶部检测循环条件,若循环条件为真( 退出状态 为0)则循环持续进行。与 for 循环 不同的是, while 循环是在不知道循环次数的情况下使用的。11.1 循环217while [ condition ]docommand(s)...done在 while 循环结构中,你不仅可以使用像 if/test 中那样的 括号结构,也可以使用用途更广泛的 双括号结构( while [[ condition ]] )。就像在 for 循环中那样,将 do 和循环条件放在同一行时需要加一个分号。while [ condition ] ; do在 while 循环中,括号结构 并不是必须存在的。比如说 getopts 结构。样例 11-15. 简单的 while 循环#!/bin/bashvar0=0LIMIT=10while [ "$var0" -lt "$LIMIT" ]# ^ ^# 必须有空格,因为这是测试结构doecho -n "$var0 " # -n 不会另起一行# ^ 空格用来分开输出的数字。var0=`expr $var0 + 1` # var0=$(($var0+1)) 效果相同。# var0=$((var0 + 1)) 效果相同。# let "var0 += 1" 效果相同。done # 还有许多其他的方法也可以达到相同的效果。echoexit 0样例 11-16. 另一个例子11.1 循环218#!/bin/bashecho# 等价于:while [ "$var1" != "end" ] # while test "$var1" != "end"doecho "Input variable #1 (end to exit) "read var1 # 不是 'read $var1' (为什么?)。echo "variable #1 = $var1" # 因为存在 "#",所以需要使用引号。# 如果输入的是 "end",也将会在这里输出。# 在结束本轮循环之前都不会再测试循环条件了。echodoneexit 0一个 while 循环可以有多个测试条件,但只有最后的那一个条件决定了循环是否终止。这是一种你需要注意到的不同于其他循环的语法。样例 11-17. 多条件 while 循环11.1 循环219#!/bin/bashvar1=unsetprevious=$var1while echo "previous-variable = $previous"echoprevious=$var1[ "$var1" != end ] # 记录下 $var1 之前的值。# 在 while 循环中有4个条件,但只有最后的那个控制循环。# 最后一个条件的退出状态才会被记录。doecho "Input variable #1 (end to exit) "read var1echo "variable #1 = $var1"done# 猜猜这是怎样实现的。# 这是一个很小的技巧。exit 0就像 for 循环一样, while 循环也可以使用双圆括号结构写得像C语言那样(也可以查看样例 8-5)。样例 11-18. C语言风格的 while 循环11.1 循环220#!/bin/bash# wh-loopc.sh: 在 "while" 循环中计数到10。LIMIT=10 # 循环10次。a=1while [ "$a" -le $LIMIT ]doecho -n "$a "let "a+=1"done # 没什么好奇怪的吧。echo; echo# +==============================================+# 现在我们用C语言风格再写一次。((a = 1)) # a=1# 双圆括号结构允许像C语言一样在赋值语句中使用空格。while (( a 255 的情况下会失效。# 如果要操作更大的数字,注释掉上面的 "return $ct" 就可以了。} <"$datafile" # 传入数据文件。在 while 循环后面可以通过 LIMIT ))do # ^^ ^ ^ ^^ 没有方括号,没有 $ 前缀。echo -n "$var "(( var++ ))done # 0 1 2 3 4 5 6 7 8 9 10exit 0如何在 for , while 和 until 之间做出选择?我们知道在C语言中,在已知循环次数的情况下更加倾向于使用 for 循环。但是在Bash中情况可能更加复杂一些。Bash中的 for 循环相比起其他语言来说,结构更加松散,使用更加灵活。因此使用你认为最简单的就好。111.1 循环224. 迭代:重复执行一个或一组命令。通常情况下,会使用 while 或者 until 进行控制。 ↩111.1 循环22511.2 嵌套循环嵌套循环,顾名思义就是在循环里面还有循环。外层循环会不断的触发内层循环直到外层循环结束。当然,你仍然可以使用 break 可以终止外层或内层的循环。样例 11-20. 嵌套循环#!/bin/bash# nested-loop.sh: 嵌套 "for" 循环。outer=1 # 设置外层循环计数器。# 外层循环。for a in 1 2 3 4 5doecho "Pass $outer in outer loop."echo "---------------------"inner=1 # 重设内层循环计数器。# =====================================# 内层循环。for b in 1 2 3 4 5doecho "Pass $inner in inner loop."let "inner+=1" # 增加内层循环计数器。done# 内层循环结束。# =====================================let "outer+=1" # 增加外层循环计数器。echo # 在每次外层循环输出中加入空行。done# 外层循环结束。exit 011.2 嵌套循环226查看 样例 27-11 详细了解嵌套 while 循环。查看 样例 27-13 详细了解嵌套 until 循环。11.2 嵌套循环22711.3 循环控制Tournez cent tours, tournez mille tours,Tournez souvent et tournez toujours . . .——保尔·魏尔伦,《木马》本节介绍两个会影响循环行为的命令。break, continuebreak 和 continue 命令 的作用和在其他编程语言中的作用一样。 break用来中止(跳出)循环,而 continue 则是略过未执行的循环部分,直接进行下一次循环。样例 11-21. 循环中 break 与 continue 的作用#!/bin/bashLIMIT=19 # 循环上界echoecho "Printing Numbers 1 through 20 (but not 3 and 11)."a=0while [ $a -le "$LIMIT" ]doa=$(($a+1))if [ "$a" -eq 3 ] || [ "$a" -eq 11 ] # 除了 3 和 11。thencontinue # 略过本次循环的剩余部分。fiecho -n "$a " # 当 a 等于 3 和 11 时,将不会执行这条语句。done111.3 循环控制228# 思考:# 为什么循环不会输出到20?echo; echoecho Printing Numbers 1 through 20, but something happens after2.################################################################### 用 'break' 代替了 'continue'。a=0while [ "$a" -le "$LIMIT" ]doa=$(($a+1))if [ "$a" -gt 2 ]thenbreak # 中止循环。fiecho -n "$a"doneecho; echo; echoexit 0break 命令接受一个参数。普通的 break 命令仅仅跳出其所在的那层循环,而break N 命令则可以跳出其上 N 层的循环。样例 11-22. 跳出多层循环11.3 循环控制229#!/bin/bash# break-levels.sh: 跳出循环.# "break N" 跳出 N 层循环。for outerloop in 1 2 3 4 5doecho -n "Group $outerloop: "# ------------------------------------------for innerloop in 1 2 3 4 5doecho -n "$innerloop "if [ "$innerloop" -eq 3 ]thenbreak # 尝试一下 break 2 看看会发生什么。# (它同时中止了内层和外层循环。)fidone# ------------------------------------------echodoneechoexit 0与 break 类似, continue 也接受一个参数。普通的 continue 命令仅仅影响其所在的那层循环,而 continue N 命令则可以影响其上 N 层的循环。样例 11-23. continue 影响外层循环11.3 循环控制230#!/bin/bash# "continue N" 命令可以影响其上 N 层循环。for outer in I II III IV V # 外层循环doecho; echo -n "Group $outer: "# --------------------------------------------------------------------for inner in 1 2 3 4 5 6 7 8 9 10 # 内层循环doif [[ "$inner" -eq 7 && "$outer" = "III" ]]thencontinue 2 # 影响两层循环,包括“外层循环”。# 将其替换为普通的 "continue",那么只会影响内层循环。fiecho -n "$inner " # 7 8 9 10 将不会出现在 "Group III."中。done# --------------------------------------------------------------------doneecho; echo# 思考:# 想一个 "continue N" 在脚本中的实际应用情况。exit 0样例 11-24. 真实环境中的 continue N# Albert Reiner 举出了一个如何使用 "continue N" 的例子:# ---------------------------------------------------# 如果我有许多任务需要运行,并且运行所需要的数据都以文件的形#+ 式存在文件夹中。现在有多台设备可以访问这个文件夹,我想将任11.3 循环控制231#+ 务分配给这些不同的设备来完成。# 那么我通常会在每台设备上执行下面的代码:while true:dofor n in .iso.*do[ "$n" = ".iso.opts" ] && continuebeta=${n#.iso.}[ -r .Iso.$beta ] && continue[ -r .lock.$beta ] && sleep 10 && continuelockfile -r0 .lock.$beta || continueecho -n "$beta: " `date`run-isotherm $betadatels -alF .Iso.$beta[ -r .Iso.$beta ] && rm -rf .lock.$betacontinue 2donebreakdoneexit 0# 这个脚本中出现的 sleep N 只针对这个脚本,通常的形式是:while truedofor job in {pattern}do{job already done or running} && continue{mark job as running, do job, mark job as done}continue 2donebreak # 或者使用类似 `sleep 600` 这样的语句来防止脚本结束。done# 这样做可以保证脚本只会在没有任务时(包括在运行过程中添加的任务)#+ 才会停止。合理使用文件锁保证多台设备可以无重复的并行执行任务(这#+ 在我的设备上通常会消耗好几个小时,所以我想避免重复计算)。并且,11.3 循环控制232#+ 因为每次总是从头开始搜索文件,因此可以通过文件名决定执行的先后#+ 顺序。当然,你可以不使用 'continue 2' 来完成这些,但是你必须#+ 添加代码去检测某项任务是否完成(以此判断是否可以执行下一项任务或#+ 终止、休眠一段时间再执行下一项任务)。continue N 结构不易理解并且可能在一些情况下有歧义,因此不建议使用。. 这两个命令是 内建命令,而另外的循环命令,如 while 和 case 则是关键词。 ↩111.3 循环控制23311.4 测试与分支case 和 select 结构并不属于循环结构,因为它们并没有反复执行代码块。但是和循环结构相似的是,它们会根据代码块顶部或尾部的条件控制程序流。下面介绍两种在代码块中控制程序流的方法:case (in) / esac在 shell 脚本中, case 模拟了 C/C++ 语言中的 switch ,可以根据条件跳转到其中一个分支。其相当于简写版的 if/then/else 语句。很适合用来创建菜单选项哟!case "$variable" in"$condition1" )command...;;"$condition2" )command...;;esac对变量进行引用不是必须的,因为在这里不会进行字符分割。条件测试语句必须以右括号 ) 结束。每一段代码块都必须以双分号 ;; 结束。如果测试条件为真,其对应的代码块将被执行,而后整个 case 代码段结束执行。case 代码段必须以 esac 结束(倒着拼写case)。样例 11-25. 如何使用 case111.4 测试与分支234#!/bin/bash# 测试字符的种类。echo; echo "Hit a key, then hit return."read Keypresscase "$Keypress" in[[:lower:]] ) echo "Lowercase letter";;[[:upper:]] ) echo "Uppercase letter";;[0-9] ) echo "Digit";;* ) echo "Punctuation, whitespace, or other";;esac # 字符范围可以用[方括号]表示,也可以用 POSIX 形式的[[双方括号]]表示。# 在这个例子的第一个版本中,用来测试是小写还是大写字符使用的是 [a-z] 和 [A-Z]。# 这在一些特定的语言环境和 Linux 发行版中不起效。# POSIX 形式具有更好的兼容性。# 感谢 Frank Wang 指出这一点。# 练习:# -----# 这个脚本接受一个单字符然后结束。# 修改脚本,使得其可以循环接受输入,并且检测键入的每一个字符,直到键入 "X"为止。# 提示:将所有东西包在 "while" 中。exit 0样例 11-26. 使用 case 创建菜单#!/bin/bash# 简易的通讯录数据库clear # 清屏。echo " Contact List"echo " ------- ----"11.4 测试与分支235echo "Choose one of the following persons:"echoecho "[E]vans, Roland"echo "[J]ones, Mildred"echo "[S]mith, Julie"echo "[Z]ane, Morris"echoread personcase "$person" in# 注意变量是被引用的。"E" | "e" )# 同时接受大小写的输入。echoecho "Roland Evans"echo "4321 Flash Dr."echo "Hardscrabble, CO 80753"echo "(303) 734-9874"echo "(303) 734-9892 fax"echo "revans@zzy.net"echo "Business partner & old friend";;# 注意用双分号结束这一个选项。"J" | "j" )echoecho "Mildred Jones"echo "249 E. 7th St., Apt. 19"echo "New York, NY 10009"echo "(212) 533-2814"echo "(212) 533-9972 fax"echo "milliej@loisaida.com"echo "Ex-girlfriend"echo "Birthday: Feb. 11";;# Smith 和 Zane 的信息稍后添加。11.4 测试与分支236* )# 缺省设置。# 空输入(直接键入回车)也是执行这一部分。echoecho "Not yet in database.";;esacecho# 练习:# -----# 修改脚本,使得其可以循环接受多次输入而不是只显示一个地址后终止脚本。exit 0你可以用 case 来检测命令行参数。#!/bin/bashcase "$1" in"") echo "Usage: ${0##*/} "; exit $E_PARAM;;# 没有命令行参数,或者第一个参数为空。# 注意 ${0##*/} 是参数替换 ${var##pattern} 的一种形式。# 最后的结果是 $0.-*) FILENAME=./$1;; # 如果传入的参数以短横线开头,那么将其替换为 ./$1#+ 以避免后续的命令将其解释为一个选项。* ) FILENAME=$1;; # 否则赋值为 $1。esac下面是一个更加直观的处理命令行参数的例子:11.4 测试与分支237#!/bin/bashwhile [ $# -gt 0 ]; do # 遍历完所有参数case "$1" in-d|--debug)# 检测是否是 "-d" 或者 "--debug"。DEBUG=1;;-c|--conf)CONFFILE="$2"shiftif [ ! -f $CONFFILE ]; thenecho "Error: Supplied file doesn't exist!"exit $E_CONFFILE # 找不到文件。fi;;esacshift # 检测下一个参数done# 摘自 Stefano Falsetto 的 "Log2Rot" 脚本中 "rottlog" 包的一部分。# 已授权使用。样例 11-27. 使用命令替换生成 case 变量#!/bin/bash# case-cmd.sh: 使用命令替换生成 "case" 变量。case $( arch ) in # $( arch ) 返回设备架构。# 等价于 'uname -m"。i386 ) echo "80386-based machine";;i486 ) echo "80486-based machine";;i586 ) echo "Pentium-based machine";;i686 ) echo "Pentium2+-based machine";;* ) echo "Other type of machine";;esacexit 011.4 测试与分支238case 还可以用来做字符串模式匹配。样例 11-28. 简单的字符串匹配11.4 测试与分支239#!/bin/bash# match-string.sh: 使用 'case' 结构进行简单的字符串匹配。match_string (){ # 字符串精确匹配。MATCH=0E_NOMATCH=90PARAMS=2 # 需要2个参数。E_BAD_PARAMS=91[ $# -eq $PARAMS ] || return $E_BAD_PARAMScase "$1" in"$2") return $MATCH;;* ) return $E_NOMATCH;;esac}a=oneb=twoc=threed=twomatch_string $a # 参数个数不够echo $? # 91match_string $a $b # 匹配不到echo $? # 90match_string $a $d # 匹配成功echo $? # 0exit 0样例 11-29. 检查输入11.4 测试与分支240#!/bin/bash# isaplpha.sh: 使用 "case" 结构检查输入。SUCCESS=0FAILURE=1 # 以前是FAILURE=-1,#+ 但现在 Bash 不允许返回负值。isalpha () # 测试字符串的第一个字符是否是字母。{if [ -z "$1" ] # 检测是否传入参数。thenreturn $FAILUREficase "$1" in[a-zA-Z]*) return $SUCCESS;; # 是否以字母形式开始?* ) return $FAILURE;;esac} # 可以与 C 语言中的函数 "isalpha ()" 作比较。isalpha2 () # 测试整个字符串是否都是字母。{[ $# -eq 1 ] || return $FAILUREcase $1 in*[!a-zA-Z]*|"") return $FAILURE;;*) return $SUCCESS;;esac}isdigit () # 测试整个字符串是否都是数字。{ # 换句话说,也就是测试是否是一个整型变量。[ $# -eq 1 ] || return $FAILUREcase $1 in*[!0-9]*|"") return $FAILURE;;*) return $SUCCESS;;esac11.4 测试与分支241}check_var () # 包装后的 isalpha ()。{if isalpha "$@"thenecho "\"$*\" begins with an alpha character."if isalpha2 "$@"then # 其实没必要检查第一个字符是不是字母。echo "\"$*\" contains only alpha characters."elseecho "\"$*\" contains at least one non-alpha character."fielseecho "\"$*\" begins with a non-alpha character."# 如果没有传入参数同样同样返回“存在非字母”。fiecho}digit_check () # 包装后的 isdigit ()。{if isdigit "$@"thenecho "\"$*\" contains only digits [0 - 9]."elseecho "\"$*\" has at least one non-digit character."fiecho}a=23skidoob=H3llo11.4 测试与分支242c=-What?d=What?e=$(echo $b) # 命令替换。f=AbcDefg=27234h=27a34i=27.34check_var $acheck_var $bcheck_var $ccheck_var $dcheck_var $echeck_var $fcheck_var # 如果不传入参数会发送什么?#digit_check $gdigit_check $hdigit_check $iexit 0 # S.C. 改进了本脚本。# 练习:# -----# 写一个函数 'isfloat ()' 来检测输入值是否是浮点数。# 提示:可以参考函数 'isdigit ()',在其中加入检测合法的小数点即可。selectselect 结构是学习自 Korn Shell。其同样可以用来构建菜单。select variable [in list]docommand...breakdone11.4 测试与分支243而效果则是终端会提示用户输入列表中的一个选项。注意, select 默认使用提示字串3(Prompt String 3, $PS3 , 即#?),但同样可以被修改。样例 11-30. 使用 select 创建菜单#!/bin/bashPS3='Choose your favorite vegetable: ' # 设置提示字串。# 否则默认为 #?。echoselect vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"doechoecho "Your favorite veggie is $vegetable."echo "Yuck!"echobreak # 如果没有 'break' 会发生什么?doneexit# 练习:# -----# 修改脚本,使得其可以接受其他输入而不是 "select" 语句中所指定的。# 例如,如果用户输入 "peas,",那么脚本会通知用户 "Sorry. That is not on the menu."如果 in list 被省略,那么 select 将会使用传入脚本的命令行参数( $@ )或者传入函数的参数作为 list。可以与 for variable [in list] 中 in list 被省略的情况做比较。样例 11-31. 在函数中使用 select 创建菜单11.4 测试与分支244#!/bin/bashPS3='Choose your favorite vegetable: 'echochoice_of(){select vegetable# [in list] 被省略,因此 'select' 将会使用传入函数的参数作为 list。doechoecho "Your favorite veggie is $vegetable."echo "Yuck!"echobreakdone}choice_of beans rice carrorts radishes rutabaga spinach# $1 $2 $3 $4 $5 $6# 传入了函数 choice_of()exit 0还可以参照 样例37-3。111.4 测试与分支245. 在写匹配行的时候,可以在左边加上左括号 (,使整个结构看起来更加优雅。case $( arch ) in # $( arch ) 返回设备架构。( i386 ) echo "80386-based machine";;# ^ ^( i486 ) echo "80486-based machine";;( i586 ) echo "Pentium-based machine";;( i686 ) echo "Pentium2+-based machine";;( * ) echo "Other type of machine";;esac↩111.4 测试与分支246第十二章 命令替换命令替换重新指定一个 或多个命令的输出。其实就是将命令的输出导到另外一个地方 。命令替换的通常形式是( `...` ),即用反引号引用命令。script_name=`basename $0`echo "The name of this script is $scirpt_name."命令的输出可以作为另一个命令的参数,也可以赋值给一个变量。甚至在 for 循环中可以用输出产生参数表。rm `cat filename` # "filename" 中包含了一系列需要被删除的文件名。## S.C. 指出这样写可能会导致出现 "arg list too long" 的错误。# 更好的写法应该是 xargs rm -- /dev/null) # 使用 'dd' 获得键值。stty "$old_tty_setting" # 恢复旧的设置。echo "You hit ${#key} key." # ${#variable} 表示 $variable 中的字符个数。## 除了按下回车键外,其余情况都会输出 "You hit 1 key."# 按下回车键会输出 "You hit 0 key."# 因为唯一的换行符在命令替换中被丢失了。# 这段代码摘自 Stéphane Chazelas。使用 echo 输出未被引用的命令代换的变量时会删掉尾部的换行。这可能会导致非常不好的情况出现。12. 命令替换249dir_listing=`ls -l`echo $dir_listing # 未被引用# 你希望会出现按行显示出文件列表。# 但是,你却看到了:# total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo# bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar5 21:13 wi.sh# 所有换行都消失了。echo "$dir_listing" # 被引用# -rw-rw-r-- 1 bozo 30 May 13 17:15 1.txt# -rw-rw-r-- 1 bozo 51 May 15 20:57 t2.sh# -rwxr-xr-x 1 bozo 217 Mar 5 21:13 wi.sh你甚至可以使用 重定向 或者 cat 命令把一个文件的内容通过命令代换赋值给一个变量。variable1=`/dev/null|grep -E "^I.*Cls=03.*Prot=01"`...fi12. 命令替换251尽量不要将一大段文字赋值给一个变量,除非你有足够的理由。也绝不要将一个二进制文件的内容赋值给一个变量。样例 12-1. 蠢蠢的脚本#!/bin/bash# stupid-script-tricks.sh: 不要在自己的电脑上尝试。# 摘自 "Stupid Script Tricks" 卷一。exit 99 ### 如果你有胆,就注释掉这行。:)dangerous_variable=`cat /boot/vmlinuz` # 压缩的 Linux 内核。echo "string-length of \$dangerous_variable = ${#dangerous_variable}"# $dangerous_variable 的长度为 794151# (更新版本的内核可能更大。)# 与 'wc -c /boot/vmlinuz' 的结果不同。# echo "$dangerous_variable"# 不要作死。否则脚本会挂起。# 将二进制文件的内容赋值给一个变量没有任何意义。exit 0尽管脚本会挂起,但并不会出现缓存溢出的情况。而这正是像 Bash 这样的解释型语言相比起编译型语言能够提供更多保护的一个例子。命令替换允许将 循环 的输出结果赋值给一个变量。这其中的关键在于循环内部的echo 命令。样例 12-2. 将循环的输出结果赋值给变量12. 命令替换252#!/bin/bash# csubloop.sh: 将循环的输出结果赋值给变量。variable1=`for i in 1 2 3 4 5doecho -n "$i" # 在这里,'echo' 命令非常关键。done`echo "variable1 = $variable1" # variable1 = 12345i=0variable2=`while [ "$i" -lt 10 ]doecho -n "$i" # 很关键的 'echo'。let "i += 1" # i 自增。done`echo "variable2 = $variable2" # variable2 = 0123456789# 这个例子表明可以在变量声明时嵌入循环。exit 0命令替换能够让 Bash 做更多的事情。而这仅仅需要在书写程序或者脚本时将结果输出到标准输出 stdout 中,然后将这些输出结果赋值给变量即可。#include /* "Hello, world." C program */int main(){printf( "Hello, world.\n" );return (0);}12. 命令替换253bash$ gcc -0 hello hello.c#!/bin/bash# hello.shgreeting=`./hello`echo $greetingbash$ sh hello.shHello, world.在命令替换中,你可以使用 $(...) 来替代反引号。output=$(sed -n /"$1"/p $file) # 摘自 "grp.sh"。# 将文本文件的内容赋值给一个变量。File_contents1=$(cat $file1)File_contents2=$(<$file2) # 这么做也是可以的。$(...) 和反引号在处理双反斜杠上有所不同。bash$ echo `echo \\`bash$ echo $(echo \\)\$(...) 允许嵌套。word_count=$( wc -w $(echo * | awk '{print $8}') )样例 12-3. 寻找变位词(anagram)#!/bin/bash312. 命令替换254# agram2.sh# 嵌套命令替换的例子。# 其中使用了作者写的工具包 "yawl" 中的 "anagram" 工具。# http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz# http://bash.deta.in/yawl-0.3.2.tar.gzE_NOARGS=86E_BADARG=87MINLEN=7if [ -z "$1" ]thenecho "Usage $0 LETTERSET"exit $E_NOARGS # 脚本需要命令行参数。elif [ ${#1} -lt $MINLEN ]thenecho "Argument must have at least $MINLEN letters."exit $E_BADARGfiFILTER='.......' # 至少需要7个字符。# 1234567Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) )# $( $( 嵌套命令集 ) )# ( 赋值给数组 )echoecho "${#Anagrams[*]} 7+ letter anagrams found"echoecho ${Anagrams[0]} # 第一个变位词。echo ${Anagrams[1]} # 第二个变位词。# 以此类推。# echo "${Anagrams[*]}" # 将所有变位词在一行里面输出。# 可以配合后面的数组章节来理解上面的代码。12. 命令替换255# 建议同时查看另一个寻找变位词的脚本 agram.sh。exit $?以下是包含命令替换的样例:1. 样例 11-82. 样例 11-273. 样例 9-164. 样例 16-35. 样例 16-226. 样例 16-177. 样例 16-548. 样例 11-149. 样例 11-1110. 样例 16-3211. 样例 20-812. 样例 A-1613. 样例 29-314. 样例 16-4715. 样例 16-4816. 样例 16-49. 在命令替换中可以使用外部系统命令,内建命令 甚至是 脚本函数。 ↩. 从技术的角度来讲,命令替换实际上是获得了命令输出到标准输出的结果,然后通过赋值号将结果赋值给一个变量。 ↩. 事实上,使用反引号进行嵌套也是可行的。但是 John Default 提醒到需要将内部的反引号进行转义。word_count=\` wc -w \\\`echo * | awk '{print $8}'\\\`\`↩12312. 命令替换256第十三章 算术扩展算术扩展为脚本中的(整数)算术操作提供了强有力的工具。你可以使用反引号、双圆括号或者 let 将字符串转换为数学表达式。差异比较使用 反引号 的算术扩展(通常与 expr 一起使用)z=`expr $z + 3` # 'expr' 命令执行了算术扩展。使用 双圆括号 或 let 的算术扩展。事实上,在算术扩展中,反引号已经被双圆括号 ((...)) 和 $((...)) 以及let 所取代。13. 算术扩展257z=$(($z+3))z=$((z+3)) # 同样正确。# 在双圆括号内,参数引用形式可用可不用。# $((EXPRESSION)) 是算术扩展。 # 不要与命令替换混淆。# 双圆括号不是只能用作赋值算术结果。n=0echo "n = $n" # n = 0(( n += 1 )) # 自增。# (( $n += 1 )) 是错误用法!echo "n = $n" # n = 1let z=z+3let "z += 3" # 引号允许在赋值表达式中使用空格。# 'let' 事实上执行的算术运算而非算术扩展。以下是包含算术扩展的样例:1. 样例 16-92. 样例 11-153. 样例 27-14. 样例 27-115. 样例 A-1613. 算术扩展258第十四章 休息时间作者开始玩不转不是外国人的游戏了。亲爱的读者可以藉此休息一下,如果可以,请帮助我们推广一下本书原作和译作。原作作者致所有读者各位 Linux 用户,你们好!你们现在正阅读的这本书能够给你们带来好运。所以赶紧打开你们的邮箱,将本文的访问链接发给你的10位朋友。但是在发邮件之前,记得粘贴一段大约100行的 Bash 脚本在邮件后面。千万不要打断这个传递,并且一定要在48小时内发送邮件!布鲁克林区的 Wilfred P. 没有发出10封邮件。当他第三天起床时发现他变成了一名COBOL 程序员。纽波特纽斯港的 Howard L. 按时发出了10封邮件。然后一个月内,他就有了足够的硬件来搭建一个100个节点的 Beowulf 集群来玩 Tuxracer。芝加哥的 Amelia V. 看到以后不屑一顾,置之不理。不久之后,她的终端炸了。现在她不得不为微软工作,撰写文档。千万不要打断这个传递!马上去发邮件吧!Courtesy 'NIX "fortune cookies", with some alterations and many apologies14. 休息时间259第五部分 高级话题目录18.正则表达式18.1正则表达式简介18.2文件名替换19. 嵌入文档20. I/O 重定向20.1 使用 exec20.2 重定向代码块20.3 应用程序22. 限制模式的Shell24. 函数24.1 复杂函数和函数复杂性24.2 局部变量24.3 不适用局部变量的递归25. 别名27. 数组30. 网络编程33. 选项34. 陷阱38. 后记38.1 作者后记38.2 关于作者38.3 从哪里可以获得帮助38.4 用来制作这本书的工具38.5 致谢38.6 免责声明第五部分 进阶话题26019 嵌入文档Here and now, boys. --Aldous Huxley, Island嵌入文档是一段有特殊作用的代码块,它用 I/O 重定向 在交互程序和交互命令中传递和反馈一个命令列表,例如 ftp,cat 或者是 ex 文本编辑器COMMAND < dir-tree.list# 创建了一个包含目录树列表的文件.: > filename# ">" 清空了文件.# 如果文件不存在,则创建了个空文件 (效果类似 'touch').# ":" 是个虚拟占位符, 不会有输出.> filename# ">" 清空了文件.# 如果文件不存在,则创建了个空文件 (效果类似 'touch').# (结果和上述的 ": >" 一样, 但在某些 shell 环境中不能正常运行.)COMMAND_OUTPUT >># 重定向标准输出到一个文件.# 如果文件不存在则创建,否则新内容在文件末尾追加.20. I/O 重定向280# 单行重定向命令 (只作用于本身所在的那行):# --------------------------------------------------------------------1>filename# 以覆盖的方式将 标准错误 重定向到文件 "filename."1>>filename# 以追加的方式将 标准输出 重定向到文件 "filename."2>filename# 以覆盖的方式将 标准错误 重定向到文件 "filename."2>>filename# 以追加的方式将 标准错误 重定向到文件 "filename."&>filename# 以覆盖的方式将 标准错误 和 标准输出 同时重定向到文件 "filename."# 在 bash 4 中才有这个新功能.M>N# "M" 是个文件描述符, 如果不明确指定,默认为 1.# "N" 是个文件名.# 文件描述符 "M" 重定向到文件 "N."M>&N# "M" 是个文件描述符, 如果不设置默认为 1.# "N" 是另一个文件描述符.#==============================================================================# 重定向 标准输出,一次一行.LOGFILE=script.logecho "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILEecho "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILEecho "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILEecho "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"."20. I/O 重定向281# 这些重定向命令在每行结束后自动"重置".# 重定向 标准错误,一次一行.ERRORFILE=script.errorsbad_command1 2>$ERRORFILE # Error message sent to $ERRORFILE.bad_command2 2>>$ERRORFILE # Error message appendedto $ERRORFILE.bad_command3 # Error message echoed tostderr,#+ and does not appear in$ERRORFILE.# 这些重定向命令每行结束后会自动“重置”.#=======================================================================2>&1# 重定向 标准错误 到 标准输出.# 错误信息发送到标准输出相同的位置.>>filename 2>&1bad_command >>filename 2>&1# 同时将 标准输出 和 标准错误 追加到文件 "filename" 中 ...2>&1 | [command(s)]bad_command 2>&1 | awk '{print $5}' # found# 通过管道传递 标准错误.# bash 4 中可以将 "2>&1 |" 缩写为 "|&".i>&j# 重定向文件描述符 i 到 j.# 文件描述符 i 指向的文件输出将会重定向到文件描述符 j 指向的文件>&j# 默认的标准输出 (stdout) 重定向到 j.# 所有的标准输出将会重定向到 j 指向的文件.20. I/O 重定向2820< FILENAME", 经常会组合使用.## grep search-word File # 写字符串到 "File".exec 3 File # 打开并分配文件描述符 3 给 "File" .read -n 4 &3 # 写一个小数点.exec 3>&- # 关闭文件描述符 3.cat File # ==> 1234.67890# 随机访问.|# 管道.# 一般是命令和进程的链接工具.# 类似 ">", 但更一般.# 在连接命令,脚本,文件和程序方面非常有用.cat *.txt | sort | uniq > result-file# 所有 .txt 文件输出进行排序并且删除复制行,# 最终保存结果到 "result-file".可以用单个命令行表示输入和输出的多个重定向或管道.20. I/O 重定向283command output-file# 或者等价: output-file # 尽管这不标准.command1 | command2 | command3 > output-file更多详情见样例 16-31 and 样例 A-14.多个输出流可以重定向到一个文件.ls -yz >> command.log 2>&1# 捕获不合法选项 "yz" 的结果到文件 "command.log."# 因为 标准错误输出 被重定向到了文件,#+ 任何错误信息都会在这.# 注意, 然而, 接下来的这个案例并 "不能" 同样的结果.ls -yz 2>&1 >> command.log# 输出一条错误信息,但是不会写入到文件.# 恰恰的, 命令输出(这个例子里为空)写入到文件, 但错误信息只会在 标准输出 输出.# 如果同时重定向 标准输出 和 标准错误输出,#+ 命令的顺序不同会导致不同.关闭文件描述符n<&-关闭输入文件描述符 n.0<&-, &-关闭输出文件描述符 n.1>&-, >&-关闭标准输出.20. I/O 重定向284子进程能继承文件描述符.这就是管道符能工作的原因.通过关闭文件描述符来防止继承 .# 只重定向到 标准错误 到管道.exec 3>&1 # 保存当前 标准输出 "值".ls -l 2>&1 >&3 3>&- | grep bad 3>&- # 关闭 'grep' 文件描述符 3(但不是 'ls').# ^^^^ ^^^^exec 3>&- # 现在关闭它.# 感谢, S.C.更多关于 I/O 重定向详情见 Appendix F.注意[1] 在 UNIX 和 Linux 中, 数据流和周边外设(device files) 都被看做文件.[2] 文件描述符 仅仅是操作系统分配的一个可追踪的打开的文件号. 可以认为是一个简化的文件指针. 类似于 C 语言的 文件句柄 .[3] 当 bash 创建一个子进程的时候使用 文件描述符 5 会有问题. 例如 exec, 子进程继承了文件描述符 5 (详情见 Chet Ramey's 归档的 e-mail, SUBJECT: RE: Filedescriptor 5 is held open). 最好将这个文件描述符单独规避.20. I/O 重定向28520.1 使用 exec一个 exec < filename 命令重定向了 标准输入 到一个文件。自此所有 标准输入 都来自该文件而不是默认来源(通常是键盘输入)。在使用 sed 和 awk 时候这种方式可以逐行读文件并逐行解析。样例 20-1. 使用 exec 重定向 标准输入20.1 使用 exec286#!/bin/bash# 使用 'exec' 重定向 标准输入 .exec 6<&0 # 链接文件描述符 #6 到标准输入.# .exec < data-file # 标准输入被文件 "data-file" 替换read a1 # 读取文件 "data-file" 首行.read a2 # 读取文件 "data-file" 第二行echoecho "Following lines read from file."echo "-------------------------------"echo $a1echo $a2echo; echo; echoexec 0<&6 6<&-# 现在在之前保存的位置将从文件描述符 #6 将 标准输出 恢复.#+ 且关闭文件描述符 #6 ( 6<&- ) 让其他程序正常使用.## <&6 6filename 重定向 标准输出 到指定文件. 他将所有的命令输出通常是 标准输出 重定向到指定的位置.20.1 使用 exec287exec N > filename 影响整个脚本或当前 shell。PID 从重定向脚本或 shell 的那时候已经发生了改变. 然而 N > filename 影响的就是新派生的进程,而不是整个脚本或 shell。样例 20-2. 使用 exec 重定向标准输出20.1 使用 exec288#!/bin/bash# reassign-stdout.shLOGFILE=logfile.txtexec 6>&1 # 链接文件描述符 #6 到标准输出.# 保存标准输出.exec > $LOGFILE # 标准输出被文件 "logfile.txt" 替换.# ----------------------------------------------------------- ## 所有在这个块里的命令的输出都会发送到文件 $LOGFILE.echo -n "Logfile: "dateecho "-------------------------------------"echoecho "Output of \"ls -al\" command"echols -alecho; echoecho "Output of \"df\" command"echodf# ----------------------------------------------------------- #exec 1>&6 6>&- # 关闭文件描述符 #6 恢复 标准输出.echoecho "== stdout now restored to default == "echols -alechoexit 0样例 20-3. 用 exec 在一个脚本里同时重定向 标准输入 和 标准输出20.1 使用 exec289#!/bin/bash# upperconv.sh# 转化指定的输入文件成大写.E_FILE_ACCESS=70E_WRONG_ARGS=71if [ ! -r "$1" ] # 指定的输入文件是否可读?thenecho "Can't read from input file!"echo "Usage: $0 input-file output-file"exit $E_FILE_ACCESSfi # 同样的错误退出#+ 等同如果输入文件 ($1) 未指定 (为什么?).if [ -z "$2" ]thenecho "Need to specify output file."echo "Usage: $0 input-file output-file"exit $E_WRONG_ARGSfiexec 4<&0exec &1exec > $2 # 将写入输出文件.# 假定输出文件可写 (增加检测?).# -----------------------------------------------cat - | tr a-z A-Z # 转化大写.# ^^^^^ # 读取标准输入.# ^^^^^^^^^^ # 写到标准输出.# 然而标准输入和标准输出都会被重定向.# 注意 'cat' 可能会被遗漏.# -----------------------------------------------exec 1>&7 7>&- # 恢复标准输出.20.1 使用 exec290exec 0<&4 4<&- # 恢复标准输入.# 恢复后, 下面这行会预期从标准输出打印.echo "File \"$1\" written to \"$2\" as uppercase conversion."exit 0I/O 重定向是种明智的规避 inaccessible variables within a subshell 问题的方法.样例 20-4. 规避子 shell#!/bin/bash# avoid-subshell.sh# Matthew Walker 的建议.Lines=0echocat myfile.txt | while read line;do {echo $line(( Lines++ )); # 递增变量的值趋近外层循环# 使用子 shell 会有问题.}doneecho "Number of lines read = $Lines" # 0# 报错!echo "------------------------"exec 3 myfile.txtwhile read line &-echo "Number of lines read = $Lines" # 8echoexit 0# 下面的行并不在脚本里.$ cat myfile.txtLine 1.Line 2.Line 3.Line 4.Line 5.Line 6.Line 7.Line 8.20.1 使用 exec29220.2 重定向代码块有如 while, until, 和 for 循环, 甚至 if/then 也可以重定向 标准输入 测试代码块. 甚至连一个函数都可以用这个方法进行重定向 (见 样例 24-11). 代码块的末尾部分的 "<"就是用来完成这个的.样例 20-5. while 循环的重定向#!/bin/bash# redir2.shif [ -z "$1" ]thenFilename=names.data # 如果不指定文件名的默认值.elseFilename=$1fi#+ Filename=${1:-names.data}# can replace the above test (parameter substitution).count=0echowhile [ "$name" != Smith ] # 为什么变量 "$name" 加引号?doread name # 从 $Filename 读取值, 而不是 标准输入.echo $namelet "count += 1"done <"$Filename" # 重定向标准输入到文件 $Filename.# ^^^^^^^^^^^^echo; echo "$count names read"; echoexit 0# 注意在一些老的脚本语言中,#+ 循环的重定向会跑在子 shell 的环境中.20.2 重定向代码块293# 因此, $count 返回 0, 在循环外已经初始化过值.# Bash 和 ksh *只要可能* 会避免启动子 shell ,#+ 所以这个脚本作为样例运行成功.# (感谢 Heiner Steven 指出这点.)# 然而 . . .# Bash 有时候 *能* 在 "只读的 while" 循环启动子进程 ,#+ 不同于 "while" 循环的重定向.abc=hiecho -e "1\n2\n3" | while read ldo abc="$l"echo $abcdoneecho $abc# 感谢, Bruno de Oliveira Schneider 上面的演示代码.# 也感谢 Brian Onn 纠正了注释的错误.样例 20-6. 另一种形式的 while 循环重定向#!/bin/bash# 这是之前的另一种形式的脚本.# Heiner Steven 提议在重定向循环时候运行在子 shell 可以作为一个变通方案#+ 因此直到循环终止时循环内部的变量不需要保证他们的值if [ -z "$1" ]thenFilename=names.data # 如果不指定文件名的默认值.elseFilename=$1fiexec 3<&0 # 保存标准输入到文件描述符 3.exec 0<"$Filename" # 重定向标准输入.20.2 重定向代码块294count=0echowhile [ "$name" != Smith ]doread name # 从重定向的标准输入($Filename)读取值.echo $namelet "count += 1"done # 从 $Filename 循环读#+ 因为第 20 行.# 这个脚本的早期版本在 "while" 循环 done <"$Filename" 终止# 练习:# 为什么这个没必要?exec 0<&3 # 恢复早前的标准输入.exec 3<&- # 关闭临时的文件描述符 3.echo; echo "$count names read"; echoexit 0样例 20-7. until 循环的重定向20.2 重定向代码块295#!/bin/bash# 同先前的脚本一样, 不过用的是 "until" 循环.if [ -z "$1" ]thenFilename=names.data # 如果不指定文件的默认值.elseFilename=$1fi# while [ "$name" != Smith ]until [ "$name" = Smith ] # 变 != 为 =.doread name # 从 $Filename 读取值, 而不是标准输入.echo $namedone <"$Filename" # 重定向标准输入到文件 "$Filename".# ^^^^^^^^^^^^# 和之前的 "while" 循环样例相同的结果.exit 0样例 20-8. for 循环的重定向20.2 重定向代码块296#!/bin/bashif [ -z "$1" ]thenFilename=names.data # 如果不指定文件的默认值.elseFilename=$1filine_count=`wc $Filename | awk '{ print $1 }'`# 目标文件的行数.## 非常作和不完善, 然而这只是证明 "for" 循环中的重定向标准输入是可行的#+ 如果你足够聪明的话.## 简介的做法是 line_count=$(wc -l < "$Filename")for name in `seq $line_count` # 回忆下 "seq" 可以输入数组序列.# while [ "$name" != Smith ] -- 比 "while" 循环更复杂的循环 --doread name # 从 $Filename 读取值, 而不是标准输入.echo $nameif [ "$name" = Smith ] # 这需要所有这些额外的设置.thenbreakfidone <"$Filename" # 重定向标准输入到文件 "$Filename".# ^^^^^^^^^^^^exit 0我们可以修改先前的样例也可以重定向循环的输出.样例 20-9. for 循环的重定向 (同时重定向标准输入和标准输出)20.2 重定向代码块297#!/bin/bashif [ -z "$1" ]thenFilename=names.data # 如果不指定文件的默认值.elseFilename=$1fiSavefile=$Filename.new # 报错的结果的文件名.FinalName=Jonah # 停止 "read" 的终止字符.line_count=`wc $Filename | awk '{ print $1 }'` # 目标文件行数.for name in `seq $line_count`doread nameecho "$name"if [ "$name" = "$FinalName" ]thenbreakfidone "$Savefile" # 重定向标准输入到文件 $Filename,# ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 并且报错结果到备份文件.exit 0样例 20-10. if/then test的重定向20.2 重定向代码块298#!/bin/bashif [ -z "$1" ]thenFilename=names.data # 如果不指定文件的默认值.elseFilename=$1fiTRUE=1if [ "$TRUE" ] # if true 和 if : 都可以工作.thenread nameecho $namefi <"$Filename"# ^^^^^^^^^^^^# 只读取文件的首行.# "if/then" test 除非嵌入在循环内部否则没办法迭代.exit 0样例 20-11. 上述样例的数据文件 names.data20.2 重定向代码块299AristotleArrheniusBelisariusCapablancaDickensEulerGoetheHegelJonahLaplaceMaroczyPurcellSchmidtSchopenhauerSemmelweissSmithSteinmetzTukhashevskyTuringVennWarshawskiZnosko-Borowski#+ 这是 "redir2.sh", "redir3.sh", "redir4.sh", "redir4a.sh", "redir5.sh" 的数据文件.代码块的标准输出的重定向影响了保存到文件的输出. 见样例 样例 3-2.嵌入文档 是种特别的重定向代码块的方法. 既然如此,它使得在 while 循环的标准输入里传入嵌入文档的输出变得可能.20.2 重定向代码块300# 这个样例来自 Albert Siersema# 得到了使用许可 (感谢!).function doesOutput()# 当然这也是个外部命令.# 这里用函数进行演示会更好一点.{ls -al *.jpg | awk '{print $5,$9}'}nr=0 # 我们希望在 'while' 循环里可以操作这些totalSize=0 #+ 并且在 'while' 循环结束时看到改变.while read fileSize fileName ; doecho "$fileName is $fileSize bytes"let nr++totalSize=$((totalSize+fileSize)) # Or: "let totalSize+=fileSize"done<&7 # *追加* 日期到文件.20.3 应用程序302# ^^^^^^^ 命令替换# 见下文.}case $LOG_LEVEL in1) exec 3>&2 4> /dev/null 5> /dev/null;;2) exec 3>&2 4>&2 5> /dev/null;;3) exec 3>&2 4>&2 5>&2;;*) exec 3> /dev/null 4> /dev/null 5> /dev/null;;esacFD_LOGVARS=6if [[ $LOG_VARS ]]then exec 6>> /var/log/vars.logelse exec 6> /dev/null # 清空输出.fiFD_LOGEVENTS=7if [[ $LOG_EVENTS ]]then# exec 7 >(exec gawk '{print strftime(), $0}' >> /var/log/event.log)# 上述行在最近高于 bash 2.04 版本会失败,为什么?exec 7>> /var/log/event.log # 追加到 "event.log".log # 写入时间和日期.else exec 7> /dev/null # 清空输出.fiecho "DEBUG3: beginning" >&${FD_DEBUG3}ls -l >&5 2>&4 # 命令1 >&5 2>&4echo "Done" # 命令2echo "sending mail" >&${FD_LOGEVENTS}# 输出信息 "sending mail" 到文件描述符 #7.20.3 应用程序303exit 020.3 应用程序304第二十二章. 限制模式的Shell限制模式下被禁用的命令在限制模式下运行一个脚本或部分脚本将禁用一些命令,尽管这些命令在正常模式下是可用的。这是个安全措施,可以限制脚本用户的权限,减少运行脚本可能带来的损害。被禁用的命令和功能:使用 cd 来改变工作目录。修改 $PATH, $SHELL, $BASH_ENV 或 $ENV 等环境变量读取或修改 $SHELLOPTS,shell环境选项。输出重定向。调用包含 / 的命令。调用 exec 来替代shell进程。其他各种会造成混乱或颠覆脚本用途的命令。在脚本中跳出限制模式。例 22-1. 在限制模式运行脚本#!/bin/bash# 在脚本开头用"#!/bin/bash -r"#+ 可以让整个脚本在限制模式运行。echoecho "改变目录。"cd /usr/localecho "现在是在 `pwd`"echo "回到家目录。"cdecho "现在是在 `pwd`"echo# 到此为止一切都是正常的,非限制模式。22. 限制模式的Shell305set -r# set --restricted 效果相同。echo "==> 现在是限制模式 bin.filesls -l bin.files # 尝试列出试图创建的文件。echoexit 022. 限制模式的Shell306第二十三章. 进程替换用管道 将一个命令的 标准输出 输送到另一个命令的 标准输入 是个强大的技术。但是如果你需要用管道输送多个命令的 标准输出 怎么办?这时候 进程替换就派上用场了。进程替换 把一个(或多个)进程 的输出送到另一个进程的 标准输入 。样板 命令列表要用括号括起来>(command_list)<(command_list)进程替换使用 /dev/fd/ 文件发送括号内进程的结果到另一个进程。[1]""与括号之间没有空格,加上空格或报错。bash$ echo >(true)/dev/fd/63bash$ echo (true) <(true)/dev/fd/63 /dev/fd/62bash$ wc <(cat /usr/share/dict/linux.words)483523 483523 4992010 /dev/fd/63bash$ grep script /usr/share/dict/linux.words | wc262 262 3601bash$ wc <(grep script /usr/share/dict/linux.words)262 262 3601 /dev/fd/6323. 进程替换307Bash用两个文件描述符创建管道, --fIn 和 fOut-- 。true 的 标准输入 连接 fOut(dup2(fOut, 0)),然后Bash 传递一个 /dev/fd/fIn 参数给 echo 。在不使用 /dev/fd/ 的系统里,Bash可以用临时文件(感谢 S.C. 指出这点)。进程替换可以比较两个不同命令的输出,或者同一个命令使用不同选项的输出。bash$ comm <(ls -l) <(ls -al)total 12-rw-rw-r-- 1 bozo bozo 78 Mar 10 12:58 File0-rw-rw-r-- 1 bozo bozo 42 Mar 10 12:58 File2-rw-rw-r-- 1 bozo bozo 103 Mar 10 12:58 t2.shtotal 20drwxrwxrwx 2 bozo bozo 4096 Mar 10 18:10 .drwx------ 72 bozo bozo 4096 Mar 10 17:58 ..-rw-rw-r-- 1 bozo bozo 78 Mar 10 12:58 File0-rw-rw-r-- 1 bozo bozo 42 Mar 10 12:58 File2-rw-rw-r-- 1 bozo bozo 103 Mar 10 12:58 t2.sh进程替换可以比较两个目录的内容——来检查哪些文件在这个目录而不在那个目录。diff <(ls $first_directory) <(ls $second_directory)进程替换的一些其他用法:read -a list < (md5sum ->mydata-orig.md5) |gzip | tee>(md5sum - | sed 's/-$/mydata.lz2/'>mydata-gz.md5)>mydata.gz# 检查解压缩结果:gzip -d file.tar.bz2) $directory_name# 调用 "tar cf /dev/fd/?? $directory_name",然后 "bzip2 -c > file.tar.bz2"。## 因为 /dev/fd/ 系统特性# 不需要在两个命令之间使用管道符## 这个可以模拟#bzip2 -c file.tar.bz2&tar cf pipe $directory_namerm pipe# 或者exec 3>&1tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&-exec 3>&-# 致谢:Stéphane Chazelas在子shell中 echo 命令用管道输送给 while-read 循环时会出现问题,下面是避免的方法:例23-1 不用 fork 的代码块重定向。#!/bin/bash# wr-ps.bash: 使用进程替换的 while-read 循环。23. 进程替换310# 示例由 Tomas Pospisek 贡献。# (ABS指南作者做了大量改动。)echoecho "random input" | while read idoglobal=3D": Not available outside the loop."# ... 因为在子 shell 中运行。doneecho "\$global (从子进程之外) = $global"# $global (从子进程之外) =echo; echo "--"; echowhile read idoecho $iglobal=3D": Available outside the loop."# ... 因为没有在子 shell 中运行。done < <( echo "random input" )# ^ ^echo "\$global (使用进程替换) = $global"# 随机输入# $global (使用进程替换)= 3D: Available outside the loop.echo; echo "##########"; echo# 同样道理 . . .declare -a inloopindex=0cat $0 | while read linedo23. 进程替换311inloop[$index]="$line"((index++))# 在子 shell 中运行,所以 ...doneecho "OUTPUT = "echo ${inloop[*]} # ... 什么也没有显示。echo; echo "--"; echodeclare -a outloopindex=0while read linedooutloop[$index]="$line"((index++))# 没有在子 shell 中运行,所以 ...done < <( cat $0 )echo "OUTPUT = "echo ${outloop[*]} # ... 整个脚本的结果显示出来。exit $?下面是个类似的例子。例 23-2. 重定向进程替换的输出到一个循环内23. 进程替换312#!/bin/bash# psub.bash# 受 Diego Molina 启发(感谢!)。declare -a array0while readdoarray0[${#array0[@]}]="$REPLY"done < <( sed -e 's/bash/CRASH-BANG!/' $0 | grep bin | awk '{print $1}' )# 由进程替换来设置'read'默认变量($REPLY)。#+ 然后将变量复制到一个数组。echo "${array0[@]}"exit $?# ====================================== ## 运行结果:bash psub.bash#!/bin/CRASH-BANG! done #!/bin/CRASH-BANG!一个读者发来一个有趣的进程替换例子,如下:# SuSE 发行版中提取的脚本片段:# --------------------------------------------------------------#while read des what mask iface; do# 一些命令 ...done < <(route -n)# ^ ^ 第一个 < 是重定向,第二个是进程替换。# 为了测试,我们让它来做点儿事情。while read des what mask iface; doecho $des $what $mask $ifacedone < <(route -n)23. 进程替换313# 输出内容:# Kernel IP routing table# Destination Gateway Genmask Flags Metric Ref Use Iface# 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo# --------------------------------------------------------------## 正如 Stéphane Chazelas 指出的,#+ 一个更容易理解的等价代码如下:route -n |while read des what mask iface; do # 通过管道输出设置的变量echo $des $what $mask $ifacedone # 这段代码的结果更上面的相同。# 但是,Ulrich Gayer 指出 . . .#+ 这段简化版等价代码在 while 循环里用了子 shell,#+ 因此当管道终止时变量都消失了。# --------------------------------------------------------------## 然而,Filip Moritz 说上面的两个例子有一个微妙的区别,#+ 见下面的代码(route -n | while read x; do ((y++)); doneecho $y # $y is still unsetwhile read x; do ((y++)); done < <(route -n)echo $y # $y has the number of lines of output of route -n)# 更通俗地说(译者注:原文本行少了注释符)(: | x=x# 似乎启动了子 shell ,就像: | ( x=x )# 而x=x < 下面的代码段来自 /etc/rc.d/init.d/single#+==> 作者 Miquel van Smoorenburg#+==> 说明了 "and" 和 "or" 列表。# ==> 带箭头的注释是本文作者添加的。[ -x /usr/bin/clear ] && /usr/bin/clear# ==> 如果 /usr/bin/clear 存在, 则调用它。# ==> 调用命令之前检查它是否存在,#+==> 可以避免出错消息和其他怪异的结果。# ==> . . .# If they want to run something in single user mode, might as well run it...for i in /etc/rc1.d/S[0-9][0-9]* ; do# 检查脚本是否存在。[ -x "$i" ] || continue# ==> 如果对应的文件在 $PWD 里*没有*找到,#+==> 则跳回到循环顶端“继续运行”。# 丢弃备份文件和 rpm 生成的文件。case "$1" in*.rpmsave|*.rpmorig|*.rpmnew|*~|*.orig)continue;;esac[ "$i" = "/etc/rc1.d/S00single" ] && continue# ==> 设置脚本名,但先不要执行$i startdone# ==> . . .and 列表 或 or 列表 的退出状态就是最后一个执行的命令的退出状态。聪明地结合 and 列表 和 or 列表 是可能的,但是程序逻辑会很容易地变得令人费解,需要密切注意操作符优先规则,而且,会带来大量的调试工作。26. 列表结构320false && true || echo false # false# 下面的代码结果相同( false && true ) || echo false # false# 但这个就不同了false && ( true || echo false ) # (什么都不显示)# 注意语句是从左到右组合和解释的。# 通常情况下最好避免这种复杂性。# 感谢, S.C.例 A-7 和 例 7-4 解释了用 and 列表 / or 列表 来测试变量。26. 列表结构32125. 别名Bash 别名 本质上不外乎是键盘上的快捷键,缩写呢是避免输入很长的命令串的一种手段.举个例子, 在 ~/.bashrc 文件中包含别名 lm="ls -l | more , 而后每个命令行输入的 lm [1] 将会自动被替换成 ls -l | more . 这可以节省大量的命令行输入和避免记住复杂的命令和选项. 设定别名 rm="rm -i" (交互的删除模式) 防止无意的删除重要文件,也许可以少些悲痛.脚本中别名作用十分有限. 如果别名可以有一些 C 预处理器的功能会更好, 例如宏扩展, 但不幸的是 bash 别名中没有扩展参数. [2] 另外, 脚本在 "复合结构" 中并不能扩展自身的别名,例如 if/then, 循环和函数. 另一个限制是,别名不能递归扩展. 基本上是我们无论怎么喜欢用别名都不如函数 function 来的更有效.样例 25-1. 脚本中的别名#!/bin/bash# alias.shshopt -s expand_aliases# 必须设置此选项, 否则脚本不能别名扩展.# 首先来点好玩的东西.alias Jesse_James='echo "\"Alias Jesse James\" was a 1959 comedystarring Bob Hope."'Jesse_Jamesecho; echo; echo;alias ll="ls -l"# 可以任意使用单引号 (') 或双引号 (") 把别名括起来.echo "Trying aliased \"ll\":"ll /usr/X11R6/bin/mk* #* 别名可以运行.echo25. 别名322directory=/usr/X11R6/bin/prefix=mk* # See if wild card causes problems.echo "Variables \"directory\" + \"prefix\" = $directory$prefix"echoalias lll="ls -l $directory$prefix"echo "Trying aliased \"lll\":"lll # 所有 /usr/X11R6/bin 文件清单以 mk 开始.# 别名可以处理连续的变量 -- 包含 wild card -- o.k.TRUE=1echoif [ TRUE ]thenalias rr="ls -l"echo "Trying aliased \"rr\" within if/then statement:"rr /usr/X11R6/bin/mk* #* 结果报错!# 别名在复合的表达式中并没有生效.echo "However, previously expanded alias still recognized:"ll /usr/X11R6/bin/mk*fiechocount=0while [ $count -lt 3 ]doalias rrr="ls -l"echo "Trying aliased \"rrr\" within \"while\" loop:"rrr /usr/X11R6/bin/mk* #* 这里的别名也没生效.# alias.sh: 行 57: rrr: 命令未找到let count+=1done25. 别名323echo; echoalias xyz='cat $0' # 列出了自身.# 注意强引.xyz# 这看起来能工作,#+ 尽管 bash 文档不介意这么做.## 然而, Steve Jacobson 指出,#+ "$0" 参数的扩展在上面的别名申明后立刻生效.exit 0取消别名的命令删除之前设置的别名.样例 25-2. unalias: 设置和取消一个别名25. 别名324#!/bin/bash# unalias.shshopt -s expand_aliases # 开启别名扩展.alias llm='ls -al | more'llmechounalias llm # 取消别名.llm# 'llm' 不再被识别后的报错信息.exit 0bash$ ./unalias.shtotal 6drwxrwxr-x 2 bozo bozo 3072 Feb 6 14:04 .drwxr-xr-x 40 bozo bozo 2048 Feb 6 14:04 ..-rwxr-xr-x 1 bozo bozo 199 Feb 6 14:04 unalias.sh./unalias.sh: llm: 命令未找到注意[1] ... 作为命令行的第一个词. 显然别名只在命令的开始有意义. [2] 然而, 别名可用来扩展位置参数.25. 别名325

1.11.21.2.11.2.21.2.2.11.2.2.21.31.3.11.3.21.3.2.11.3.2.21.3
1.3.6.41.41.4.11.4.1.11.4.1.21.4.1.31.4.21.4.2.11.4.2.1.11.4
3高级Bash脚本编程指南中文版
《Advanced Bash-Scripting Guide》inChinese《高级Bash脚本编程
还剩 321页未读,点此继续全文在线阅读

免费下载高级Bash脚本编程指南中文版到电脑,使用更方便!

本文推荐: 高级Bash脚本编程指南中文版.pdf全文阅读下载  关键词: 脚本   编程  
学文库温馨提示:文档由用户自行上传分享,文档预览可能有差异,下载后仅供学习交流,未经上传用户书面授权,请勿作他用。 文档下载资源交流QQ群:317981604

文档相关搜索

< / 325>

QQ|小黑屋|网站声明|网站地图|学文库 ( 冀ICP备06006432号 )

GMT+8, 2020-8-13 21:41 , Processed in 0.393584 second(s), 5 queries , Gzip On, Redis On.

Powered by 学文库 1.0

Copyright © 2019-2020, 学文库

返回顶部