版权所有IBM公司2007商标LPI102考试准备,主题109:Shell、脚本、编程和编译第1页,共34LPI102考试准备,主题109:Shell、脚本、编程和编译初级管理(LPIC-1)主题109IanShields高级程序员IBM2007年3月23日在这个教程中,IanShields将继续帮助您准备参加LinuxProfessionalInstitute的初级管理(LPIC-1)考试102.
这是9篇系列教程中的第5篇.
在本教程中,Ian将向您介绍Bashshell、脚本和Bashshell编程方面的内容.
在学完本教程之后,您将掌握如何对自己的shell环境进行定制,如何使用shell编程结构来创建函数和脚本,如何设置和取消环境变量,以及如何使用各种登录脚本.
查看本系列更多内容开始之前本节解释这些教程讲授什么内容,以及如何从这些教程获得最大的收益.
关于本系列教程LinuxProfessionalInstitute(LPI)对Linux系统管理员的认证分为两级:初级(也称为"认证级别1")和中级(也称为"认证级别2").
要获得认证级别1,您必须通过101和102的考试;要获得认证级别2,您必须通过201和202的考试.
developerWorks提供教程来帮助您准备这4门考试.
每门考试包含几个主题,每个主题在developerWorks上都有一个对应的自学教程.
对于LPI102考试,有以下9个主题和对应的developerWorks教程:表1.
LPI102考试:教程和主题LPI102考试主题developerWorks教程教程摘要主题105LPI102考试准备:内核学习如何安装和维护Linux内核和内核模块.
主题106LPI102考试准备:引导、初始化、关机和运行级别学习如何引导系统、设置内核参数以及关闭或重新引导系统.
主题107LPI102考试准备:打印学习如何在Linux系统上管理打印机、打印队列和用户的打印作业.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第2页,共34主题108LPI102考试准备:文档学习如何使用并管理本地文档、查找Internet上的文档以及使用自动化登录消息来通知用户系统事件的发生.
主题109LPI102考试准备:shell、脚本、编程和编译(本教程).
学习如何对自己的shell环境进行定制以满足用户需求、如何为经常使用的命令序列编写Bash函数、如何编写简单的新脚本、使用shell语法进行循环和测试,以及如何对现有脚本进行定制.
参见下面详细的目标.
主题111LPI102考试准备:管理作业敬请期待!
主题112LPI102考试准备:网络基础敬请期待!
主题113LPI102考试准备:网络服务敬请期待!
主题114LPI102考试准备:安全性敬请期待!
要想通过考试101和102(并获得1级认证),您应该能够:在Linux命令行上进行操作.
执行简单的维护作业:帮助用户、向更大的系统中添加用户、备份和恢复、关机和重新引导.
安装和配置工作站(包括X)并将它连接到LAN,或者通过调制解调器将单独的PC连接到Internet.
要想继续准备1级认证,请参考针对LPI101和102考试的developerWorks教程以及全套developerWorksLPI教程.
LinuxProfessionalInstitute不为任何第三方考试准备资料或技术做担保.
详情请联系info@lpi.
org.
关于本教程欢迎阅读"Shell、脚本、编程和编译",这是针对LPI102考试而设计的9篇教程中的第5篇.
在本教程中,您将学习如何使用Bashshell,如何使用shell编程结构来创建函数和脚本,如何设置并取消环境变量,以及如何使用各种登录脚本.
本教程的标题与LPI102考试的主题是对应的,也包括了"编程和编译";不过LPI的目标将"编程"限定于编写shell函数和脚本.
有关编译程序的其他目标并没有包含在这个主题内.
本教程是按照这个主题的LPI目标组织的.
大致来说,权值越高的学习目标,在考试中出的题就越多.
表2.
Shell、脚本、编程和编译:本教程中涉及的考试目标LPI考试目标目标权值目标摘要1.
109.
1定制并使用shell环境权值5定制shell环境以满足用户需求.
设置环境变量(在登录时或在派生新shell时).
对经常使用的命令序列编写Bash函数.
1.
109.
2定制或编写简单脚本权值3编写简单Bash脚本并对现有脚本进行定制.
ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第3页,共34前提条件要想从本教程获得最大的收益,您应该具备Linux的基础知识并且拥有一个可以用来实践的Linux系统.
本教程依赖于本LPI考试系列前面的教程介绍的内容,所以您可能需要先参考针对考试101的教程.
您尤其应该熟悉"LPI101考试准备(主题103):GNU和UNIX命令"教程,因为其中有许多对本教程很有帮助的内容,尤其是"使用命令行"一节.
不同的程序版本可能会导致不同格式的输出,所以您在进行实践时获得的结果可能会与本教程中的清单和图不完全一样.
Shell定制本节介绍了初级管理(LPIC-1)考试102的1.
109.
1主题的内容.
这个主题的权值为5.
在本节中,我们将学习如何:设置并取消环境变量使用配置文件在登录或派生新shell时设置环境变量对经常使用的命令序列编写shell函数使用命令列表Shell和环境在出现图形界面之前,程序员都是使用打字机终端或ASCII显示终端连接到UNIX系统的.
用户可以使用打字机终端输入命令,输出结果通常会被打印到连续的纸张上.
大部分ASCII显示终端都是每行80个字符,每屏25行,不过也有比这更大或更小的终端.
程序员输入一条命令并按下回车键之后,系统就会解释并执行这条命令.
尽管在当今这个使用拖拽式图形界面的时代,这一切看起来似乎太过原始,但是与原来编写程序、打卡、对卡迭(carddeck)进行汇编并运行程序的方式相比,这已经是非常大的一个进步了.
随着编辑器的出现,程序员甚至可以作为卡像来创建程序,并在终端会话中编译程序.
在终端中输入的字节流向shell提供了一个标准输入流,shell返回的字符流可以打印到纸上,也可以显示到标准输出上.
接受并执行命令的程序称为shell.
它位于您和操作系统之间.
UNIXshell和Linuxshell的功能都非常强大,可以通过组合一些基本的函数来构造非常复杂的操作.
通过使用编程结构则可以构建一些函数在shell中直接执行,或者将这些函数保存成shell脚本的形式,这样就可以一次次重用这些函数了.
有时需要在系统引导之前就执行一些命令,以便能够进行终端连接;有时又需要周期性地执行命令,而不管您登录与否.
shell可以为您完成这些功能.
标准输入和输出并不需要来自于(或定向到)终端处的真实用户.
在本节中,将学习更多有关shell的内容.
具体来说,您将学习有关bash(又称为Bourneagain)shell的内容,它是对原来Bourneshell的一个增强,另外还提供了其他shell所具有的一些特性,以及对Bourneshell所做的一些更改以使其更加兼容POSIX.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第4页,共34POSIX是PortableOperatingSystemInterfaceforuniX的简称,它是一系列IEEE标准,总称为IEEE1003.
这些标准中的第一个标准是IEEEStandard1003.
1-1988,它是在1988年发布的.
其他知名的shell包括Kornshell(ksh)、Cshell(csh)及其派生产品tcsh、Almquistshell(ash)及其Debian派生产品(dash).
一些脚本常常需要用到上述某个shell的特性,所以要对这些shell有一些了解.
您与计算机的很多交互特性在这些会话中都是相同的.
回想一下在教程"LPI101考试准备(主题103):GNU和UNIX命令"中,当使用Bashshell时,就拥有了一个shell环境,它定义了很多内容,例如提示符格式、主目录、工作目录、shell名、已经打开的文件、已经定义的函数等.
每个shell进程都可以使用这个环境.
shell(包括bash)让您可以创建并修改shell变量,并可以将其导出到环境中由在shell中运行的其他进程或从当前shell中派生的其他shell使用.
环境变量和shell变量都有名称.
您可以通过在变量名前加上一个'$'符号来引用变量的值.
一些常用的bash变量如表3所示.
表3.
常用bash环境变量变量名功能USER已登录用户的用户名UID已登录用户的数字用户idHOME用户的主目录PWD当前工作目录SHELLshell名$进程id(或正在运行的Bashshell进程或其他进程的PID)PPID启动这个进程的进程的进程id(即父进程的id)上一个命令的退出码设置变量在Bashshell中,可以通过在一个名字后面紧跟上一个等号(=)来创建或设置shell变量.
变量名(或标识符)是由字符、数字和下划线构成的单词,它只能由字符或下划线开头.
变量是大小写敏感的,例如var1和VAR1是不同的两个变量.
按照惯例,变量——尤其是导出后的变量——都采用大写,不过这并不是硬性要求.
通常,$$和$是shell参数,而不是变量.
它们只能被引用;无法对它们进行赋值.
在创建shell变量时,通常都会希望将该变量导出到环境中,这样从这个shell中启动的其他进程也都可以使用该变量了.
但所导出的变量对父shell不可用.
可以使用export命令导出一个变量名.
在bash中,可以在一个步骤中完成赋值和导出.
为了展示赋值和导出操作,让我们在Bashshell中运行bash命令,然后在这个新Bashshell中在运行Kornshell(ksh).
我们会使用ps命令来显示有关正在运行的命令的信息.
清单1.
设置并导出shell变量[ian@echidnaian]$ps-p$$-o"pidppidcmd"PIDPPIDCMD3057630575-bashibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第5页,共34[ian@echidnaian]$bash[ian@echidnaian]$ps-p$$-o"pidppidcmd"PIDPPIDCMD1635330576bash[ian@echidnaian]$VAR1=var1[ian@echidnaian]$VAR2=var2[ian@echidnaian]$exportVAR2[ian@echidnaian]$exportVAR3=var3[ian@echidnaian]$echo$VAR1$VAR2$VAR3var1var2var3[ian@echidnaian]$echo$VAR1$VAR2$VAR3$SHELLvar1var2var3/bin/bash[ian@echidnaian]$ksh$ps-p$$-o"pidppidcmd"PIDPPIDCMD1644816353ksh$exportVAR4=var4$echo$VAR1$VAR2$VAR3$VAR4$SHELLvar2var3var4/bin/bash$exit$[ian@echidnaian]$echo$VAR1$VAR2$VAR3$VAR4$SHELLvar1var2var3/bin/bash[ian@echidnaian]$ps-p$$-o"pidppidcmd"PIDPPIDCMD1635330576bash[ian@echidnaian]$exit[ian@echidnaian]$ps-p$$-o"pidppidcmd"PIDPPIDCMD3057630575-bash[ian@echidnaian]$echo$VAR1$VAR2$VAR3$VAR4$SHELL/bin/bash注意:1.
在这些操作开始时,Bashshell的PID是30576.
2.
第二个Bashshell的PID是16353,其父shell的PID是30576,也就是原来的Bashshell.
3.
我们在第二个Bashshell中创建了VAR1、VAR2和VAR3三个变量,但是只导出了VAR2和VAR3.
4.
在Kornshell中,我们创建了VAR4.
echo命令只显示了VAR2、VAR3和VAR4的值,这就证实了VAR1的确没有导出.
看到提示符改变之后,SHELL变量的值却还未改变,您会非常奇怪么通常不能总依赖SHELL来告诉您正在哪个shell下运行,不过ps命令的确可以告诉您实际的命令.
注意ps会在第一个Bashshell前面放上一个连字符(-)来说明这是一个登录shell.
5.
现在回到第二个Bashshell中,我们可以看到VAR1、VAR2和VAR3.
6.
最后,当我们返回到原始的shell中时,新变量都不存在了.
清单2显示了在这些常用的bash变量中可以看到什么.
清单2.
环境和shell变量[ian@echidnaian]$echo$USER$UIDian500[ian@echidnaian]$echo$SHELL$HOME$PWD/bin/bash/home/ian/home/ian[ian@echidnaian]$(exit0);echo$;(exit4);echo$04[ian@echidnaian]$echo$$$PPID3057630575developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第6页,共34环境和Cshell在诸如C和tcshshell之类的shell中,可以使用set命令在shell中设置变量,使用setenv命令来设置并导出变量.
清单3中给出的语法与export命令的语法稍有不同.
请注意在使用set命令时使用的等号(=).
清单3.
在Cshell中设置环境变量ian@attic4:~$echo$VAR1$VAR2ian@attic4:~$csh%setVAR1=var1%setenvVAR2var2%echo$VAR1$VAR2var1var2%bashian@attic4:~$echo$VAR1$VAR2var2取消变量可以使用unset命令从Bashshell中清除变量.
可以使用-v选项来确保删除变量定义.
函数可以使用与变量相同的名字,因此如果希望清除函数定义,就请使用-f选项.
在没有使用-f或-v的情况下,如果存在这样一个变量,那么bash的unset命令就会清除变量定义;否则,如果存在这样一个函数,这个命令就清除函数定义(函数将在后面的Shell函数一节中更详细地加以介绍).
清单4.
bashunset命令ian@attic4:~$VAR1=var1ian@attic4:~$VAR2=var2ian@attic4:~$echo$VAR1$VAR2var1var2ian@attic4:~$unsetVAR1ian@attic4:~$echo$VAR1$VAR2var2ian@attic4:~$unset-vVAR2ian@attic4:~$echo$VAR1$VAR2默认情况下,bash会将取消的变量视为该变量的值为空,因此您可能会纳闷为什么一定要取消变量,为什么不仅仅为其赋一个空值呢.
如果引用了未定义的变量,Bash和很多其他shell都会允许您生成一个错误.
使用命令set-u可以针对引用未定义的变量的情况生成一个错误,使用set+u可以禁用这种警告,如清单5所示.
ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第7页,共34清单5.
针对取消的变量生成错误ian@attic4:~$set-uian@attic4:~$VAR1=var1ian@attic4:~$echo$VAR1var1ian@attic4:~$unsetVAR1ian@attic4:~$echo$VAR1-bash:VAR1:unboundvariableian@attic4:~$VAR1=ian@attic4:~$echo$VAR1ian@attic4:~$unsetVAR1ian@attic4:~$echo$VAR1-bash:VAR1:unboundvariableian@attic4:~$unset-vVAR1ian@attic4:~$set+uian@attic4:~$echo$VAR1ian@attic4:~$注意取消一个不存在的变量并不会产生错误,即使在指定set-u时也是如此.
配置文件在登录Linux系统时,您的id就有了一个默认shell,它就是您的登录shell.
如果这个shell是bash,那么它就会在您控制系统之前先执行几个配置脚本.
如果存在/etc/profile文件,就首先执行这个文件.
根据发行版的不同,/etc中的其他脚本也可能会执行,例如/etc/bash.
bashrc或/etc/bashrc.
这些脚本运行之后,如果主目录中存在脚本,该脚本也会被执行.
Bash会按照~/.
bash_profile、~/.
bash_login和~/.
profile的顺序来查找文件.
最先找到的文件会首先执行.
当您登出系统时,如果主目录中存在~/.
bash_logout脚本,bash就会执行它.
一旦登录进系统并使用bash,您还可以启动另外一个shell(称为交互式shell)来运行命令,例如在后台运行命令.
在这种情况中,bash只会执行~/.
bashrc脚本(假设这个脚本存在).
通常可以使用如清单6所示的命令在~/.
bash_profile检查这个脚本,以便可以在登录时或在启动交互式shell时执行它.
清单6.
检查~/.
bashrc#include.
bashrcifitexistsif[-f~/.
bashrc];then.
~/.
bashrcfi可以使用--login选项强制bash像登录shell一样读取配置文件.
如果不希望执行登录shell的配置文件,可以指定--noprofile选项.
类似地,如果希望对某个交互式shell不执行~/.
bashrc文件,可以使用--norc选项来启动bash.
也可以通过指定--rcfile选项加上希望使用的文件名来强制bash使用~/.
bashrc之外的文件.
清单7展示了创建一个名为testrc的简单文件并使用--rcfile选项来使用这个文件的例子.
注意VAR1变量并不是在外部shell中设置的,而是通过testrc文件针对内部shell设置的.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第8页,共34清单7.
使用--rcfile选项ian@attic4:~$echoVAR1=var1>testrcian@attic4:~$echo$VAR1ian@attic4:~$bash--rcfiletestrcian@attic4:~$echo$VAR1var1以其他方式启动bash除了前面介绍的这种在终端中运行bash的标准方法之外,bash也可以通过其他方法加以使用.
除非您引用(source)脚本在当前shell中运行,否则它就会在自己的非交互式shell中运行,上面的配置文件都不会被读取.
然而,如果设置了BASH_ENV变量,那么bash就会对这个值进行扩展,并假设它是一个文件名.
如果这个文件存在,那么bash就会在非交互式shell中执行任何脚本或命令之前先执行这个文件.
清单8通过两个简单的文件展示了这一点.
清单8.
使用BASH_ENVian@attic4:~$cattestenv.
sh#!
/bin/bashecho"Testingtheenvironment"ian@attic4:~$catsomescript.
sh#!
/bin/bashecho"Doingnothing"ian@attic4:~$exportBASH_ENV="~/testenv.
sh"ian@attic4:~$.
/somescript.
shTestingtheenvironmentDoingnothing非交互式shell也可以使用--login选项启动,从而强制配置文件的执行.
Bash也可以使用--posix选项以POSIX模式启动.
这种模式与非交互式shell非常类似,只不过在这种模式下,要执行的文件是在ENV环境变量中设定的.
在Linux系统中常常会使用一个符号链接来以/bin/sh运行bash.
当bash检测到它正在以sh的名义运行时,它就会试图遵循老式Bourneshell的启动行为,而同时又可以兼容POSIX标准.
当作为登录shell运行时,bash会试图读取并执行/etc/profile和~/.
profile文件.
当使用sh命令作为一个交互式shell运行时,bash会试图执行由ENV变量指定的文件,与在POSIX模式下被调用时一样.
当作为sh交互运行时,它只会使用由ENC变量指定的文件;--rcfile选项会一直被忽略.
如果bash是由远程shell守护进程调用的,那么它的行为就与交互式shell非常类似,如果存在~/.
bashrc文件就会使用该文件.
Shell别名Bashshell允许为命令定义一些别名.
使用别名的最常见原因是为了给命令提供其他名字,或者为命令提供一些默认参数.
很多年以来,vi编辑器一直都是UNIX和Linux系统上的一个主要工具.
vim(ViIMproved)编辑器与vi非常类似,不过有很多改进.
因此如果您在使用编辑器时习惯于输入"vi",但是实际上却更喜欢使用vim,那么您就可以借助于别名.
清单9显示了如何使用alias命令来实现这种功能.
ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第9页,共34清单9.
使用vi作为vim的别名[ian@pinguino~]$aliasvi='vim'[ian@pinguino~]$whichvialiasvi='vim'/usr/bin/vim[ian@pinguino~]$/usr/bin/whichvi/bin/vi注意在这个例子中,如果使用which命令来查看vi程序的位置,那就会看到两行输出:第一个是别名,第二个是vim的位置(/usr/bin/vim).
然而,如果使用完整路径来执行which命令(/usr/bin/which),就可以获得vi命令的位置.
如果您猜测这可能意味着which命令本身在这个系统上就是一个别名,那么您就猜对了.
可以使用alias命令来显示所有的别名(如果没使用任何选项,或者只使用了-p选项),还可以通过给出别名作为参数但不进行赋值来显示一个或多个别名.
清单10显示了which和vi的别名.
清单10.
which和vi的别名[ian@pinguino~]$aliaswhichvialiaswhich='alias|/usr/bin/which--tty-only--read-alias--show-dot--show-tilde'aliasvi='vim'which命令的别名有些奇怪.
为什么会将alias命令(没有参数)的输出定向到/usr/bin/which上呢如果查看一下which命令的手册页,就会发现--read-alias选项通知which从标准输入读取一个别名列表,并将匹配项输出到标准输出设备上.
这允许which命令报告别名和PATH中的命令,这种用法非常常见,因此您的发行版可能已将其作为默认设置了.
这是很好的一个做法,因为如果别名和命令名相同,那么shell就首先执行别名.
知道了这一点以后,就可以使用aliaswhich来加以检查.
还可以通过运行whichwhich命令来了解是否为which命令设置了这种别名.
.
别名的另外一种常见用法是自动为命令添加参数,正如在上面看到的which命令的--read-alias和其他几个参数一样.
这种方法也可用在root用户使用cp、mv和rm命令的时候,这样在删除或覆盖文件之前能够显示一个提示.
具体用法如清单11所示.
清单11.
为了安全起见添加参数[root@pinguino~]#aliascpmvrmaliascp='cp-i'aliasmv='mv-i'aliasrm='rm-i'命令列表在之前的教程"LPI101考试准备(主题103):GNU和UNIX命令"中,您已经学习了命令序列或列表.
您刚刚又看到了别名中使用的管道(|)操作符,您也可以使用命令列表.
举个简单的例子来说,假设您希望使用一个命令来显示当前目录中的内容,以及当前目录及其子目录所使用的空间.
让我们就将其称为lsdu命令.
因此您可以简单地将ls和du命令序列赋值给别名lsdu.
清单12给出了实现这种功能的正确方法和错误方法.
在阅读之前请仔细查看一下,并考虑为什么第一次尝试会失败.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第10页,共34清单12.
命令序列的别名[ian@pinguinodeveloperworks]$aliaslsdu=ls;du-sh#Wrongway2.
9M.
[ian@pinguinodeveloperworks]$lsduatutorialnew-article.
shnew-tutorial.
shreadmetoolsxslmy-articlenew-article.
vbsnew-tutorial.
vbsschemaweb[ian@pinguinodeveloperworks]$alias'lsdu=ls;du-sh'#Rightwayway[ian@pinguinodeveloperworks]$lsduatutorialnew-article.
shnew-tutorial.
shreadmetoolsxslmy-articlenew-article.
vbsnew-tutorial.
vbsschemaweb2.
9M.
在引用构成别名的完整序列时需要非常仔细.
如果使用shell变量作为别名的一部分,还需要注意是使用双引号还是使用单引号.
您希望在定义或执行别名时让shell对变量进行扩展吗清单13显示了创建名为mywd定制命令来打印当前工作目录名的错误方法.
清单13.
定制pwd——尝试1[ian@pinguinodeveloperworks]$aliasmywd="echo\"Myworkingdirectoryis$PWD\""[ian@pinguinodeveloperworks]$mywdMyworkingdirectoryis/home/ian/developerworks[ian@pinguinodeveloperworks]$cd.
.
[ian@pinguino~]$mywdMyworkingdirectoryis/home/ian/developerworks注意双引号会导致bash在执行命令之前就对变量进行扩展.
清单14使用了alias命令来显示所生成的别名实际上是什么样子,从中可以看出我们的错误是很明显的.
清单14还给出了定义这个别名的正确方法.
清单14.
定制pwd——尝试2[ian@pinguinodeveloperworks]$aliasmywdaliasmywd='echo\"Myworkingdirectoryis$PWD\"'[ian@pinguinodeveloperworks]$mywd"Myworkingdirectoryis/home/ian/developerworks"[ian@pinguinodeveloperworks]$cd.
.
[ian@pinguino~]$mywd"Myworkingdirectoryis/home/ian"终于成功了.
Shell函数别名让您可以对某个命令或命令列表选用一种简写或其他名字.
此外,还可以添加其他一些内容,例如在which命令中加上希望查找的程序名.
当shell执行用户的输入时,就会对别名进行扩展;之后输入的其他内容都会在最后一个命令或命令列表执行之前添加到该扩展.
这意味着只能在命令或命令列表之后添加参数,也只能在最后一个命令中使用这些参数.
函数提供了更多功能,包括对参数进行处理的能力.
函数是POSIXshell定义的一部分,在诸如bash、dash和ksh之类的shell中可以使用,但在csh或tcsh中不能使用.
在接下来的几节中,将逐步构建一个复杂的命令:从很小的构建块开始,逐渐在每个步骤加以完善,并将其转换成一个函数,以供以后使用.
ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第11页,共34假想问题可以使用ls命令显示有关文件系统中目录和文件的各种信息.
假设您喜欢使用一个命令,假定就是ldirs,来显示目录名,所显示的内容如清单15所示.
清单15.
ldirs命令输出结果[ian@pinguinodeveloperworks]$ldirs*[st]*tools/*a*mydwarticleschematoolstools/javaxsl为了保持简单性起见,本节中的例子使用了developerWorksauthorpackage中的目录和文件(请参看参考资料),如果您想为developerWorks编写文章和教程,也可以使用它们.
在这些例子中,我们使用了这个包中提供的new-article.
sh脚本来为一篇我们称之为"mydwarticle"的文章创建一个模板.
在撰写本文时,developerWorksauthorpackage的版本是5.
6,因此如果您使用更新的版本,可能会发现一些不同之处.
或者您也可以只使用自己的文件和目录.
ldirs命令也可以处理这些内容.
在developerWorksauthorpackage提供的工具中,可以找到其他bash函数的例子.
查找目录项如果在ls命令中使用了上述别名例子所示的颜色选项,请暂时忽略*[st]*tools/*a*,这样就可以看到类似于图1所示的输出结果.
图1.
使用ls命令区分文件和目录在本例中,目录都是使用深蓝色显示的,不过使用在本系列教程中所学到的知识还不足以解释这个问题.
不过,使用-l选项会对如何继续处理给出一点线索:目录列表在第一个位置处有一个"d"字符.
因此第一个步骤应该是使用grep对这个长列表中的内容进行一些简单的过滤,如清单16所示.
清单16.
使用grep过滤目录项[ian@pinguinodeveloperworks]$ls-l|grep"^d"drwxrwxr-x2ianian4096Jan2417:06mydwarticledrwxrwxr-x2ianian4096Jan1816:23readmedrwxrwxr-x3ianian4096Jan1907:41schemadrwxrwxr-x3ianian4096Jan1915:08toolsdrwxrwxr-x3ianian4096Jan1716:03webdrwxrwxr-x3ianian4096Jan1910:59xsldeveloperWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第12页,共34截取目录项可以考虑使用awk而不是grep,来在一个步骤中既对列表进行过滤,又截取每行的最后一部分内容,也就是目录名,如清单17所示.
清单17.
使用awk代替grep进行处理[ian@pinguinodeveloperworks]$ls-l|awk'/^d/{print$NF}'articlereadmeschematoolswebxsl清单17中的方法有一个问题:它无法正确处理名字中有空格的那些目录名,例如"mydwarticle".
就像是Linux和我们生活中的大部分事情一样,解决一个问题通常有很多方法,不过此处的目标是学习函数的知识,因此让我们回到使用grep方法上来.
在本系列文章中我们学过的另外一个工具是cut,它可以从一个文件(包括stdin)中截取出很多域.
现在让我们在回过头来看一下清单16,在文件名之前,可以看到8个由空格分隔的域.
在之前的命令后面加上cut就可以得到如清单18所示的输出结果.
注意-f9-选项告诉cut打印第9个域以及之后的域的内容.
清单18.
使用cut截取名称[ian@pinguinodeveloperworks]$ls-l|grep"^d"|cut-d""-f9-mydwarticlereadmeschematoolswebxsl如果我们在tools目录而不是当前目录上执行这个命令,使用这种方法存在的一个小问题就会变得十分明显,如清单19所示.
清单19.
使用cut存在的问题[ian@pinguinodeveloperworks]$ls-ltools|grep"^d"|cut-d""-f9-11:25java[ian@pinguinodeveloperworks]$ls-ldtools/[fjt]*-rw-rw-r--1ianian4798Jan814:38tools/figure1.
gifdrwxrwxr-x2ianian4096Oct3111:25tools/java-rw-rw-r--1ianian39431Jan1823:31tools/template-dw-article-5.
6.
xml-rw-rw-r--1ianian39407Jan1823:32tools/template-dw-tutorial-5.
6.
xml时间戳为什么会出现呢两个模板文件都有5个数字的大小,而java目录的大小则只有4个数字,因此cut会将多出来的空格当作另外一个域分隔符来解释.
使用seq来查找分割点cut命令也可以使用字符位置而不是域来进行分割.
除了计算字符个数之外,bashshell还有很多工具可以使用,因此可以尝试使用seq和printf命令来在长目录列表上面打印一个标尺,这样就可以ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第13页,共34方便地确定在什么地方对输出行的内容进行分割了.
seq命令最多可以使用3个参数,这就允许您可以打印出给定值之前的所有数字,或者打印出一个值到另一个值之间的所有数字,又或者打印出从某个值开始按给定的步值到第三个数值结束的所有数字.
使用seq可以实现的其他有趣功能(包括打印8进制和16进制数字)请参看手册页.
现在,让我们使用seq和printf命令来打印一个标尺,每10个字符处的位置就标记一下,如清单20所示.
清单20.
使用seq和printf打印标尺[ian@pinguinodeveloperworks]$printf2.
d"`seq101060`;printf"\n";ls-l.
.
.
.
+.
.
.
10.
.
.
.
+.
.
.
20.
.
.
.
+.
.
.
30.
.
.
.
+.
.
.
40.
.
.
.
+.
.
.
50.
.
.
.
+.
.
.
60total88drwxrwxr-x2ianian4096Jan2417:06mydwarticle-rwxr--r--1ianian215Sep2716:34new-article.
sh-rwxr--r--1ianian1078Sep2716:34new-article.
vbs-rwxr--r--1ianian216Sep2716:34new-tutorial.
sh-rwxr--r--1ianian1079Sep2716:34new-tutorial.
vbsdrwxrwxr-x2ianian4096Jan1816:23readmedrwxrwxr-x3ianian4096Jan1907:41schemadrwxrwxr-x3ianian4096Jan1915:08toolsdrwxrwxr-x3ianian4096Jan1716:03webdrwxrwxr-x3ianian4096Jan1910:59xsl啊哈!
现在可以使用ls-l|grep"^d"|cut-c40-命令来截取从位置40处开始的内容了.
我们的第一反应是这也没有真正解决问题,因为更大的文件依然会将正确的分割位置向右移.
您可以自己试验一下.
救援的sedsed是UNIX和Linux工具包中的一个功能非常强大的编辑过滤器,它使用了正则表达式.
您知道我们的任务是从以"d"开头的每一个输出行去掉它前面的8个单词和之后的空格.
可以使用sed来实现这种功能:使用模式匹配表达式/^d/选择感兴趣的行,并使用替换命令s/^d\\)\(8\}//将前8个单词替换为空字符串.
使用-n选项可以只打印那些通过p命令指定的行,如清单21所示.
清单21.
使用sed截取目录名[ian@pinguinodeveloperworks]$ls-l|sed-ne's/^d\8\}//p'mydwarticlereadmeschematoolswebxsl[ian@pinguinodeveloperworks]$ls-ltools|sed-ne's/^d\8\}//p'java要学习更多有关sed的内容,请参看参考资料一节的内容.
最终的函数现在我们已经得到满足ldirs函数功能的复杂命令了,接下来应该学习如何将其编写成一个函数.
函数由函数名加上后面的()构成,然后是一系列复合命令.
对于现在来说,复合命令可以是任何命令或命令列表,使用一个分号结束,并使用一对花括号包括起来(且必须使用空格与其他符号分隔开来).
在后面Shell脚本一节中您将学到其他的复合命令.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第14页,共34注意:在Bashshell中,函数名前面可以加上单词"function",但这并不是POSIX规范的一部分,诸如dash之类的更简单的shell并不支持这种用法.
在Shell脚本一节中,您将学习在使用了不同的shell时,如何确保脚本会被适当的shell解释.
在函数内部,可以使用表4中给出的bash特殊变量来引用参数.
可以像其他shell变量一样在这些变量前面加上一个$符号来引用这些变量.
表4.
函数的Shell参数参数用途0,1,2,.
.
.
从参数0开始的位置参数.
参数0指的是启动bash的程序名;如果函数是在一个shell脚本中运行的,就是这个shell脚本的名字.
有关其他可能的信息,请参看bash的手册页,例如使用-c参数启动bash时的情况.
以单引号或双引号括起来的字符串都会当作一个参数传递,引号会被剥离掉.
在双引号的情况中,诸如$HOME之类的shell变量会在调用函数之前被展开.
您可能需要使用单引号或双引号来传递参数,这些参数可以包含对shell具有特殊意义的嵌入空格或其他字符.
*从参数1开始的位置参数.
如果已经把双引号中的内容展开了,那么展开后就是一个单词,使用域间分隔符(IFS)特殊变量的第一个字符来分隔参数;如果IFS为空,就不会插入任何分隔.
默认的IFS值可以是空白、制表符和换行符.
如果IFS没有设置,那么所使用的分隔符就是空白,就像默认的IFS一样.
@从参数1开始的位置参数.
如果已经把双引号中的内容展开了,那么每个参数都变成一个单词,因此"$@"就等于"$1""$2".
.
.
.
如果参数中可能会包含嵌入空白,就可以使用这种格式.
#参数个数,不包括参数0.
注意:如果参数多于9个,就不能使用$10来引用第10个参数.
而必须首先处理或保存第一个参数($1),然后使用shift命令来删除第1个参数,并将其他参数下移1位,这样$10就变成了$9,依此类推.
$#的值也同时会被更新,从而反应剩余参数的个数.
现在可以定义一个简单函数,其功能仅仅是说明有多少个参数,并显示这些参数;如清单12所示.
清单22.
函数参数[ian@pinguinodeveloperworks]$testfunc(){echo"$#parameters";echo"$@";}[ian@pinguinodeveloperworks]$testfunc0parameters[ian@pinguinodeveloperworks]$testfuncabc3parametersabc[ian@pinguinodeveloperworks]$testfunca"bc"2parametersabc不管使用的是还是"$@",在上面这个函数的输出结果中并没有太大区别,不过当问题变得复杂时,可以肯定区别将会变得非常大.
现在,用这个到目前为止最为复杂的命令来创建一个ldirs函数,使用"$@"表示参数.
可以像前面的例子一样将全部函数都输入到一行中;当然bash也允许在多行中输入命令,在这种情况中会自动ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第15页,共34添加分号,如清单23所示.
清单23还显示了使用type命令来显示函数定义.
注意在type的输出结果中,ls命令已经被它别名的展开值替换掉了.
如果需要避免这个问题,可以使用/bin/ls而不是单单的ls.
清单23.
第一个ldirs函数[ian@pinguinodeveloperworks]$#Enterthefunctiononasingleline[ian@pinguinodeveloperworks]$ldirs(){ls-l"$@"|sed-ne's/^d\8\}//p';}[ian@pinguinodeveloperworks]$#Enterthefunctiononmultiplelines[ian@pinguinodeveloperworks]$ldirs()>{>ls-l"$@"|sed-ne's/^d\8\}//p'>}[ian@pinguinodeveloperworks]$typeldirsldirsisafunctionldirs(){ls--color=tty-l"$@"|sed-ne's/^d\8\}//p'}[ian@pinguinodeveloperworks]$ldirsmydwarticlereadmeschematoolswebxsl[ian@pinguinodeveloperworks]$ldirstoolsjava现在您的函数似乎已经可以正常工作了.
但是如果像清单24那样运行ldirs*会如何呢清单24.
运行ldirs*[ian@pinguinodeveloperworks]$ldirs*5.
6javawww.
ibm.
com5.
6感到惊奇吗实际上,您并没有找到当前目录中的目录,而是找到了第2级子目录的内容.
查看一下ls命令的手册页或本系列前面的教程就可以理解这是为什么了.
或者像清单25那样运行find命令来查找第2级子目录名.
清单25.
查找第2级子目录[ian@pinguinodeveloperworks]$find.
-mindepth2-maxdepth2-typed.
/tools/java.
/web/www.
ibm.
com.
/xsl/5.
6.
/schema/5.
6添加测试使用通配符暴露了这种方法在逻辑上存在的一个问题.
我们忽略了这样的一个事实,即不使用任何参数时ldirs显示的是当前目录的子目录,而ldirstools显示的是tools目录中的java子目录,而不是tools目录本身,这与将ls命令用于文件而非目录的情形是一样的.
理想情况下,如果没有给定参数,就应该使用ls-l;如果给定了一些参数,就应该使用ls-ld命令.
可以使用developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第16页,共34test命令来测试参数个数,然后使用&&和||来构建一个命令列表,并执行适当的命令.
使用test的[testexpression]格式,您的表达式可能会是这样:gt0]&&/bin/ls-ld"$@"||/bin/ls-l}|sed-ne.
.
.
.
不过这段代码还有一个小问题,如果ls-ld命令不能找到任何匹配文件或目录,就会产生一条错误消息,并返回一个非0的退出代码,这会导致ls-l命令也会被执行.
这可能并不是我们所期望的.
一个解决的方案是为第一个ls命令构造一个复合命令,这样如果命令失败,就可以对参数个数再次进行测试.
可以对原来的函数进行扩充来包含这种功能,现在这个函数应该如清单26所示.
可以利用清单26中的参数来尝试使用该函数,也可以利用您自己的参数来体验一下,看这个函数是怎样工作的.
清单26.
使用ldirs处理通配符[ian@pinguino~]$typeldirsldirsisafunctionldirs(){{[$#-gt0]&&{/bin/ls-ldgt0]}||/bin/ls-l}|sed-ne's/^d\8\}//p'}[ian@pinguinodeveloperworks]$ldirs*mydwarticlereadmeschematoolswebxsl[ian@pinguinodeveloperworks]$ldirstools/*tools/java[ian@pinguinodeveloperworks]$ldirs*xxx*/bin/ls:*xxx*:Nosuchfileordirectory[ian@pinguinodeveloperworks]$ldirs*a**s*mydwarticlereadmeschemaschematoolsxsl最终版本现在,在清单26中给出的这个例子中,可以看到一个目录被列出了两次.
如果希望,可以通过sort|uniq对sed的输出结果进行过滤,从而扩充原来的函数来解决这个问题.
从一些基本的构造块开始,现在您已经构建了一个非常复杂的shell函数了.
定制击键组合您在终端会话中输入的击键组合,以及在诸如FTP之类的程序中使用的击键组合,都是由readline库进行处理的,并且可以进行配置.
默认情况下,定制文件是主目录中的.
inputrc文件;如果系统中存在这个文件,就会在bash启动过程中读取这个文件.
可以通过设置INPUTRC变量来配置不同的文件.
如果没有设置这个变量,就会使用主目录中的.
inputrc文件.
很多系统在/etc/inputrc中都有一个默认的键映射,因此您通常会希望使用$include指令来包含它.
ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第17页,共34清单27展示了如何将ldirs函数绑定到Ctrl-t的键盘组合上(按下并一直按着Ctrl键,然后按下t).
如果希望此命令执行时不使用任何参数,可以在配置行末尾添加\n.
清单27.
样例.
inputrc文件#Mycustomkeymappings$include/etc/inputrc可以通过先按Ctrl-x再按Ctrl-r来强制再次读取INPUTRC文件.
注意如果没有自己的.
inputrc文件,有些发行版会设置INPUTRC=/etc/inputrc,因此如果您在这种系统上创建了.
inputrc文件,就需要先登出系统,然后再登录一次,这样才能使用新的定义.
只将INPUTRC设置为空或将其指向新文件只会重新读取原来的文件,而不是新的规范.
INPUTRC文件可以包括一些条件规范.
例如,您的键盘行为可能会根据您使用的是emacs编辑模式(bash默认值)还是vi模式而有所不同.
有关如何定制键盘的更多细节,请参看bash的手册页.
保存别名和函数您可以将自己的别名和函数添加到自己的~/.
bashrc文件中,不过也可以将它们保存到任何您喜欢的文件中.
不管怎样做,都请记住使用source或.
命令来引用这些文件,这样就会读取文件的内容,并在当前环境中执行这个文件.
如果创建了一个脚本并简单执行它,那么这个脚本就是在一个子shell中执行的,当这个子shell退出并将控制权返回给您时,所有有价值的定制就全部丢失了.
在下一节中,将学习如何超越这些简单的函数,如何添加一些编程结构,例如条件测试和循环结构,并将它们与多个函数结合起来来创建或修改bashshell脚本.
Shell脚本本节将介绍初级管理(LPIC-1)考试102主题1.
109.
2的内容.
这个主题的权值为3.
在本节中,您将学习如何:使用标准的shell语法,例如循环和测试使用命令替换测试命令的成功、失败或其他返回值向超级用户条件性地发送邮件通过#!
行选择正确的脚本解释器管理脚本的位置、所有者、执行和suid权限本节是在上一节中所学习的有关简单函数的基础知识上构建的,将展示增加shell编程能力的一些技术和工具.
您已经看到使用&&和||操作符的一些简单逻辑,它们让您可以根据前一个命令是正常退出还是错误退出来执行某个命令.
在ldirs函数中,可以使用这种方法来根据是否向ldirs函数传递了参数来修改对ls的调用.
现在您将学习如何扩展这些基本技术来进行更加复杂的shell编程.
测试在学习如何为变量赋值和传递参数之后,在任何编程语言中接下来要做的第一件事情都是对这些值和参数进行测试.
在shell中所做的测试会设置返回状态,这与其他命令的做法类似.
实际上,test是一个内嵌的命令!
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第18页,共34test和[test内嵌命令会根据对表达式expr的计算结果来确定返回0(True)或1(False).
也可以使用方括号,testexpr和[expr]是等效的.
可以通过显示$来检查返回值;或者使用本节后面介绍的各种条件结构来对它进行测试.
清单28.
几个简单的测试例子[ian@pinguino~]$test3-gt4&&echoTrue||echofalsefalse[ian@pinguino~]$["abc"!
="def"];echo$0[ian@pinguino~]$test-d"$HOME";echo$0在第一个例子中,-gt操作符用来在两个数值之间进行数学比较运算.
在第二个例子中,使用了另外一种[]形式来比较两个字符串是否相等.
在最后一个例子中,使用-d一元操作符对HOME变量的值进行测试,看它是否是一个目录.
数值可以使用-eq、-ne、-lt、-le、-gt或-ge进行比较,分别表示等于、不等于、小于、小于或等于、大于、大于或等于.
字符串可以使用操作符=、!
=、分别进行等于、不等于或第一个字符串是在第二个字符串之前还是之后的比较操作.
一元操作符-z测试字符串是否为空;如果字符串不为空,那么-n或不使用任何操作符就返回True.
注意:操作符也可以由shell用来进行重定向,因此必须使用\对它们进行转义.
清单29给出了几个字符串测试的例子.
请检查它们是否如您所期望的那样.
清单29.
几个字符串测试的例子[ian@pinguino~]$test"abc"="def";echo$1[ian@pinguino~]$["abc"!
="def"];echo$0[ian@pinguino~]$["abc"\"def"];echo$1[ian@pinguino~]$["abc"\"abc"];echo$1表5给出了几个常见的文件测试的例子.
如果所测试的文件是一个系统中存在的文件,并且具有指定的特性,测试结果就是True.
表5.
几个文件测试的例子操作符特性-d目录-e存在(也可以使用-a)-f普通文件-h符号链接(也可以使用-L)ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第19页,共34-p命名的管道-r可以读取-s非空-SSocket-w可以写入-N上次读取之后已经被修改过了除了上面的一元测试之外,还可以使用表6中给出的二元操作符对两个文件进行比较.
表6.
文件对测试操作符如果符合该条件就为True-nt测试文件1是否比文件2更新.
修改日期会在这个比较和下个比较中使用.
-ot测试文件1是否比文件2更旧.
-ef测试文件1是否是到文件2的硬链接.
其他几个测试允许检查诸如文件权限之类的事情.
有关详细信息请参看手册页,也可以通过helptest来查看有关内置测试的简单介绍.
还可以使用help命令获得关于其他内置功能的信息.
-o操作符允许测试不同的shell选项,这些选项可通过set-ooption进行设置,如果选项已设置就返回True(0),否则返回False(1),如清单30所示.
清单30.
测试shell选项[ian@pinguino~]$set+onounset[ian@pinguino~]$[-onounset];echo$1[ian@pinguino~]$set-u[ian@pinguino~]$test-onounset;echo$0最后,-a和-o操作允许分别使用逻辑AND和OR来合并表达式,而一元操作符!
则是对测试含义取反.
可以使用圆括号来对表达式进行分组,并覆盖默认的优先级顺序.
记住shell通常会在一个子shell中运行表达式,因此需要使用\(和\)对圆括号进行转义,或者使用单引号或双引号将这些操作符围起来.
清单31展示了deMorgan定律在表达式中的应用.
清单31.
对测试进行合并和分组[ian@pinguino~]$test"a"!
="$HOME"-a3-ge4;echo$1[ian@pinguinoa"="$HOME"-o3-lt4\)];echo$1[ian@pinguinoa"="$HOME"-o'('3-lt4echo$1((和[[test命令的功能非常强大,但是在对转义和字符串与数值比较之间的区别的处理上有些吃力.
幸运的是,bash有两种方法可以按照那些熟悉C、C++或Java语法的人更加习惯的方式进行测试.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第20页,共34(())复合命令可以计算一个算术表达式的值,如果这个表达式的值为0就将退出状态设置为1;如果表达式的值不为0,就将退出状态设置为0.
不需要对((和))之间的操作符进行转义.
数值计算是按照整型进行的.
被除数为0会产生错误,但溢出不会产生错误.
也可以执行在C语言中很常见的数值、逻辑和位操作.
let命令也可以执行一个或多个算术表达式.
它通常用来对数值变量进行赋值.
清单32.
对算术表达式进行赋值和测试[ian@pinguino~]$letx=2y=2**3z=y*3;echo$$x$y$z02824[ian@pinguino~]$((w=(y/x)x)&0x0f)));echo$$x$y$w03816[ian@pinguino~]$((w=(y/x)x)&0x0f)));echo$$x$y$w04813与(())类似,[[]]复合命令让您可以使用更加自然的语法进行文件名和字符串测试.
可以使用圆括号和逻辑操作符组合test命令所允许的测试.
清单33.
使用[[进行组合[ian@pinguinod"$HOME"w"$HOME">echo"homeisawritabledirectory"homeisawritabledirectory在使用了=或!
=操作符时,[[也可以对字符串进行模式匹配,如清单34所示.
清单34.
使用[[进行通配符测试[ian@pinguino~]$[["abcdef.
d,x--"==a[abc]*\d*]];echo$0[ian@pinguino~]$[["abcdefc"==a[abc]*\d*]];echo$1[ian@pinguino~]$[["abcdefd,x"==a[abc]*\d*]];echo$1甚至可以在[[复合命令中进行数学测试,不过这要非常谨慎.
除非是在((复合命令内部,否则操作符会将操作数当作字符串进行比较,并按照当前的比较序列的顺序测试其顺序.
清单35给出了几个例子.
清单35.
使用[[进行数学测试[ian@pinguino~]$[["abcdefd,x"==a[abc]*\d*||((3>2))]];echo$0[ian@pinguino~]$[["abcdefd,x"==a[abc]*\d*||3-gt2]];echo$0[ian@pinguino~]$[["abcdefd,x"==a[abc]*\d*||3>2]];echo$0[ian@pinguino~]$[["abcdefd,x"==a[abc]*\d*||a>2]];echo$0[ian@pinguino~]$[["abcdefd,x"==a[abc]*\d*||a-gt2]];echo$-bash:a:unboundvariable条件测试可以使用上面的测试以及&&和||控制操作符来完成很多编程,但bash还包括了大家更加熟悉的"if,then,else"和case结构.
在学习这些内容之后,您将学习有关循环结构的内容,到那时您的工具箱就更丰富了.
ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第21页,共34If,then,else语句bash的if命令是一个复合命令,它对测试或命令的返回值($)进行测试,并根据该值是True(0)还是False(非0)来进行分支跳转.
尽管上面的测试只会返回0或1,但是这些命令也可以返回其他值.
在本教程稍后您将学习更多有关这种测试的内容.
bash中的if命令有一个then子句,其中包含了如果测试或命令返回0时要执行的命令列表;还可以包含一个或多个可选的elif子句,每个elif子句中都可以有另外一个测试和一个then子句,后者中列有相关的命令列表;最后,还可以包括一个可选的else子句以及一个命令列表,如果最初测试或elif子句中使用的测试都不为true,并且后面有一个终止fi标记着结构的末尾,这些命令就会执行.
使用到现在为止所学习到的内容,就可以构建一个简单的计算器来计算数学表达式的值,如清单36所示.
清单36.
使用if,then,else语句计算表达式[ian@pinguino~]$functionmycalc()>{>localx>if[$#-lt1];then>echo"Thisfunctionevaluatesarithmeticforyouifyougiveitsome">elifthen>letx="$*">echo"$*=$x">else>echo"$*=0orisnotanarithmeticexpression">fi>}[ian@pinguino~]$mycalc3+43+4=7[ian@pinguino~]$mycalc3+4**33+4**3=67[ian@pinguino~]$mycalc3+(4**3/2)-bash:syntaxerrornearunexpectedtoken`('[ian@pinguino~]$mycalc3+"(4**3/2)"3+(4**3/2)=35[ian@pinguino~]$mycalcxyzxyz=0orisnotanarithmeticexpression[ian@pinguino~]$mycalcxyz+3+"(4**3/2)"+abcxyz+3+(4**3/2)+abc=35计算器使用local语句将x声明为本地变量,它只能在mycalc函数内部使用.
let函数有几个可能的选项,与之密切相关的declare函数也是一样.
请查看bash的手册页或使用helplet来获得更多信息.
正如在清单36中所看到的一样,如果使用了shell元字符,例如(、)、*、>和Bash有另外一个类似于case的结构,它也可以用来将输出结果显示在终端上并让用户选择适当的项.
它就是select语句,不过在这里不会对它进行过多的介绍.
请参看bash的手册页,或者输入helpselect来学习更多内容.
当然,使用这种简单方法也有很多问题;比如无法一次购买两份饮料,这个函数也不能处理以小写形式输入的内容.
那么能否采用大小写不敏感的匹配形式呢答案是肯定的,下面就让我们看一下如何实现这种功能.
返回值Bashshell有一个内嵌的shopt,可以用来设置或取消很多shell选项.
其中一个选项是nocasematch,如果这个选项设置了,就会通知shell忽略字符串匹配中的大小写.
您的第一想法可能是使用在test命令中学习到的-o操作数.
不幸的是,nocasematch并不能应用-o选项,因此只能考虑其他方法.
shopt命令与大部分UNIX和Linux命令一样,都会设置一个返回值,可以使用$来查看这个返回值的内容.
返回值不仅仅存在于您之前学习过的测试中,如果您仔细分析一下在if语句中进行的测试,就会发现它们实际上是在测试底层test命令的返回值是True(0)还是False(1或其他非0值).
即使您不使用测试,而是使用其他命令,也是如此.
返回值为0表示命令执行成功,返回值不为0表示命令执行失败.
了解了这些知识,您现在就可以测试nocasematch选项了,如果还没有设置这个选项,现在就先设置它,然后当您的函数结束时,将它返回到用户的首选项中.
shopt命令有4个方便的选项:-pqsu,分别用来打印当前值、不打印任何东西、设置选项或去除选项的设置.
-p和-q选项将返回值设置为0,表示这个shell选项已经设置了;为1表示该选项没有设置.
-p选项会打印将这个选项设置为当前值所需要的命令,而-q选项则简单地将返回值设置为0或1.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第24页,共34修改后的函数使用shopt中的返回值来设置代表nocasematch选项的当前状态的本地变量、设置这个选项、运行case命令,然后再将nocasematch选项重置成原来的值.
实现这种功能的一种方法如清单40所示.
清单40.
测试命令的返回值[ian@pinguino~]$typemyordermyorderisafunctionmyorder(){localrestorecase;ifshopt-qnocasematch;thenrestorecase="-s";elserestorecase="-u";shopt-snocasematch;fi;case"$*"in"coffee"|"decaf")echo"Hotcoffeecomingrightup";;"tea")echo"Hotteaonitsway";;"soda")echo"Yourice-coldsodawillbereadyinamoment";;*)echo"Sorry,wedon'tservethathere";;esac;shopt$restorecasenocasematch}[ian@pinguino~]$shopt-pnocasematchshopt-unocasematch[ian@pinguino~]$#nocasematchiscurrentlyunset[ian@pinguino~]$myorderDECAFHotcoffeecomingrightup[ian@pinguino~]$myorderSodaYourice-coldsodawillbereadyinamoment[ian@pinguino~]$shopt-pnocasematchshopt-unocasematch[ian@pinguino~]$#nocasematchisunsetagainafterrunningthemyorderfunction如果您希望自己的函数(或脚本)返回一个其他函数或命令可以测试的值,就请在自己的函数中使用return语句.
清单41显示了如何实现当顾客购买所能提供的饮料时返回0,当顾客请求购买其他东西就返回1.
清单41.
设置函数的返回值[ian@pinguino~]$typemyordermyorderisafunctionmyorder(){localrestorecase=$(shopt-pnocasematch)rc=0;shopt-snocasematch;case"$*"in"coffee"|"decaf")echo"Hotcoffeecomingrightup";;"tea")echo"Hotteaonitsway";;ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第25页,共34"soda")echo"Yourice-coldsodawillbereadyinamoment";;*)echo"Sorry,wedon'tservethathere";rc=1;;esac;$restorecase;return$rc}[ian@pinguino~]$myordercoffee;echo$Hotcoffeecomingrightup0[ian@pinguino~]$myordermilk;echo$Sorry,wedon'tservethathere1如果没有指定自己的返回值,那么返回值就是最后一个命令执行的结果.
函数总是习惯在您意想不到的情况下被重用,因此最好设置自己的返回值.
命令也可以返回0和1之外的值,有时需要对此进行区分.
例如,如果找到可匹配模式,grep命令就返回0;否则就返回1;但是如果模式无效或该文件规范并不能匹配任何文件,就会返回2.
如果需要区分除成功(0)或失败(非0)之外的更多的返回值,可能就需要使用case命令,也可以使用带有多个elif的if命令.
命令替换在"LPI101考试准备(主题103):GNU和UNIX命令"教程中您已经见到过命令替换的用法了,不过下面让我们快速回顾一下相关内容.
命令替换让您可以通过简单地在命令两边加上一个$(和)或使用一对反单引号`来将一个命令的输出结果作为另外一个命令的输入使用.
如果希望嵌套地使用一个命令的输出结果作为生成最终结果的另外一个命令的一部分,就会发现$()格式的优点;它也使得确定要执行什么操作变得更加简单,因为圆括号区分左、右,而两边的反单引号是完全相同的.
当然,选择权在您手里,反单引号也很常见.
您通常都会在循环(在下面的循环一节中进行介绍)中使用命令替换功能.
也可以使用它来简化刚才创建的myorder函数.
由于shopt-pnocasematch会打印出用来将nocasematch选项设置为其当前值的命令,因此只需要保存输出结果并在case语句的结尾执行它即可.
不管您对它进行了修改与否,这都会恢复nocasematch选项.
修订后的函数如清单42所示.
您可以自己尝试一下.
清单42.
使用命令替换而不是返回值测试[ian@pinguino~]$typemyordermyorderisafunctionmyorder(){localrestorecase=$(shopt-pnocasematch)rc=0;shopt-snocasematch;case"$*"in"coffee"|"decaf")echo"Hotcoffeecomingrightup";;"tea")echo"Hotteaonitsway";;developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第26页,共34"soda")echo"Yourice-coldsodawillbereadyinamoment";;*)echo"Sorry,wedon'tservethathere"rc=1;;esac;$restorecasereturn$rc}[ian@pinguino~]$shopt-pnocasematchshopt-unocasematch[ian@pinguino~]$myorderDECAFHotcoffeecomingrightup[ian@pinguino~]$myorderTeAHotteaonitsway[ian@pinguino~]$shopt-pnocasematchshopt-unocasematch调试如果您自己输入了一些函数,并在输入时出现一些错误,您可能会纳闷究竟是什么地方出现了问题,您也可能会非常想弄清楚该如何对函数进行调试.
幸运的是,shell允许您设置-x选项来在shell执行函数的同时对命令及其参数进行跟踪.
清单43显示了这对于清单42给出的myorder函数来说是如何工作的.
清单43.
跟踪函数执行[ian@pinguino~]$set-x++echo-ne'\033]0;ian@pinguino:~'[ian@pinguino~]$myordertea+myordertea++shopt-pnocasematch+local'restorecase=shopt-unocasematch'rc=0+shopt-snocasematch+case"$*"in+echo'Hotteaonitsway'Hotteaonitsway+shopt-unocasematch+return0++echo-ne'\033]0;ian@pinguino:~'[ian@pinguino~]$set+x+set+x对于别名、函数或脚本都可以使用这种技术.
如果需要更多信息,可以添加-v选项进行更详细的输出.
循环Bash和其他shell都有一些循环结构,这与C语言使用的循环结构非常类似.
每个循环都会执行一个命令列表零次到多次.
命令列表使用单词do和done包含起来,其中每条语句前面都有一个分号.
ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第27页,共34for循环有两种形式.
shell脚本编程中最常见的形式是对一组值进行迭代,对每个值都执行命令列表一次.
这组值可能为空,在这种情况下命令列表就不会被执行.
另外一种形式更加类似于传统的Cfor循环,使用3个数学表达式来控制循环的起始条件、步进函数和结束条件.
while循环每次都在循环开始时计算一个条件的值,如果这个条件为true,就执行命令列表.
如果这个条件最初不为true,那么这些命令就永远都不会执行.
until循环执行一个命令列表,并在每个循环结束时计算某个条件的值.
如果这个条件为true,就再次执行这个循环.
即使条件最初不为true,这些命令也会至少被执行一次.
如果所测试的条件是一系列命令,那么所使用的就是最后执行的命令的返回值.
清单44给出了循环命令的例子.
清单44.
For、while和until循环[ian@pinguino~]$forxinabd2"mystuff";doecho$x;doneabd2mystuff[ian@pinguino~]$for((x=2;x3"3parametersUsing$*[abc][a][bc][1][2][3]Using"$*"[abc|abc|123]Using$@[abc][a][bc][1][2][3]Using"$@"[abc][abc][123]我们需要仔细学习它们之间的差异,尤其是对引号中的格式和包括诸如空格和换行符之类的参数的用法.
Break和continuebreak命令让您可以从一个循环中立即退出.
如果使用了嵌套循环,也可以指定退出的层次数.
因此如果在for循环中有一个until循环,而这个for循环在另外一个for循环之中,所有这些循环又全部在一个while循环中,那么break3就会立即结束until循环和2个for循环,并将控制权返回给while循环中的代码.
continue语句可以跳过命令列表中的剩下的语句,直接跳转到下一次循环的开头.
清单47.
使用break和continue[ian@pinguino~]$forwordinredbluegreenyellowviolet;do>if["$word"=blue];thencontinue;fi>if["$word"=yellow];thenbreak;fi>echo"$word">doneredgreenibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第29页,共34再访ldirs还记得为了让ldirs函数能够从一个长列表中提取出文件名并确定它是否是一个目录,我们做了多少工作吗您开发的那个最终函数还算不错,不过现在您掌握了本教程中的所有信息之后,还会创建相同的函数吗也许就不会了.
现在您知道如何使用[-d$name]来测试一个名字是否是目录了,并且了解了for循环的用法.
清单48给出了可以编写ldirs函数的另外一种方法.
清单48.
实现ldirs的另外一种方法[ian@pinguinodeveloperworks]$typeldirsldirsisafunctionldirs(){if[$#-gt0];thenforfilein"$@";do[-d"$file"]&&echo"$file";done;elseforfilein*;do[-d"$file"]&&echo"$file";done;fi;return0}[ian@pinguinodeveloperworks]$ldirsmydwarticlemy-tutorialreadmeschematoolswebxsl[ian@pinguinodeveloperworks]$ldirs*s*tools/*schematoolsxsltools/java[ian@pinguinodeveloperworks]$ldirs*www*[ian@pinguinodeveloperworks]$您会注意到如何没有目录可以匹配您给出的条件,这个函数就会安静地返回.
这也许符合您的预期,也许并不符合,不过这个函数可能比使用sed解析ls命令输出的那个版本更容易理解.
至少现在您的工具箱中又多了一个工具.
创建脚本您可能还记得myorder一次只能处理一份饮料.
现在可以使用一个for循环对这个单一饮料的函数进行组合,从而对参数进行迭代来处理多份饮料.
这非常简单,就像是将您的函数放到一个文件中,并添加一些for指令一样.
清单49给出了新myorder.
sh脚本的内容.
清单49.
购买多份饮料[ian@pinguino~]$catmyorder.
shfunctionmyorder(){localrestorecase=$(shopt-pnocasematch)rc=0;shopt-snocasematch;case"$*"in"coffee"|"decaf")developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第30页,共34echo"Hotcoffeecomingrightup";;"tea")echo"Hotteaonitsway";;"soda")echo"Yourice-coldsodawillbereadyinamoment";;*)echo"Sorry,wedon'tservethathere";rc=1;;esac;$restorecase;return$rc}forfilein"$@";domyorder"$file";done[ian@pinguino~]$.
myorder.
shcoffeetea"milkshake"HotcoffeecomingrightupHotteaonitswaySorry,wedon'tservethathere注意这个脚本使用了.
命令来将其引用到当前shell环境中运行,而不是在它自己的shell中运行.
为了能够执行脚本,可以引用它,也可以使用chmod-x命令将这个脚本标记成是可执行的,如清单50所示.
清单50.
将脚本标记成可执行的[ian@pinguino~]$chmod+xmyorder.
sh[ian@pinguino~]$.
/myorder.
shcoffeetea"milkshake"HotcoffeecomingrightupHotteaonitswaySorry,wedon'tservethathere指定shell拥有了一个全新的shell脚本之后,您可能会问这个脚本是否在所有的shell中都能很好地工作.
清单51给出了相同的shell脚本在Ubuntu系统上首先使用bashshell然后再使用dashshell执行时的情况.
清单51.
Shell的区别ian@attic4:~$.
/myorderteasoda-bash:.
/myorder:Nosuchfileordirectoryian@attic4:~$.
/myorder.
shteasodaHotteaonitswayYourice-coldsodawillbereadyinamomentian@attic4:~$dash$.
/myorder.
shteasoda.
/myorder.
sh:1:Syntaxerror:"("unexpected这可不太好.
记得我们在前面曾经说过单词"function"在bash函数定义中是可选的,但它并不是POSIXshell规范的一部分吗与bash相比,dash更小更轻,它并不支持这种可选特性.
由于无法确保用户可能会喜欢使用哪种shell,因此应该总要确保脚本可以移植到所有shell环境中,这可能会非常困难;也可以使用所谓的shebang(#!
)方法来指定自己的脚本就在某个特定的shell中运行.
shebang行必须位ibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第31页,共34于脚本的第1行,在该行中的其他部分指定程序运行必须使用的shell,因此在myorder.
sh脚本中就应该是#!
/bin/bash.
清单52.
使用shebang$head-n3myorder.
sh#!
/bin/bashfunctionmyorder(){$.
/myorder.
shTeaCoffeeHotteaonitswayHotcoffeecomingrightup可以使用cat命令来显示/etc/shells的内容,即系统中支持的shell列表.
有些系统也会列出没有安装的一些shell,有些列出的shell(可能是/dev/null)可能是为了确保FTP用户不会意外地脱离开自己受限的环境.
如果需要修改默认的shell,可以使用chsh命令,这会更新/etc/passwd中userid对应的项.
Suid权限和脚本位置在前面的教程LPI101考试准备:设备、Linux文件系统与文件系统层次标准中,您已经学习了如何修改文件的所有者和组,以及如何设置文件的suid和sgid权限.
具有这些权限的可执行文件会在一个具有文件所有者(用于suid)或组(用于sgid)的有效权限的shell中运行.
因此,程序可以根据权限位的设置来执行文件所有者或组可以执行的任何操作.
一些程序需要这样做是有原因的.
例如,passwd程序需要更新/etc/shadow,chsh命令(用来修改默认shell的命令)需要更新/etc/passwd.
如果为ls使用了别名,再试图列出这些程序可能就会导致红色高亮显示的列表出现,以示警告,如图2所示.
注意这两个程序都设置了suid位,因此运转起来就像是root用户在运行它们一样.
图2.
具有suid权限的程序清单53显示了一个普通用户可以运行suid程序并更新root所拥有的文件.
清单53.
使用suid的程序ian@attic4:~$passwdChangingpasswordforian(current)UNIXpassword:EnternewUNIXpassword:RetypenewUNIXpassword:passwd:passwordupdatedsuccessfullyian@attic4:~$chshPassword:ChangingtheloginshellforianEnterthenewvalue,orpressENTERforthedefaultLoginShell[/bin/bash]:/bin/dashian@attic4:~$find/etc-mmin-2-ls3088654drwxr-xr-x108rootroot4096Jan2922:52/etcfind:/etc/cups/ssl:Permissiondeniedfind:/etc/lvm/archive:Permissiondeniedfind:/etc/lvm/backup:PermissiondenieddeveloperWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第32页,共34find:/etc/ssl/private:Permissiondenied3111704-rw-r--r--1rootroot1215Jan2922:52/etc/passwd3097444-rw-r-----1rootshadow782Jan2922:52/etc/shadowian@attic4:~$grepian/etc/passwdian:x:1000:1000:IanShields,,,:/home/ian:/bin/dash尽管可以对shell脚本设置suid和sgid权限,但是大部分现代shell都会对脚本忽略这些位的设置.
正如您已经看到的一样,shell是一种功能非常强大的脚本语言,还有很多在本教程中没有介绍的特性,例如解释和执行任意表达式的能力.
这些特性允许的限制太过宽泛,这就使环境变得非常不安全.
因此,如果您对一个shell脚本设置了suid或sgid权限,就不要期望脚本在执行时能够遵守这些设置.
之前您修改了myorder.
sh的权限将其标记成可执行的(x).
不管怎样,仍然需要通过在名字前面加上.
/才能运行脚本,除非是在当前shell中引用它.
要只通过名字来执行shell脚本,就需要将它放到路径中(以PATH变量表示).
通常,您不会希望将当前目录添加到路径中,因为这可能会引起安全问题.
在完成对脚本的测试并确定它非常理想之后,如果这是一个个人脚本,就应该将它放到~/nom中;如果它也可以由系统中的其他人使用,就应该将它放到/usr/local/bin中.
如果简单地使用chmod-x来将它标记成可执行的,那么每个用户都可以执行它(所有者、同组用户以及全体用户).
这通常是我们希望的情况,不过如果需要对这个脚本进行一些限制,从而只有特定组的用户可以执行它,就请回过头来重新阅读一下前面的教程LPI101考试准备:设备、Linux文件系统与文件系统层次标准.
您可能已经注意到shell通常都位于/bin目录中,而不是在/usr/bin目录中.
根据文件系统层次标准,/usr/bin可以位于一个各系统共享的文件系统中,因此在初始化时这个文件系统可能尚不能使用.
因此,特定的功能,例如shell,都应该在/bin目录中,这样即使/usr/bin没有加载,它们也依然可以使用.
用户创建的脚本通常都不需要保存到/bin(或/sbin)目录中,因为这些目录中的程序应该给您提供了足够的工具来启动和运行系统,并让您可以挂载/usr文件系统.
向root用户发送邮件如果您的脚本在午夜您睡觉时仍需要在系统上运行一些管理任务,而此时出现了问题该怎么办呢幸运的是,错误信息或日志文件可以很容易地通过邮件发送给您自己或另外一个管理员或root用户,方法是将消息通过管道发送给mail命令,并使用-s选项添加一个主题行,如清单54所示.
清单54.
将错误消息发送给某个用户ian@attic4:~$echo"Midnighterrormessage"|mail-s"Adminerror"ianian@attic4:~$mailMailversion8.
1.
201/15/2001.
Typeforhelp.
"/var/mail/ian":1message1new>N1ian@localhostMonJan2923:5814/420Adminerror&Message1:Fromian@localhostMonJan2923:58:272007X-Original-To:ianTo:ian@localhostSubject:AdminerrorDate:Mon,29Jan200723:58:27-0500(EST)From:ian@localhost(IanShields)Midnighterrormessage&d&qibm.
com/developerWorks/cn/developerWorksLPI102考试准备,主题109:Shell、脚本、编程和编译第33页,共34如果需要发送日志文件,可以使用<重定向函数来将它作为mail命令的输入进行重定向.
如果需要发送多个文件,可以使用cat命令对它们进行合并,并将输出通过管道发送给mail命令.
在清单54中,邮件被发送给了用户ian,他碰巧也是运行该命令的那个人,不过管理脚本更喜欢将邮件发送给root用户或其他管理员.
像往常一样,您可以参考mail的手册页来了解可以指定的其他选项.
本教程到这里就结束了.
我们已经介绍了大量有关shell和脚本编程的内容.
不要忘记对本教程进行评价并给出您的反馈.
developerWorksibm.
com/developerWorks/cn/LPI102考试准备,主题109:Shell、脚本、编程和编译第34页,共34关于作者IanShieldsIanShields为developerWorksLinux专区的许多Linux项目工作.
他是IBM北卡罗莱那州ResearchTrianglePark的一名高级程序员.
他于1973年作为一名系统工程师加入IBM位于澳大利亚堪培拉的子公司.
之后,在加拿大蒙特利尔和北卡罗莱那州RTP从事通信系统和普及运算.
他拥有多项专利.
他毕业于AustralianNationalUniversity,本科学位是纯数学和哲学.
他拥有北卡罗来纳州立大学的计算机学士和博士学位.
搬瓦工今天正式对外开卖荷兰阿姆斯特丹机房走联通AS9929高端线路的VPS,官方标注为“NL - China Unicom Amsterdam(ENUL_9)”,三网都走联通高端网络,即使是在欧洲,国内访问也就是飞快。搬瓦工的依旧是10Gbps带宽,可以在美国cn2 gia、日本软银与荷兰AS9929之间免费切换。官方网站:https://bwh81.net优惠码:BWH3HYATVBJW,节约6...
Hostodo在九月份又发布了两款特别套餐,开设在美国拉斯维加斯、迈阿密和斯波坎机房,基于KVM架构,采用NVMe SSD高性能磁盘,最低1.5GB内存8TB月流量套餐年付34.99美元起。Hostodo是一家成立于2014年的国外VPS主机商,主打低价VPS套餐且年付为主,基于OpenVZ和KVM架构,美国三个地区机房,支持支付宝或者PayPal、加密货币等付款。下面列出这两款主机配置信息。CP...
BGPTO是一家成立于2017年的国人主机商,从商家背景上是国内的K总和有其他投资者共同创办的商家,主营是独立服务器业务。数据中心包括美国洛杉矶Cera、新加坡、日本大阪和香港数据中心的服务器。商家对所销售服务器产品拥有自主硬件和IP资源,支持Linux和Windows。这个月,有看到商家BGPTO日本和新加坡机房独服正进行优惠促销,折扣最低65折。第一、商家机房优惠券码这次商家的活动机房是新加坡...