【目标】 本章的主要目的是为大家介绍 Shell 的编程方法,在讲Shell 编程方法之前,还要介绍一些有关Shell的概念和Shell的启动过程等,对于系统管理员或程序员来说,熟练地使用Shell脚本将对日常的系统维护及管理非常有用,如果我们想作一个合格的系统管理员或程序员,建议进一步深入的了解和使用 Shell。 【重点内容】 Shell 概述 Shell 启动 Shell 变量和运算符 过程 脚本执行命令 控制 Shell Shell 的简单编程 Shell 程序的调试 Shell 脚本举例 专家答疑 下面让我们马上开始继续学习Shell吧! 5.1 Shell 概述 5.1.1 概念 我们平时所说的Shell可以理解为是Linux或UNIX系统提供用户的使用界面。Shell为用户提供了输入命令和参数、并可得到命令执行结果的环境。 当一个用户登录Linux系统之后,系统初始化程序init就根据/etc/passwd文件中的设定,为每一个用户运行一个称为shell(外壳)的程序。 确切一点说,Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至是编写一些程序。Shell处在内核与外层应用程序之间,起着协调用户与系统的一致性、在用户与系统之间进行交互的作用,即Shell为用户提供了输入命令和参数并可得到命令执行结果的环境。为了让大家能够更直观的了解Shell的概念,请参考下面的图5_1:
图5_1 Linux、UNIX系统层次结构图 Shell解释用户输入的命令行,提交到系统内核处理,并将结果返回给用户;Shell与Linux、UNIX命令一样都是实用程序,但是它们之间还是有区别的。一旦用户注册到系统后,Shell 就被系统装入内存并一直运行到用户退出系统之止;而一般命令仅当被调用时,才由系统装入内存执行。而且与一般命令相比,Shell除了是一个命令行解释器外,同时还是一个功能相当强大的编程语言,而且易编写、易调试、灵活性较强,作为一种命令级语言,Shell是解释性的,多数高级语言是编译性的, Shell命令组合功能很强, 与系统有密切的关系。大多数Linux系统的启动相关文件(一般在 /etc/rc.d 目录下)都是使用shell脚本。同传统的编程语言一样,shell提供了很多特性,这些特性可以使我们的shell 脚本编程更为有用,如:数据变量、参数传递、判断、流程控制、数据输入和输出,子程序及以中断处理等。 目前Shell的版本有很多种,如Bourne Shell、C shell、Bash、ksh、tcsh等,它们各有特点,下面我们来简要介绍一下。 第一个重要的 shell 是 Bourne shell,这个命名是为了纪念此shell的发明者Steven Bourne。1979起Unix就开始使用Bourne shell。Bourne shell的主文件名为sh,以后的开发者们便以sh做为Bourne shell的主要识别名称。 虽然Linux与Unix一样,可以支持多种shell, 但 Bourne shell 的重要地位至今仍然没有改变. 许多Unix系统中仍然使用sh做为重要的管理工具。它的工作从开机到关机, 几乎无所不包。Linux中用户shell主要是bash,但在启动脚本、编辑等很多工作中仍然使用Bourne shell。 C shell是最广为流行使用的shell变种。C shell主要在 BSD 版的Unix 系统中使用,作者是柏克莱大学的 Bill Joy。C shell因为其语法和C语言相类似而得名。这也使得Unix的系统工程师在学习C shell时感到相当的方便。 Bourne Shell和C Shell形成了shell 的两大主流,后来的变种大都吸取这两种shell的特点。 比如 Korn, tcsh 及 bash。 Bash shell是GNU计划的重要工具软件之一, 也是 GNU系统中标准的shell。Bash与sh兼容,所以许多早期开发出来的 Bourne shell程序都可以继续在bash中运行。现在我们使用的Linux就是使用Bash做为用户的基本shell。 Bash在1988年发布,并在1995~1996 期间推出 bash 2.0。在这之前, 广为使用的版本是 1.14, 它增加了许多新的功能, 以及更好的兼容性。要想具体的了解这些shell,请参看下面Shell版本列表中所示的详细内容:
表5_2 Shell版本列表 注意: Shell的两种主要语法类型:Bourne和C,这两种语法彼此不兼容。Bourne家族主要包括:sh、ksh、bash、psh、zsh;C家族主要包括:csh、tcsh (bash和zsh在不同程度上支持csh 的语法。) 大家可能会问,有这么多的Shell版本,那我们该应用那个版本呢?通常选择合适的Shell版本应考虑的主要因素有下面这几点:(1)Bourne Shell在任何一个Linux、UNIX系统平台上都存在,因此又成为标准Shell;(2)Bourne Shell家族有更丰富的程序语言,而 C Shell家族有简单的程序接口;(3)Shell各类变种功能越来越强大,但学习和使用也越难,因此可依据使用者编程经验来选择;(4)Shell编程的脚本是个人使用还是公用,即要考虑移植性问题。 5.2 Shell 启动 通常Shell的启动有两种方式:系统启动时Shell的启动和命令行状态下Shell的启动,也称Shell的交互使用。下面分别介绍给大家这两种Shell 的启动方式。 5.2.1 系统启动时Shell启动 即在系统启动前,先在/etc/passwd文件中指定要启动的Shell。例如: root:x:0:1:super user:/:/bin/sh oralce:x:201:starf:/home:/bin/csh 这样,当系统启动后,如果是root用户登陆,那么使用的Shell版本将会是sh;如果是oracle用户登陆,那么使用的Shell版本将会是csh。 5.2.2 命令行状态下Shell启动 在命令行状态下启动Shell的方法很简单,因为在系统中本身有多种版本的Shell存在,可通过下面相应的命令来启动,如: sh-2.04$ bash [oracle@mirror oracle]$ sh sh-2.04$ csh [oracle@mirror ~]$ 在命令提示符下,输入bash可进入bash环境,输入sh可进入sh环境,输入csh可进入csh环境。这样即可实现Shell不同版本之间的交互使用。 5.2.3 Shell与内核的交互作用 系统启动过程中内核将加载至内存直到系统关机,在启动过程中,init进程将扫描Linux系统中的/etc/inittab文件,在此文件中将列出可用的终端及其属性,一旦找到活动的终端,getty(mingetty,LINUX)函数将会给出login提示符和口令,确认完成后将启动相应的shell(即在/etc/passwd文件中指定用户使用的Shell环境)。 上面介绍的过程可以用下面的流程图表示:
在上面的流程图中,getty(mingetty)提示输入用户及口令,然后将用户名及口令传递给login, login验证用户及口令是否匹配,如果身分验证通过,login将会自动转到其用户的$HOME环境中,并启动在/etc/passwd文件中所列出的shell程序(如在/etc/passwd文件中用户的shell域为/bin/sh),然后将控制权移交到所启动的任务。 5.3 Shell 变量和运算符 下面先给大家介绍一下Shell的变量,当Shell脚本需要保存一些信息时(或许是一个文件名),就把它存放在一个变量中。变量是计算机内存的单元,其中存放的值可以改变。每个变量有一个名字,所以很容易引用它。变量可以定制用户本身的工作环境。使用变量可以保存有用信息,使系统获知用户相关设置。变量也用于保存暂时信息。 变量的名字必须以字母或下划线开头,可以包括字母、数字和下划线。Shell变量能够而且只能存储正文字符串,即它只有一种类型的变量——串变量。从赋值的形式上看,则可以分成四种类型的变量,也变量形式。 在Linux系统中经常使用的Bourne Shell中有如下四种变量: 用户自定义变量 位置变量 环境变量 预定义变量 5.3.1 Shell编程中的特殊字符 $ 美元符号。用来表示变量的值。如变量NAME的值为Mike,则使用$NAME就可以得到“Mike”这个值。 # 井号。除了做为超级用户的提示符之外,还可以在脚本中做为注释的开头字母,每一行语句中,从#号开始的部分就不执行了。 {} 大括号。一般与变量值标识符号$配合使用,表示变量的起始位置,还可以在其中进行各种变量的赋值。如${NAME}string表示变量名是NAME,它的值与后面的“string”字符串连接。 “” 双引号。shell不会将一对双引号之间的文本中的大多数特殊字符进行解释,如#不再是注释的开头,它只表示一个井号“#”。但$仍然保持特殊含义。 '’ 单引号。shell不会将一对单引号之间的任何字符做特殊解释。 `` 倒引号。命令替换。在倒引号内部的shell命令首先被执行,其结果输出代替用倒引号括起来的文本,不过特殊字符会被shell解释。 \ 斜杠。用来去掉在shell解释中字符的特殊含义。在文本中,跟在\后面的一个字符不会被shell特殊解释,但其余的不受影响。 5.3.2 用户自定义变量 用户自定义变量指登陆用户自身进行定义的变量,例如: $ COUNT=1 $ NAME="Frank Chu" 上面的例子中用户定义了两个变量分别为COUNT和NAME,并分别赋值为“1”和“Frank
Chu”。 注意: 因为大部分Linux、UNIX命令使用小写字符,因此在shell编程中通常使用全大写变量,当然这并不是强制性的,但使用大写字符可以在编程中方便地识别变量。 对用户自定义变量进行调用需要在变量前加$,而且有时需要用{}括起来,同其他字符分开,例如: # echo $HOME /root # WEEK=Satur # echo Today is $WEEKday Today is # echo Today is ${WEEK}day Today is Saturday 这说明只有用大括号{}将变量名称确定后,shell才能识别加以执行。 设置变量的默认值 在变量未赋值之前其值为空。 但Bourne Shell允许对变量设置默认值,其格式如下:${variable:-defaultvalue} 注意: 在Bourne Shell中可以使变量替换在特定条件下执行,即有条件的环境变量替换。这种变量替换总是用大括号括起来的。 例如: #
echo Hello $UNAME Hello
这时UNAME未被赋值,所以显示为空 # echo Hello ${UNAME:-there} Hello there # UNAME=hbwork # echo Hello ${UNAME:-there} Hello hbwork 在UNAME已经被赋值为hbwork时,默认值there就被取代了。 改变变量的值 既然我们可以对变量赋值,当然也就可以改变变量的值,改变变量的格式如下:${variable:=value} 让我们举例说明: # echo Hello $UNAME Hello
# echo Hello ${UNAME:=there} Hello there # echo $UNAME there
上面的例子中第一行的变量 UNAME由于没有赋值,所以为空,第三行中变量 UNAME的值被改变为 there。所以在最后一行显示变量UNAME的值时,显示的值为there。 当然还有其它一些关于对自定义用户变量操作的使用方法。如下所示: 5.3.3 位置变量 Shell变量可以使用位置变量来存取脚本参数。例如我们创建的一个脚本会处理两个参数,那么就可以编写读取位置变量的脚本,来得到实际的数值。传给脚本的文件名存放在变量$1和$2中。在相关数字之前加上一个美元符号,就可以存取任意多个参数。在Shell 脚本中位置变量可用$1、$2、$3..$9表示,$0表示内容通常为当前执行程序的文件名。当存取的参数超过第10个时,就要用大括号把这个数字括起来,如:${14} 和${18}等。 在下面的这个rm命令执行中带着3个文件名做为参数。 $1的值=mom.txt $2的值=/usr/local/bin/kl.sh $3的值=/etc/hosts.bak 举例: # cat count.sh #!/bin/sh A=$1
# 将位置$1的数值读入,并赋给变量A B=$2
# 将位置$2的数值读入,并赋给变量B C=$[$A+$B] # 将变量A与B的值相加,将结果赋给C echo
$C # 显示C的数值 这个脚本count.sh可以运算两个数字的和并显示出来。在运行的时候,就需要同时输入需要相加的两个数字: # ./count.sh 3 6 9 # ./count.sh 34 28 62 注意:我们下面就要开始尝试自己编写脚本程序了。这些程序实质上都是文本文件,在被创建的时候默认权限是644,不具备执行权限,需要用chmod为它们加上执行权限。可以参考下面的例子。 举例说明,我们来看下面的脚本例子5_1: 例子5_1 showdata — 利用位置变量显示脚本参数 #! /bin/bash echo "\ $ 0= * $0
*" echo "\ $ 1= * $1
*" echo "\ $ 2= * $2
*" echo "\ $ 3= * $3
*" 以下是这个脚本运行的示例: $ chmod +x
showdata # 附加执行权限 $ ./showdata teacher
student \ $ 0= * ./showdata * \ $ 1= * teacher * \ $ 2= * student * \ $ 3= * * 这个脚本主要说明两点:
第一,有时把值用(*)括起来很有用,这样可以更容易地指出一个变量何时为空。因为只有两个参数传给该脚本,所以$3的值是空的。第二,变量 $0 存放脚本自身的名字。这个变量可以用来创建日志文件,更容易使用这些文件与脚本关联起来。 有关位置变量还有另外三个: $* 这个变量包括参数的列表 $@ 这个变量包括参数的列表 $# 这个变量包括参数的个数 Bash 保留$* 和$@ 这两个变量,从而更容易从其他Shell程序中移植脚本。下面的脚本例子5_2中,展示了$* 、$# 和$@ 变量的用法。 例子5_2 showparam — 利用位置变量显示脚本参数 #! /bin/bash echo "There are $#
parameters." echo "The parameters
are * ${*} * " echo "The parameters
are * $@ *" 以下是这个脚本执行的示例: $ chmod +x showparam $ ./showparam one two three There are 3 parameters. The parameters are * two two three
* The parameters are * two two three
* 提示: 注意在第二个echo命令中是如何使用大括号的,从而使变量值被星号括起来。大括号迟早有用,不限于上面一种情况。 5.3.4 环境变量 Shell执行环境由一系列环境变量组成,这些变量是由Shell维护和管理的。所有这些变量都可被用户重新设置,变量名由大写字母或数字组成。 例如下面的环境变量: CDPATH:执行cd命令时使用的搜索路径; HOME: 用户的home目录; IFS: 内部的域分隔符,一般为空格符、制表符或换行符; MAIL: 指定特定文件(信箱)的路径,供邮件系统用; PATH: 寻找命令或可执行文件的搜索路径; PS1 : 主命令提示符,默认为“$”; PS2 : 从命令提示符,默认为“>”; TERM: 使用的终端类型。 例如我们显示下面的环境变量: $ PS1="test:";export PS1 test: $ echo $MAIL /var/spool/mail/username 5.3.4 预定义变量 在Shell中有一组预定义变量也称为特殊变量,其变量名和变量值只有Shell本身才可以设置。 预定义的变量主要有下面几个: $ shell变量名的开始,如$var | 管道,将标准输出转到下一个命令的标准输入 $# 记录传递给Shell的自变量个数 # 注释开始 & 在后台执行一个进程 ? 匹配一个字符 * 匹配0到多个字符(与DOS不同,可在文件名中间使用,并且含.) $- 使用set及执行时传递给shell的标志位 $! 最后一个子进程的进程号 $? 取最近一次命令执行后的退出状态(返回码) $* 传递给shell script的参数 $@ 所有参数,个别的用双引号括起来 $0 当前shell的名字 $n (n:1-) 位置参数 $$ 进程标识号(Process Identifier Number, PID) > 输出重定向 < 输入重定向 >> 附加输出重定向 [] 列出字符变化范围,如[a-z] 例如预定义变量 $# 是用来记录传递给Shell的自变量个数,那么下面的命令行: $
showparam a b c 其中 showparam 是上面所讲位置变量内容中的脚本例子3_2中,用来利用位置变量显示脚本参数,那么此时预定义变量 $#的值为3。 上面内容中的预定义变量$? ,是用来取最近一次命令执行后的退出状态(返回码),一般情况下,如果执行成功返回码为0,执行失败则返回码为非0。 举例如下: # test -r my_file # echo $? 0 #cat nofile cat: nofile: No such file or directory # echo $? 1 当试图cat不存在的nofile文件时出错,则$?为非空。 5.3.5 运算符 运算符告诉计算机要执行什么动作。像大多数语言一样,Shell也有很多运算符。运算符是对计算机发送的指令,指示它执行某些任务。所有运算符产生的动作都在运算对象上执行。运算对象可以是字面值(或3或者23.3)、变量(如$ count)或者表达式(如$ count + 1)。一个良好的表达定义是某些运算符和运算对象的组合体,它们作为一个单位进行求值运算。 运算对象本质上是递归的。表达式1 + 9(两个运算对象和一个加法运算符)可以被看作是其值为10的一个运算对象。(1 + 9)-2是一个表达式,它由两个运算对象相减组成,其中第一个运算对象是(1 + 9),第二个运算对象是2。 理解运算符优先级是能够解释表达式的关键。运算优先级的顺序表明在每个表达式或子表达式中哪一个运算对象首先被求值。 表5-2以降级顺序列出Shell所用的全部运算符。具有较高优先级级别的运算符先于较低级别的运算符进行求值运算。 表5-2 Shell运算符和它们的优先级顺序
注意: 读者或许认为表5-2 列出了大量运算符,其实不然,这个列表实际上是相当有限的。例如,Shell不支持幂运算符。对此以及更高级的运算符需要用Perl或者Tcl。 在讨论某些更深奥的运算符之前,先看一下运算符优先级的顺序是如何影响表达式求值过程的: $ echo $[1+2*4] 9 $ echo $[(1+2)*4] 12 注意: $ [ ]表示形式告诉Shell对方括号中的表达式求值。 第一个命令首先计算乘运算符 ,因为它的优先级是11,而加运算符的优先级是10。因此,第一个命令就成为echo $ [1 + 8] ,即9。第二个命令使用圆括号,使加运算符首先计算。圆括号中的表达式总是首先计算的。 取模运算符 取模运算符用来求出两个运算对象相除得到的余数。例如,9 %7等于2,因为9 / 7 等于1余下2。 当脚本需要对整个列表反复进行,对多个元素的每一个都执行某些处理时,取模运算符很有用。例5-3示出如何确定一个列表中每隔2个元素的内容。 例5-3 modulus 利用取模运算符每隔2个元素进行一次处理 # ! /bin/bash count=1 for element in $@ do if test $[$count%3] = 0 then echo
$element fi let count=$count+1 done 以下是这个脚本的执行示例: $ chmod + x moudulus $ . /modulus one two three
four five six three six 这个取模脚本中唯一懂的部分是test命令。test命令将在本章下面小节中给出全面介绍,所以在这里仅做简单要介绍。需要计算的表达式是$count % 3 ,它在$ [ ]表示形式中,与该表达式进行比较的值在等号的右边。这样,当$ count的值达到了时,方括号中表达式的计算结果是0,则echo语句执行。 应注意每隔两项打印一个值。通过修改取模运算符右边的值,可以影响到哪些项被处理。把该值改为5 就意味着每隔4项打印一个消息。 按位运算符 按位(bitwise)运算符对两个运算对象的单个的二进制进行操作。第一个运算对象的每个位与第二个运算对象的相应位进行比较(除按位取反运算符以外,它只作用在一个运算符对象上)。 表5- 3 提供了Shell中使用的每个按位运算符的详细说明。 表5-3 按位运算符
逻辑运算符 逻辑运算符允许程序根据多种条件做出判断。每个运算对象求得的值为真或者为假,然后利用各运算对象的真值或者假值来确定整个表达式的值。 表5-4列出两个运算对象利用&&运算符组合起来的四种不同方式。从中可以看出,仅当两个运算对象都为真时,其结果才为真。 表5-4 &&结果表
表5-5列出两个运算对象利用 | | 运算符组合起来的四种不同方式。从中可以看出,只要有一个运算对象为真,其结果就为真。 表5-5 | | 结果表逻辑运算符
赋值运算符 基本赋值运算符在本章前面的很多示例中已经用过。它简单地把等号右边的表达式的值赋予等号左边的变量。 Shell也有简便赋值运算符,它们把基本赋值运算与另外的运算符组合在一起。例如,不必用let $count = $count + $change,而可以用let $count + = $change。采用简便运算符的优点除了输入的字符较少以外,就是使我们的赋值意图更加明了。 5.3.6 变量替换 上面的内容中我们已经介绍过,当Shell脚本需要保存一些信息(或许是一个文件名)供以后使用, 就把它存放在一个变量中。变量是计算机内存单元,其中存放的值可以改变。每个变量有一个名字,所以很容易引用它。 下面的例5-4是一个简短的脚本,它只是把值monday.dat赋予变量logfile。 例5-4 assign_logfile——简单变量赋值 # ! /bin/bash # The following line is the
assignment command logfile="monday.dat" echo "The value of
logfile is :" # The dollar sign is used to
access a variable's value echo "$logfile" 下面是这个脚本运行的示例: $chmod + x assign_logfile $.
/asslgn_logfile <-execute the file Tho value of logfile is: monday
dat <-the correct value is <-displayed $ echo $logfile <-yikes! how come no value <-is displayed? 按照默认情况,例5-4所示的变量仅在创建它们的Shell中可以访问。在进—步解释这个问题之前,让我们看一看在脚本中如何使用$字符来存取变量的值。 当把一个值赋给一个变量时,只是使用该变量的名字(在赋值号的左边)。例如: logfile
= "monday.dat" 然而,当存取该变量的值时,就需要用某个字符来告诉Shell,要进行变量替换。变量替换是用变量的值替代该变量名,并且Bash使用$字符表明要做替换。例如: echo "The,log file name is $logfile" 5.3.7 source和export命令 在上面的例子中,为什么在命令行上运行echo $logfile时它不显示任何信息,而在该脚本内运行时它可以显示日志文件名?这是因为每一个脚本都在自己的Shell中运行——脚本几乎是全封闭的,除了打开文件之外,它断绝与其他程序的联系。 不管正运行的脚本如何,每个Shell都有内存,专门用来保存自己变量的信息。Shell维护的变量信息称做该Shell的“环境”。当执行赋值语句时,assign_ logfile脚本修改了它自己的环境。第一个echo $logfile命令起作用,因为它与赋值命令在同一环境中运行。第二个echo命令不起作用,因为它运行在assign_logfile脚本的父Shell环境中。asmgn_logfile脚本的父Shell就是我们的注册Shell,它也称做当前Shell(即当前通过键盘与之交互作用的Shell)。 我们利用source命令可以强行让一个脚本影响当前Shell的环境。它执行该脚本中的全部命令,而不管脚本文件的权限如何设置。 例如: $ source assign_logfile The value of logfile is: monday.dat $ echo $ logfile monday.dat 上面的source 命令让脚本将影响它们父Shell的环境。如果要影响子Shell应怎么办呢 这时应该使用export,这个命令可以让脚本影响其子Shell的环境。请看下面的例子. 例5-6 dilplay_logfile——显示logfile变量的值 Variable #! /bin/bash echo "Logfile is
$logfile." 例5-6 export_olgfile——简单变量赋值 #! /bin/bash # The following line is the
assignment command. logfile =
"Monday.dat" # call the display script
before using export echo "Before Export:
" . / display_logfile export logfile # call the display scriptb
after export echo "After Export:
" . / display_logfile 以下是这个脚本运行情况的示例: $ unset logfile $ chmod + x export_logfile $. /export_logfile Before Export: Logfile is After Export: Logfile is Monday.dat $ ./display_logfile logfile is 上面的unset命令从环境中删除一个变量。要想大致了解何时测试脚本使用环境变量的情况,使用这个命令是很方便的。由于display_logfile脚本第二次运行时显示出logfile变量的值,从而可看出export命令产生的效果。 它让子Shell(即display_logfile的第二次运行)访问logfile变量。大家会看到,父Shell(即注册Shell)仍不能见到logfile变量,因为在命令行上运行时,display_logfile脚本不显示logfile变量的值。 注意: 为了如上所示的那样执行,对这个测试需要删除logfile变量或者取消其定义,如同本章先前运行source命令的情况。如果没有给出unset命令,则在命令行上运行display_logfile脚本时就会显示logfile变量。 让我们先看一下另一个使用export命令的示例。下面我们在注册Shell中创建一个变量,然后试着在子Shell中访问它。 试执行以下命令序列: $ count = 19 $ echo $count 19 $ bash $ echo $count $
exit $ echo $count 19 这个命令序列表明,在当前Shell或注册Shell中定义的count变量不能在子Shell中对其进行访问。 注意: 在assignment命令中或者使用let命令时,在等号的前后一定不能有任何空格。 我们下面使用export命令使count变量可用于子Shell: $ export count =
19 $
bash $ echo
$count 19 $
exit 注意:, export命令可以用作assignment命令的一部分。这个特性有助于缩小脚本尺寸,减少交叉访问。如果export命令以后在脚本中运行,我们想了解该脚本正在做什么事情,那么就需要检查assignment命令,看看它的值是什么。 当修改变量时我们应对底部行注意,保证让正确的Shell访问它们。 变量名前后的大括号告诉Shell,变量名从何处开始,到何处结束。Shell有一些表示形式要用到大括号。表5-5给出了所有大括号表示形式。 表5-6 利用大括号表示变量替换
5.3.7 影响命令的变量 Shell有若干以变量为工作对象的命令,其中有些命令似乎重复了。例如,可以用declare、export和typeset命令来创建全局(或转出)的变量。typeset命令是declare的同义词。 Declare 命令 语法: declare [options] [name [=
value]] 摘要: 用于显示或设置变量。 declare命令使用四个选项: -f 只显示函数名 -r 创建只读变量。只读变量不能被赋予新值或取消设置,除非使用declare或者typeset命令 -x 创建转出(exported)变量 -i 创建整数变量。如果我们想给一个整数变量赋予文本值,实际上是赋予0使用+ 代替-,可以颠倒选项的含义。 如果没有使用参数,则declare显示当前已定义变量和函数的列表。让我们关注一下-r选项: $
declare –r title=" paradise Lost" $ title = "
Xenogenesis" bash: title: read-only
variable $ declare title= "
Xenogenesis" $ echo $title Xecogenesis $ typeset title = " The
Longing Ring” $ echo $title The Longing Ring 这个示例表明,只有declare或typeset命令可以修改只读变量的值。 export命令 语法: export [options] [name
[= value]] 摘要: 用于创建传给子Shell的变量。 export命令使用四个选项: -- 表明选项结束。所有后续参数都是实参 -f 表明在“名-值”对中的名字是函数名 -n 把全局变量转换成局部变量。换句话说,命名的变量不再传给子Shell -p 显示全局变量列表 如果没有用参数,则假定是一个-p参数,并且显示出全局变量的列表: $ export declare –x ENV =
"/home/medined/ . bashrc" declare –x HISTFILESIZE =
"1000" … declare –xi numPages =
"314" declare –xr title = "The
Longing Ring" declare –xri numChapters =
"32" 这种显示的一个有趣的特性是,它告诉我们哪些变量只能是整数、是只读的,或者二者皆可。 let命令 语法: let expression 摘要: 用于求整数表达式的值。 let命令计算整数表达式的值。它通常用来增加计数器变量的值,如例5-9所示。 例5-9 let——使用let命令 # ! /bin/bash count=1 for element in $@ do echo "
$element is element $count" let count+=1 done 下面是这个脚本运行结果示例: $ chmod + x let $ . /let one two three one is element 1 two is element 2 three is element 3 注意:如果我们习惯在表达式中使用空格,那么要用双引号把表达式括起来,如: let "count + =1" 否则会导致语句错误。 local 命令 语法: local
[name [= value]] 摘要: 用于创建不能传给子Shell的变量。这个命令仅在过程内部有效。 简单说来,local命令创建的变量不能被子Shell存取。因此,只能在函数内部使用local命令。我们可以在命令行或脚本中使用“变量=值”这种形式的赋值命令。如果使用local时不带实参,那么当前已定义的局部变量列表就送往标准输出显示。 readonly命令 语法: readonly
[options] [name[ = value]] 摘要: 用于显示或者设置只读变量。 Readonly命令使用两个选项: -- 表明选项结束。所有后续参数都是实参 -f 创建只读函数 如果没有用参数,则readonly显示当前已定义的只读变量和函数的列表。 set命令 语法: set
[--abefhkmnptuvxidCHP] [-o option] [name [= value]] 摘要: 用于设置或者重置各种Shell选项。 set 命令可实现很多不同的功能——并非其中所有的功能都与变量有关。由于本节的其他命令重复了通过set命令可用的那些变量选项,所以这里对set命令不做详细说明。 shift命令 语法 shift [n] 摘要: 用于移动位置变量。 shift命令调整位置变量,使$3的值赋予$2,而$2的值赋予$1。当执行shift命令时,这种波动作用影响到所定义的各个位置变量。往往使用shift命令来检查过程参数的特定值——如为选项设置标志变量时。 typeset命令 语法: typeset [options] [name [=
value]] 摘要: 用于显示或者设置变量。 typeset 命令是declare命令的同义词。 unset命令 语法: unset [options] name [name …] 摘要: 用于取消变量定义。 unset命令使用两个选项: -- 表明选项结束,所有后续参数都是实参 -f 创建只读函数 unset命令从Shell环境中删除指定的变量和函数。注意,不能取消对PATH、IFS、PPID、PS1、PS2、UID和EUID的设置。如果我们取消RANDOM、SECONDS、LINENO或HISTCMD等变量的设置,它们就失去特有属性。 5.4 过程 本节介绍如何创建称做过程(procedure)的命令包。每当需要时就可调用这些命令包,它们用来存放在脚本中经常调用的一组命令。它们可以使脚本小而精,因为同样的命令序列不需要在整个脚本中重复多次。 Shell过程很少被使用,因为实质上Shell脚本追求的目标是小而且直截了当。然而,在个别情况下也会需要由过程提供额外帮助。其最大好处之一是允许创建高级别名。不是简单的把ls–l化名为ll,而是通过使用和管理过程中的参数可以得到更多创新。本章仅涉及基本的过程语法和如何使用参数。 过程(Procedure)具有给定名字(供今后使用)的命令块。过程是给定名字的Shell命令块,所以每当需要时就可使用它们。过程把代码组织成易于理解和易于使用的片断。让我们逐渐地构建脚本,按着这种方式测试它们。 当我们准备完成一个复杂的脚本时,应把它划分成若干任务,为每个任务建一个过程。随着我们开发出一小群过程,就会发现,写脚本变得容易了——有时就像用新方法把过程串在一起那样容易。这就称做代码重用(code reuse)。 当过程被source命令作用时,它们就被定义,好像我们输入它们所包含的Shell 命令。利用source命令可以使过程在原环境中执行(也可以用一个·代替source命令)。例3-1的命令行表示source命令在起作用。其中上讲,source命令表明该脚本并不在子Shell内部运行,而是运行在当前Shell环境中。 可以由Shell命令行直接调用过程(被source命令处理后)。事实上,创建很多过程都是为了增强现有的Shell命令,而创建其他过程是为了组合命令或者在显示一个命令的输入之前对它进行处理。 过程看起来像什么呢?例5-10给出了一个过程,它把该tar文件移到存储区分中,跟踪对tar文件的提取,展开tar文件。untar过程建立一个审计流水帐,它列出所用的tar文件的名字和其中的文件。 例5-10 untar——为tar命令创建一个审计流水帐 # ! /bin/bash untar ( ) { cp $1
store #
1 echo
"-----" >>
log/tar.log # 2 echo $1
>>
log/tar.log #
3 date
>>
log/tar.log #
4 tar tf $1
>>
log/tar.log #
5 tar xvf
$1 #
6 rm -rf
$1 #
7 ln -s
store/$1
$1 #
8 } 这个脚本可以完成一项比较复杂的工作: 1)把tar文件拷贝到/store目录。在我的系统上,/store目录是一个单独的分区,我只用它存放tar文件,这样以后出现紧急事件时,就可以使用它们。 2)把一个分隔符行附加到用户的主目录的日志(log)文件中。这个日志文件记录tar命令的活动情况,供以后审计使用。 3)把tar文件的名字附加到日志文件。 4)把时间标签附加到日志文件。 5)把tar文件内部的文件清单附加到日志文件。 6)提取tar文件内部的文件并将其展开。 7)删除tar文件,以节省当前分区中的空间。 8)创建一个符号连接,连到当前目录中tar文件(在/data分区中)的新位置。这个符号连接并不是时常需要的,但是按磁盘空间来说它很便宜。 注意: 事实上,运行untar脚本还需要存在 /store目录,并且当前用户对目录可写。如果不是这种情况,那么可以在第1、7和8行之外加注释;或者简单地在主目录中创建一个存放目录,修改第1行和第7行,用~/store代替/store。 读者应该熟悉这个语法。$1位置变量在本章前面的内容中已介绍过,在那里它用来存取命令行参数。在例5-10中,它用来存取给该过程的参数。在这种情况下,它就是正被取消归档的tar文件的名字。 下面是该脚本执行的示例 $ chmod + x untar $ source untar $ untar medined.tar 提示: 运行一个过程不需要任何特殊机制,简单地使用它,就像任何其他Shell命令一样——当然,该脚本要被source命令处理之后。我们可以由命令行或者由另一个脚本文件运行untar过程。一旦过程被source命令处理,它就变成该Shell的一部分,可用于“常规”Bash命令能出现的任何地方。 untar命令不显示任何输出。相反,其信息被送往称做~/tar.log的日志文件中。如果我们想试一试这个过程,那么要检查在主目录中创建的这个日志文件。在以上命令序列执行后,日志文件如下所示: ------- medinets.tar Sat Mar 14 15: 42: 31
EST 1998 name.pl desk.pl pen.pl 这个输出表示,在1998年3月14日下午3:42从medinets.tar文件中提了三个文件。如果我们与tar文件打得交道很多,我们想记住一个特定文件来自哪个tar文件,那么这个消息可能迟早有用。 如前面提到的,可以在脚本内部使用过程(或我们创建的任何其他过程)。调用一个过程就意味着Shell停止执行当前的一系列命令,执行流跳到该过程内部的命令那里。当过程完成时,Shell又跳回到该过程的调用点。程序继续从那一点向下执行。过程定义的基本语法是: 过程名 ( ) { } 亦即:根本没有任何复杂的东西,在“过程名”这个地方使用我们自己的过程名,并且在一对大括号之间放上过程的各个命令。 在过程内部使用变量 在本章中介绍了位置变量——$1、$2等等,当与过程一道使用这些变量时,Shell在进入以前保留变量的值,在退出过程后恢复它们。 让我们更仔细地看看这种行为。例5-11给出一个过程,它简单地说明如何把很多参数传递给它。 例5-11 03lst02.sh——打印所接收的参数个数的简单过程 test ( ) { echo $# } 试执行以下使用例5-11命令: $ source 03lst02.sh $ test one two three 3 $ test one 1 $ test 0 $# 特殊变量总是包含传给脚本的参数个数。在一个过程内部$# 的值出现什么情况呢?请见例5-12。 例5-12 03lst03.sh——考察过程内部$ # 的值 # ! /bin /bash test_two ( ) { echo
"Test Two: $# " } test ( ) { echo
"Test One: $# " test_two
one two test_two echo
"Test One : $# " } 试执行以下使用例5-12的命令: $ source 03lst02.sh $ test one Test One: 1 Test Two: 2 Test Two: 0 Test One: 1 从输出中可以看出,在test_two被调用之前$# 变量被保存起来,以后,在退出test_two之后,恢复其原有值。 5.4.2 shift 命令 3.2 shift命令 shift命令调整位置变量,使$3的值赋给$2, $2 的值赋给$1。当执行shift命令时,这种波动作用影响所定义的任何位置变量。shift命令往往用于核查传给过程的参数的特定值——如下面示例中为选项设定标志(flag)变量。 例5-13 核查其所有参数是否是-x选项,然后将下面的参数作日志文件的名字。 例5-13 test_shift——使用shift命令设置标志变量 # ! /bin/bash for element in $@ do #
ignore all elements except "-x" if
test "$1" = "-x" then logfile=$2 fi shift done echo "logfile=
$logfile" 下面是这个脚本执行的示例: $ chmod + x test_shift $ test_shift one two three logfile = $ test_shift one –x shift.log
three logfile = shift.log 当循环语句遇到-x字符串作为一个参数时,就把下一个参数(即$2)的值赋给logfile变量。shift命令保证依次检测每个参数。 5.4.3 建立局部过程变量 从例5-12可以看出,Shell保存并恢复位置变量的值。实际上,当进入一个过程时,其$ #变量(或位置变量)的值都是局部的。然而,普通变量(regular)不能受到这个特殊对待。一般情况下,它们的值是全局的。例5-14解释了多数变量的全局属性。 例5-14 03lst06.sh——变量默认是全局的。 #! /bin/bash test_two ( ) { test_var =5 echo
"Test Two: $test_var" } test ( ) { test_var =1 echo
"Test One: $test_var" test_two echo
"Test One: $test_var" } 利用例5-14 试执行以下命令序列: $ source 03lst06.sh $ test Test One: 1 Test Two: 5 Test One: 5 这些结果表明,test_two过程能“永久地”改变$ test_var的值(当过程退出时其初始值不被恢复)。这意味着对两个过程来说$ test_var是全局的。 可以利用local命令改变这种性质。利用local命令对过程的变量所作的修改不影响其他过程。例5-15显示出这种情况。 例5-15 03lst07.sh——local命令所起作用 #! /bin/bash test_two ( ) { test_var =5 echo
"Test Two: $ test_var" } test ( ) { test_var =1 echo
"Test One: $ test_var" test_two echo
"Test One: $test_var" } 使用例5-15 试执行以下命令序列: $ source 03lst07.sh $ test Test One: 1 Test Two: 5 Test One: 1 请注意$ test_var最初的值是如何保留的。如果我们创建大的脚本,包括很多过程,那么迟早要用local命令。使变量值局于一个过程就更容易理解它们,因为在一个过程中所做的修改不影响其他过程。 5.4.4 过程返回值 一个过程的返回值始终是其最后执行的那条命令的返回值。 提示: 如果我们愿意,不必使用return ( )函数来返回值,因为Bash会自动返回最后计算的表达式值。 我们用过的程序设计语言或许能区分开函数和子例程。二者的差别在于函数返回值,而子例程不返回值。Bash不做这种区分。所有的过程、命令、程序和脚本都有返回值——即使仅为表示成功的默认值(即0)。 5.5 脚本执行命令 本节介绍控制脚本执行的命令。迄今为止本书所有介绍的Shell脚本基本上是顺序执行的。尽管我们有必要使用顺序脚本,但多数时间却会要求更多的灵活性。本章提供的命令能测试过程和命令的返回值,并且执行一个以上的语句块。我们也能见到如何捕获信号和随意离开脚本,而不是仅在脚本文件的末尾才离开。 表5-7 列出的命令提供了脚本的灵活性或对它的控制。利用这些命令可做出判定。重复执行命令块(也称做循环),以及用其他方式控制脚本。 表5-7 控制脚本执行的命令
注意: 为了避免对Shell脚本的读者(或我们自己!)造成混乱,给变量和过程起名字时应与Shell命令区分开。 5.5.1 exit 命令 语法: exit [n] 摘要: 强行使一个脚本终止,将n的值返回给调用进程。 exit命令允许我们用受限方式在两个脚本间进行通信。它给一个脚本分配一个终止值,而该脚本可以在另一个脚本中计算。利用下述三个小的脚本(例5-16~例5-18)看一看这种情况是如何实现的。 例5-16 给出success脚本。当它运行时,直接以0值终止,这是标准的成功指示符。 例5-16 success——永远返回成功值的命令 # ! /bin/bash exit 0 例 5-17给出failure脚本。当它运行时,直接以值1终止,这是标准的失败指示符。实际上,任何非0值都表示失败。有些程序对不同的非0值赋予特定的意义。 例5-17 failure——永远返回失败值的命令 # ! /bin/bash exit 1 例5-18 test_return——测试它的第一个参数或实参的命令 # ! /bin/bash if `$1` then echo "
The Test Worked. " else echo "
The Test Failed. " fi 有关if命令的更多信息,参见本章后面的5.5.3节。 下面是这些脚本执行情况的示例: $ chmod + x success failure
test_return $ ./test_return ./success The Test Worked. $ ./test_return ./failure The Test Failed. test_return脚本运行其实参,并测试其终止值。结果终止值为0,那么就表示成功,从而运行第一个echo命令。否则就表示失败,运行第二个echo命令。 success 和failure脚本显式地告诉exit命令用哪个值;我们也可以用Shell命令替换字面值。例如,以下代码行是完美的: exit `failure` 假设我们已经建立了failure脚本,这可能是增加脚本文件可读性的更好的方式(是以某些额外的CPU周期为代价)。但是当我们把脚本从一台机器移植另一台机器时,也需要把success和failure脚本移过去。或许这不是最好的办法。 5.5.2 trap命令 语法: trap [-1] [commands] signals] 摘要: 让我们的脚本接受信号,并有选择地对它们起作用。 什么是信号?信号就是当特定事件发生时,由Shell向我们的脚本发送一个信号。例如,如果我们的脚本拨入另一台机器,此时电话线断了,那么就产生一个信号——SIGHUP或者挂断(hangup)信号。 trap命令的最简单用途就是用它忽略信号。如果想让我们的脚本忽略CTRL-C组合键,可在脚本前面使用下述代码行: trap " " 2 或者 trap " " SIGINT 例5-19的程序从键盘(实际是标准输入,但我们不想太技术性了)上读取输入行。用户可以按CTRL-C不会导致程序中断。当用户只键入“a”,然后按< ENTER>,该脚本就循环。如果读到任何其他输入,同脚本结束。 例5-19 test_ sigint——用SIGINT进行实验的脚本 #! /bin/bash #trap "" SIGINT input= "a" while [$input = "a"
] do echo "
Type 'a' to continue, anything else to quit: " read input done echo " The script is
done. " 首先运行被注释出的带trap命令的脚本(在第二行开头使用了#字符)。这导致以下输出: $ chmod + x test_sigint $ test_sigint Type 'a' to continue, anything
else to quit: a <
- while 'a' is entered, the script loops. Type 'a' to continue, anything
else to quit: ^C <-CRTL-C
is pressed. And the script exits. 注意: 当按下CTRL-C时,该脚本立即退出,最后的echo命令未被执行。 现在试一下例5-19,它带激活的trap命令。删除用来注释trap行的#字符。下面是这个脚本的运行情况: $ test_sigint Type 'a' to continue, anything
else to quit: a <
- while 'a' is entered, the script loops. Type 'a' to continue, anything
else to quit: ^C <-CRTL-C
is pressed. And the script does not exit. Type 'a' to continue, anything
else to quit: w the end The script is done. 此刻我们可能感到奇怪,Shell能用的其他类型的信号是什么。利用带-1选项的trap命令可以发现信号号码和助忆名。 $ trap-1
表5-8对这些信号分别作了简要说明 表5-8 信号列表
当接到一个信号时如果想做某些事情,那么就要创建一个信号处理程序。例如,当脚本结束时,我们想显示部分消息(“Bye Bye”)。要完成这个任务,需要执行以下步骤: 1)开发一个脚本,显示部分消息。 2)捕获EXIT信号。 第1步相当容易,直接用echo命令,如下所示: echo "Bye Bye" 把这个命令用单引号括起来,作为传给trap命令的参数。例5-20说明如何把echo和trap命令组合在一起。 例5-20 PARTING——MSG——使用trap命令显示部分消息 #! /bin/bash trap 'echo "Bye
Bye"; exit' EXIT echo "Line One" 下面是这个脚本执行的示例: $ chmod +x parting_msg $ ./parting_msg. Line One Bye Bye 注意,为什么在“Bye Bye”消息之前打印echo“Line One”呢?这表示创建了信号处理程序,但在脚本结束(生成EXIT信号)之前未被解发。 信号处理程序也称做事件或中断处理程序。 5.5.3 if命令 语法1: if test-commands then . . commands
. . fi 语法2: if test-commands then . . commands
. . fi 语法3: if test-commands then . .
commands . . elif test . .
then commands . . else . .commands . . fi 摘要: 判定一个或多个活动历程。 很多时候,脚本要根据某些准则决定执行哪组命令块。例如,如果某个应用程序的配置文件不存在,我们就要建立一个。 If test ! –e config.dat then echo " Create
the file " fi test命令可以检查各种条件。在上面示例中,它查看一个名为config.dat的文件在当前目录中是否存在,然后,感叹号(即!)字符将测试意义取反,所以,如果文件不存在,则test命令返回真;如果文件存在,则返回假。 5.5.4 case命令 语法: case string in regular_expression_1) . .
commands. . ;; regular_expression_2) .
.commands. . ;; esac 摘要: 根据单个变量的多个值做出判定。 当需要针对一个变量的很多不同的值进行测试时——例如针对具体人名的第一个字核查名为GIVEN——NAME的变量,以便对某些变量初始化。在这个实例中,case语句的用法如下所示: case $GIVEN_NAME in David* ) echo "
David, Welcome. You have ALL privileges. " Kathy*) echo
"Kathy, Welcome. You have READ privileges." ;; * ) echo
"Sorry, I don’t know you. Please go away." exit 1 ;; esac 这个例子表明case命令的灵活性。GIVEN_NAME变量在两个正则表达式David*和Kathy*中被测试。如果GIVEN_NAME的值是以“David”或者“Kathy”开头,就执行对应的echo命令。最后的正则表达是*,它匹配所有的字符串。因此,如果没有找到其他匹配,就运行最后一组命令。 5.5.5 for语句 语法: for variable [in list] do . . command block
. . . done 摘要: 在值表上迭代,对值表中每个值执行一次指定的语句块。 理解for语句有两个关键点,一个是命令块的概念,另一个是迭代的概念。 命令块是我们想一起执行的一组命令。在上面的语法项中,命令块用do和done关键字括起来。迭代(针对这里的介绍)是命令块的一次执行过程。命令块(command block)一起执行的一组命令。迭代(iteration)一个命令块的一次执行过程。 把这些命令放在一起就形成了for命令,它在一个值表上迭代执行,对值表中的每个值执行一次命令块。 使用for命令的最简单的示例之一就是找出列表中的最大数(见例5-21)。 例5-21 04LST06——使用for命令找出最大数 #! /bin/bash max_n=0 for n in 9 8 7 6 5 4 3 2 1 do if test
$max_n –lt $n then max_n
= $n fi done echo "The maximum value
is & max_n. " 下面是该脚本执行的示例: $ chmod + x 04lst06 $. /04lst06 The maximum value is 9. 虽然这个例子是有些简单了,但是可以利用它说明变量n如何取得表中每个连续的值。我们查看一下for命令前面两个迭代过程的变量max_n和n的值(见表5-10) 表5-10 查看例5-21 中变量的值
我们能够看出if命令如何在该循环的第一次迭代过程中把9赋给max_n吗?很好。在第二次迭代过程中,n的值是8,它小于max_n,因此,不执行任何赋值命令。 4.6 while命令 语法: while test-commands do . .commands.. done 摘要: 重复执行命令块,直到test-commands命令返回0值。 在上一节我们介绍了for命令,它利用一个列表来控制一个语句块被反复执行多少次。在这一节我们学习while命令,它并不使用列表来控制循环。相反,执行test命令并计算其返回值。当返回值为0时,while命令结束。 我们看一下如何利用while命令对一个命令块执行指定次数。例5-22使用while命令对一个命令块反向计数,从5到1。 例5-22
count_backwards_while——利用while命令把1到5反向计数 # ! /bin/bash count=5 while test $count -gt 0 do echo $count let count=$count-1 done echo "The end count is
$count." 下面是该脚本的执行情况: $ chmod + x
count_backward_while $. /count_backward_while 5 4 3 2 1 The end count is 0. 这个脚本执行命令块5次,如输出所示。应重点注意,结束时count的值是0,而不是我们乍一看到代码时想象的值1。当count的值为1时,test命令为真,所以再次执行该命令块,从而count减1变为0。然后,下次执行test命令时,测试失败,循环结束,同时count等于0。 应注意,如果第一次test命令就失败,该命令块从未得到执行。通过颠倒上例中的test命令就可见到这种现象: while test $count –lt 0 下面是该脚本执行的情况: % . /count_backwards The end count is 5. 这个例子说明,当循环开始时,如果test命令失败,则命令块从来也得不到执行。 4.7 until命令 语法: until test-commands do . .statements. . done 摘要: 重复执行一个命令块,直到test命令成功或者返回0。 除测试条件相反以外,until命令等价于while命令,即:它不是测试到失败,而是测试到成功为止。为了解释until命令,可以利用针对while命令用过的(例5-22)相同的脚本,简单地把测试条件由-gt改为-lt,如例5-23所示。 例5-23
count_backwards_until—利用until命令,将1到5反向计数 count=5 until test $count –lt 0 do echo $count let
count=$count-1 done echo "The end count is
$count." 下面是该脚本执行情况: $ chmod + x
count_backwards_until $ . /count_backwards_until 5 4 3 2 1 0 The end count is –1. 这个例子与例5-21之间的最大差别是count的结束值是-1而不是0。这是由于0并不小于0,因此,命令块以多执行一次。 4.8 break 命令 语法: break [n] 摘要: 退出当前的命令块。 break命令立即退出一个命令块。例如,我们想把一个命令执行5次,除非查出错误条件(我们想由这一点退出),如例5-23所示。 例5-23 test_break——使用break命令退出一个循环命令块 count=5 while $ count –gt 0 then . .commands. . if test –e
error_file_flag fi done 如果该语句块内的一个命令创建了一个error_file_flag文件,那么while命令就终止,不再执行迭代过程。 给break命令提供要退出的循环号码,就可以用它从嵌套循环命令中退出。首先,我们看一个使用break命令的嵌套循环的例子,如例5-24所示。 例5-24 nested_loop——如何从嵌套循环中退出 # ! /bin/bash x=3 while test $x –gt 0 do y=3 while test $y –gt
0 do echo
" [$x, $y] " if
test $y –eq 2 then echo
"breaking." break fi let
y=$y-1 done let x =$x-1 done 下面是该脚本执行情况: % chmod + x nested_loop % ./nested_loop [3, 3] [3, 2] breaking. [2, 3] [2, 2] breaking. [1, 3] [1, 2] breaking. 我们会看到,每当内层循环变量y等于2时,就退出内层循环,而外层循环继续执行另一次迭代。y称做循环变量,因为它控制内层循环的迭代过程。 4.9 continue命令 语法: continue [n] 摘要: 跳过命令块中剩余的命令。 continue命令立即开始循环命令的下一次迭代过程。请参考下面的示例: 例5-25 using_continue——使用continue命令 #! /bin/bash x=3 while test $x –gt 0 do y=3 while
test $y –gt 0 do echo
"[$x, $y]" if
test $y –eq 2 then echo
"continuing" continue fi let y = $y-1 done let x = $x-1 done 下面是该脚本执行情况: $ chmod + x double _loops $ ./double_loops [3, 3] [3,
2] < - these two lines continuing < -
indefinitely repeat [3 , 2] continuing [3, 2] continuing 5.6 控制Shell 本节包括Shell程序设计的某些零散和最后的内容。我们会学到建立一个命令表来一次执行一个以上的命令,以及为什么这种技术很有用;还将学到如何用文件代替键盘给程序提供输入,以及其他很多有趣的技术。 5.6.1 创建命令表 每个Shell命令是一个进程,当命令停止时,每条命令都产生一个退出值,命令执行成功时典型的退出值是0,任何其他的退出值都表示某种形式的失败。我们会发现,对特定的问题分配退出值很有用。例如,我们的脚本使用退出值5表明初始化文件未找到。作为选择对象,我们可以使用200~250之间的值表示不同类型的输入错误。 命令表可以利用一个命令的退出值来控制是否执行另一个命令。例如,如果初始化文件不存在,就不能开始后备进程。 表5-11 示出创建命令表时所用的表示形式。 表5-11 三种命令表
我们看一下每种类型的示例。如果a失败,则与类型阻止b执行。何时需要这种能力呢?我们可以利用它确保在运行一个程序之前初始化必须文件存在: test -e init.dat &&
count_users 这一行利用test命令查看名为init.dat的文件是否存在。如果它存在,则Shell执行count_users程序。 或列表类型可用来创建初始文化(如需要的话): test -e init.dat ||
create_init count_users 这行使用test命令核查init.dat文件,如果它尚未存在,则运行create_init程序。最后运行count_users命令。 把这两种列表形式组合起来时使用,就可编写出智能化脚本,在运行关键应用程序之前核查系统的状态。 5.6.2 创建复合命令 复合命令(compound command)执行命令表,好像它是单个命令那样。表中最后一个命令的退出值就作为整个复合命令的退出值。表5-12示出两种类型的复合命令。 表5-12 两种类型的复合命令
注意: 不要把复合命令所用的圆括号与影响表达式中运算符优先级的圆括号混为一谈。表达式仅用于赋值或条件判断语句。如果在所见到的任何字符的开头有一个圆括号,那么很有可能它是一个复合命令。 (list)表示形式让我们把若干一起组合在同一个子Shell中。如果我们想临时改变环境变量,这种能力就特别有用。 这种复合命令的典型示例是: parentDir = ` (cd .. ; pwd)` 这一行将parentDir变量置为父目录的名字。如果当前目录是/var/local,则parenDir就置为/var。我们看一下交互式地运行下述命令会发生什么情况: % PS1 = " [\ w]
%" # 1 [/var/local] % cd . . ;
pwd # 2 /var [/var] % cd local [/var/local] % (cd . . ;
pwd) # 3 /var [/var/local] % 编号为1的行改变Shell提示符,从而显示出当前目录——更容易看到该命令产生的结果。 编号为2的行执行两条命令,即cd命令和pwd命令。cd命令改到新目录,pwd显示当前式作目录。应注意,cd命令改变了当前目录,但它并不是我们需要的行为。其目的是恢复父目录名字而不更改当前Shell。 在编号3的行中用圆括号括起来的表达式运行与# 2行相同的命令,不同点是它们是在子Shell中运行,因此它们与注册Shell隔离开。cd命令照常对子Shell起作用,所以pwd显示注册Shell父目录的名字。 现在回到原来的复合命令的例子: parentDir = ` (cd . . ; pwd)` 执行倒引号括起来的字符串,其产生的输出代替原来命令中用倒引号括起来的字符串。从而,这个示例简化为: parentDir = /var 顺便说一下,即使在“功能更强的”perl和Tcl语言中,利用(cd ..; pwd)对Shell转换也可能是确定父目录名的最容易的方法。学习基础知识(即Shell语言)总是好的想法。 如果init.dat文件不存在,下面例子就显示一个错误消息。在运行这个示例之前,我确定使init.dat不存在,所以一定会出现错误。 !!!!例子不存在 5.6.3 读取输入 虽然Shell程序通常说来不需要与用户进行大量的交互,但我们可以使用read命令从STDIN申请一行输入。 read命令的一般语法是: read [
variable_name, . . .] 如果我们没有提供变量名,则输入行就赋予REPLY。如果只提供了一个变量名,则整个输入行赋予该变量。如果提供了一个以上的变量名,则输入行分为若干字。一个接一个地赋予各个变量,而命令行上的最后一行变量取得剩余的所有的字。 下面是read命令的某些示例: %
read <
- use the dafault variable This is a great
day! < - which is named REPLY. % echo $REPLY This is a great day! 这是一个简单的示例,使用默认变量来保存整个输入行。 $ read one
two < - use your own
variables This! Is a great day! $ echo " * $ one * * $
two *" * This! Is * *a
great day! * < - spaces end words, <
- not exclamation characters 这个例子稍微难一些。输入行第一个字的值赋予第一个变量。但如何区分各个字呢?原来Bash有一个名为IFS(内部字段分隔符的缩写)的变量,它保存字区分字符。通常,用空格、制表符和换行符区分各个字。这就是为什么在最后的例子中第一个字是“This! is”。任何讲英语的人会说“This! is”应是两个字,但是Bash只认识空格、制表符和换行符。 通过在IFS变量的末尾添加感叹号,可以使Bash更加灵活。例如: $ IFS = "$IFS \!" $ read one two This! is a great day! $ echo " * $one * * $two
*" * This * is a great day! 现在Shell识别“This”和“is”是分开的字。注意感叹号定界符并不出现。Shell抛弃各定界符。 注意: 对把感叹号附加到IFS变量的方式应格外小心,如果感叹号没有被反斜线“转义”,则Shell把它解释成关于Shell命令历史的特殊字符。本书中并不包括命令历史的题目。如果我们感兴趣的话,则着手学习命令历史的最佳地点是Bash的手册页(使用man bash命令)。 如果我们决定不想通过修改IFS变量对Shell环境做长期修改,那么可以使用如下所示的组合命令: $ (IFS= "$IFS \! ";
read one two; echo " * $ one * *$ two*") This! Is a! great! day. * This* * is! a!
great! day. * 5.6.4 使用后台进程 我们通过键盘与之通信的Shell(即我们的注册Shell)通常每次运行一个程序。我们键入一个命令,就执行相应的程序,并显示其结果。键入另一条命令,就执行下一个程序,并显示它执行的结果。这是命令井然有序地前进、执行和显示结果的过程。 然而当我们运行的一个程序花费很长时间才完成时,问题就出来了。例如,假设我们正使用find命令查找时间超过两周的所有备份文件。这样一个命令或许要花费5分钟或者更长时间才能完成。在此期间,终端不能使用,因为find命令对它进行控制。 解决这个问题的办法是使find命令在后台运行。当一个程序在后台运行时,它们不再控制键盘。事实上,它们很难获取输入,除非我们用管道由一个文件传送给它们。 后台进程可能相当方便。它们让我们同时运行多个程序。实际上,我们可以同时运行大量的程序。 每当一个程序执行时,就分配给它一个进程ID号,即PID。利用PID可以查找有关运行程序的信息。一旦该程序完成,则PID消失。 在一个命令行的末尾添加一个&字符,就把程序放入后台。例如: $ find / -name
"temp.dat" -print & [1] 301 $ find: /etc/uucp: permission
denied find: /var/spool/at:
permission denied date Mon Jun 22 14:13: 01 EDT 1998 [1] + Exit 1 find/
-name "temp.dat" -print $ 方括号中的数字告诉我们启动了多少后台进程。在上面情况下,find命令是第一个后台进程。第二个数字(即301)是进程ID与。每当我们启动一个新的后台进程时,就会见到一个新的PID。这个PID也可以通过ps命令显示出来: $ ps PID TTY STAY TIME COMMAND 263 1 S 0:00/BIN/LOGIN
– MEDINED 301 1 R 0:00
find / -name temp.dat –print 323 1 R 0:00
ps 用斜体字示出ps命令的输出中的相关行,其中重要的信息包括PID、TTY和正被执行的命令行。了解如何停止后台进程就像了解如何启动它们一样重要。我们创建一个从来也不终止的后台进程,这样就能学会如何停止失去控制的或者不起作用的进程。例5-26中的脚本从不终止,因为后台进程断开与键盘的关联。因此,没有任何输入,read命令就永远等待下去。 例5-26 background.sh——从不终止的脚本 read 让我们启动几个从不终止的后台进程: % background.sh & [1] 341 % background.sh & [2] 342 [1] Stopped (tty input)
background.sh % background.sh & [3] 343 [2] Stopped (tty input)
background.sh % ps PID TTY STAT TIME COMMAND 327 1 S 0:00
/bin/login -medined 328 1 T 0:00
-bash 341 1 T 0:00
-bash 342 1 T 0:00
-bash 343 1 T 0:00
-bash 344 1 R 0:00
ps [3] Stopped (tty input)
background.sh ps命令示出所有三个后台进程(斜体字的行)。由于每个进程都等待着来自TTY的输入,所以被停止——因为read命令未满足。 有两种方法能摆脱后台作业,可以退出系统或使用kill命令,如下所示: % kill -SIGKILL 341 342 343 [1]
–killed backgrounds.h [2]
+Killed background.sh [3] +
Killed background.sh % ps PID TTY STAT TIME COMMAND 327 1 S 0:00
/bin/login-medined 328 1 T 0:00
–bash 348 1 R 0:00
ps 重点应注意,每次只能把一个命令放入后台。如果我们键入find/ -nmae"sam.dat"
–print;find / -name
"jon.dat" -prin t &, 那么只有最后的find命令在后台执行,而第一个find命令在前台执行,并且在该命令完成之前,终端被锁住。 我们可以使用复合命令来避免这个问题,如下所示: (find / -nmae
"sam.dat" –print; \ find / -name
"jon.dat" -print) & 关于复合命令在本章前面的5.6.2节中作过介绍 5.7 Shell 的简单编程 编写Shell 脚本的过程其实并不想想象的那样复杂,可以简单的这样理解:就是把许多Linux、UNIX命令写入或集成到一条新的命令里,就像Dos下的批处理命令一样。比如几个例子来说,我想编写一个数据库数据完全备份的脚本,该如何做呢 首先,用vi 命令开始为自己要编写的脚本命名,并在此命名文件中插入要执行的命令,例如: $ vi myname 然后,在新建的myname 文件中加入如下信息: ORACLE_HOME=/export/oracle
#设置oracle 用户的环境
export
ORACLE_HOME
ORACLE_SID=bdwx
#设置 oracle 数据库
SID export
ORACLE_SID
cd
/export/oracle
#切换目录
/export/oracle/bin/exp
USERID=system/123456 FILE=fully.dmp full=Y
#完全导出数据库数据
gzip fully.dmp
#压缩完全导出的数据文件
mv fully.dmp.gz
/bak/full.dmp.gz #移动文件到指定目录下 tar rvf /dev/rmt/0h
/bak/fully.dmp.gz #将文件备份到磁带机 接下来,赋予myname 可执行权限,即可运行myname 的脚本: $ chmod a+x myname $ ./myname 也可以不赋予myname 可执行权限,直接运行下面的命令: $ sh myname 再给大家举一个更简单的例子,比如说写一个获得系统时间的脚本,操作如下:
在myname2文件中加入:
下面运行myname2脚本,会显示出脚本中的命令:
Shell脚本编辑应该注意的事项:(1)在shell编程中经常要对某些正文行进行注释,以增加程序的可读性。在Shell中以字符“#”开头的正文行表示注释行。(2) 在赋予脚本可执行权限时,应该安全的考虑问题,如设置700权限等,这样可以保证只有用户本身可以执行此脚本命令,而组用户和其它成员没有任何读、写和执行的权限(当然root用户可以执行)。 (3)任何优秀的脚本都应该具有帮助和输入参数。 下面创建的脚本就是一个很标准的实例:
在文件myname3 加入下面信息:
修改权限:
执行脚本:(其中test.gz为压缩文件)
在myname3脚本里使用了一个特殊的变量$1。该变量包含了传递给该程序的第一个参数值。也就是说,当运行:./myname3
test.gz时,$1 就是字符串test.gz。 这样,一个可以自动解压bzip2, gzip 和zip 类型的压缩文件的Shell脚本就写好了,而且只有用户本身可以执行。 5.8 Shell 程序的调试 有句话说的好,程序员(人)总是会犯错误的,而计算机是不会错的。所以在编写程序时,我们难免会出错,有了错,我们就要调试。 Shell 程序最简单的调试命令当然是使用echo命令。使用echo在任何怀疑出错的地方打印任何变量值。这也是绝大多数的shell程序员要花费80%的时间来调试程序的原因。Shell程序的好处在于不需要重新编译,插入一个echo命令也不需要多少时间。 然而有的情况,比如说,脚本里的内容很多,我们就很难找到脚本里哪个地方有错误,遇到这样的情况,我们要使用上面5.1.3 Shell 命令分析一节中讲到的Shell 命令的-x 参数,执行后会进入跟踪方式,显示所执行的每一条命令,可用于调度和对Shell 程序的调试。 例如:上面讲到的myname3脚本当调试成功时,会显示如下信息:
当myname3脚本当调试不成功时,会显示如下信息:
根据显示的信息和结果,程序员可以针对报错的信息对脚本跟踪和调试。shell还有一个不需要执行脚本只是检查语法的模式。可以这样使用:sh -n your_script,这将返回所有语法错误。 5.9 Shell 脚本举例 (1)下面是一个选菜单的脚本。
下面是该脚本运行的结果:
(2)在下面的脚本例子中,将分别打印ABC到屏幕上。
下面是该脚本运行的结果:
(3)下面是一个更为有用的脚本showrpm,其功能是打印一些RPM包的统计信息。
这里出现了第二个特殊的变量$*,该变量包含了所有输入的命令行参数值。如果当运行“showrpm openssh.rpm w3m.rpm
webgrep.rpm”, 此时$* 包含了3个字符串,即openssh.rpm, w3m.rpm and webgrep.rpm。 (4)在下面脚本例子中,对多个文件进行重命名,并且使用here documents打印帮助。
这是一个复杂一些的例子。让我们详细讨论一下。第一个if表达式判断输入命令行参数是否小于3个 (特殊变量$# 表示包含参数的个数) 。如果输入参数小于3个,则将帮助文字传递给cat命令,然后由cat命令将其打印在屏幕上。打印帮助文字后程序退出。 如果输入参数等于或大于3个,我们就将第一个参数赋值给变量OLD,第二个参数赋值给变量NEW。下一步,我们使用shift命令将第一个和第二个参数从参数列表中删除,这样原来的第三个参数就成为参数列表$*的第一个参数。然后我们开始循环,命令行参数列表被一个接一个地被赋值给变量$file。接着我们判断该文件是否存在,如果存在则通过sed命令搜索和替换来产生新的文件名。然后将反短斜线内命令结果赋值给newfile。这样就达到了脚本运行的目的:得到了旧文件名和新文件名。然后使用mv命令进行重命名。 (5) 下面是一个叫做xtitlebar的脚本,使用这个脚本可以改变终端窗口的名称。这里使用了一个叫做help的函数。
(6) 二进制到十进制转换的脚本,例如将二进制数 (比如 1101) 转换为相应的十进制数。这也是一个用expr命令进行数学运算的例子。
该脚本使用的算法是利用十进制和二进制数权值 (1,2,4,8,16,..),比如二进制"10"可以这样转换成十进制: 0 * 1 + 1 * 2 = 2。 为了得到单个的二进制数我们用了lastchar 函数。该函数使用wc –c计算字符个数,然后使用cut命令取出末尾一个字符。Chop函数的功能则是移除最后一个字符。 (7) 如果我们想将所有发出的邮件保存到一个文件中,但是在过了几个月以后,这个文件可能会变得很大以至于使对该文件的访问速度变慢。下面的脚本可以解决这个问题。这个脚本可以重命名邮件并保存文件(假设为outmail)为outmail.1,而对于outmail.1就变成了outmail.2,依次类推直到outmail.n。
此脚本运行原理:这个脚本在检测用户提供了一个文件名以后,进行一个9到1的循环。文件9被命名为10,文件8重命名为9等等。循环完成之后,将原始文件命名为文件1同时建立一个与原始文件同名的空文件。依次类推,这样就可以实现重命名邮件并保存文件了。 看完本节的脚本举例后,希望大家现在可以开始写自己的shell脚本,这样一边学习,一边练习写脚本,大家很快就能熟悉掌握Shell 编程的方法。 5.10 专家答疑 1. Shell 要如何分类呢? 答: 一般来说,Shell可以分成两类。第一类是由Bourne Shell衍生出来的包括sh,ksh,bash,与zsh。第二类是由C Shell衍生出来的,包括csh与tcsh。除此之外还有一个rc,有人认为该自成一类,有人认为该归类在Bourne Shell。 把上面的分类法记住,就可以写出所有Bourne Shell类的Shell或是所有C Shell类的 Shell都可用的脚本程序。 2. 如何产生一个以当日日期为后缀的文件? 答: file =
date '+%m%d'` ; touch todayis.$file 或者做一个shell脚本,例如:
注意定义file变量中的“`”不是“ '”,而是左上角数字键1傍边那个(左单引号)。 3.请问如何用 Shell 脚本修改用户口令? 答: 下面就是一个修改用户口令的脚本,这是个简单的,不过是给root用的脚本。
4.我要如何将 csh 的 stdout 与 stderr 导向到不同的地方呢 答: 在csh中,用">"将stdout导向,用">&"则能将stdout与stderr一起导向。可是不能只单独把stderr转向。最好的方法是: ( command >stdout_file ) >&stderr_file,这个的命令会开一个subshell执行"command";而这个subshell的stdout则被转向到 stdout_file,同时这个subshell的stdout和stderr则都被转向到stderr_file,但是因为 stdour已经先被转向了,所以stderr就会被转到stderr_file了。如果我们只是单纯的不想把stdout做转向,那么就用sh来帮我们吧。例如:
5.写 Shell 脚本程序时,要如何从 terminal 读入字元? 答: 在sh中,我们可以用read,通常是在循环语句中使用,如下例:
在csh中,则用$<,如下例:
很可惜的,csh并没有方法判断空白行和档案结尾(end-of-file)的不同。如果我们要用sh从终端读一个字元,那么我们可以试试下面的脚本:
5.11 课堂练习题 1. 什么叫Shell? 请试着画出有关Shell的Linux、UNIX系统层次结构图。 2. Shell的特点和双重特性是什么? 3. 请Shell与内核交互作用的流程图。 4. Shell都有哪些版本?各自的特点是什么? 5. Shell的启动有几种方法?口述一下具体的步骤。 6. Shell的变量都有哪些? 7. Shell的控制结构都有哪些? 8. 请描述一下Shell编程的具体步骤。 9. Shell程序的调试方法都有哪些? 10.请分别写出一个数据库增量、累计和完全备份的脚本。 |
|