Linux环境中,Shell不仅是常用的命令解释程序,而且是高级编程语言。本讲介绍Shell概述、Shell变量、位置参数、特殊符号、别名、各种控制语句、函数等Shell编程知识。 Shell是Unix/Linux系统中一个重要的层次,它是用户与系统交互作用的界面。在以前介绍Linux命令时,Shell都作为命令解释程序出现:它接收用户输入的命令,进行分析,创建子进程,由子进程实现命令所规定的功能,等子进程终止工作后发出提示符。这是Shell最常见的使用方式。 Shell还是一种高级编程语言,它有变量、关键字,有各种控制语句,如if、case、while、for等语句,支持函数模块,有自己的语法结构。利用Shell程序设计语言可以编写出功能很强、但代码简单的程序。特别是它把相关的Linux命令有机地组合在一起,可大大提高编程的效率,充分利用Linux系统的开放性能,能够设计出适合自己要求的命令。 Shell概述 Shell的概念最初是在Unix操作系统中形成和得到广泛应用的。Unix的Shell有很多种类,Linux系统继承了Unix系统中Shell的全部功能,现在默认使用的是bash。 1.Shell的特点 Shell具有如下突出特点: (1)把已有命令进行适当组合构成新的命令。 (2)提供了文件名扩展字符(通配符,如* 、 ?、 [ ]),使得用单一的字符串可以匹配多个文件名,省去键入一长串文件名的麻烦。 (3)可以直接使用Shell的内置命令,而不需创建新的进程,如Shell中提供的cd、echo、exit、pwd、kill等命令。为防止因某些Shell不支持这类命令而出现麻烦,许多命令都提供了对应的二进制代码,从而也可以在新进程中运行。 (4)Shell允许灵活地使用数据流,提供通配符、输入/输出重定向、管道线等机制,方便了模式匹配、I/O处理和数据传输。 (5)结构化的程序模块,提供了顺序流程控制、条件控制、循环控制等。 (6)Shell提供了在后台执行命令的能力。 (7)Shell提供了可配置的环境,允许创建和修改命令、命令提示符和其它的系统行为。 (8)Shell提供了一个高级的命令语言,能够创建从简单到复杂的程序。这些Shell程序称为Shell脚本,利用Shell脚本,可把用户编写的可执行程序与Unix命令结合在一起,当作新的命令使用,从而便于用户开发新的命令。 2.常用Shell类型 Linux系统提供多种不同的Shell以供选择。常用的有Bourne Shell(简称sh)、C-Shelll(简称csh)、Korn Shell(简称ksh)和Bourne Again Shell (简称bash)。 (1)Bourne Shell是AT&T Bell实验室的 Steven Bourne为AT&T的Unix开发的,它是Unix的默认Shell,也是其它Shell的开发基础。Bourne Shell在编程方面相当优秀,但在处理与用户的交互方面不如其它几种Shell。 (2)C Shell是加州伯克利大学的Bill Joy为BSD Unix开发的,与sh不同,它的语法与C语言很相似。它提供了Bourne Shell所不能处理的用户交互特征,如命令补全、命令别名、历史命令替换等。但是,C Shell与BourneShell并不兼容。 (3)Korn Shell是AT&T Bell实验室的David Korn开发的,它集合了C Shell和Bourne Shell的优点,并且与Bourne Shell向下完全兼容。Korn Shell的效率很高,其命令交互界面和编程交互界面都很好。 (4)Bourne Again Shell (即bash)是自由软件基金会(GNU)开发的一个Shell,它是Linux系统中一个默认的Shell。Bash不但与Bourne Shell兼容,还继承了C Shell、Korn Shell等优点。 3. Shell脚本的建立 Shell程序可以存放在文件中,这种被Shell解释执行的命令文件称为Shell脚本 (Shell Script)。Shell脚本可以包含任意从键盘键入的Linux命令。 建立Shell脚本的步骤同建立普通文本文件的方式相同,利用编辑器(如vi)进行程序录入和编辑加工。例如,要建立一个名为ex1的Shell脚本,可在提示符后输入命令: $ vi ex1 进入vi的插入方式后,就可录入程序行。完成编辑之后,将编辑缓冲区内容写入文件中,返回到Shell命令状态。 4.执行Shell脚本的方式 执行Shell脚本的方式基本上有三种: (1)输入定向到Shell脚本 这种方式是用输入重定向方式让Shell从给定文件中读入命令行并进行相应处理。其一般形式是: $ bash 例如: $ bash Shell从文件ex1中读取命令行,并执行它们。当Shell到达文件末尾时就终止执行,并把控制返回到Shell命令状态。此时,脚本名后面不能带参数。 (2)以脚本名作为参数 其一般形式是: $ bash 脚本名 [参数] 例如: $ bash ex2 /usr/meng /usr/zhang 其执行过程与上一种方式一样,但这种方式的好处是能在脚本名后面带有参数,从而将参数值传递给程序中的命令,使一个Shell脚本可以处理多种情况,就如同函数调用时可根据具体问题给定相应的实参。 如果以目前Shell(以·表示)执行一个Shell脚本,则可以使用如下简便形式: $ · 脚本名 [参数] 以Shell脚本作为Shell的命令行参数,这种方式可用来进行程序调试。 (3)将Shell脚本的权限设置为可执行,然后在提示符下直接执行它。 通常用户是不能直接执行由正文编辑器(如vi)建立的Shell脚本的,因为直接编辑生成的脚本文件没有“执行”权限。如果要把Shell脚本直接当作命令执行,就需要利用命令chmod 将它置为有“执行”权限。例如, $ chmod a+x ex2 就把Shell脚本ex2置为对所有用户都有“执行”权限。然后,在提示符后输入脚本名ex2就可直接执行该文件。注意,此时该脚本所在的目录应被包含在命令搜索路径(PATH)中。例如: $ ex2 Shell接收用户输入的命令(脚本名),并进行分析。如果文件被标记为可执行的,但不是被编译过的程序,Shell就认为它是一个Shell脚本。Shell将读取其中的内容,并加以解释执行。所以,从用户的观点看,执行Shell脚本的方式与执行一般的可执行文件的方式相似。 因此,用户开发的Shell脚本可以驻留在命令搜索路径的目录之下(通常是“/bin”、 “/usr/bin”等),像普通命令一样使用。这样,也就开发出自己的新命令。如果打算反复使用编好的Shell脚本,那么采用这种方式就比较方便。 Shell脚本经常被用来执行重复性的工作,例如,当进入系统时要查看有无信件、列出谁在系统中、将工作目录改到指定目录并予以显示、印出当前日期等。完成这些工作的命令是固定的。为了减少录入时间,可把这些命令建立在一个Shell脚本中,以后每次使用该文件名就可执行这些工作。 另外,完成某些固定工作时需输入的命令很复杂,例如文件系统的安装(mount),要带多个选项和参数。此时,利用Shell脚本存放该命令,以后用时就很方便了。 Shell变量 Shell中也采用变量,用来存放字符串。Shell变量比C语言中的变量简单得多,没有众多存储类及类型的限制,也不需要预先定义、然后才能赋值,可以在使用时“现定义、现赋值”。 Shell有环境变量和临时变量。环境变量是永久性变量,其值不会随Shell脚本执行结束而消失。临时变量是在Shell程序内部定义的,其使用范围仅限于定义它的程序,出了本程序就不能再用它;而且当程序执行完毕,它的值也就不存在了。 1.用户定义的变量 用户定义的变量是最普通的Shell变量。变量名是以字母或下线符打头的字母、数字和下线符序列,并且大小写字母意义不同。如dir与Dir是不同的变量。这与C语言中标识符的定义相同。变量名的长度不受限制。 定义变量并赋值的一般形式是: 变量名=字符串 例如: myfile=/usr/meng/ff/m1.c 注意,在赋值语句中,赋值号“=”的两边没有空格,否则在执行时会引起错误。 变量的值可以改变,只须利用赋值语句重新给它赋值即可。一个未明确赋过值的变量仅含一个空字符串。 在程序中使用变量的值时,要在变量名前面加上一个符号“$”。例如, $ dir=/usr/meng/ff $ echo $dir /usr/meng/ff ←——— 显示结果 $ echo dir dir ←——— 显示结果 如果在赋给变量的值中要含有空格、制表符或换行符,那么,就应该用双引号把这个字符串括起来。例如, names="Zhangsan Lisi Wangwu" 可以将一个命令的执行结果赋值给变量。有两种形式的命令替换:一种是使用倒引号引用命令,其一般形式是: `命令表`。 例如:将当前工作目录的全路径名存放到变量dir中,输入以下命令行: $ dir=`pwd` 另一种形式是:$(命令表)。上面的命令行也可以改写为: $ dir=$(pwd) 2.数组 bash只提供一维数组,并且没有限定数组的大小。类似与C语言,数组元素的下标由0开始编号。获取数组中的元素要利用下标。下标可以是整数或算术表达式,其值应大于或等于0。用户可以使用赋值语句对数组变量赋值。对数组元素赋值的一般形式是:数组名[下标]=值例如: $ city[0]=Beijing $ city[1]=Shanghai $ city[2]=Tianjin $ 也可以用declare命令显式声明一个数组,一般形式是: declare -a 数组名 读取数组元素值的一般格式是: ${数组名[下标]} 例如: $ echo ${city[0]} Beijing 一个数组的各个元素可以利用上述方式一个元素一个元素地赋值,也可以组合赋值。定义一个数组并为其赋初值的一般形式是: 数组名=(值1 值2 ... 值n) 其中,各个值之间以空格分开。 例如: $ A=(this is an example of shell script) $ echo ${A[0]} ${A[2]} ${A[3]} ${A[6]} this an example script $ echo ${A[8]} $ 由于值表中初值共有7个,所以A的元素个数也是7。A[8]超出了已赋值的数组A的范围,就认为它是一个新元素,由于预先没有赋值,所以它的值是空串。 若没有给出数组元素的下标,则数组名表示下标为0的数组元素,如city就等价于city[0]。 使用*或@做下标,则会以数组中所有元素取代 或[@]。 3.变量引用 除了上面所介绍的变量引用方式外,在bash中还有其它的引用方式。归纳起来,有效的变量引用表达式有以下形式: $name ${name#pattern} ${name} ${name##pattern} ${name[n]} ${name % pattern} ${name } ${name %% pattern} ${name [@]} ${#@} ${name:-word} ${$#*} ${name:=word} ${# name } ${name:?word} ${# name } ${name:+word} ${#name[@]} (1)表达式$name表示变量name的值,若变量未定义,则用空值替换。 (2)表达式${name}将被变量name的值替换。用花括号括起name,目的在于把变量名与后面的字符分隔开,避免出现混淆。替换后花括号被取消。 (3)${name[n]}表示数组变量name中第n个元素的值。 (4)表达式${name }和${name[@]}都表示数组name中所有非空元素的值,每个元素的值用空格分开。如果用双引号把它们都括起来,那么二者的含义就有区别:对于"${name }",它被扩展成一个词(即字符串),这个词由以空格分开的各个数组元素组成;对于"${name[@]}",它被扩展成多个词,每个数组元素是一个词。如果数组name中没有元素,则${name[@]}被扩展为空串。 (5)表达式${name:-word}、${name:=word}、${name:+word}、${name:?word}的计算方法在下面介绍。 (6)表达式${name#pattern}和${name##pattern},如果pattern(表示匹配模式)与name值的开头匹配,那么name的值去掉匹配部分后的结果就是该表达式的值;否则, name的值就是该表达式的值。在第一种格式中,name值去掉的部分是与pattern匹配的最少的部分;而第二种格式中,name值去掉的部分是与pattern匹配的最多的部分。 (7)表达式${name % pattern}和${name %% pattern},如果pattern与name值的末尾匹配,那么name的值中去掉匹配部分后的结果就是该表达式的值;否则,该表达式的值就是name的值。在第一种格式中,去掉的部分是最少的匹配部分;而第二种格式中,去掉的部分是最多的匹配部分。 (8)表达式${#@}和${#*}的值分别是由$@和$*返回的参数的个数。 (9)表达式${#name}的值是数组name第i个元素值的长度(字符个数)。 (10)表达式${#nane }和${#name[@]}的值都是数组name中已经设置的元素的个数。 4.交互输入变量值 利用read命令可以从键盘上读取数据,然后赋给指定的变量。read命令的一般格式是: read 变量1 [ 变量2 …] 例如: read a b c 输入数据时,数据间以空格或制表符作为分隔符。如果变量个数与给定数据个数相同,则依次对应赋值;如果变量个数少于数据个数,则从左至右对应赋值,但最后一个变量被赋予剩余的所有数据;如果变量个数多于给定数据个数,则依次对应赋值,而没有数据与之对应的变量取空串。 5.位置参数 执行Unix/Linux命令或Shell 脚本时可以带有实参。相应地,在Shell脚本中应有变量。执行Shell程序时,用实参去替代这些变量。在Shell脚本中这类变量的名称很特别,分别是0、1、2……这类变量称作位置变量,因为它们与命令行上具体位置的实参相对应:命令名(脚本名)对应位置变量0,第一个实参对应位置变量1,第二个实参对应位置变量2……如果位置变量是由两个或更多个数字构成,那么,必须把它们用一对花括号括起来,如{10}、{11}。命令行实参与脚本中位置变量的对应关系如图 1所示。 例如: $ set `pwd;ls;date` $ echo $1 $2 $3 $9 ${10} ${11} /home/mengqc bash_1 ex1 12 3 21:52:32 如果在脚本中使用的位置参数不超过9个,那么只用$1~$9即可。但是,实际给定的命令行参数有可能多于9个,此时就需要用shift命令移动位置参数。每执行一次shift命令,就把命令行上的实参向左移一位,即相当于位置参数向右移动一个位置,如图2所示。 图2虚线示出实参与位置参数的对应关系。可以看出,shift命令执行后新$1的值是原$2的值,新$2的值是原$3的值,依此类推。 shift命令不能将$0移走,所以经shift右移位置参数后, $0的值不会发生变化。 6. 预先定义的特殊变量 在Shell中,预先定义了几个有特殊含义的Shell变量,它们的值只能由Shell根据实际情况进行赋值,而不能通过用户重新设置。下面给出这些特殊变量的表示形式及意义: $# 命令行上实际参数的个数,但不包含Shell脚本名。 $?上一条命令执行后的返回值(也称作 “退出码”)。它是一个十进制数。多数Shell命令执行成功时,则返回值为0;如果执行失败,则返回非0值。 $$ 当前进程的进程号。 $! 上一个后台命令对应的进程号,这是一个由1~5位数字构成的数字串。 $- 由当前Shell设置的执行标志名组成的字符串。例如: set -xv 这个命令行给Shell设置了标志-x和-v(用于跟踪输出)。 $* 表示在命令行中实际给出的所有实参字符串,它并不仅限于9个实参。 $ @ 它与$*基本功能相同,但“$@”与“$*”不同。 7.环境变量 Linux环境(也称为Shell环境)由许多变量及这些变量的值组成。这些变量和变量的值决定了用户环境的外观。注意,如果要使用环境变量或其它Shell变量的值,必须在变量名之前加上一个“$”符号,不能直接使用变量名。 常用的环境变量如下: HOME 用户主目录的全路径名。 LOGNAME 即注册名,由Linux自动设置。它是与系统交互的名字或字符串。 MAIL 系统信箱的路径。 PATH 查找命令的目录列表。PATH变量包含带冒号分界符的字符串,这些字符串指向含有所使用命令的目录。 PS1 Shell的主提示符。bash默认的主提示符一般为“\s-\v\$ ”。其中,\s表示Shell的名称;\v表示bash的版本号。当然,也可以随意设置PS1的值,例如: PS1="Enter Command> " 则主提示符改成“Enter Command> ”。 PWD 当前工作目录的路径,它指出目前在Linux文件系统中处在什么位置。 SHELL 当前使用的Shell,它也指出Shell解释程序放在什么地方。 TERM 终端类型。 可以使用unset命令删除一个环境变量(如NAME): $ unset NAME 可以创建一个新的环境变量,或者改变一个已有的环境变量的值,其形式与设置一般变量相同。如果变量值的字符串中带有空格等特殊字符,需要用引号把整个字符串括起来。利用export命令可以将这些变量导出,使它们成为公用量,如: export HOME HZ LOGNAME TERM 可以利用env命令列出所有的环境变量,包括本进程及以前的“祖先进程”所导出的变量。 8.参数置换变量 参数置换变量是另一种为变量赋值的方式,其一般形式是: 变量2=$ {变量1 op 字符串} 其中,op表示操作符,它可以是下列四个操作符之一“: :-”、“: =” 、“: +” 和“: ?”。变量2的值取决于变量1(参数)是否为空串、利用哪个操作符及字符串的取值。在操作符的前后不留空格。 命令历史 bash提供了命令历史功能,即系统为每个用户维护一个命令历史文件(~/.bash_history),它在注册用户的主目录(用~表示)之下。该文件由编号的表格构成。每当注册后,用户输入命令并执行它时,该命令就自动地加到这个命令历史表中。 使用命令历史机制,用户可以方便地调用或者修改以前的命令,可以把全部或部分先前命令作为新命令予以快捷执行。这一功能称为历史替换(history substitution)。 ◆history命令可以显示命令历史表中的命令。其语法格式是: history [option][arg ?] (1)如果不带任何参数,则history命令会显示历史命令的清单(包括刚输入的history命令)。所有这些命令都被称作事件,一个事件表示一个操作已经发生,即一个命令已被执行。显示的各行命令之前的数字表示相应命令行在命令历史表中的序号,称为历史事件号。历史事件号从1开始顺序向下排,最后执行的命令的事件号最大。 (2)如果history 后给出一个正整数,例如: history 50 那么,就只显示历史表中的最后50行命令。 (3)如果history后给出一个文件名,例如: history al 那么,就把al作为历史文件名。 ◆执行历史命令是命令替换之一,它以字符“!”开头、后随1个或多个字符来定义用户所需的某种类型的历史命令。它可以出现在输入行的任何地方,除非已在进行历史替换。如果在“!”之前加上反斜线“\”,或者在其后跟着空格、制表符、换行符、等号“=”或开括号“(”,那么“!”就作为普通字符对待,失去特殊意义。 历史替换可以作为输入的命令行的一部分或全体。当输入行的正文中包含历史替换时,将在完成相应的替换后,在终端上显示输入的命令行,从而用户可以看到实际执行的命令(显示命令后立即执行)。 ◆在默认方式下,bash使用用户主目录下面的文件“.bash_history”来保存命令历史。但是,用户也可以通过重新为环境变量HISTFILE赋值来改变存放历史命令的文件。例如: $ HISTFILE="/home/mengqc/.myhistory" 历史文件中能够保留的命令个数有限,其默认值是500。如果用户输入的命令太多,超过限定值,那么最早输入的命令就会从历史表中删除,而新输入的命令会加到该表尾部。用户可以利用HISTSIZE变量重新设定该值。例如: $ HISTSIZE=600 通常用户不必对命令历史表进行管理(如设置HISTSIZE的值等),由系统自动管理。利用命令历史功能,用户可以对先前输入的命令重新进行编辑、修改和执行,从而简化用户的操作。 别名 使用别名可以简化输入,方便用户。对于常用的选项或参数较多的固定命令采用别名替换,既缩短击键次数,又减少出错机率。 定义别名要使用Shell内部命令alias,其一般语法格式为: alias [name[=value]]… 如果没有指定参数,将在标准输出(屏幕)上显示别名清单,其格式为name=value,其中name是用户(或系统)定义的别名名称,value是别名所代表的内容。注意,在赋值号“=”两边不能有空格。 例如: $ alias ll=' ls -l ' 定义了别名ll,它代表“ls -l”。当输入ll命令后,Shell将寻找它们所维护的别名表(放在内存中的一个内部表格)。若在该表中找到命令行的第一个字段(即“ll”)时,该命令就会被别名定义的内容所替换。定义别名时,往往用单引号将它代表的内容括起来,从而防止Shell对其中的内容产生歧义,如对空格和特殊字符另作解释。 如果想取消先前定义的别名,可使用如下命令: unalias name… 执行后,就从别名表中删除由name指定的别名。 unalias也可以在一个命令上同时取消多个别名的定义,只须在unalias之后依次列出要取消的别名名称。也可以一次将所有的别名都从别名表中删除,使用如下命令: unalias -a Shell特殊字符 Shell中除使用普通字符外,还使用了一些特殊字符,它们有特定的含义,如通配符“*”和“?”、管道线(|)及单引号、双引号等。在使用时应注意它们表示的意义和作用范围。 1.一般通配符 通配符用于模式匹配,如文件名匹配、路径名搜索、字符串查找等。常用的通配符有四种: * 匹配任意字符0次或多次出现。例如,f*可以匹配以f 打头的任意字符串。但应注意,文件名前面的圆点( . ) 和路径名中的斜线( / )必须显式匹配。 ? 匹配任意一个字符,例如,f ?匹配f1、fa、fb等,但不能匹配 f 、fabc、 f12等。 [ ] 其中有一个字符组,它匹配该字符组所限定的任何一个字符。该字符组可以由直接给出的字符组成,也可以由表示限定范围的起始字符、终止字符及中间一个连字符(-)组成。例如,f[a-d]与f[abcd]作用相同。 ! 表示不在一对方括号中所列出的字符。例如,f[!1—9].c 表示以f打头,后面一个字符不是数字1至9的.c文件名,它匹配fa.c、fb.c、fm.c等。 在一个正则表达式中,可以同时使用“*”和“?”。 2.模式表达式 模式表达式是那些包含一个或多个通配符的字。bash除支持一般通配符外,还提供了特有的扩展模式匹配表达式,其形式和含义如下: (1)*(模式表) 匹配给定模式表中“模式”的0次或多次出现,各模式之间以“|”分开。例如,file*(.c|.o)将匹配文件file、file.c、file.o、file.c.c、file.0.0、file.c.o、file.o.c等,但不匹配file.h或file.s等。 (2)+(模式表) 匹配给定模式表中“模式”的1次或多次出现,各模式之间以“|”分开。例如,file+(.c | .o)匹配文件file.c、file.o、file.c.o、file.c.c等,但不匹配file。 (3)?(模式表) 匹配模式表中任何一种“模式”的0次或1次出现,各模式之间以“|”分开。例如,file?(.c|.o)只匹配file、file.c和file.0,它不匹配多个模式或模式的重复出现,即不匹配file. c. c、file. c. 0等。 (4)@(模式表) 仅匹配模式表中给定“模式”的一次出现,各模式之间以“|”分开。例如,file@(.c|.0)匹配file.c和file.0,但不匹配file、file.c.c、file.c.o等。 (5)!(模式表) 除给定模式表中的一个“模式”之外,它可以匹配其它任何东西。 可以看出,模式表达式的定义是递归的,每个表达式中都可以包含一个或多个模式。例如file*( .[cho]|.sh)是合法的模式表达式。但在使用时应注意,由于带“*”和“+”的表达式可以匹配给定模式的组合,若利用此种表达式去删除文件就存在危险,有可能误将系统配置文件删除。因此,必须小心使用。 3.引号 在Shell中引号分为三种:单引号、双引号和倒引号。 (1)双引号 由双引号括起来的字符,除$、倒引号(`)和反斜线(\)仍保留其特殊功能外,其余字符均作为普通字符对待。“$”表示变量替换,即用其后指定的变量的值来代替$和变量;倒引号表示命令替换;仅当“\”后面的字符是下述字符之一时,“\”才是转义字符,这些字符是:“$”、“`”、“"”、“\”或换行符。转义字符告诉Shell不要对其后面的那个字符进行特殊处理,只是当作普通字符。例如: $ echo "My current dir is `pwd` and logname is $LOGNAME" My current dir is /home/mengqc and logname is mengqc (2)单引号 由单引号括起来的字符都作为普通字符出现。例如, $ echo 'The time is ` date ` , the file is $HOME/abc ' The time is ` date ` , the file is $HOME/abc (3)倒引号 倒引号括起来的字符串被shell解释为命令行,在执行时,Shell会先执行该命令行,并以它的标准输出结果取代整个倒引号部分。在前面示例中已经见过。例如, $ echo current directory is ` pwd ` current directory is /home/mengqc 4.注释 Shell程序中以“#”开头的正文行表示注释。例如: #!/bin/bash # If no arguments, then listing the current directory. # Otherwise, listing each subdirectory. if test $# = 0 then ls · else for i do ls -l $i | grep '^d' done fi 上面程序由if语句构成,其中else部分是for循环语句。其功能是检测位置参数个数,若等于0,则列出当前目录本身;否则,对于每个位置参数显示其所包含的子目录。 上面代码中,第一行#!/bin/bash表示下面的脚本是用bash编写的,必须调用bash程序对它解释执行。后面两行以“ # ”开头,表示这是注释行。注释行可用来说明程序的功能、结构、算法和变量的作用等,增加程序的可读性。在执行时Shell将忽略注释行。 命令执行操作符 ◆多条命令可以在一行中出现。它们可以从左到右顺序执行。此时,各条命令之间应以分号( ;)隔开,如: pwd ; who | wc -l ; cd /usr/bin ◆在相邻命令间可存在逻辑关系,即逻辑“与”和逻辑“或”。 逻辑与操作符“&&”可把两个命令联系在一起,其一般形式: 命令1 && 命令2 其功能是先执行命令1,如果执行成功,才执行命令2;否则,若命令1执行不成功,则不执行命令2。例如: cp ex1 ex10 && rm ex1 如果成功地把文件ex1拷贝到文件ex10中,则把ex1删除。 逻辑或操作符“||”可把两个命令联系起来,其一般形式是: 命令1 || 命令2 其功能是先执行命令1,如果执行不成功,则执行命令2;否则,若命令1执行成功,则不执行命令2。例如: cat abc || pwd 如果不能将文件abc的内容列出来,则显示当前工作目录的路径。 ◆在Shell中有两种方式可以将若干命令组合在一起,使其在逻辑上被视为一条命令,它们是用花括号{ }将各命令括起来和用圆括号( )括起来。 以花括号括起来的全部命令可视为语法上的一条命令,出现在管道符的一边。成组命令的执行顺序是根据命令出现的先后次序,由左至右执行。在管道线中,成组命令把各命令的执行结果汇集在一起,形成一个输出流,这个流作为该管道线中下一个命令的输入。例如, { echo "User Report for ` date ` . "; who ; } | pr 使用花括号时在格式上应注意:左括号 “{ ”后面应有一个空格;右括号“}”之前应有一个分号( ;)。 成组命令也可以用圆括号括起来。例如: (echo "Current directory is ` pwd ` . " cd /home/mengqc ; ls -l ; cp m1 em1 && rm m1 cat em1) | pr 如上所示,在用圆括号括起成组命令时,左括号后不必有空格,右括号之前也不需加上分号。 二者执行过程相同,但是存在重要区别:用花括号括起来的成组命令只是在本Shell内执行命令表,不产生新的进程;而用圆括号括起来的成组命令是在新的子Shell内执行,要建立新的子进程。因此,在圆括号内的命令不会改变父Shell的变量值及工作目录等。 算术运算 bash中执行整数算术运算的命令是let,其语法格式为: let arg … 其中,arg是单独的算术表达式。这里的算术表达式使用C语言中表达式的语法、优先级和结合性。除++、--和逗号(,)之外,所有整型运算符都得到支持。此外,还提供了方幂运算符“**”。命名的参数可以在算术表达式中直接利用名称访问,不要前面带有“$”符号。当访问命名参数时,就作为算术表达式计算它的值。算术表达式按长整数进行求值,并且不检查溢出。当然,用0作除数就产生错误。 let 命令的替代表示形式是: ((算术表达式)) 例如,let ″j=i*6+2″等价于((j=i*6+2))。 如果表达式的值是非0,那么返回的状态值是0;否则,返回的状态值是1。 当表达式中有Shell的特殊字符时,必须用双引号将其括起来。例如,let ″val=a|b″。如果不括起来,Shell会把命令行let val=a|b中的“|”看成管道符,将其左右两边看成不同的命令,因而无法正确执行。 控制结构 Shell具有一般高级程序设计语言所具有的控制结构和其它的复杂功能,如if语句、 case语句、循环结构、函数等。其实在Shell中,这些控制结构也称作“命令”。为了程序设计的习惯,才把它们称作语句。 1.if语句 if语句用于条件控制结构中,其一般格式为: if 测试条件 then 命令1 else 命令2 fi 其中,if、then、else和fi是关键字。例如: if test -f "$1" then echo "$1 is an ordinary file . " else echo "$1 is not an ordinary file . " fi 应该注意,if语句中else部分可以缺省。另外,if 语句的else部分还可以是else—if结构,此时可以用关键字“elif”代替“else if”。 通常,if的测试部分是利用test命令实现的。其实,条件测试可以利用一般命令执行成功与否来作判断。如果命令正常结束,则表示执行成功,其返回值为0,条件测试为真;如果命令执行不成功,其返回值不等于0,条件测试就为假。所以if的语句的更一般形式是: if 命令表1 then 命令表2 else 命令表3 fi 2.条件测试 条件测试有三种常用形式:一种是用test命令,如上所示。另一种是用一对方括号将测试条件括起来。这两种形式是完全等价的。例如,测试位置参数$1是否是已存在的普通文件,可写为test -f "$1"。也可写成[ -f "$1" ]。利用一对方括号表示条件测试时,在左方括号“[”之后、右方括号“]”之前各应有一个空格。 第三种形式是: [[条件表达式]] test命令可以和多种系统运算符一起使用。这些运算符可以分为四类:文件测试运算符(文件的属性及权限等)、字符串测试运算符(两个串是否相同及是否为空)、数值测试运算符(大小关系)和逻辑运算符(逻辑与、或、非)。 3.case语句 case语句允许进行多重条件选择。其一般语法形式是: case 字符串 in 模式字符串1) 命令 … 命令;; 模式字符串2) 命令 … 命令;; … 模式字符串n) 命令 … 命令;; esac 其执行过程是用“字符串”的值依次与各模式字符串进行比较,如果发现同某一个匹配,那么就执行该模式字符串之后的各个命令,直至遇到两个分号为止。如果没有任何模式字符串与该字符串的值相符合,则不执行任何命令。 在使用case语句时应注意: (1)每个模式字符串后面可有一条或多条命令,其最后一条命令必须以两个分号(即;;)结束。 (2)模式字符串中可以使用通配符。 (3) 如果一个模式字符串中包含多个模式,那么各模式之间应以竖线(|)隔开,表示各模式是“或”的关系,即只要给定字符串与其中一个模式相配,就会执行其后的命令表。 (4)各模式字符串应是惟一的,不应重复出现。并且要合理安排它们的出现顺序。例如,不应将“*”作为头一个模式字符串,因为“*”可以与任何字符串匹配,它若第一个出现,就不会再检查其它模式了。 (5)case语句以关键字case开头,以关键字esac(是case倒过来写!)结束。 (6)case的退出(返回)值是整个结构中最后执行的那个命令的退出值。若没有执行任何命令,则退出值为零。 4. while语句 Shell中有三种用于循环的语句,它们是while语句、for语句和until语句。 while语句的一般形式是: while 测试条件 do 命令表 done 其执行过程是,先进行条件测试,如果结果为真,则进入循环体(do—done之间部分), 执行其中命令;然后再做条件测试……直至测试条件为假时才终止while语句的执行。例如: while [ $1 ] do if [ -f $1 ] then echo "display : $1 " cat $1 else echo "$1 is not a file name . " fi shift done 这段程序对各个给定的位置参数,首先判断其是否是普通文件,若是,则显示其内容;否则,显示它不是文件名的信息。每次循环处理一个位置参数$1,利用shift命令可把后续位置参数左移。 测试条件部分除使用test命令或等价的方括号外,还可以是一组命令。根据其最后一个命令的退出值决定是否进入循环体执行。 5.until语句 until语句的一般形式是: until 测试条件 do 命令表 done 它与while语句很相似,只是测试条件不同:当测试条件为假时,才进入循环体,直至测试条件为真时终止循环。 6.for语句 for语句是最常用的建立循环结构的语句。其使用格式主要有三种,取决于循环变量的取值方式。格式一: for 变量 in 值表 do 命令表 done 例如: for day in Monday Wednesday Friday Sunday do echo $day done 其执行过程是,变量day依次取值表中各字符串,即第一次将“Monday”赋给day,然后进入循环体,执行其中的命令,显示出Monday。第二次将“Wednesday”赋给day,然后执行循环体中命令,显示出Wednesday。依次处理,当day把值表中各字符串都取过一次之后,下面day的值就变为空串,从而结束for循环。因此,值表中字符串的个数就决定了for循环执行的次数。在格式上,值表中各字符串之间以空格隔开。 格式二: for 变量 in 文件正则表达式 do 命令表 done 其执行过程是,变量的值依次取当前目录下(或给定目录下)与正则表达式相匹配的文件名,每取值一次,就进入循环体执行命令表,直至所有匹配的文件名取完为止,退出for循环。 格式三: for i in $* 或者 for i do do 命令表 命令表 done done 这两种形式是等价的。其执行过程是,变量i 依次取位置参数的值,然后执行循环体中的命令表,直至所有位置参数取完为止。 7.break命令和continue命令 break命令可以使我们从循环体中退出来。其语法格式是: break [ n ] 其中,n表示要跳出几层循环。默认值是1,表示只跳出一层循环。 continue命令跳过循环体中在它之后的语句,回到本层循环的开头,进行下一次循环。其语法格式是: continue [ n ] 其中,n表示从包含continue语句的最内层循环体向外跳到第几层循环。默认值为1。循环层数是由内向外编号。 函数 在Shell脚本中可以定义并使用函数。其定义格式为: [function]函数名( ) { 命令表 } 其中,关键字function可以缺省。 函数应先定义,后使用。调用函数时,直接利用函数名,如showfile,不必带圆括号,就像一般命令那样使用。Shell脚本与函数间的参数传递可利用位置参数和变量直接传递。变量的值可以由Shell脚本传递给被调用的函数,而函数中所用的位置参数$1、$2,等对应于函数调用语句中的实参,这一点是与普通命令不同的。下面是使用函数的示例: #func is a function name # it echos the values of variables and arguments func( ) { echo "Let's begin now. " echo $a $b $c echo $1 $2 $3 echo "The end. " } a=" Working directory " b="is" c=`pwd` func Welcome You Byby echo "Today is ` date ` " Shell中的函数把若干命令集合在一起,通过一个函数名加以调用。如果需要,还可被多次调用。执行函数并不创建新的进程,是通过Shell进程执行。 通常,函数中的最后一个命令执行之后,就退出被调函数。也可利用return命令立即退出函数,其语法格式是: return [ n ] 其中,n值是退出函数时的退出值(退出状态),即$?的值。当n值缺省时,则退出值是最后一个命令执行后的退回值。 本讲介绍了有关Shell编程的知识和示例,从中可以体会到bash的特点和长处。很显然,对于系统管理员来说,利用bash编写脚本解决问题,可以大大减轻工作强度,有效提高编程效率。建议读者多上机练习。 (责任编辑:IT) |