·ChinaUnix首页 ·论坛 ·博客 
Linux首页 | Linux新闻 | Linux文档 | Linux论坛 | Linux下载 | Linux博客 | Linux搜索
新手入门 | 安装启动 | 管理员指南 | 开发手册 | 桌面应用 | 程序开发 | 数据库 | 网络技术| CentOS | Fedora | MySQL | Apache | Ubuntu | Gentoo| OSCON08
  Linux时代 >> 技术文档 >> 程序开发
 
bash编程实例
来源: ChinaUnix博客  日期: 2008.03.14 16:52 (共有条评论) 我要评论
 
第一部分    热身
++++++++++++++++
shell是一个命令解释器.是介于操作系统kernel与用户之间的一个绝缘层.准确地说,它也是一
一种强力的计算机语言.一个shell程序,被称为一个脚本,是一种很容易使用的工具,它可以通过
将系统调用,公共程序,工具,和编译过的二进制程序粘合在一起来建立应用.事实上,所有的UNIX
命令和工具再加上公共程序,对于shell脚本来说,都是可调用的.如果这些你还觉得不够,那么
shell内建命令,比如test与循环结构,也会给脚本添加强力的支持和增加灵活性.Shell脚本对于
管理系统任务和其它的重复工作的例程来说,表现的非常好,根本不需要那些华而不实的成熟
紧凑的程序语言.
第1章    为什么使用shell编程
===========================
没有程序语言是完美的.甚至没有一个唯一最好的语言,只有对于特定目的,比较适合和不适合
的程序语言.
                                                               Herbert Mayer
对于任何想适当精通一些系统管理知识的人来说,掌握shell脚本知识都是最基本的,即使这些
人可能并不打算真正的编写一些脚本.想一下Linux机器的启动过程,在这个过程中,必将运行
/etc/rc.d目录下的脚本来存储系统配置和建立服务.详细的理解这些启动脚本对于分析系统的
行为是非常重要的,并且有时候可能必须修改它.
学习如何编写shell脚本并不是一件很困难的事,因为脚本可以分为很小的块,并且相对于shell
特性的操作和选项[1]部分,只需要学习很小的一部分就可以了.语法是简单并且直观的,编写脚
本很像是在命令行上把一些相关命令和工具连接起来,并且只有很少的一部分规则需要学习.
绝大部分脚本第一次就可以正常的工作,而且即使调试一个长一些的脚本也是很直观的.
一个shell脚本是一个类似于小吃店的(quick and dirty)方法,在你使用原型设计一个复杂的
应用的时候.在工程开发的第一阶段,即使从功能中取得很有限的一个子集放到shell脚本中来
完成往往都是非常有用的.使用这种方法,程序的结果可以被测试和尝试运行,并且在处理使用
诸如C/C++,Java或者Perl语言编写的最终代码前,主要的缺陷和陷阱往往就被发现了.
Shell脚本遵循典型的UNIX哲学,就是把大的复杂的工程分成小规模的子任务,并且把这些部件
和工具组合起来.许多人认为这种办法更好一些,至少这种办法比使用那种高\大\全的语言更
美,更愉悦,更适合解决问题.比如Perl就是这种能干任何事能适合任何人的语言,但是代价就是
你需要强迫自己使用这种语言来思考解决问题的办法.
什么时候不使用Shell脚本
   
   资源密集型的任务,尤其在需要考虑效率时(比如,排序,hash等等)
   需要处理大任务的数学操作,尤其是浮点运算,精确运算,或者复杂的算术运算
   (这种情况一般使用C++或FORTRAN来处理)
   有跨平台移植需求(一般使用C或Java)
   复杂的应用,在必须使用结构化编程的时候(需要变量的类型检查,函数原型,等等)
   对于影响系统全局性的关键任务应用。
   对于安全有很高要求的任务,比如你需要一个健壮的系统来防止入侵,破解,恶意破坏等等.
   项目由连串的依赖的各个部分组成。
   需要大规模的文件操作
   需要多维数组的支持
   需要数据结构的支持,比如链表或数等数据结构
   需要产生或操作图形化界面GUI
   需要直接操作系统硬件
   需要I/O或socket接口
   需要使用库或者遗留下来的老代码的接口
   私人的,闭源的应用(shell脚本把代码就放在文本文件中,全世界都能看到)
如果你的应用符合上边的任意一条,那么就考虑一下更强大的语言吧--或许是Perl,Tcl,Python,
Ruby -- 或者是更高层次的编译语言比如C/C++,或者是Java.即使如此,你会发现,使用shell
来原型开发你的应用,在开发步骤中也是非常有用的.
我们将开始使用Bash,Bash是"Bourne-Again shell"首字母的缩写,也是Stephen Bourne的经典
的Bourne shell的一个双关语,(译者:说实话,我一直搞不清这个双关语是什么意思,为什么叫
"Bourn-Again shell",这其中应该有个什么典故吧,哪位好心,告诉我一下^^).Bash已经成为了
所有UNIX中shell脚本的事实上的标准了.同时这本书也覆盖了绝大部分的其他一些shell的原
则,比如Korn Shell,Bash从ksh中继承了一部分特性,[2]C Shell和它的变种.(注意:C Shell
编程是不被推荐的,因为一些特定的内在问题,Tom Christiansen在1993年10月指出了这个问题
请在
http://www.etext.org/Quart...
中查看具体内容.)
接下来是脚本的一些说明.在展示shell不同的特征之前,它可以减轻一些阅读书中例子
的负担.本书中的例子脚本,都在尽可能的范围内进行了测试,并且其中的一些将使用在真
实的生活中.读者可以运行这些例子脚本(使用scriptname.sh或者scriptname.bash的形式),
[3]并给这些脚本执行权限(chmod u+rx scriptname),然后执行它们,看看发生了什么.如果存
档的脚本不可用,那么就从本书的HTML,pdf或者text的发行版本中把它们拷贝粘贴出来.考虑到
这些脚本中的内容在我们还没解释它之前就被列在这里,可能会影响读者的理解,这就需要读者
暂时忽略这些内容.
除非特别注明,本书作者编写了本书中的绝大部分例子脚本.
注意事项:
[1]        这些在builtins章节被引用,这些是shell的内部特征.
[2]        ksh88的许多特性,甚至是一些ksh93的特性都被合并到Bash中了.
[3]        根据惯例,用户编写的Bourne shell脚本应该在脚本的名字后边加上.sh扩展名.
       一些系统脚本,比如那些在/etc/rc.d中的脚本,则不遵循这种命名习惯.
第2章    带着一个Sha-Bang出发(Sha-Bang指的是#!)
==============================================
在一个最简单的例子中,一个shell脚本其实就是将一堆系统命令列在一个文件中.它的最基本的
用处就是,在你每次输入这些特定顺序的命令时可以少敲一些字.
Example 2-1 清除:清除/var/log下的log文件
################################Start Script#######################################
1 # Cleanup
2 # 当然要使用root身份来运行这个脚本
3
4 cd /var/log
5 cat /dev/null > messages
6 cat /dev/null > wtmp
7 echo "Logs cleaned up."
################################End Script#########################################
这根本就没什么稀奇的, 只不过是命令的堆积, 来让从console或者xterm中一个一个的输入命
令更方便一些.好处就是把所有命令都放在一个脚本中,不用每次都敲它们.这样的话,对于特定
的应用来说,这个脚本就很容易被修改或定制.
Example 2-2 清除:一个改良的清除脚本
################################Start Script#######################################
1 #!/bin/bash
2 # 一个Bash脚本的正确的开头部分.
3
4 # Cleanup, 版本 2
5
6 # 当然要使用root身份来运行.
7 # 在此处插入代码,来打印错误消息,并且在不是root身份的时候退出.
8
9 LOG_DIR=/var/log
10 # 如果使用变量,当然比把代码写死的好.
11 cd $LOG_DIR
12
13 cat /dev/null > messages
14 cat /dev/null > wtmp
15
16
17 echo "Logs cleaned up."
18
19 exit # 这个命令是一种正确并且合适的退出脚本的方法.
################################End Script#########################################
现在,让我们看一下一个真正意义的脚本.而且我们可以走得更远...
Example 2-3. cleanup:一个增强的和广义的删除logfile的脚本
################################Start Script#######################################
1 #!/bin/bash
2 # 清除, 版本 3
3
4 #  Warning:
5 #  -------
6 #  这个脚本有好多特征,这些特征是在后边章节进行解释的,大概是进行到本书的一半的
7 #  时候,
8 #  你就会觉得它没有什么神秘的了.
9 #
10
11
12
13 LOG_DIR=/var/log
14 ROOT_UID=0     # $UID为0的时候,用户才具有根用户的权限
15 LINES=50       # 默认的保存行数
16 E_XCD=66       # 不能修改目录?
17 E_NOTROOT=67   # 非根用户将以error退出
18
19
20 # 当然要使用根用户来运行
21 if [ "$UID" -ne "$ROOT_UID" ]
22 then
23   echo "Must be root to run this script."
24   exit $E_NOTROOT
25 fi  
26
27 if [ -n "$1" ]
28 # 测试是否有命令行参数(非空).
29 then
30   lines=$1
31 else  
32   lines=$LINES # 默认,如果不在命令行中指定
33 fi  
34
35
36 #  Stephane Chazelas 建议使用下边
37 #+ 的更好方法来检测命令行参数.
38 #+ 但对于这章来说还是有点超前.
39 #
40 #    E_WRONGARGS=65  # 非数值参数(错误的参数格式)
41 #
42 #    case "$1" in
43 #    ""      ) lines=50;;
44 #    *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;;
45 #    *       ) lines=$1;;
46 #    esac
47 #
48 #* 直到"Loops"的章节才会对上边的内容进行详细的描述.
49
50
51 cd $LOG_DIR
52
53 if [ `pwd` != "$LOG_DIR" ]  # 或者    if[ "$PWD" != "$LOG_DIR" ]
54                             # 不在 /var/log中?
55 then
56   echo "Can't change to $LOG_DIR."
57   exit $E_XCD
58 fi  # 在处理log file之前,再确认一遍当前目录是否正确.
59
60 # 更有效率的做法是
61 #
62 # cd /var/log || {
63 #   echo "Cannot change to necessary directory." >&2
64 #   exit $E_XCD;
65 # }
66
67
68
69
70 tail -$lines messages > mesg.temp # 保存log file消息的最后部分.
71 mv mesg.temp messages             # 变为新的log目录.
72
73
74 # cat /dev/null > messages
75 #* 不再需要了,使用上边的方法更安全.
76
77 cat /dev/null > wtmp  #  ': > wtmp' 和 '> wtmp'具有相同的作用
78 echo "Logs cleaned up."
79
80 exit 0
81 #  退出之前返回0,返回0表示成功.
82 #
################################End Script#########################################
因为你可能希望将系统log全部消灭,这个版本留下了log消息最后的部分.你将不断地找到新
的方法来完善这个脚本,并提高效率.
要注意,在每个脚本的开头都使用"#!",这意味着告诉你的系统这个文件的执行需要指定一个解
释器.#!实际上是一个2字节[1]的魔法数字,这是指定一个文件类型的特殊标记, 换句话说, 在
这种情况下,指的就是一个可执行的脚本(键入man magic来获得关于这个迷人话题的更多详细
信息).在#!之后接着是一个路径名.这个路径名指定了一个解释脚本中命令的程序,这个程序可
以是shell,程序语言或者是任意一个通用程序.这个指定的程序从头开始解释并且执行脚本中
的命令(从#!行下边的一行开始),忽略注释.[2]
如:
1 #!/bin/sh
2 #!/bin/bash
3 #!/usr/bin/perl
4 #!/usr/bin/tcl
5 #!/bin/sed -f
6 #!/usr/awk -f
上边每一个脚本头的行都指定了一个不同的命令解释器,如果是/bin/sh,那么就是默认shell
(在Linux系统中默认是Bash).[3]使用#!/bin/sh,在大多数商业发行的UNIX上,默认是Bourne
shell,这将让你的脚本可以正常的运行在非Linux机器上,虽然这将会牺牲Bash一些独特的特征.
脚本将与POSIX[4] 的sh标准相一致.
注意: #! 后边给出的路径名必须是正确的,否则将会出现一个错误消息,通常是
"Command not found",这将是你运行这个脚本时所得到的唯一结果.
当然"#!"也可以被忽略,不过这样你的脚本文件就只能是一些命令的集合,不能够使用shell内建
的指令了,如果不能使用变量的话,当然这也就失去了脚本编程的意义了.
   注意:这个例子鼓励你使用模块化的方式来编写脚本,平时也要注意收集一些零碎的代码,
       这些零碎的代码可能用在你将来编写的脚本中.这样你就可以通过这些代码片段来构
       造一个较大的工程用例. 以下边脚本作为序,来测试脚本被调用的参数是否正确.
################################Start Script#######################################
1 E_WRONG_ARGS=65
2 script_parameters="-a -h -m -z"
3 #                  -a = all, -h = help, 等等.
4
5 if [ $# -ne $Number_of_expected_args ]
6 then
7   echo "Usage: `basename $0` $script_parameters"
8   # `basename $0`是这个脚本的文件名
9   exit $E_WRONG_ARGS
10 fi
################################End Script#########################################
大多数情况下,你需要编写一个脚本来执行一个特定的任务,在本章中第一个脚本就是一个这样
的例子, 然后你会修改它来完成一个不同的,但比较相似的任务.用变量来代替写死的常量,就是
一个好方法,将重复的代码放到一个函数中,也是一种好习惯.
2.1 调用一个脚本
----------------
编写完脚本之后,你可以使用sh scriptname,[5]或者bash scriptname来调用它.
(不推荐使用sh 更方便的方法是让脚本本身就具有可执行权限,通过chmod命令可以修改.
比如:
   chmod 555 scriptname (允许任何人都具有 可读和执行权限) [6]  
或:
   chmod +rx scriptname (允许任何人都具有 可读和执行权限)
   chmod u+rx scriptname (只给脚本的所有者 可读和执行权限)
既然脚本已经具有了可执行权限,现在你可以使用./scriptname.[7]来测试它了.如果这个脚本
以一个"#!"行开头,那么脚本将会调用合适的命令解释器来运行.
最后一步,在脚本被测试和debug之后,你可能想把它移动到/usr/local/bin(当然是以root身份)
,来让你的脚本对所有用户都有用.这样用户就可以直接敲脚本名字来运行了.
注意事项:
[1]        那些具有UNIX味道的脚本(基于4.2BSD)需要一个4字节的魔法数字,在#!后边需要一个
       空格#! /bin/sh.
[2]        脚本中的#!行的最重要的任务就是命令解释器(sh或者bash).因为这行是以#开始的,
       当命令解释器执行这个脚本的时候,会把它作为一个注释行.当然,在这之前,这行语句
       已经完成了它的任务,就是调用命令解释器.
       如果在脚本的里边还有一个#!行,那么bash将把它认为是一个一般的注释行.
        1 #!/bin/bash
        2
        3 echo "Part 1 of script."
        4 a=1
        5
        6 #!/bin/bash
        7 # 这将不会开始一个新脚本.
        8
        9 echo "Part 2 of script."
       10 echo $a  # Value of $a stays at 1.
[3]        这里可以玩一些小技巧.
        1 #!/bin/rm
        2 # 自删除脚本.
        3
        4 # 当你运行这个脚本时,基本上什么都不会发生...除非这个文件消失不见.
        5
        6 WHATEVER=65
        7
        8 echo "This line will never print (betcha!)."
        9
       10 exit $WHATEVER  # 没关系,脚本是不会在这退出的.
       当然,你还可以试试在一个README文件的开头加上#!/bin/more,并让它具有执行权限.
       结果将是文档自动列出自己的内容.(一个使用cat命令的here document可能是一个
       更好的选则,--见Example 17-3).
[4]        可移植的操作系统接口,标准化类UNIX操作系统的一种尝试.POSIX规范可以在
      
http://www.opengroup.org/o...
中查阅.
[5]        小心:使用sh scriptname来调用脚本的时候将会关闭一些Bash特定的扩展,脚本可能
       因此而调用失败.
[6]        脚本需要读和执行权限,因为shell需要读这个脚本.
[7]        为什么不直接使用scriptname来调用脚本?如果你当前的目录下($PWD)正好有你想要
       执行的脚本,为什么它运行不了呢?失败的原因是,出于安全考虑,当前目录并没有被
       加在用户的$PATH变量中.因此,在当前目录下调用脚本必须使用./scriptname这种
       形式.
2.2 初步的练习
--------------
1. 系统管理员经常会为了自动化一些常用的任务而编写脚本.举出几个这种有用的脚本的实例.
2. 编写一个脚本,显示时间和日期,列出所有的登录用户,显示系统的更新时间.然后这个脚本
  将会把这些内容保存到一个log file中.
第二部分    基本
++++++++++++++++
第3章    特殊字符
================
#        注释,行首以#开头为注释(#!是个例外).
       1 # This line is a comment.
       注释也可以存在于本行命令的后边.
       1 echo "A comment will follow." # 注释在这里
       2 #                            ^ 注意#前边的空白
       注释也可以在本行空白的后边.
       1     # A tab precedes this comment.
       注意:命令是不能跟在同一行上注释的后边的,没有办法,在同一行上,注释的后边想
       要再使用命令,只能另起一行.
       当然,在echo命令中被转义的#是不能作为注释的.
       同样的,#也可以出现在特定的参数替换结构中或者是数字常量表达式中.
       1 echo "The # here does not begin a comment."
       2 echo 'The # here does not begin a comment.'
       3 echo The \# here does not begin a comment.
       4 echo The # 这里开始一个注释
       5
       6 echo ${PATH#*:}       # 参数替换,不是一个注释
       7 echo $(( 2#101011 ))  # 数制转换,不是一个注释
       8
       9 # Thanks, S.C.
       标准的引用和转义字符("'\)可以用来转义#
;        命令分隔符,可以用来在一行中来写多个命令.
       1 echo hello; echo there
       2
       3
       4 if [ -x "$filename" ]; then    # 注意:"if"和"then"需要分隔
       5                                # 为啥?
       6   echo "File $filename exists."; cp $filename $filename.bak
       7 else
       8   echo "File $filename not found."; touch $filename
       9 fi; echo "File test complete."
       有时候需要转义
;;        终止"case"选项.
       1 case "$variable" in
       2 abc)  echo "\$variable = abc" ;;
       3 xyz)  echo "\$variable = xyz" ;;
       4 esac
.        .命令等价于source命令(见Example 11-20).这是一个bash的内建命令.
.        .作为文件名的一部分.如果作为文件名的前缀的话,那么这个文件将成为隐藏文件.
       将不被ls命令列出.
       bash$ touch .hidden-file
       bash$ ls -l         
       total 10
       -rw-r--r--    1 bozo      4034 Jul 18 22:04 data1.addressbook
       -rw-r--r--    1 bozo      4602 May 25 13:58 data1.addressbook.bak
       -rw-r--r--    1 bozo       877 Dec 17  2000 employment.addressbook
      
      
       bash$ ls -al         
       total 14
       drwxrwxr-x    2 bozo  bozo      1024 Aug 29 20:54 ./
       drwx------   52 bozo  bozo      3072 Aug 29 20:51 ../
       -rw-r--r--    1 bozo  bozo      4034 Jul 18 22:04 data1.addressbook
       -rw-r--r--    1 bozo  bozo      4602 May 25 13:58 data1.addressbook.bak
       -rw-r--r--    1 bozo  bozo       877 Dec 17  2000 employment.addressbook
       -rw-rw-r--    1 bozo  bozo         0 Aug 29 20:54 .hidden-file
       .命令如果作为目录名的一部分的话,那么.表达的是当前目录.".."表示上一级目录.
       bash$ pwd
       /home/bozo/projects
       bash$ cd .
       bash$ pwd
       /home/bozo/projects
       bash$ cd ..
       bash$ pwd
       /home/bozo/
       .命令经常作为一个文件移动命令的目的地.
       bash$ cp /home/bozo/current_work/junk/* .
.        .字符匹配,这是作为正则表达是的一部分,用来匹配任何的单个字符.
"        部分引用."STRING"阻止了一部分特殊字符,具体见第5章.
'        全引用. 'STRING' 阻止了全部特殊字符,具体见第5章.
,        逗号链接了一系列的算术操作,虽然里边所有的内容都被运行了,但只有最后一项被
       返回.
       如:
       1 let "t2 = ((a = 9, 15 / 3))"  # Set "a = 9" and "t2 = 15 / 3"
\        转义字符,如\X等价于"X"或'X',具体见第5章.
/        文件名路径分隔符.或用来做除法操作.
`        后置引用,命令替换,具体见第14章
:        空命令,等价于"NOP"(no op,一个什么也不干的命令).也可以被认为与shell的内建命令(true)作用相同.":"命令是一
       个bash的内建命令,它的返回值为0,就是shell返回的true.
       如:
       1 :
       2 echo $?   # 0
       死循环,如:
        1 while :
        2 do
        3    operation-1
        4    operation-2
        5    ...
        6    operation-n
        7 done
        8
        9 # 与下边相同:
       10 #    while true
       11 #    do
       12 #      ...
       13 #    done
       在if/then中的占位符,如:
       1 if condition
       2 then :   # 什么都不做,引出分支.
       3 else
       4    take-some-action
       5 fi
       在一个2元命令中提供一个占位符,具体见Example 8-2,和"默认参数".如:
       1 : ${username=`whoami`}
       2 # ${username=`whoami`}   如果没有":"的话,将给出一个错误,除非"username"是
       3 #                        个命令
       在here document中提供一个占位符,见Example 17-10.
       使用"参数替换"来评估字符串变量(见Example 9-14).如:
       1 : ${HOSTNAME?} ${USER?} ${MAIL?}
       2 #     如果一个或多个必要的环境变量没被设置的话,
       3 #+ 就打印错误信息.
       "变量扩展/子串替换"
       在和 > (重定向操作符)结合使用时,把一个文件截断到0长度,没有修改它的权限.
       如果文件在之前并不存在,那么就创建它.如:
       1 : > data.xxx        #文件"data.xxx"现在被清空了.
       2
       3 #与 cat /dev/null >data.xxx 的作用相同
       4 #然而,这不会产生一个新的进程,因为":"是一个内建命令.
       具体参见Example 12-14.
       在和>>重定向操作符结合使用时,将不会对想要附加的文件产生任何影响.
       如果文件不存在,将创建.
       注意: 这只适用于正规文件,而不是管道,符号连接,和某些特殊文件.
       也可能用来作为注释行,虽然我们不推荐这么做.使用#来注释的话,将关闭剩余行的
       错误检查,所以可以在注释行中写任何东西.然而,使用:的话将不会这样.如:
       1 : This is a comment thar generates an error,(if [ $x -eq 3] ).
       ":"还用来在/etc/passwd和$PATH变量中用来做分隔符.
       bash$    echo $PATH
       /usr/local/bin:/bin:/usr/X11R6/bin:/sbin:/usr/sbin:/usr/games
!        取反操作符,将反转"退出状态"结果,(见Example 6-2).也会反转test操作符的意义.比
       如修改=为!=.!操作是Bash的一个关键字.
       在一个不同的上下文中,!也会出现在"间接变量引用"见Example 9-22.
       在另一种上下文中,!还能反转bash的"history mechanism"(见附录J 历史命令)
       需要注意的是,在一个脚本中,"history mechanism"是被禁用的.
*        万能匹配字符,用于文件名匹配(这个东西有个专有名词叫file globbing),或者是正则
       表达式中.注意:在正则表达式匹配中的作用和在文件名匹配中的作用是不同的.
       bash$ echo *
       abs-book.sgml add-drive.sh agram.sh alias.sh
*        数学乘法.
       **是幂运算.
?        测试操作.在一个确定的表达式中,用?来测试结果.
       (())结构可以用来做数学计算或者是写c代码,那?就是c语言的3元操作符的
       一个.
       在"参数替换"中,?测试一个变量是否被set了.
?        在file globbing中和在正则表达式中一样匹配任意的单个字符.
$        变量替换
       1 var1=5
       2 var2=23skidoo
       3
       4 echo $var1     # 5
       5 echo $var2     # 23skidoo
$        在正则表达式中作为行结束符.
${}        参数替换,见9.3节.
$*,$@    位置参数
$?        退出状态变量.$?保存一个命令/一个函数或者脚本本身的退出状态.
$$        进程ID变量.这个$$变量保存运行脚本进程ID
()        命令组.如:
       1 (a=hello;echo $a)
       注意:在()中的命令列表,将作为一个子shell来运行.
       在()中的变量,由于是在子shell中,所以对于脚本剩下的部分是不可用的.
       如:
       1 a=123
       2 ( a=321; )         
       3
       4 echo "a = $a"   # a = 123
       5 # 在圆括号中a变量,更像是一个局部变量.
       用在数组初始化,如:
       1 Array=(element1,element2,element3)
{xxx,yyy,zzz...}
       大括号扩展,如:
       1 cat {file1,file2,file3} > combined_file
       2 # 把file1,file2,file3连接在一起,并且重定向到combined_file中.
       3
       4
       5 cp file22.{txt,backup}
       6 # 拷贝"file22.txt" 到"file22.backup"中
       一个命令可能会对大括号中的以逗号分割的文件列表起作用[1]. file globbing将对
       大括号中的文件名作扩展.
       注意: 在大括号中,不允许有空白,除非这个空白是有意义的.
       echo {file1,file2}\ :{\ A," B",' C'}
       file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C
{}        代码块.又被称为内部组.事实上,这个结构创建了一个匿名的函数.但是与函数不同的
       是,在其中声明的变量,对于脚本其他部分的代码来说还是可见的.如:
       bash$
       {
           local a;
           a= 123;
       }
       bash中的local申请的变量只能够用在函数中.
       1 a=123
       2 { a=321; }
       3 echo "a = $a"   # a = 321   (说明在代码块中对变量a所作的修改,影响了外边的变量a)
       4
       5 # Thanks, S.C.
       下边的代码展示了在{}结构中代码的I/O重定向.
Example 3-1. 代码块和I/O重定向
################################Start Script#######################################
1 #!/bin/bash
2 # 从 /etc/fstab中读行
3
4 File=/etc/fstab
5
6 {
7 read line1
8 read line2
9 }  "$1.test"       # 把代码块中的所有输出都重定向到文件中
35
36 echo "Results of rpm test in file $1.test"
37
38 # 查看rpm的man页来查看rpm的选项
39
40 exit 0
################################End Script#########################################
       注意: 与()中的命令不同的是,{}中的代码块将不能正常地开启一个新shell.[2]
{} \;    路径名.一般都在find命令中使用.这不是一个shell内建命令.
       注意: ";"用来结束find命令序列的-exec选项.
[]        test.
       test的表达式将在[]中.
       值得注意的是[是shell内建test命令的一部分,并不是/usr/bin/test中的扩展命令
       的一个连接.
[[]]    test.
       test表达式放在[[]]中.(shell关键字)
       具体查看[[]]结构的讨论.
[]        数组元素
       Array[1]=slot_1
       echo ${Array[1]}
[]        字符范围
       在正则表达式中使用,作为字符匹配的一个范围
(())    数学计算的扩展
       在(())结构中可以使用一些数字计算.
       具体参阅((...))结构.
>&>>&>>filename 重定向脚本的输出到文件中.覆盖文件原有内容.
       command &>filename 重定向stdout和stderr到文件中
       command >&2    重定向command的stdout到stderr
       scriptname >>filename 重定向脚本的输出到文件中.添加到文件尾端,如果没有文件,
       则创建这个文件.
       进程替换,具体见"进程替换部分",跟命令替换极其类似.
       (command)>
        可用来做字符串比较
        可用在数学计算比较
        ASCII比较
        1 veg1=carrots
        2 veg2=tomatoes
        3
        4 if [[ "$veg1"     正则表达式中的单词边界.如:
       bash$grep '\' textfile
   
|        管道.分析前边命令的输出,并将输出作为后边命令的输入.这是一种产生命令链的
       好方法.
       1 echo ls -l | sh
       2 #  传递"echo ls -l"的输出到shell中,
       3 #+ 与一个简单的"ls -l"结果相同.
       4
       5
       6 cat *.lst | sort | uniq
       7 # 合并和排序所有的".lst"文件,然后删除所有重复的行.
      
       管道是进程间通讯的一个典型办法,将一个进程的stdout放到另一个进程的stdin中.
       标准的方法是将一个一般命令的输出,比如cat或echo,传递到一个过滤命令中(在这个
       过滤命令中将处理输入),得到结果,如:
       cat $filename1 | $filename2 | grep $search_word
       当然输出的命令也可以传递到脚本中.如:
################################Start Script#######################################
1 #!/bin/bash
2 # uppercase.sh : 修改输出,全部转换为大写
3
4 tr 'a-z' 'A-Z'
5 #  字符范围必须被""引用起来
6 #+ 来阻止产生单字符的文件名.
7
8 exit 0
################################End Script#########################################
      
       现在让我们输送ls -l的输出到一个脚本中.
       bash$ ls -l | ./uppercase.sh
       -RW-RW-R--    1 BOZO  BOZO       109 APR  7 19:49 1.TXT
       -RW-RW-R--    1 BOZO  BOZO       109 APR 14 16:48 2.TXT
       -RW-R--R--    1 BOZO  BOZO       725 APR 20 20:56 DATA-FILE
       注意:管道中的一个进程的stdout必须被下一个进程作为stdin读入.否则,数据流会阻
       塞,并且管道将产生非预期的行为.
       如:
       1 cat file1 file2 | ls -l | sort
       2 #从"cat file1 file2"中的输出并没出现
       作为子进程的运行的管道,不能够改变脚本的变量.
       1 variable="initial_value"
       2 echo "new_value" | read variable
       3 echo "variable = $variable"            #variable = initial_value
       如果管道中的某个命令产生了一个异常,并中途失败,那么这个管道将过早的终止.
       这种行为被叫做a broken pipe,并且这种状态下将发送一个SIGPIPE信号.
>|        强制重定向(即使设置了noclobber选项--就是-C选项).这将强制的覆盖一个现存文件.
||        或-逻辑操作.
&        后台运行命令.一个命令后边跟一个&,将表示在后台运行.
       bash$sleep 10 &
       [1] 850
       [1]+    Done            sleep 10
       在一个脚本中,命令和循环都可能运行在后台.
Example 3-3. 在后台运行一个循环
################################Start Script#######################################
1 #!/bin/bash
2 #background-loop.sh
3
4 for i in 1 2 3 4 5 6 7 8 9 10                #第一个循环
5 do
6    echo -n "$i"
7 done&                        #在后台运行这个循环
8                                #在第2个循环之后,将在某些时候执行.
9
10 echo    #这个'echo'某些时候将不会显示.
11
12 for i in 11 12 13 14 15 16 17 18 19 20        #第二个循环
13 do
14     echo -n "$i"
15 done
16
17 echo    #这个'echo'某些时候将不会显示.
18
19 #--------------------------------------------------------
20
21 #期望的输出应该是
22 #1 2 3 4 5 6 7 8 9 10
23 #11 12 13 14 15 16 17 18 19 20
24
25 #然而实际的结果有可能是
26 #11 12 13 14 15 16 17 18 19 20
27 #1 2 3 4 5 6 7 8 9 10 bozo $
28 #(第2个'echo'没执行,为什么?)
29
30 #也可能是
31 #1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
32 #(第1个'echo'没执行,为什么?)
33
34 #非常少见的执行结果,也有可能是:
35 #11 12 13 1 2 3 4 5 6 7 8 9 10 14 15 16 17 18 19 20
36 #前台的循环先于后台的执行
37
38 exit 0
39
40 #    Nasimuddin Ansari 建议加一句 sleep 1
41 #+    在 6行和14行的 echo -n "$i"之后加
42 #+    将看到一些乐趣
################################End Script#########################################
       注意:在一个脚本内后台运行一个命令,有可能造成这个脚本的挂起,等待一个按键
           响应.幸运的是,我们可以在Example 11-24附近,看到这个问题的解决办法.
&&        与-逻辑操作.
-        选项,前缀.在所有的命令内如果想使用选项参数的话,前边都要加上"-".
       COMMAND -[Option1][Option2][...]
       ls -al
       sort -dfu $filename
       set -- $variable
        1 if [ $file1 -ot $file2 ]
        2 then
        3   echo "File $file1 is older than $file2."
        4 fi
        5
        6 if [ "$a" -eq "$b" ]
        7 then
        8   echo "$a is equal to $b."
        9 fi
       10
       11 if [ "$c" -eq 24 -a "$d" -eq 47 ]
       12 then
       13   echo "$c equals 24 and $d equals 47."
       14 fi
-        用于重定向 stdin 或 stdout.
################################Start Script#######################################
1 (cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)
2 # 从一个目录移动整个目录树到另一个目录
3 # [courtesy Alan Cox , with a minor change]
4
5 # 1) cd /source/directory    源目录
6 # 2) &&                      与操作,如果cd命令成功了,那么就执行下边的命令
7 # 3) tar cf - .              'c'创建一个新文档,'f'后边跟'-'指定目标文件作为stdout
8 #                            '-'后边的'f'(file)选项,指明作为stdout的目标文件.
9 #                            并且在当前目录('.')执行.
10 # 4) |                       管道...
11 # 5) ( ... )                 一个子shell
12 # 6) cd /dest/directory      改变当前目录到目标目录.
13 # 7) &&                      与操作,同上.
14 # 8) tar xpvf -              'x'解档,'p'保证所有权和文件属性,
15 #                            'v'发完整消息到stdout
16 #                            'f'后边跟'-',从stdin读取数据
17 #
18 #                            注意:'x' 是一个命令, 'p', 'v', 'f' 是选项.
19 # Whew!
20
21
22
23 # 更优雅的写法应该是
24 #   cd source/directory
25 #   tar cf - . | (cd ../dest/directory; tar xpvf -)
26 #
27 #     当然也可以这么写:
28 # cp -a /source/directory/* /dest/directory
29 #     或者:
30 # cp -a /source/directory/* /source/directory/.[^.]* /dest/directory
31 #     如果在/source/directory中有隐藏文件的话.
################################End Script#########################################
################################Start Script#######################################
1 bunzip2 linux-2.6.13.tar.bz2 | tar xvf -
2 # --未解压的tar文件--    | --然后把它传递到"tar"中--
3 # 如果 "tar" 没能够正常的处理"bunzip2",
4 # 这就需要使用管道来执行2个单独的步骤来完成它.
5 # 这个练习的目的是解档"bzipped"的kernel源文件.
################################End Script#########################################
       注意:在上边这个例子中'-'不太象是bash的操作符,而更像是tar的参数.
       bash$echo "whatever" | cat -
       whatever
       在需要一个文件名的地方,-重定向输出到stdout(如在tar和cf命令中),或者从
       stdin中接受输入,而不是从一个文件中接受输入.这是在管道中作为一个过滤
       器,来使用文件定位工具的一种办法.
       bash$file
       用法: file [-bciknvzl] [-f namefile] [-m magicfiles] file...
       上边这个例子file将会出错,提示你如何使用file命令.
       添加一个"-"将得到一个更有用的结果.这将使得shell等待用户输入.
       bash$file -
       abc
       standard input:                    ASCII text
       bash$file -
       #!/bin/bash
       standard input:                    Bourn-Again shell script tesxt executable
       现在命令从stdin中接受了输入,并分析它.
       "-"常用于管道后边的命令,具体参看33.7节,来看使用技巧.
       使用diff命令来和另一个文件的一部分进行比较.
       grep Linux file1 | diff file2 -
       最后,一个真实世界的使用tar命令的例子.
Example 3-4. 备份最后一天所有修改的文件.
################################Start Script#######################################
1 #!/bin/bash
2
3 #  在一个"tarball"中(经过tar和gzip处理过的文件)
4 #+ 备份最后24小时当前目录下d所有修改的文件.
5
6 BACKUPFILE=backup-$(date +%m-%d-%Y)
7 #                 在备份文件中嵌入时间.
8 #                 Thanks, Joshua Tschida, for the idea.
9 archive=${1:-$BACKUPFILE}
10 #  如果在命令行中没有指定备份文件的文件名,
11 #+ 那么将默认使用"backup-MM-DD-YYYY.tar.gz".
12
13 tar cvf - `find . -mtime -1 -type f -print` > $archive.tar
14 gzip $archive.tar
15 echo "Directory $PWD backed up in archive file \"$archive.tar.gz\"."
16
17
18 #  Stephane Chazelas指出上边代码,
19 #+ 如果在发现太多的文件的时候,或者是如果文件
20 #+ 名包括空格的时候,将执行失败.
21
22 # Stephane Chazelas建议使用下边的两种代码之一
23 # -------------------------------------------------------------------
24 #   find . -mtime -1 -type f -print0 | xargs -0 tar rvf "$archive.tar"
25 #      使用gnu版本的find.
26
27
28 #   find . -mtime -1 -type f -exec tar rvf "$archive.tar" '{}' \;
29 #         对于其他风格的UNIX便于移植,但是比较慢.
30 # -------------------------------------------------------------------
31
32
33 exit 0
################################End Script#########################################
       注意:以"-"开头的文件名在使用"-"作为重定向操作符的时候,可能会产生问题.
       应该写一个脚本来检查这个问题,并给这个文件加上合适的前缀.如:
       ./-FILENAME, $PWD/-FILENAME,或$PATHNAME/-FILENAME.
       如果变量的值以"-"开头,可能也会引起问题.
       1 var="-n"
       2 echo $var
       3 #具有"echo -n"的效果了,这样什么都不会输出的.
-        之前工作的目录."cd -"将回到之前的工作目录,具体请参考"$OLDPWD"环境变量.
       注意:一定要和之前讨论的重定向功能分开,但是只能依赖上下文区分.
-        算术减号.
=        算术等号,有时也用来比较字符串.
       1 a=28
       2 echo $a   # 28
+        算术加号,也用在正则表达式中.
+        选项,对于特定的命令来说使用"+"来打开特定的选项,用"-"来关闭特定的选项.
%        算术取模运算.也用在正则表达式中.
~        home目录.相当于$HOME变量.~bozo是bozo的home目录,并且ls ~bozo将列出其中的
       内容. ~/就是当前用户的home目录,并且ls ~/将列出其中的内容,如:
       bash$ echo ~bozo
       /home/bozo
       bash$ echo ~
       /home/bozo
       bash$ echo ~/
       /home/bozo/
       bash$ echo ~:
       /home/bozo:
       bash$ echo ~nonexistent-user
       ~nonexistent-user
~+        当前工作目录,相当于$PWD变量.
~-        之前的工作目录,相当于$OLDPWD内部变量.
=~        用于正则表达式,这个操作将在正则表达式匹配部分讲解,只有version3才支持.
^        行首,正则表达式中表示行首."^"定位到行首.
控制字符
       修改终端或文本显示的行为.控制字符以CONTROL + key组合.
       控制字符在脚本中不能正常使用.
       Ctl-B        光标后退,这应该依赖于bash输入的风格,默认是emacs风格的.
       Ctl-C        Break,终止前台工作.
       Ctl-D        从当前shell登出(和exit很像)
                   "EOF"(文件结束符).这也能从stdin中终止输入.
                   在console或者在xterm window中输入的时候,Ctl-D将删除光标下字符.
                   当没有字符时,Ctrl-D将退出当前会话.在xterm window也有关闭窗口
                   的效果.
       Ctl-G        beep.在一些老的终端,将响铃.
       Ctl-H        backspace,删除光标前边的字符.如:
                    1 #!/bin/bash
                    2 # 在一个变量中插入Ctl-H
                    3
                    4 a="^H^H"                  # 两个 Ctl-H (backspaces).
                    5 echo "abcdef"             # abcdef
                    6 echo -n "abcdef$a "       # abcd f
                    7 # 注意结尾的空格 ^              ^ 两个 twice.
                    8 echo -n "abcdef$a"        # abcdef
                    9 #  结尾没有空格             没有 backspace 的效果了(why?).
                   10                           # 结果并不像期望的那样
                   11 echo; echo
       Ctl-I        就是tab键.
       Ctl-J        新行.
       Ctl-K        垂直tab.(垂直tab?新颖,没听过)
                   作用就是删除光标到行尾的字符.
       Ctl-L        clear,清屏.
       Ctl-M        回车
################################Start Script#######################################
1 #!/bin/bash
2 # Thank you, Lee Maschmeyer, for this example.
3
4 read -n 1 -s -p $'Control-M leaves cursor at beginning of this line. Press Enter. \x0d'
5                                   #当然,'0d'就是二进制的回车.
6 echo >&2   #  '-s'参数使得任何输入都不将回显出来
7            #+ 所以,明确的重起一行是必要的.
8
9 read -n 1 -s -p $'Control-J leaves cursor on next line. \x0a'
10 echo >&2   #  Control-J 是换行.
11
12 ###
13
14 read -n 1 -s -p $'And Control-K\x0bgoes straight down.'
15 echo >&2   #  Control-K 是垂直制表符.
16
17 # 关于垂直制表符效果的一个更好的例子见下边:
18
19 var=$'\x0aThis is the bottom line\x0bThis is the top line\x0a'
20 echo "$var"
21 #  这句与上边的例子使用的是同样的办法,然而:
22 echo "$var" | col
23 #  这将造成垂直制表符右边的部分在左边部分的上边.
24 #  这也解释了为什么我们要在行首和行尾加上一个换行符--
25 #+ 来避免一个混乱的屏幕输出.
26
27 # Lee Maschmeyer的解释:
28 # ---------------------
29 #  In the [first vertical tab example] . . . the vertical tab
29 #  在这里[第一个垂直制表符的例子中] . . . 这个垂直制表符
30 #+ makes the printing go straight down without a carriage return.
31 #  This is true only on devices, such as the Linux console,
32 #+ that can't go "backward."
33 #  The real purpose of VT is to go straight UP, not down.
34 #  It can be used to print superscripts on a printer.
34 #  它可以用来在一个打印机上打印上标.
35 #  col的作用,可以用来模仿VT的合适的行为.
36
37 exit 0
################################End Script#########################################
       Ctl-Q        继续(等价于XON字符),这个继续的标准输入在一个终端里
       Ctl-S        挂起(等价于XOFF字符),这个被挂起的stdin在一个终端里,用Ctl-Q恢复
       Ctl-U        删除光标到行首的所有字符,在某些设置下,删除全行.
       Ctl-V        当输入字符时,Ctl-V允许插入控制字符.比如,下边2个例子是等价的
                   echo -e '\x0a'
                   echo
                   Ctl-V在文本编辑器中十分有用,在vim中一样.
       Ctl-W        删除当前光标到前边的最近一个空格之间的字符.
                   在某些设置下,删除到第一个非字母或数字的字符.
       Ctl-Z        终止前台工作.
      
空白部分
       分割命令或者是变量.包括空格,tab,空行,或任何它们的组合.
       在一些特殊情况下,空白是不允许的,如变量赋值时,会引起语法错误.
       空白行在脚本中没有效果.
       "$IFS",对于某些命令输入的特殊变量分割域,默认使用的是空白.
       如果想保留空白,使用引用.
注意事项:
[1]        shell做大括号的命令扩展.但是命令本身需要对扩展的结果作处理.
[2]        例外:在pipe中的一个大括号中的代码段可能运行在一个子shell中.
       1 ls | { read firstline; read secondline; }
       2 #  错误,在打括号中的代码段,将运行到子shell中.
       3 #+ 所以ls的输出将不能传递到代码块中.
       4 echo "First line is $firstline; second line is $secondline"  # 不能工作
       5
       6 # Thanks, S.C.
[3]        换行符也被认为是空白.这也解释了为什么一个空行也会被认为是空白.
第4章 变量和参数的介绍
======================
4.1 变量替换
------------
$        变量替换操作符
       只有在变量被声明,赋值,unset或exported或者是在变量代表一个signal的时候,
       变量才会是以本来的面目出现在脚本里.变量在被赋值的时候,可能需要使用"=",
       read状态或者是在循环的头部.
       在""中还是会发生变量替换,这被叫做部分引用,或叫弱引用.而在''中就不会发生变
       量替换,这叫做全引用,也叫强引用.具体见第5章的讨论.
       注意:$var与${var}的区别,不加{},在某些上下文将引起错误,为了安全,使用2.
           具体见9.3节 参数替换.
Example 4-1. 变量赋值和替换
################################Start Script#######################################
1 #!/bin/bash
2
3 # 变量赋值和替换
4
5 a=375
6 hello=$a
7
8 #-------------------------------------------------------------------------
9 # 强烈注意,在赋值的前后一定不要有空格.
10 # 如果有空格会发生什么?
11
12 #  如果"VARIABLE =value",
13 #              ^
14 #+ 脚本将尝试运行一个"VARIABLE"的命令,带着一个"=value"参数.
15
16 #  如果"VARIABLE= value",
17 #               ^
18 #+ script tries to run "value" command with
18 #+ 脚本将尝试运行一个"value"的命令,带着
19 #+ the environmental variable "VARIABLE" set to "".
19 #+ 一个被赋成""值的环境变量"VARIABLE".
20 #-------------------------------------------------------------------------
21
22
23 echo hello    # 没有变量引用,不过是个hello字符串
24
25 echo $hello
26 echo ${hello} # 同上
27
28 echo "$hello"
29 echo "${hello}"
30
31 echo
32
33 hello="A B  C   D"
34 echo $hello   # A B C D
35 echo "$hello" # A B  C   D
36 # 就象你看到的echo $hello   和    echo "$hello"   将给出不同的结果.
37 #                                      ^      ^
38 # Quoting a variable preserves whitespace.
38 # 引用一个变量将保留其中的空白,当然,如果是变量替换就不会保留了.
39
40 echo
41
42 echo '$hello'  # $hello
43 #    ^      ^
44 # 全引用的作用
45 #+ 将导致"$"变成一个单独的字符.
46
47 # 注意两种引用不同的效果
48
49
50 hello=    # 设置为空值
51 echo "\$hello (null value) = $hello"
52 #  注意设置一个变量为空,与unset它,不是一回事,虽然看起来一样
53 #
54
55 # --------------------------------------------------------------
56
57 #  可以在同一行上设置多个变量.
58 #+ 要以空白分隔
59 #  小心,这会降低可读性,和可移植性.
60
61 var1=21  var2=22  var3=$V3
62 echo
63 echo "var1=$var1   var2=$var2   var3=$var3"
64
65 # 在老版本的"sh"上,可能会有问题.
66
67 # --------------------------------------------------------------
68
69 echo; echo
70
71 numbers="one two three"
72 #           ^   ^
73 other_numbers="1 2 3"
74 #               ^ ^
75 #  如果变量中有空白,那么引用就必要了.
76 #
77 echo "numbers = $numbers"
78 echo "other_numbers = $other_numbers"   # other_numbers = 1 2 3
79 echo
80
81 echo "uninitialized_variable = $uninitialized_variable"
82 # Uninitialized变量为空值(根本就没赋值).
83 uninitialized_variable=   #  声明,但是没被初始化
84                           #+ 其实和前边设置为空值得作用是一样的.
85 echo "uninitialized_variable = $uninitialized_variable"
86                           # 还是一个空值
87
88 uninitialized_variable=23       # 赋值
89 unset uninitialized_variable    # Unset it.
90 echo "uninitialized_variable = $uninitialized_variable"
91                                 # 还是空值
92 echo
93
94 exit 0
################################End Script#########################################
注意: 一个空值变量,或者是根本就没声明的变量,在赋值之前使用它可能会引起问题.
       但是还是可以用来做算术运算
################################Start Script#######################################
1 echo "$uninitialized"                                # (blank line)
2 let "uninitialized += 5"                             # Add 5 to it.
3 echo "$uninitialized"                                # 5
4
5 #  结论:
6 #  对于一个空值变量在做算术操作的时候,就好像它的值为0一样.
8 #  This is undocumented (and probably non-portable) behavior.
7 #  这并没被文档化(可能是不可移植)的行为.
################################End Script#########################################
具体参考    Example 11-21
4.2 变量赋值
------------
=        赋值操作符(前后都不能有空白)
       不要与-eq混淆,那个是test,并不是赋值.
       注意,=也可被用来做test操作,这依赖于上下文.
Example 4-2. 一般的变量赋值
################################Start Script#######################################
1 #!/bin/bash
2 # "裸体"变量
3
4 echo
5
6 # 变量什么时候是"裸体"的,比如前边少了$的时候.
7 # 当它被赋值的时候,而不是被引用的时候.
8
9 # 赋值
10 a=879
11 echo "The value of \"a\" is $a."
12
13 # 使用let赋值
14 let a=16+5
15 echo "The value of \"a\" is now $a."
16
17 echo
18
19 # 在for循环中
20 echo -n "Values of \"a\" in the loop are: "
21 for a in 7 8 9 11
22 do
23   echo -n "$a "
24 done
25
26 echo
27 echo
28
29 # 在read命令状态中
30 echo -n "Enter \"a\" "
31 read a
32 echo "The value of \"a\" is now $a."
33
34 echo
35
36 exit 0
################################End Script#########################################
Example 4-3. 变量赋值,一般的和比较特殊的
################################Start Script#######################################
1 #!/bin/bash
2
3 a=23              # Simple case
4 echo $a
5 b=$a
6 echo $b
7
8 # 现在让我们来点小变化
9
10 a=`echo Hello!`   # 把echo命令的结果传给变量a
11 echo $a
12 #  注意,如果在命令扩展结构中使用一个(!)的话,在命令行中将不能工作
13 #+ 因为这触发了Bash的"历史机制".
14 #  但是,在校本里边使用的话,历史功能是被关闭的,所以就能够正常运行.
15
16
17 a=`ls -l`         # 把ls -l的结果给a
18 echo $a           # 别忘了,这么引用的话,ls的结果中的所有空白部分都没了(包括换行)
19 echo
20 echo "$a"         # 这么引用就正常了,保留了空白
21                   # (具体参阅章节"引用")
22
23 exit 0
################################End Script#########################################
使用$(...)机制进行的变量赋值(除去使用``来赋值的另外一种新方法).事实上这两种方法都是
命令替换的一种形式.
# 来自于/ect/rc.d/rc.local
R=$(cat /ect/redhat-release)
arch=$(uname -m)
4.3 Bash变量是不分类型的
------------------------
不像其他程序语言一样,Bash并不对变量区分"类型".本质上,Bash变量都是字符串.
但是依赖于上下文,Bash也允许比较操作和算术操作.决定这些的关键因素就是,变量中的值
是否只有数字.
Example 4-4 整型还是string?
################################Start Script#######################################
1 #!/bin/bash
2 # int-or-string.sh: 整形还是string?
3
4 a=2334                   # 整型
5 let "a += 1"
6 echo "a = $a "           # a = 2335
7 echo                     # 还是整型
8
9
10 b=${a/23/BB}             # 将23替换成BB
11                          # 这将把b变量从整型变为string
12 echo "b = $b"            # b = BB35
13 declare -i b             # 即使使用declare命令也不会对此有任何帮助,9.4节有解释
14 echo "b = $b"            # b = BB35
15
16 let "b += 1"             # BB35 + 1 =
17 echo "b = $b"            # b = 1
18 echo
19
20 c=BB34
21 echo "c = $c"            # c = BB34
22 d=${c/BB/23}             # S将BB替换成23
23                          # 这使得$d变为一个整形
24 echo "d = $d"            # d = 2334
25 let "d += 1"             # 2334 + 1 =
26 echo "d = $d"            # d = 2335
27 echo
28
29 # 关于空变量怎么样?
30 e=""
31 echo "e = $e"            # e =
32 let "e += 1"             # 算术操作允许一个空变量?
33 echo "e = $e"            # e = 1
34 echo                     # 空变量将转换成一个整型变量
35
36 # 关于未声明的变量怎么样?
37 echo "f = $f"            # f =
38 let "f += 1"             # 算术操作允许么?
39 echo "f = $f"            # f = 1
40 echo                     # 未声明的变量将转换成一个整型变量
41
42
43
44 # 所以说Bash中的变量都是无类型的.
45
46 exit 0
################################End Script#########################################
4.4 特殊的变量类型
------------------
local variables
       这种变量只有在代码块或者是函数中才可见(具体见23.2和23章)
environmental variables
       这种变量将改变用户接口和shell的行为.
       在一般的上下文中,每个进程都有自己的环境,就是一组保持进程可能引用的信息的
       变量.这种情况下,shell于一个一般进程是相同的.
       每次当shell启动时,它都将创建自己的环境变量.更新或者添加新的环境变量,将导
       致shell更新它的环境,同时也会影响所有继承自这个环境的所有子进程(由这个命令
       导致的).
       注意:分配给环境变量的空间是受限的.创建太多的环境变量将引起空间溢出,这会引
       起问题.
       关于eval命令,具体见第11章
       bash$ eval "`seq 10000 | sed -e 's/.*/export var&=ZZZZZZZZZZZZZZ/'`"
       bash$ du
       bash: /usr/bin/du: Argument list too long
       如果一个脚本设置了环境变量,需要export它,来通知本脚本的环境,这是export
       命令的功能,关于export命令,具体见11章.
      
       脚本只能对它产生的子进程export变量.一个从命令行被调用的脚本export的变量,将
       不能影响调用这个脚本的那个命令行shell的环境.
positional parameters
       就是从命令行中传进来的参数,$0, $1, $2, $3...
       $0就是脚本文件的名字,$1是第一个参数,$2为第2个...,参见[1](有$0的说明),$9
       以后就需要打括号了,如${10},${11},${12}...
       两个值得注意的变量$*和$@(第9章有具体的描述),表示所有的位置参数.
Example 4-5 位置参数
################################Start Script#######################################
1 #!/bin/bash
2
3 # 作为用例,调用这个脚本至少需要10个参数,如
4 # ./scriptname 1 2 3 4 5 6 7 8 9 10
5 MINPARAMS=10
6
7 echo
8
9 echo "The name of this script is \"$0\"."
10 # 添加./是为了当前目录
11 echo "The name of this script is \"`basename $0`\"."
12 # 去掉目录信息,具体见'basename'命令
13
14 echo
15
16 if [ -n "$1" ]              # 测试变量被被引用
17 then
18  echo "Parameter #1 is $1"  # "#"没被转义
19 fi
20
21 if [ -n "$2" ]
22 then
23  echo "Parameter #2 is $2"
24 fi
25
26 if [ -n "$3" ]
27 then
28  echo "Parameter #3 is $3"
29 fi
30
31 # ...
32
33
34 if [ -n "${10}" ]  # 大于9的参数必须出现在{}中.
35 then
36  echo "Parameter #10 is ${10}"
37 fi
38
39 echo "-----------------------------------"
40 echo "All the command-line parameters are: "$*""
41
42 if [ $# -lt "$MINPARAMS" ]        #$#是传到脚本里的位置参数的个数
43 then
44   echo
45   echo "This script needs at least $MINPARAMS command-line arguments!"
46 fi  
47
48 echo
49
50 exit 0
################################End Script#########################################
       {}标记法是一种很好的使用位置参数的方法.这也需要间接引用(见Example 34-2)
       1 args=$#           # 位置参数的个数
       2 lastarg=${!args}
       3 # 或:       lastarg=${!#}
       4 # 注意 lastarg=${!$#} 将报错
       一些脚本可能会依赖于使用不同的调用名字,而表现出不同的行为,这样一般都需要
       判断$0,而其他的名字都是通过ln命令产生的链接.(具体参见Example 12-2)
       如果脚本需要一个命令行参数,而调用的时候,没用这个参数,这就有可能造成分配一个
       空变量,这样估计就会引起问题.一种解决办法就是在这个位置参数,和相关的变量后
       边,都添加一个额外的字符.具体见下边的例子.
################################Start Script#######################################
1 variable1_=$1_  # 而不是 variable1=$1
2 # 这将阻止一个错误,即使在调用时没使用这个位置参数.
3
4 critical_argument01=$variable1_
5
6 # 这个扩展的字符是可以被消除掉的,就像这样.
7 variable1=${variable1_/_/}
8 # 副作用就是$variable1_多了一个下划线
9 # 这里使用了一个参数替换模版(后边会有具体的讨论)
10 # (Leaving out the replacement pattern results in a deletion.)
10 # (在一个删除动作中,节省了一个替换模式)
11
12
13 # 一个解决这种问题的更简单的做法就是,判断一下这个位置参数是否传递下来了
14 if [ -z $1 ]
15 then
16   exit $E_MISSING_POS_PARAM
17 fi
18
19
20 #  但是上边的方法将可能产生一个意外的副作用
21 #  参数替换的更好的办法应该是:
22 #         ${1:-$DefaultVal}
23 #  具体察看"Parameter Substition"节
24 #+ 在第9章
################################End Script#########################################
      
Example 4-6 wh,whois节点名字查询
################################Start Script#######################################
1 #!/bin/bash
2 # ex18.sh
3
4 # Does a 'whois domain-name' lookup on any of 3 alternate servers:
5 #                    ripe.net, cw.net, radb.net
6
7 # 把这个脚本重命名为'wh',然后放到/usr/local/bin下
8
9 # 需要3个符号链接
10 # ln -s /usr/local/bin/wh /usr/local/bin/wh-ripe
11 # ln -s /usr/local/bin/wh /usr/local/bin/wh-cw
12 # ln -s /usr/local/bin/wh /usr/local/bin/wh-radb
13
14 E_NOARGS=65
15
16
17 if [ -z "$1" ]
18 then
19   echo "Usage: `basename $0` [domain-name]"
20   exit $E_NOARGS
21 fi
22
23 # Check script name and call proper server.
23 # 检查脚本名字,然后调用合适的服务器
24 case `basename $0` in    # Or:    case ${0##*/} in
25     "wh"     ) whois $1@whois.ripe.net;;
26     "wh-ripe") whois $1@whois.ripe.net;;
27     "wh-radb") whois $1@whois.radb.net;;
28     "wh-cw"  ) whois $1@whois.cw.net;;
29     *        ) echo "Usage: `basename $0` [domain-name]";;
30 esac
31
32 exit $?
################################End Script#########################################
shift        shift命令重新分配位置参数,其实就是向左移动一个位置.
   $1 21 \z                     
22 EOF                   # \z
23
24 cat 25 \\z                     
26 EOF                   # \z
################################End Script#########################################
   分配给变量的字符串的元素也会被转义,但是只把一个转义符分配给变量将会报错.
################################Start Script#######################################
1 variable=\
2 echo "$variable"
3 # Will not work - gives an error message:
3 # 将不能正常工作- 将给出一个错误消息:
4 # test.sh: : command not found
5 # 一个"裸体的" 转义符将不能够安全的分配给变量.
6 #
7 #  What actually happens here is that the "\" escapes the newline and
7 #  这里其实真正发生的是variable=\,这句被shell认为是没有完成,\被认为是一个续行符
8 #+ 这样,下边的这句echo,也被认为是上一行的补充.所以,总的来说就是一个非法变量分配
9
10 variable=\
11 23skidoo
12 echo "$variable"        #  23skidoo
13                         #  这句就可以使用,因为这是一个合法的变量分配
14
15 variable=\
16 #        \^   转义一个空格
17 echo "$variable"        # 显示空格
18
19 variable=\\
20 echo "$variable"        # \
21
22 variable=\\\
23 echo "$variable"
24 # 不能正常工作,给出一个错误
25 # test.sh: \: command not found
26 #
27 #  第一个转义符把第2个\转义了,但是第3个又变成"裸体的"了,
28 #+ 与上边的例子的原因相同
29
30 variable=\\\\
31 echo "$variable"        # \\
32                         # 转了两个\
33                         # 没问题
################################End Script#########################################
转义一个空格,在命令行参数列表中将会阻止单词分隔问题.
################################Start Script#######################################
1 file_list="/bin/cat /bin/gzip /bin/more /usr/bin/less /usr/bin/emacs-20.7"
2 # 列出的文件都作为命令的参数.
3
4 # Add two files to the list, and list all.
4 # 加2个文件到list中,并且列出全部.
5 ls -l /usr/X11R6/bin/xsetroot /sbin/dump $file_list
6
7 echo "-------------------------------------------------------------------------"
8
9 # 如果我们转义2个空格,会发生什么?
10 ls -l /usr/X11R6/bin/xsetroot\ /sbin/dump\ $file_list
11 # 错误: 因为前3个路径名被合并成一个参数传给了'ls -l'
12 #        因为2个转义符阻止了参数(单词)分离
################################End Script#########################################
转义符也提供续行功能.一般,每一行都包含一个不同的命令,但如果在行尾加上\,那就会接受
新行的输入,作为这一行的补充.
1 (cd /source/directory && tar cf - . ) | \
2 (cd /dest/directory && tar xpvf -)
3 # 重复了 Alan Cox的目录树拷贝命令
4 # 为了增加可读性分成2行.
5
6 # 也可以使用如下方式:
7 tar cf - -C /source/directory . |
8 tar xpvf - -C /dest/directory
9 # 察看下边的注意事项
注意:如果一个脚本以|(管道字符)结束.那么一个\(转义符),就不用非加上不可了.
   但是一个好的shell脚本编写风格,还是应该在行尾加上\,以增加可读性.
################################Start Script#######################################
1 echo "foo
2 bar"
3 #foo
4 #bar
5
6 echo
7
8 echo 'foo
9 bar'    # 没区别
10 #foo
11 #bar
12
13 echo
14
15 echo foo\
16 bar     # 续行
17 #foobar
18
19 echo
20
21 echo "foo\
22 bar"     # 与上边一样,\还是作为续行符
23 #foobar
24
25 echo
26
27 echo 'foo\
28 bar'     # 由于是强引用,所以\没被解释成续行符
29 #foo\
30 #bar
################################End Script#########################################
第6章 退出和退出状态
====================
exit命令被用来结束脚本,就像C语言一样.他也会返回一个值来传给父进程,父进程会判断是否
可用.
每个命令都会返回一个exit状态(有时候也叫return状态).成功返回0,如果返回一个非0值,通
常情况下都会被认为是一个错误码.一个编写良好的UNIX命令,程序,和工具都会返回一个0作为
退出码来表示成功,虽然偶尔也会有例外.
同样的,脚本中的函数和脚本本身都会返回退出状态.在脚本或者是脚本函数中执行的最后的命
令会决定退出状态.在脚本中,exit nnn命令将会把nnn退出码传递给shell(nnn必须是10进制数
0-255).
当一个脚本以不带参数exit来结束时,脚本的退出状态就由脚本中最后执行命令来决定.
1 #!/bin/bash
2
3 COMMAND_1
4
5 . . .
6
7 # 将以最后的命令来决定退出状态
8 COMMAND_LAST
9
10 exit $?
1 #!/bin/bash
2
3 COMMAND1
4
5 . . .
6
7 # 将以最后的命令来决定退出状态
8 COMMAND_LAST
$?读取最后执行命令的退出码.函数返回后,$?给出函数最后执行的那条命令的退出码.这种给
函数返回值的方法是Bash的方法.对于脚本来说也一样.总之,一般情况下,0为成功,非0失败W.
Example 6-1 exit/exit状态
################################Start Script#######################################
1 #!/bin/bash
2
3 echo hello
4 echo $?    # 返回0,因为执行成功
5
6 lskdf      # 不认识的命令.
7 echo $?    # 返回非0值,因为失败了.
8
9 echo
10
11 exit 113   # 将返回113给shell.
12            # To verify this, type "echo $?" after script terminates.
12            # 为了验证这个,在脚本结束的地方使用"echo $?"
################################End Script#########################################
$?对于测试脚本中的命令的结果特别有用(见Example 12-32和Example 12-17).
注意: !逻辑非操作,将会反转test命令的结果,并且这会影响exit状态.
Example 6-2 否定一个条件使用!
################################Start Script#######################################
1 true  # true是shell内建命令,什么事都不做,就是shell返回0
2 echo "exit status of \"true\" = $?"     # 0
3
4 ! true
5 echo "exit status of \"! true\" = $?"   # 1
6 # 注意:"!"需要一个空格
7 #    !true   将导致一个"command not found"错误
8 #
9 # 如果一个命令以'!'开头,那么将使用Bash的历史机制.就是显示这个命令被使用的历史.
10
11 true
12 !true
13 # 这次就没有错误了.
14 # 他不过是重复了之前的命令(true).
################################End Script#########################################
注意事项:
   特定的退出码都有预定的含义(见附录D),用户不应该在自己的脚本中指定他.
第7章 Tests
===========
每个完整的合理的编程语言都具有条件判断的功能.Bash具有test命令,不同的[]和()操作,和
if/then结构.
7.1 Test结构
------------
一个if/then结构可以测试命令的返回值是否为0(因为0表示成功),如果是的话,执行更多命令.
有一个专用命令"["(左中括号,特殊字符).这个命令与test命令等价,但是出于效率上的考虑,
它是一个内建命令.这个命令把它的参数作为比较表达式或是文件测试,并且根据比较的结果,
返回一个退出码.
在版本2.02的Bash中,推出了一个新的[[...]]扩展test命令.因为这种表现形式可能对某些语
言的程序员来说更加熟悉.注意"[["是一个关键字,并不是一个命令.
Bash把[[ $a -lt $b ]]看作一个单独的元素,并且返回一个退出码.
((...))和let...结果也能够返回一个退出码,当它们所测试的算术表达式的结果为非0的时候,
他们的退出码将返回0.这些算术扩展(见第15章)结构被用来做算术比较.
1 let "1 /dev/null  # 阻止输出.
2 then echo "Files a and b are identical."
3 else echo "Files a and b differ."
4 fi
5
6 # 非常有用的"if-grep" 结构:
7 # ------------------------
8 if grep -q Bash file
9 then echo "File contains at least one occurrence of Bash."
10 fi
11
12 word=Linux
13 letter_sequence=inu
14 if echo "$word" | grep -q "$letter_sequence"
15 # "-q"选项是用来阻止输出
16 then
17   echo "$letter_sequence found in $word"
18 else
19   echo "$letter_sequence not found in $word"
20 fi
21
22
23 if COMMAND_WHOSE_EXIT_STATUS_IS_0_UNLESS_ERROR_OCCURRED
24 then echo "Command succeeded."
25 else echo "Command failed."
26 fi
一个if/then结构可以包含多级比较和tests.
1 if echo "Next *if* is part of the comparison for the first *if*."
2
3   if [[ $comparison = "integer" ]]
4     then (( a 操作还是能够工作在一个[[]]test之中.
注意:在if后边,test命令和[]或[[]]都不是必须的.如下:
   1 dir=/home/bozo
   2
   3 if cd "$dir" 2>/dev/null; then   # "2>/dev/null" hides error message.
   4   echo "Now in $dir."
   5 else
   6   echo "Can't change to $dir."
   7 fi
if命令将返回if后边的命令的退出码.
与此相似,当在一个在使用与或列表结构的时候,test或中括号的使用,也并不一定非的有if不可
   1 var1=20
   2 var2=22
   3 [ "$var1" -ne "$var2" ] && echo "$var1 is not equal to $var2"
   4
   5 home=/home/bozo
   6 [ -d "$home" ] || echo "$home directory does not exist."
(())结构扩展并计算一个算术表达式的结果.如果表达式的结果为0,它将返回1作为退出码,或
者是"false".而一个非0表达式的结果将返回0作为退出码,或者是"true".
Example 7-3 算数测试使用(( ))
################################Start Script#######################################
1 #!/bin/bash
2 # 算数测试
3
4 # The (( ... )) construct evaluates and tests numerical expressions.
4 # (( ... ))结构计算并测试算数表达式的结果.
5 # 退出码将与[ ... ]结构相反!
6
7 (( 0 ))
8 echo "Exit status of \"(( 0 ))\" is $?."         # 1
9
10 (( 1 ))
11 echo "Exit status of \"(( 1 ))\" is $?."         # 0
12
13 (( 5 > 4 ))                                      # true
14 echo "Exit status of \"(( 5 > 4 ))\" is $?."     # 0
15
16 (( 5 > 9 ))                                      # false
17 echo "Exit status of \"(( 5 > 9 ))\" is $?."     # 1
18
19 (( 5 - 5 ))                                      # 0
20 echo "Exit status of \"(( 5 - 5 ))\" is $?."     # 1
21
22 (( 5 / 4 ))                                      # 除法也行
23 echo "Exit status of \"(( 5 / 4 ))\" is $?."     # 0
24
25 (( 1 / 2 ))                                      # 出发结果/dev/null                          # 除数为0的错误
30 #           ^^^^^^^^^^^
31 echo "Exit status of \"(( 1 / 0 ))\" is $?."     # 1
32
33 # What effect does the "2>/dev/null" have?
33 # "2>/dev/null"的作用是什么?
34 # 如果删除"2>dev/null"将会发生什么?
35 # Try removing it, then rerunning the script.
35 # 尝试删除它,然后再运行脚本.
36
37 exit 0
################################End Script#########################################
7.2 文件测试操作
----------------
返回true如果...
-e        文件存在
-a        文件存在
       这个选项的效果与-e相同.但是它已经被弃用了,并且不鼓励使用
-f        file是一个regular文件(不是目录或者设备文件)
-s        文件长度不为0
-d        文件是个目录
-b        文件是个块设备(软盘,cdrom等等)
-c        文件是个字符设备(键盘,modem,声卡等等)
-p        文件是个管道
-h        文件是个符号链接
-L        文件是个符号链接
-S        文件是个socket
-t        关联到一个终端设备的文件描述符
       这个选项一般都用来检测是否在一个给定脚本中的stdin[-t0]或[-t1]是一个终端
-r        文件具有读权限(对于用户运行这个test)
-w        文件具有写权限(对于用户运行这个test)
-x        文件具有执行权限(对于用户运行这个test)
-g        set-group-id(sgid)标志到文件或目录上
       如果一个目录具有sgid标志,那么一个被创建在这个目录里的文件,这个目录属于创建
       这个目录的用户组,并不一定与创建这个文件的用户的组相同.对于workgroup的目录
       共享来说,这非常有用.见第58页.
-u        set-user-id(suid)标志到文件上
       如果运行一个具有root权限的文件,那么运行进程将取得root权限,即使你是一个普通
       用户.[1]这对于需要存取系统硬件的执行操作(比如pppd和cdrecord)非常有用.如果
       没有suid标志的话,那么普通用户(没有root权限)将无法运行这种程序.
       见第58页.
          -rwsr-xr-t    1 root       178236 Oct  2  2000 /usr/sbin/pppd
       对于设置了suid的文件,在它的权限标志中有"s".
-k        设置粘贴位,见第65页.
       对于"sticky bit",save-text-mode标志是一个文件权限的特殊类型.如果设置了这
       个标志,那么这个文件将被保存在交换区,为了达到快速存取的目的.如果设置在目录
       中,它将限制写权限.对于设置了sticky bit位的文件或目录,权限标志中有"t".
          drwxrwxrwt    7 root         1024 May 19 21:26 tmp/
       如果一个用户并不时具有stick bit位的目录的拥有者,但是具有写权限,那么用户只
       能在这个目录下删除自己所拥有的文件.这将防止用户在一个公开的目录中不慎覆盖
       或者删除别人的文件,比如/tmp(当然root或者是目录的所有者可以随便删除或重命名
       其中的文件).
-O        你是文件的所有者.
-G        文件的group-id和你的相同.
-N        从文件最后被阅读到现在,是否被修改.
f1 -nt f2
       文件f1比f2新
f1 -ot f2
       f1比f2老
f1 -ef f2
       f1和f2都硬连接到同一个文件.
!        非--反转上边测试的结果(如果条件缺席,将返回true)
Example 7-4 test死的链接文件
################################Start Script#######################################
1 #!/bin/bash
2 # broken-link.sh
3 # Written by Lee bigelow
4 # Used with permission.
5
6 #一个真正有用的shell脚本来找出死链接文件并且输出它们的引用
7 #以便于它们可以被输入到xargs命令中进行处理 :)
8 #比如: broken-link.sh /somedir /someotherdir|xargs rm
9 #
10 #这里,不管怎么说,是一种更好的方法
11 #
12 #find "somedir" -type l -print0|\
13 #xargs -r0 file|\
14 #grep "broken symbolic"|
15 #sed -e 's/^\|: *broken symbolic.*$/"/g'
16 #
17 #但这不是一个纯粹的bash,最起码现在不是.
18 #小心:小心/proc文件系统和任何的循环链接文件.
19 ##############################################################
20
21
22 #如果没对这个脚本传递参数,那么就使用当前目录.
23 #否则就使用传递进来的参数作为目录来搜索.
24 #
25 ####################
26 [ $# -eq 0 ] && directorys=`pwd` || directorys=$@
27
28 #建立函数linkchk来检查传进来的目录或文件是否是链接和是否存在,
29 #并且打印出它们的引用
30 #如果传进来的目录有子目录,
31 #那么把子目录也发送到linkchk函数中处理,就是递归目录.
32 ##########
33 linkchk () {
34     for element in $1/*; do
35     [ -h "$element" -a ! -e "$element" ] && echo \"$element\"
36     [ -d "$element" ] && linkchk $element
37     # Of course, '-h' tests for symbolic link, '-d' for directory.
37     # 当然'-h'是测试链接,'-d'是测试目录.
38     done
39 }
40
41 #如果是个可用目录,那就把每个从脚本传递进来的参数都送到linkche函数中.
42 #如果不是,那就打印出错误消息和使用信息.
43 #
44 ################
45 for directory in $directorys; do
46     if [ -d $directory ]
47     then linkchk $directory
48     else
49         echo "$directory is not a directory"
50         echo "Usage: $0 dir1 dir2 ..."
51     fi
52 done
53
54 exit 0
################################End Script#########################################
Example 28-1, Example 10-7, Example 10-3, Example 28-3, 和Example A-1 也会说明文件
测试操作的使用过程.
注意事项:
[1]        小心suid,可能引起安全漏洞,但是不会影响shell脚本.
[2]        在当代UNIX系统中,已经不使用sticky bit了,只在目录中使用.
7.3 其他比较操作
----------------
二元比较操作符,比较变量或者比较数字.注意数字与字符串的区别.
整数比较
-eq        等于,如:if [ "$a" -eq "$b" ]
-ne        不等于,如:if [ "$a" -ne "$b" ]
-gt        大于,如:if [ "$a" -gt "$b" ]
-ge        大于等于,如:if [ "$a" -ge "$b" ]
-lt        小于,如:if [ "$a" -lt "$b" ]
-le        小于等于,如:if [ "$a" -le "$b" ]
        大于(需要双括号),如:(("$a" > "$b"))
>=        大于等于(需要双括号),如:(("$a" >= "$b"))
字符串比较
=        等于,如:if [ "$a" = "$b" ]
==        等于,如:if [ "$a" == "$b" ],与=等价
       注意:==的功能在[[]]和[]中的行为是不同的,如下:
       1 [[ $a == z* ]]    # 如果$a以"z"开头(模式匹配)那么将为true
       2 [[ $a == "z*" ]]  # 如果$a等于z*(字符匹配),那么结果为true
       3
       4 [ $a == z* ]      # File globbing 和word splitting将会发生
       5 [ "$a" == "z*" ]  # 如果$a等于z*(字符匹配),那么结果为true
       一点解释,关于File globbing是一种关于文件的速记法,比如"*.c"就是,再如~也是.
       但是file globbing并不是严格的正则表达式,虽然绝大多数情况下结构比较像.
!=        不等于,如:if [ "$a" != "$b" ]
       这个操作符将在[[]]结构中使用模式匹配.
        大于,在ASCII字母顺序下.如:
       if [[ "$a" > "$b" ]]
       if [ "$a" \> "$b" ]
       注意:在[]结构中">"需要被转义.
       具体参考Example 26-11来查看这个操作符应用的例子.
-z        字符串为"null".就是长度为0.
-n        字符串不为"null"
       注意:
       使用-n在[]结构中测试必须要用""把变量引起来.使用一个未被""的字符串来使用! -z
       或者就是未用""引用的字符串本身,放到[]结构中(见Example 7-6)虽然一般情况下可
       以工作,但这是不安全的.习惯于使用""来测试字符串是一种好习惯.[1]
Example 7-5 数字和字符串比较
################################Start Script#######################################
1 #!/bin/bash
2
3 a=4
4 b=5
5
6 #  这里的变量a和b既可以当作整型也可以当作是字符串.
7 #  这里在算术比较和字符串比较之间有些混淆,
8 #+ 因为Bash变量并不是强类型的.
9
10 #  Bash允许对整型变量操作和比较
11 #+ 当然变量中只包含数字字符.
12 #  但是还是要考虑清楚再做.
13
14 echo
15
16 if [ "$a" -ne "$b" ]
17 then
18   echo "$a is not equal to $b"
19   echo "(arithmetic comparison)"
20 fi
21
22 echo
23
24 if [ "$a" != "$b" ]
25 then
26   echo "$a is not equal to $b."
27   echo "(string comparison)"
28   #     "4"  != "5"
29   # ASCII 52 != ASCII 53
30 fi
31
32 # 在这个特定的例子中,"-ne"和"!="都可以.
33
34 echo
35
36 exit 0
################################End Script#########################################
Example 7-6 测试字符串是否为null
################################Start Script#######################################
1 #!/bin/bash
2 #  str-test.sh: 测试null字符串和非引用字符串,
3 #+ but not strings and sealing wax, not to mention cabbages and kings . . .
4 #+ 上边这句没看懂
5 # Using   if [ ... ]
6
7
8 # 如果一个字符串没被初始化,那么它就没有定义的值(像这种话,总感觉像屁话)
9 # 这种状态叫做"null"(与zero不同)
10
11 if [ -n $string1 ]    # $string1 没被声明和初始化
12 then
13   echo "String \"string1\" is not null."
14 else  
15   echo "String \"string1\" is null."
16 fi  
17 # 错误的结果.
18 # 显示$string1为非空,虽然他没被初始化.
19
20
21 echo
22
23
24 # 让我们再试一下.
25
26 if [ -n "$string1" ]  # 这次$string1被引用了.
27 then
28   echo "String \"string1\" is not null."
29 else  
30   echo "String \"string1\" is null."
31 fi                    # ""的字符串在[]结构中
32
33
34 echo
35
36
37 if [ $string1 ]       # 这次$string1变成"裸体"的了
38 then
39   echo "String \"string1\" is not null."
40 else  
41   echo "String \"string1\" is null."
42 fi  
43 # 这工作得很好.
44 # 这个[]test操作检测string是否为null.
45 # 然而,使用("$string1")是一种很好的习惯
46 #
47 # As Stephane Chazelas points out,
48 #    if [ $string1 ]    有1个参数 "]"
49 #    if [ "$string1" ]  有2个参数,空的"$string1"和"]"
50
51
52
53 echo
54
55
56
57 string1=initialized
58
59 if [ $string1 ]       # 再来,$string1"裸体了"
60 then
61   echo "String \"string1\" is not null."
62 else  
63   echo "String \"string1\" is null."
64 fi  
65 # 再来,给出了正确的结果.
66 # 不过怎么说("$string1")还是好很多,因为. . .
67
68
69 string1="a = b"
70
71 if [ $string1 ]       # 再来,$string1 再次裸体了.
72 then
73   echo "String \"string1\" is not null."
74 else  
75   echo "String \"string1\" is null."
76 fi  
77 # 非引用的"$string1"现在给出了一个错误的结果!
78
79 exit 0
80 # Thank you, also, Florian Wisser, for the "heads-up".
################################End Script#########################################
Example 7-7 zmore
################################Start Script#######################################
1 #!/bin/bash
2 # zmore
3
4 #使用'more'来查看gzip文件
5
6 NOARGS=65
7 NOTFOUND=66
8 NOTGZIP=67
9
10 if [ $# -eq 0 ] # 与 if [ -z "$1" ]同样的效果
11 # 应该是说前边的那句注释有问题,$1是可以存在的,比如:zmore "" arg2 arg3
12 then
13   echo "Usage: `basename $0` filename" >&2
14   # 错误消息到stderr
15   exit $NOARGS
16   # 脚本返回65作为退出码.
17 fi  
18
19 filename=$1
20
21 if [ ! -f "$filename" ]   # 将$filename ""起来,来允许可能的空白
22 then
23   echo "File $filename not found!" >&2
24    # 错误消息到stderr
25   exit $NOTFOUND
26 fi  
27
28 if [ ${filename##*.} != "gz" ]
29 # 在变量替换中使用中括号
30 then
31   echo "File $1 is not a gzipped file!"
32   exit $NOTGZIP
33 fi  
34
35 zcat $1 | more
36
37 # 使用过滤命令'more'
38 # 如果你想的话也可使用'less'
39
40
41 exit $?   # 脚本将返回pipe的结果作为退出码
42 # 事实上,不用非的有"exit $?",但是不管怎么说,有了这句,能正规一些
43 # 将最后一句命令的执行状态作为退出码返回
################################End Script#########################################
混合比较
-a        逻辑与
       exp1 -a exp2    如果exp1和exp2都为true的话,这个表达式将返回true
-o        逻辑或
       exp1 -o exp2    如果exp1和exp2中有一个为true的话,那么这个表达式就返回true
这与Bash的比较操作符&&和||很相像.在[[]]中使用它.
       1 [[ condition1 && condition2 ]]
-o和-a一般都是和test命令或者是[]一起工作.
       1 if [ "$exp1" -a "$exp2" ]
请参考Example 8-3,Example 26-16和Example A-28来查看混合比较操作的行为.
注意事项:
[1]        S.C.(这家伙是个人名)指出,在使用混合比较的时候即使"$var"也可能会产生问题.
       如果$string为空的话,[ -n "$string" -o "$a" = "$b" ]可能在某些版本的Bash中
       会有问题.为了附加一个额外的字符到可能的空变量中的一种安全的办法是,
       [ "x$string" != x -o "x$a" = "x$b" ](the "x's" cancel out)(没看懂).
       cancel out是抵消的意思.
7.4 嵌套的if/then条件test
-------------------------
可以使用if/then来进行嵌套的条件test.最终的结果和上边的使用&&混合比较操作是相同的.
   1 if [ condition1 ]
   2 then
   3   if [ condition2 ]
   4   then
   5     do-something  # 这里只有在condition1和condition2都可用的时候才行.
   6   fi  
   7 fi
具体请查看Example 34-4.
7.5 检查你的test知识
--------------------
系统范围的xinitrc文件可以用来启动X server.这个文件中包含了相当多的if/then test,
就像下边的节选一样:
1 if [ -f $HOME/.Xclients ]; then
2   exec $HOME/.Xclients
3 elif [ -f /etc/X11/xinit/Xclients ]; then
4   exec /etc/X11/xinit/Xclients
5 else
6      # 故障保险设置,虽然我们永远都不会走到这来.
7      # (我们在Xclients中也提供了相同的机制)它不会受伤的.
8      xclock -geometry 100x100-5+5 &
9      xterm -geometry 80x50-50+150 &
10      if [ -f /usr/bin/netscape -a -f /usr/share/doc/HTML/index.html ]; then
11              netscape /usr/share/doc/HTML/index.html &
12      fi
13 fi
对上边的"test"结构进行解释,然后检查整个文件,/etc/X11/xinit/xinitrc,并分析if/then
test结构.你可能需要查看一下后边才能讲解到的grep,sed和正则表达式的知识.
第8章 操作符和相关的主题
========================
8.1 操作符
----------
等号操作符
变量赋值
   初始化或者修改变量的值
=
   无论在算术运算还是字符串运算中,都是赋值语句.
   1 var=27
   2 category=minerals  # No spaces allowed after the "=".
   注意:不要和"="test操作符混淆.
       1 #    = as a test operator
       2
       3 if [ "$string1" = "$string2" ]
       4 # if [ "X$string1" = "X$string2" ] is safer,
       5 # to prevent an error message should one of the variables be empty.
       6 # (The prepended "X" characters cancel out.)
       7 then
       8    command
       9 fi
算术操作符
+        加法
-        减法
*        乘法
/        除法
**        幂运算
       1 # Bash, version 2.02, introduced the "**" exponentiation operator.
       2
       3 let "z=5**3"
       4 echo "z = $z"   # z = 125
%        取模
       bash$ expr 5 % 3
       2
       5/3=1余2
       模运算经常用在其它的事情中,比如产生特定的范围的数字(Example 9-24,
       Example 9-27)和格式化程序的输出(Example 26-15,Example A-6).它甚至可以用来
       产生质数,(Example A-16).事实上取模运算在算术运算中使用的频率惊人的高.
Example 8-1 最大公约数
################################Start Script#######################################
1 #!/bin/bash
2 # xxx.sh: 最大公约数
3 #         使用Euclid's 算法
4
5 #  最大公约数,就是2个数能够同时整除的最大的数.
6 #
7
8 #  Euclid's算法采用连续除法.
9 #  在每个循环中
10 #+ 被除数 >        右移1位(每次右移都将除2)
>>=        右移几位
&        按位与
&=        按位与赋值
|        按位或
|=        按位或赋值
~        按位非
!        按位否?(没理解和上边的~有什么区别?),感觉是应该放到下边的逻辑操作中
^        按位异或XOR
^=        异或赋值
逻辑操作:
&&        逻辑与
       1 if [ $condition1 ] && [ $condition2 ]
       2 # 与:  if [ $condition1 -a $condition2 ] 相同
       3 # 如果condition1和condition2都为true,那结果就为true.
       4
       5 if [[ $condition1 && $condition2 ]]    # 也可以.
       6 # 注意&&不允许出现在[ ... ]中.
       注意:&&也可以用在and list中(见25章),但是使用的时候需要依赖上下文.
||        逻辑或
       1 if [ $condition1 ] || [ $condition2 ]
       2 # 与:  if [ $condition1 -o $condition2 ] 相同
       3 # 如果condition1或condition2为true,那结果就为true.
       4
       5 if [[ $condition1 || $condition2 ]]    # 也可以
       6 # 注意||不允许出现在[ ... ]中.
       注意:Bash将test每个连接到逻辑操作的状态的退出状态(见第6章).
Example 8-3 使用&&和||进行混合状态的test
################################Start Script#######################################
1 #!/bin/bash
2
3 a=24
4 b=47
5
6 if [ "$a" -eq 24 ] && [ "$b" -eq 47 ]
7 then
8   echo "Test #1 succeeds."
9 else
10   echo "Test #1 fails."
11 fi
12
13 # 错误:   if [ "$a" -eq 24 && "$b" -eq 47 ]
14 #+         尝试执行' [ "$a" -eq 24 '
15 #+         因为没找到']'所以失败了.
16 #
17 #  注意:  如果 [[ $a -eq 24 && $b -eq 24 ]]  能够工作.
18 #  那这个[[]]的test结构就比[]结构更灵活了.
19 #
20 #    (在17行的"&&"与第6行的"&&"意义不同)
21 #    Thanks, Stephane Chazelas, for pointing this out.
22
23
24 if [ "$a" -eq 98 ] || [ "$b" -eq 47 ]
25 then
26   echo "Test #2 succeeds."
27 else
28   echo "Test #2 fails."
29 fi
30
31
32 #  -a和-o选项提供了
33 #+ 一种可选的混合test方法.
34 #  Thanks to Patrick Callahan for pointing this out.
35
36
37 if [ "$a" -eq 24 -a "$b" -eq 47 ]
38 then
39   echo "Test #3 succeeds."
40 else
41   echo "Test #3 fails."
42 fi
43
44
45 if [ "$a" -eq 98 -o "$b" -eq 47 ]
46 then
47   echo "Test #4 succeeds."
48 else
49   echo "Test #4 fails."
50 fi
51
52
53 a=rhino
54 b=crocodile
55 if [ "$a" = rhino ] && [ "$b" = crocodile ]
56 then
57   echo "Test #5 succeeds."
58 else
59   echo "Test #5 fails."
60 fi
61
62 exit 0
################################End Script#########################################
   &&和||操作也能在算术运算的上下文中找到.
       bash$ echo $(( 1 && 2 )) $((3 && 0)) $((4 || 0)) $((0 || 0))
       1 0 1 0
混杂操作:
,        逗号操作符
       逗号操作符可以连接2个或多个算术运算.所有的操作都会被执行,但是只有最后一个
       操作作为结果.
       1 let "t1 = ((5 + 3, 7 - 1, 15 - 4))"
       2 echo "t1 = $t1"               # t1 = 11
       3
       4 let "t2 = ((a = 9, 15 / 3))"  # Set "a" and calculate "t2".
       5 echo "t2 = $t2    a = $a"     # t2 = 5    a = 9
       ","主要用在for循环中,具体见Example 10-12.
8.2 数字常量
------------
shell脚本默认都是将数字作为10进制数处理,除非这个数字某种特殊的标记法或前缀开头.
以0开头就是8进制.以0x开头就是16进制数.使用BASE#NUMBER这种形式可以表示其它进制
表示法
Example 8-4 数字常量的处理
################################Start Script#######################################
1 #!/bin/bash
2 # numbers.sh: 数字常量的几种不同的表示法
3
4 # 10进制: 默认
5 let "dec = 32"
6 echo "decimal number = $dec"             # 32
7 # 一切都很正常
8
9
10 # 8进制: 以'0'(零)开头
11 let "oct = 032"
12 echo "octal number = $oct"               # 26
13 # 表达式的结果用10进制表示.
14 #
15
16 # 16进制表示:数字以'0x'或者'0X'开头
17 let "hex = 0x32"
18 echo "hexadecimal number = $hex"         # 50
19 # 表达式的结果用10进制表示.
20
21 # 其它进制: BASE#NUMBER
22 # BASE between 2 and 64.
22 # 2到64进制都可以.
23 # NUMBER必须在BASE的范围内,具体见下边.
24
25
26 let "bin = 2#111100111001101"
27 echo "binary number = $bin"              # 31181
28
29 let "b32 = 32#77"
30 echo "base-32 number = $b32"             # 231
31
32 let "b64 = 64#@_"
33 echo "base-64 number = $b64"             # 4031
34 # 这种64进制的表示法中的每位数字都必须在64进制表示法的限制字符内.
35 # 10 个数字+ 26 个小写字母+ 26 个大写字母+ @ + _
36
37
38 echo
39
40 echo $((36#zz)) $((2#10101010)) $((16#AF16)) $((53#1aA))
41                                          # 1295 170 44822 3375
42
43
44 #  重要的注意事项:
45 #  ---------------
46 #  如果使用的每位数字超出了这个进制表示法规定字符的范围的话,
47 #+ 将给出一个错误消息.
48
49 let "bad_oct = 081"
50 # (部分的) 错误消息输出:
51 #  bad_oct = 081: too great for base (error token is "081")
52 #              Octal numbers use only digits in the range 0 - 7.
53
54 exit 0       # Thanks, Rich Bartell and Stephane Chazelas, for clarification.
################################End Script#########################################
第三部分    超越基本
++++++++++++++++++++
第9章    变量重游
================
如果变量使用恰当,将会增加脚本的能量和灵活性.但是前提是这需要仔细学习变量的细节知识.
9.1 内部变量
------------
Builtin variable
   这些内建的变量,将影响bash脚本的行为.   
   $BASH
       这个变量将指向Bash的二进制执行文件的位置.
       bash$ echo $BASH
       /bin/bash
   $BASH_ENV
       这个环境变量将指向一个Bash启动文件,这个启动文件将在调用一个脚本时被读取.
   $BASH_SUBSHELL
       这个变量将提醒subshell的层次,这是一个在version3才被添加到Bash中的新特性.
       见Example 20-1.
   $BASH_VERSINFO[n]
       记录Bash安装信息的一个6元素的数组.与下边的$BASH_VERSION很像,但这个更加详细.
        1 # Bash version info:
        2
        3 for n in 0 1 2 3 4 5
        4 do
        5   echo "BASH_VERSINFO[$n] = ${BASH_VERSINFO[$n]}"
        6 done  
        7
        8 # BASH_VERSINFO[0] = 3                      # 主版本号
        9 # BASH_VERSINFO[1] = 00     &nbs