linux操作系统 第10章 Shell程序设计
第10章 Shell程序设计
10.1 Shell语言概述
10.1.1 Shell语言的特点
与其他编程语言相比,Shell语言具有如下特点:
(1) Shell是一种解释性语言。这就是说,用Shell语言写的程序不需编译,可以直接由Shell进程解释执行。解释性语言的特点是快捷方便,可以即编即用,但与编译性语言的目标程序来比,解释性语言程序的运行速度要低一些。
(2) Shell是基于字符串的语言。Shell只是做字符串处理,不支持复杂的数据结构和运算。Shell的输出也全部是字符方式的。
(3) Shell是命令级语言。Shell程序全部由命令而不是语句组成,几乎所有的Shell命令和可执行程序都可用来编写Shell程序。Shell命令十分丰富,命令的组合功能也十分强大。所以,用简单的命令和命令组合形成的Shell程序即简洁又高效,可以实现各种复杂的功能。
另外需要说明的是,不同版本的Shell程序不完全兼容,差别可能是细微的,也可能是明显的。本章介绍的是Bash编程,它的应用较广泛,兼容性也很好。
10.1.2 Shell程序
Shell程序也称Shell脚本(script),是由一系列Shell命令为基本元素构成的文本文件。简单的Shell程序可以只是一个命令序列,高级Shell程序中还可以包含复杂的命令组合,定义各种参数和变量、使用条件命令、控制结构以及其他高级特性。
例10.1 第1个Shell程序:
$ cat hello #hello程序
# This is a shell script to say hello.
echo Hello World!
echo -n “Today is ”
date “+%A, %B %d, %Y.”
$ . hello #运行hello程序
Hello World!
Today is Saturday, October 13, 2007.
$
这个hello程序的第1行是注释,后面3行是命令。在执行此程序时,Shell依次执行这3个命令并输出显示信息。
Shell程序中的成分:
Shell 脚本是以行为单位的,在执行脚本的时候会分解成一行一行依次执行。脚本中所包含的成分主要有 :注释、命令、Shell变量和结构控制语句。
注释:用于对脚本进行解释和说明,在注释行的前面加上符号#,这样在执行脚本的时候Shell就不会对该行进行解释。
命令:在Shell脚本中可以出现任何在交互方式下可以使用的命令。
变量: Shell支持字符串变量和整型变量
结构控制语句:用于编写复杂脚本的流程控制语句
10.1.3 Shell程序的建立与执行
Shell脚本是文本文件,因此可以用任何文本编辑器(如vi、emacs等)建立和编辑脚本。Shell脚本文件的名称没有限定的扩展名,通常不带扩展名或带“.sh”扩展名。
Shell脚本的执行方式主要有3种。
(1) 将脚本作为可执行文件执行:
$ chmod a+x hello
$ ./hello
用文本编辑器生成的脚本文件默认是没有x权限的,也就是说是不可直接执行的。赋予x权限后,脚本就可以像一般的Shell命令那样执行了。如果脚本不在系统存放命令的标准目录下,需要在执行时指定脚本的路径。上例中脚本hello放在当前目录下,所以要用./hello来运行。
(2) 启动一个Shell子进程来执行脚本文件:
$ bash hello #或bash < hello
执行此命令行时,Shell进程先启动一个bash子进程,让它执行hello脚本的内容,执行完毕后bash子进程也终止。在这种方式中,脚本是作为命令参数传给子Shell的。子Shell运行时读取该文件并执行其内容,因此脚本文件不必有执行权限。这种方法常用于运行一个其他版本的Shell脚本。假如当前Shell是bash,而chello是用C Shell语言写的脚本,则要执行这个脚本时可以用csh chello命令启动一个csh进程来执行它。
(3) 让当前Shell进程执行脚本文件:
$ . hello #注意.后面的空格
“.”是一个Shell内部命令,hello是它的参数。“.”命令的功能是读取参数指定的文件,执行其内容。这种执行方式与第2种方式类似,区别在于此方式是由当前Shell进程来执行脚本文件的。
10.2 Shell特殊字符
Shell定义了一些特殊的字符,称为元字符(meta-characters),它们对Shell有特殊的含义。Shell在读入命令行后,要先对命令行进行扫描,找出元字符并进行相应的替换或处理,以确定要执行的程序和它的参数及执行方式等。
Shell的元字符包括:文件通配符、输入/输出重定向及管道符、注释符、命令执行控制符、命令组合与替换符、转义符等。恰当地运用这些字符能够使Shell的功能得到充分发挥。以下分类介绍这些特殊字符的含义和用法。
10.2.1 通配符
通配符用于描述命令中的文件名参数。当Shell在命令的参数中遇到带有通配符的文件名模式时,它将目录中的所有文件与该模式进行匹配,并用匹配的文件名替换参数中的文件名模式。表9-1列出了常用的通配符。
利用通配符来描述文件参数可以简化对多个文件的处理操作。例10.2示意了用通配符构造的文件名模式与文件名之间的匹配关系。
例10.2 通配符的匹配作用:
zip* 匹配以字符zip开始的任何字符串;
*zip 匹配以字符zip结尾的任何字符串;
rc?.d 匹配以rc开始、以.d结束, 中间为任何单个字符的字符串;
[a-d,x,y] 匹配字符a、b、c、d、x、y;
[!Z] 匹配不为Z的单个字符;
[a-f]* 匹配字符a到f开头的字符串,如abc,d2,e3.c,f.dat;
*[!o] 匹配不以o结尾的字符串。
10.2.2 输入/输出重定向与管道符
输入/输出重定向和管道符的作用是改变命令的输入/输出环境。当Shell在命令行中遇到输入/输出重定向或管道符时,它将对命令的标准输入/输出文件作相应的更改,然后再执行命令。表9-2列出了常用的输入/输出重定向与管道符。
1. 标准输入/输出重定向
“<”是标准输入重定向符,它将标准输入stdin重定向到一个文件。“>”是标准输出重定向符,它将标准输出重定向到一个文件。为了区分是哪种输出重定向,可以在符号前加一个文件描述符fd。stdout的fd是1,stderr的fd是2,所以1>表示标准输出重定向,2>表示标准错误输出重定向。未指定fd时,默认地表示是1>。
例10.3 将标准输入改为infile,标准输出改为outfile,标准错误输出改为errfile文件:
$ myproc >outfile 2>errfile <infile
2. 合并重定向与归并重定向
“&>”是标准输出合并重定向符,它将标准输出与标准错误输出合在一起重定向到一个文件。“>&”是标准输出归并重定向符,它将一种标准输出归并到另一种标准输出流中。符号的前后各用一个fd来表示归并的方式。1>&2表示将stdout归并到stderr流中,>&2默认地表示是1>&2。2>&1表示将stderr归并到stdout流中。
例10.4 将标准输出和标准错误输出改向到out文件:
$ myprog &> out
例10.5 将标准输出改向到out文件,并将标准错误输出并入到标准输出中:
$ myprog > out 2>&1 #等价于myprog &> out
例10.6 将标准输出并入标准错误输出流:
$ myprog 1>&2 #避免标准输出被管道改向
3. 附加重定向
“>>”是标准输出附加重定向符,它将标准输出stdout或标准错误输出stderr用追加的方式重定向到一个文件。1>>或>>表示stdout附加重定向,2>>表示stderr附加重定向。
例10.7 在.bash_profile文件的尾部添加一行:
$ echo ‘PATH=$PATH:.’ >> .bash_profile
4. here文档
“<<”是一种特殊的标准输入重定向机制,称为“here文档(here document)”。here文档的表示格式是“<< 结束标记字符串”,它的作用是指示Shell将本命令行后面的输入行作为命令的标准输入传给命令,直到遇到结束标记字符串为止。
例10.8 here文档的使用:
$ sort << End
> Jone Doe
> David Nice
> Masood Shah
> End
David Nice
Jone Doe
Masood Shah
$ cat here-doctest
sort << End
Jone Doe
David Nice
Masood Shah
End
$ here-doctest
David Nice
Jone Doe
Masood Shah
$
here文档主要用在Shell脚本中。它允许将脚本中某个命令的标准输入直接写在该命令行之后。这样,当执行到该命令行时,它不再去等待标准输入而是在本文档内(here文档)直接获取输入进行处理,如上例中here-doctest脚本所示。
5. 管道
“|”是管道符,它将前一命令的标准输出作为后一命令的标准输入。“| tee”是T型管道符,它将前一命令的标准输出存入一个文件中,并传递给后一命令作为标准输入。
例10.9 将一个目录下的文件列表按名逆序排序后浏览:
$ ls /dev | sort -r | more
例10.10 将一个文件的内容排序后保存并统计其行数:
$ sort mylist | tee sort-list | wc -l
10.2.3 命令执行控制符
命令执行控制符用于控制命令的执行方式,指示Shell何时该执行这个命令以及在何处(前台、后台)执行这个命令。表9-3列出了常用的命令执行控制符。
1. 顺序执行
“;”是顺序执行符,它将两个或多个命令组合在一个命令行中,指示Shell顺序执行这些命令。
例10.11 转到上一级目录,显示目录的路径名和目录的文件列表:
$ cd ..; pwd; ls
2. 条件执行
“&&”是逻辑与执行符,它将两个或多个命令组合在一个命令行中,指示Shell依次执行这些命令直到某个命令失败为止。“||”是逻辑或执行符,它将两个或多个命令组合在一个命令行中,指示Shell依次执行这些命令直到某个命令成功为止。
例10.12 将文件file1复制到file2,如果成功则删除file1:
$ cp file1 file2 && rm file1
例10.13 将文件file1复制到file2,如果失败则显示file1:
$ cp file1 file2 || cat file1
3. 后台执行
“&”是后台执行符,它指示Shell将该命令放在后台执行。后台执行的命令不占用终端与用户交互,因此Shell在执行后台命令后可以立即返回提示符。
例10.14 在后台运行yes命令,丢弃输出:
$ yes > /dev/null &
10.2.4 命令组合符
命令组合符的作用是指示Shell将多个命令组合在一起执行。组合的目的是对这些命令统一进行某种操作,如管道、后台运行、输入/输出重定向等。
命令的组合形式有两种:{ 命令; 命令; … } 和 ( 命令; 命令; … )。
两种组合形式的区别在于前者只在本Shell中执行命令列表,不派生新的Shell子进程,命令执行的结果会影响当前的Shell环境;后者是派生一个新的子Shell进程来执行命令列表,命令在子Shell环境中执行,其执行的结果不会影响当前的Shell环境。
例10.15 在后台顺序执行两命令,5分钟后跳出提示信息“Tea is ready”:
$ ( sleep 300; echo Tea is ready ) &
例10.16 将两命令的输出送到mydoc,mydoc的第1行是Report,后面是file的内容:
$ ( echo Report; cat file ) > mydoc
例10.17 统计两个命令的输出行数,这个数字就是常规命令的数目:
$ { ls /bin; ls /usr/bin } | wc -l
例10.18 两种括号的区别:
$ pwd
/home/cherry
$ { cd book; pwd } #由本Shell进程执行命令表
/home/cherry/book
$ pwd
/home/cherry/book (本Shell进程的当前目录改变了)
$ ( cd ..; pwd ) #生成一个子Shell进程执行命令表
/home/cherry (子进程的当前目录已改变)
$ pwd
/home/cherry/book (本Shell进程的当前目录没有变)
$
10.2.5 命令替换符
当一个字符串被括在反撇号“`”中时,该字符串将先被Shell作为命令解释执行,然后用命令执行后的输出结果替换‘字符串’。
例10.19 命令替换符的用法:
$ echo Today is `date +%A` #替换后为echo Today is
Thursday
Today is Thursday
$
Shell在解析这个命令行时遇到替换符,于是先执行了date命令,用它的输出替代了原date命令所在的位置,然后执行echo命令。
10.2.6 其他元字符
表9-4列出了其他几个常用的元字符。
空格是命令行元素的分割符,它指示Shell如何识别和拆分完整的命令名、选项及参数。$字符的作用将在10.3节中介绍。#字符用于注释,它表示Shell忽略其后的内容。
例10.20 使用注释符对命令进行说明:
$ echo hello # say hello
hello
$
10.2.7 元字符的引用
当需要引用元字符的原始含义,而不是它的特殊含义时,就必须用引用符对它进行转义,消除其特殊含义。当Shell遇到引用符时,它将该引用符作用范围内的字符看作是普通字符。常用的引用符有3种,即转义符、单引号和双引号。表9-5列出了它们的含义。
例10.21 在命令行中引用元字符:
$ echo “* is a wildcard.” #消除*字符的特殊含义
* is a wildcard.
$ echo ‘The prompter is “$”’ #消除双引号字符的特殊含义
The prompter is “$”
$ echo “Don’t do that!” #消除单引号字符的特殊含义
Don’t do that!
$ echo “Name ID Age Class” #消除空格符的特殊含义
Name ID Age Class
$ echo Name ID Age Class #未转义的空格被看作是分隔符
Name ID Age Class
$ echo \\\* #第1个和第3个\字符是转义符
\*
$
10.3 Shell 变 量
Shell提供了定义和使用变量的功能。用户可以将一些常用的数据存放在Shell变量中,并在命令行中引用这些变量。使用变量可以定制Shell的行为,方便Shell的使用和编程。
10.3.1 变量的定义与使用
变量是具有名字的一块内存空间,用于保存程序中要用到的数据。Shell是基于字符串的编程语言,Shell的变量只能存储字符串,因此Shell变量只有两种类型,即字符串和字符串数组。
1. 定义变量
在Shell中,对变量的定义与赋值是同时完成的。有3种方式可以为变量赋值:
(1) 用赋值命令,格式是:
变量名=字符串
注意:变量的名字必须以字母或下划线开头,可以包括字母、数字和下划线。赋值号“=”两边不能有空格。如果字符串中含有空格,应用引号将字符串括起。
例10.22 用变量赋值命令定义变量:
$ nodehost=beijing.WEB
$ user=“zhang ming”
$ path=/bin:/usr/bin:/etc/bin
$ count=10
(2) 用read命令,从标准输入读入字符串赋给变量,格式是:
read变量名 [变量名…]
例10.23 定义3个变量并为它们输入值:
$ read usera userb userc
joe zhao ming
$
执行该read命令时,它将等待用户的输入。用户为每个变量输入一个字符串值,中间用空格分开。
(3) 在for命令中定义变量,用于进行循环控制。具体的格式和用法将在10.5.3小节介绍。
2. 引用变量
引用变量即是求出变量的值(字符串),替换在发生引用的位置。
引用变量的方法是在变量名前加引用字符“$”,格式是:
$变量名 或 ${变量名}
当命令行中出现$字符时,Shell将紧跟其后面的字符串解释为变量名,并对其进行求值和替换。若$字符后面没有合法的变量名,则Shell将此$字符作为普通字符看待。
例10.24 在命令中引用变量:
$ dir=/home/cherry/cprogram
$ echo $dir #实际执行echo /home/cherry/cprogram
/home/cherry/cprogram
$ cd $dir/hello #实际执行cd /home/cherry/cprogram/hello
$ pwd
/home/cherry/cprogram/hello
$ echo $ dir #这里的$被看作普通字符
$ dir
$
在执行echo命令时,Shell先识别出变量名dir,然后求出dir的值,替换到$dir的位置,最后再执行echo。在执行cd命令时,Shell也是先识别出变量名为dir(因为后面的/字符不是变量名的合法字符),然后引用该变量的值形成实际运行参数。最后的echo命令中,$后面是空格而不是合法的变量名,因此$被作为普通字符对待。
注意:引用未定义的变量将得到一个空字符串。若变量名后紧随有字母、数字或下划线,则应将变量名用{}括起。
例10.25 引用变量的方法:
$ echo $dir_1 # dir_1变量未定义,实际执行echo
(空串)
$ echo ${dir}_1 #实际执行echo /home/cherry/cprogram_1
/home/cherry/cprogram_1
$ str=“This is a string”
$ echo “${str}ent test of variables”
This is a stringent test of variables
$ echo “$strent test of variables” #实际执行echo “test of
variables”
test of variables
$
2. 本地变量与导出变量
本地变量包括那些没有被导出的用户变量和环境变量,以及所有的特殊变量。根据作用域的规则,这些变量只能在本Shell中使用。导出变量包括那些导出的用户变量和环境变量。这些导出的变量既可以在本Shell中使用,也可以被本Shell的所有子进程使用。大多数的环境变量都是导出的,用户变量根据需要决定是否导出。所有的特殊变量都是本地的。
一个Shell的导出变量的全体(图9‑1中右侧部分)构成了该Shell的命令执行环境。用不带参数和选项的env命令可以显示Shell的命令执行环境,即所有的导出变量。有关Shell命令执行环境的介绍见10.3.4小节。
3. 设置只读变量
为了防止变量的值被修改(也就是被重新赋值),可以用readonly命令将变量设置为只读的。readonly命令的格式是:
readonly 变量名 [变量名…]
例10.26 设置只读变量:
$ readonly dir
$ dir=/home/cherry/project #对只读变量进行赋值
bash: dir: readonly variable (赋值失败)
$
4. 清除变量
用unset命令清除变量。清除后的变量变为未定义变量,引用其值将得到空字符串。注意:只读变量是不能被清除的。
unset命令的格式是:
unset变量名[变量名…]
例10.27 清除变量:
$ unset dir
$ echo $dir
$
10.3.2 变量的作用域
变量的作用域是指变量可以被引用的范围。根据变量的作用域来划分,Shell变量可以分为两类,即本地变量和导出变量(也可称为局部变量和全局变量)。
1. 本地变量
在一个Shell中定义的变量默认只在此Shell中才有意义,也就是说它们的作用是局部的。我们称这种变量为本地变量。本地变量只在本Shell中有定义,而在子Shell中是不存在的。
例10.28 本地变量的作用域:
$ dir=/home/cherry/cprogram
$ echo $dir
/home/cherry/cprogram
$ bash #进入子Shell
$ echo $dir
(空串表示变量未定义)
$
2. 导出变量
当Shell执行一个命令或脚本时,它通常会派生出一个子进程,由此子进程来执行命令。在很多情况下,我们希望Shell的变量在其子进程中也可以使用,这可以通过“导出(export)”操作,将Shell的变量传递给子进程。导出的变量称为导出变量,它与本地变量的区别在于导出变量可以被任何子进程引用,而本地变量仅在定义它的进程环境下才能使用。
导出变量的命令是export,格式为:
export 变量名[变量名…]
当Shell的一个子进程开始运行时,它继承了该Shell进程的全部导出变量。子进程可以修改继承来的变量的值,但修改只是对自己的变量副本进行,不影响父进程中的变量的值。
例10.29 导出变量与本地变量的使用:
$ name=Zhang; export name #定义并导出变量name
$ title=Dr.; export title #定义并导出变量title
$ greeting=“Good morning” #定义变量greeting
$ cat var_test
name=Wang
echo “$greeting $title $name!”
$ bash var_test #在子Shell中引用变量
Dr. Wang!
$ echo “$greeting $title $name!” #在本Shell中引用变量
Good morning Dr. Zhang!
$
从执行var_test的子Shell进程的输出可以看出,子进程继承了父Shell进程的两个导出变量,但没有继承父进程的本地变量greeting。在子进程中修改了name的值。它的输出显示未经修改的title变量值与父进程相同;name变量的值已被修改;greeting变量则是未定义的。最后一个echo命令显示子进程对name的修改不影响父进程的name变量值。
用命令export还可以将已导出的变量“收回”,使其变回为本地变量,不再为子进程可用。收回变量的命令格式是:
export -n变量名
10.3.3 变量的分类
根据用途和定义方式的不同,Shell变量可以大致分为3类,即用户变量、环境变量和特殊变量。按照作用域的不同,Shell变量又可以分为本地变量与导出变量两类。如图9‑1所示。
图9‑1 Shell变量的分类
1. 用户变量、环境变量与特殊变量
用户变量是由用户为实现某种应用目的而定义的变量。例如,用户可以将一个目录的路径名记在一个变量中,在命令行中可以直接引用该变量,从而避免冗长的输入,如例10.所示。根据需要,用户变量可多可少,可有可无。必要时可以将用户变量导出,使其为子进程可用。
环境变量是由系统预定义的一组变量,用于为Shell提供有关运行环境的信息。环境变量定义在Shell的启动文件中,Shell启动后这些变量就已经存在了。在随后的Shell运行过程中,用户可以直接引用环境变量,也可以对其重新赋值以改变环境设置。导出的环境变量可被其子进程继承使用。有关环境变量的含义及环境配置的介绍见10.3.4小节。
特殊变量是由Shell自定义的一组变量,用于记录Shell当前的运行状态的一些信息,如运行参数、进程号等。特殊变量是本地的、只读的,用户可以引用这些变量,但不能修改它们的值,也不能导出它们。有关特殊变量的含义和使用方法的介绍见10.3.5小节。
用不带参数和选项的set命令可以显示Shell的所有变量(不包括特殊变量)。
10.3.4 环境变量
1. 环境与环境变量
Shell执行时需要了解一些有关系统和用户的基本信息,比如系统名、用户名、终端类型、使用的语言以及其他默认选项等。这些信息以变量的形式提供,称为环境变量。环境变量的全体就称为Shell的执行环境。
通常,环境变量是系统预定义的,是对Shell运行环境的标准设置。不过用户也可以根据需要添加自己的环境变量,实现对Shell的运行环境的特殊设置。系统环境变量的名称全部是大写的,它们是系统预留的,不能用做它用。表9-6列出了常用的几个系统预定义的环境变量。
表9-6中除PS1和PS2变量之外的环境变量都是导出变量,可以在命令子进程中直接使用。
2. 环境变量的定义
环境变量定义在Shell的启动配置文件中,主要的文件有/etc/profile、~/.bash_profile和~/.bashrc。Shell启动时通过执行这些配置文件建立起运行环境。启动完成后,这些环境变量都已经被赋予相应的值。在随后的Shell执行过程中,用户可以直接使用这些变量。用户也可以通过编辑配置文件来定制自己的Shell环境,如添加自己的环境变量或修改系统环境变量的初值。
例10.30 在/etc/profile文件中定义的部分环境变量:
# /etc/profile
…
USER=“`id -un`”
LOGNAME=USER
MAIL=“/var/spool/mail/$USER”
HOSTNAME=`/bin/hostname`
…
export … USER LOGNAME MAIL HOSTNAME
关于环境配置的更多介绍见10.3.3小节。
3. 环境变量的使用
Shell启动后,用户可以直接引用环境变量,也可以对它们重新赋值。对环境变量的修改只在本次Shell会话中有效,若要使修改长久有效则需要修改启动配置文件。
例10.31 在Shell中引用和修改环境变量:
$ echo “Hello $LOGNAME!” #使用LOGNAME变量
Hello cherry!
$ echo $PATH #显示PATH变量的当前值
/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/home/cherry/bin
$ myprog #运行当前目录下的一个程序
bash: myprog: command not found (找不到命令)
$ ./myprog #指定命令的路径,运行程序
(执行命令)
$ PATH=$PATH:. #修改PATH变量,使其包含当前目录
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/home/cherry/bin:.
$ myprog #Shell在当前目录下搜索到命令
(执行命令)
$ echo PS1 is “$PS1”, PS2 is “$PS2”
PS1 is “$”, PS2 is “>”
$ cat << End > namelist #将后续行的输入写入namelist文件
> Jone Doe
> David Nice
> Masood Shah
>End
$ PS1=“[[email protected]$HOSTNAME]$” #修改主命令提示符
$ PS2=! #修改次命令提示符
[[email protected]]$ cat << End > namelist
! Jone Doe
! David Nice
! Masood Shah
! End
[[email protected]]$
4. Shell命令的执行环境
所有导出的环境变量和用户变量都可以被Shell的子进程继承使用,它们构成了Shell命令的执行环境。Shell命令的可执行代码有两种形式,即脚本文件和二进制的可执行代码,它们均以Shell的子进程方式运行。命令子进程通过访问命令执行环境可以获取有关运行环境的信息,并将其应用在自己的处理逻辑中。因此,通过对导出变量的设置可以改变Shell下的命令的执行环境,从而影响命令的执行结果。
命令程序访问执行环境的方法是:脚本程序可以直接引用或修改其执行环境中的变量;C程序可以用setenv()和getenv()函数访问其执行环境中的变量。
例10.32 在脚本中访问执行环境:
$ WORKDIR=$HOME/project/src; export WORKDIR
$ cd ~/scripts
$ cat env_test
echo “I am [email protected]$HOSTNAME.”
cd $WORKDIR
echo “I am now in $PWD.”
$ ./env_test
I am [email protected]
I am now in /home/cherry/project/src.
$ echo $PWD
/home/cherry/scripts
$
脚本中不加定义地引用了4个环境变量,其中LOGNAME、HOSTNAME和PWD是系统环境变量,WORKDIR是用户定义的导出变量。它们构成了该脚本命令的执行环境。注意:脚本在执行cd命令时修改了PWD环境变量的值,但这种修改只在子进程内有效,它不会影响到父Shell进程的环境变量的取值。
10.3.5 特殊变量
Shell中有一组预定义的特殊的变量,其功能是记录Shell当前的运行状态的一些信息,主要包括运行参数、进程标识和命令退出状态等。特殊变量的变量名和值由Shell自动设置。特殊变量都是只读变量,因此在程序中可以引用这些变量,但不能直接对它们赋值。表9-7列出了几个常用的特殊变量。
注意:由于都是只读变量,表9-7中列出的是变量的引用表达式,而不是变量名。用户可以用$*、$#等来引用变量,但不能将*或#等看作为变量名来进行赋值、导出等操作。
1. 参数变量
向Shell脚本传递数据的途径有两种,一种是通过导出变量,另一种是通过命令行参数。Shell在解析命令行时首先将命令的参数识别出来,存入专门设置的变量中,这些记录运行参数的变量就称为参数变量。命令在其执行期间可以直接引用这些变量,获得此次运行的参数。
参数变量主要有以下几个:
(1) $#:记录命令行参数的个数。
(2) $*:记录命令行的整个参数。
(3) [email protected]:记录了命令行的各个参数。
(4) $i:称为位置变量,是按位置记录命令参数的一组变量,分别为$0,$1,$2,…,$9,${10},…。其中,$0为命令名本身,$1为命令的第1个参数,$2为命令的第2个参数,…。所有超过参数个数的位置变量i(i>$#)的值为空字符串。
例如,某个命令的名称为myprog,执行时的命令行是myprog -s “How are you!” joe jean。当该命令被执行时,Shell隐含地为它建立起一系列的参数变量,各参数变量的内容如下:
$#:4
$0:myprog
$1:-s
$2:How are you!
$3:joe
$4:jean
$*:-s How are you! joe jean
“S*”:“-s How are you! joe jean”
[email protected]:-s How are you! joe jean
“[email protected]”:“-s” “How are you!” “joe” “jean”
例10.33 在程序中引用参数变量:
$ cat var_test
echo My name is $0, I have $# parameters, they are [email protected]
echo The first of them is \“$1\”
$ ./var_test How are you
My name is var_test, I have 3 parameters, they are How are you
The first of them is “How”
$ ./var_test “How are you” cherry
My name is var_test, I have 2 parameters, they are How are you cherry
The first of them is “How are you”
$
$*与[email protected]变量之间有着细微的差别,[email protected]的各参数字符串之间是用一个特殊的分隔符分隔的,而$*的各参数之间是用一个空格作为分隔符的。在不带引号的情况下,$*与[email protected]变量的作用相同,它们都等价于$1 $2 $3 …。但在带有引号时,它们的作用就不同了。引号使空格失去分隔符的作用,因此“$*”是一个含有普通空格字符的字符串,它等价于“$1 $2 $3 …”;而[email protected]中的分隔符在引号下仍然有效,因此“[email protected]”是由分隔符分隔的各个参数字符串构成的字符串序列,等价于“$1” “$2”“$3”…。当需要逐个处理参数字符串时(例如在for循环中,见10.5.3小节),使用“[email protected]”更为安全,因为只有它能正确地区分出各个参数。
2. 设置参数变量
参数变量是只读的,因此用户不能直接对参数变量重新赋值,但却可以通过Shell提供的命令来设置这些变量。
1) 用set命令设置位置变量
用set命令可以对位置变量及其他参数变量强制赋值,对位置变量赋值后,参数变量$#、[email protected]、$*等也相应地被重新赋值。
格式:set字符串1字符串2…
其中,字符串i是要赋给第i个位置变量的值。注意:set不能对$0赋值。
用set --命令可以清除所有的位置变量。相应地,[email protected]和$*变量也被清除,$#变量被清0。
例10.34 设置位置变量:
$ date
五 10月 24 12:08:05 CST 2008
$ set `date`
$ echo $6 $2 $3 $1
2008 10 月 24 五
$
date命令输出了7个字符串,它们被依次赋给了$1~$7变量。随后的echo命令显示了其中的4个变量。
2) 用shift命令移动位置变量
shift命令的功能是将位置变量与命令行参数的对应关系右移指定的位数,同时将$#变量的值减去相应的数,[email protected]、$*等也相应地被重新赋值。注意:shift只移动$1及其后面的位置变量的值,$0变量的值保持不变。
格式:shift [位移量]
未指定位移量参数时,右移1位。
例10.35 用shift命令移动位置变量的值:
$ cat proc1
echo $0; $#; $1; $2; $3; $4; $5;
shift
echo $0; $#; $1; $2; $3; $4; $5;
shift 2
echo $0; $#; $1; $2; $3; $4; $5;
$ bash proc1 A B C D E
proc1; 5; A; B; C; D; E;
proc1; 4; B; C; D; E; ;
proc1; 2; D; E; ; ; ;
$
3. 其他特殊变量
其他常用的特殊变量包括纪录命令退出状态的$?变量和纪录进程PID的$$、$!变量等。
1) 退出状态变量
在Linux系统中,每个命令在执行结束退出时都要返回给系统一个状态值。例如在C程序中是调用exit(status)函数退出,在Shell脚本中则是用exit status命令退出。其中的status就是返回给系统的状态值。通常的约定是,程序成功结束时返回0状态值;程序出错时返回非0的状态值(比如1、2、-1等)。
$?变量记录了最近一条命令执行结束后的退出状态。当一个命令子进程退出时,它调用exit(status)将退出状态码放入自己的PCB中。随后,Shell处理子进程的善后工作,将它的退出状态码保存在$?变量中。如果该命令执行成功后退出则$?为0,否则为非0。Shell通过$?来判断上一条命令的成功与否。例如,在进行逻辑与执行“命令1 && 命令2”和逻辑或执行“命令1 || 命令2”时,Shell就是通过命令1执行后的$?值来决定是否执行命令2。
例10.36 从$?变量获得命令的退出状态:
$ rm file1
$ echo $?
0 (删除文件成功)
$ rm file2
rm: cannot remove ‘file2’ : No such file or directory
$ echo $?
1 (删除文件失败)
$
2) 命令的进程号
$$变量记录了本命令的执行进程的进程号PID。当一个命令以子进程的方式开始运行时,Shell将它的进程号PID存入到它的$$变量中。命令程序中可以引用该变量,但不能修改它。另外一个可能用到的变量是$!变量,它记录了当前作业(即最后一个进入后台的那个作业)的PID。
例10.37 从$$变量获得命令的进程号:
$ echo $0, $$ #显示本Shell进程的命令名和PID
bash, 15452
$ sleep 30 &
[1] 4439
$ echo $!
4439
$ cat pid_test
echo “My name is $0, my PID is $$.”
$ pid_test #以子进程方式执行脚本
My name is pid_test, my PID is 16620.
$ . pid_test #由本Shell进程执行该脚本
My name is bash, my PID is 15452.
$
从上例可以看出,当以命令方式直接运行脚本时,它是由一个子进程执行的,具有自己的PID;当用“.”命令执行脚本时,它是由本Shell进程执行的,其报出的PID就是本Shell进程的PID。
10.4 Shell表达式
Shell语言支持表达式计算。Shell表达式主要有两种形式,一是用于数值计算的算术表达式,其结果是数值;另一种是用于进行条件测试或判断的逻辑表达式,其结果是真假值。
10.4.1 数字运算表达式
与高级语言中的变量不同,Shell变量只有字符类型,它们只能存放整数数字字符串,如“127”等。Shell本身也没有数字运算的能力,必须借助某些命令来进行算术运算。expr就是用来进行数字表达式计算的命令。
expr命令
【功能】计算表达式。
【格式】expr 数值1 运算符 数值2
【参数】expr支持以下运算符:
+、-、*、/、% 加、减、乘、除、取余。
&、| 逻辑与、逻辑或。
=、==、!= 等于、恒等于、不等于。
>、<、>=、<= 大于、小于、大于等于、小于等于。
【输出】+、-、*、/、%运算输出结果数值;&运算当两个数值都非0时输出第1个数值,否则输出0;| 运算当第1个数值非0时输出第1个数,否则输出第2个数;比较运算为真时输出1,否则输出0。
【退出状态】算术运算返回状态0;逻辑和比较运算结果为真(非0)时返回状态0,为假(0)时返回状态1;出错时返回状态2。
【说明】
(1) 运算符两侧必须留有空格,与运算数分开。
(2) 算术运算的数值必须是整数,可以是数字字符串常量(如“123”),也可以是数字字符串变量(如$a)。expr命令负责将数字字符串解释为整数,然后进行运算。
(3) 运算符如果是Shell的元字符,如*、&、|、>、<等,必须用转义符‘\’使其失去特殊意义,不被Shell解释执行。
例10.38 expr命令用法示例:
$ a=13
$ expr “$a” - 4 + 2 # 13-4+2,注意运算符两旁要留有空格
11
$ expr 4 \* 5 # 4*5
20
$ expr 5 + 7 / 3 # 5+7/3,/运算优先于+运算,结果取整
7
$ expr `expr 5 + 7` / 3 # (5+7)/3,用命令替换改变运算次序
4
$ expr $a \>= 3; echo $? # 13 >= 3?
1
0
$ expr 5 \& “$a”; echo $? # 5 and 13
5
0
$ expr 5 \& 0; echo $? # 5 and 0
0
1
$ expr 5 \| $a; echo $? # 5 or 13
5
0
$ a=`expr $a - 2` #替换后为a=11
$ echo $a
11
$
10.4.2 逻辑测试表达式
test命令可对字符串、整数及文件进行各类测试。test命令并不输出任何结果,而是用退出状态来表示测试的结果:退出状态为0时表示test成功,测试结果为真;退出状态为1表示test失败,测试结果为假。test的测试结果用于在控制结构中进行条件判断。
test命令使用的表达式形式见表9-8。
test命令
【功能】测试表达式的真假值。
【格式】test有两种等价的格式:
test 表达式
[ 表达式 ] (注意“[”和“]”两侧的空格)。
【参数】表达式的常用形式见表9-8。
【退出状态】测试结果为真时返回状态0;为假时返回状态1;出错时返回状态2。
【说明】
(1) 运算符两侧必须留有空格,与运算数分开。
(2) 表达式中若使用Shell的元字符,如>、<、(、)等,必须用转义符“\”使其失去特殊意义,不被Shell解释执行。
1. 字符串测试
例10.39 字符串测试:
$ user=smith
$ test “$user” = smith #两字符串相等?
$ echo $? #显示测试结果
0 (是)
$ test -z “$user” #字符串为空串?
$ echo $?
1 (否)
例10.40 含有空格的字符串及空字符串的测试:
$ user1=“Tom Smith”
$ test “$user1” = “Tom Smith” # test “Tom Smith” = “Tom Smith”
$ echo $?
0 (真)
$ test $user1 = “Tom Smith” # test Tom Smith = “Tom Smith”
bash: test: too many arguments
$ echo $?
2 (出错)
$ test “$user2” = smith # test “” = smith
$ echo $?
1 (假)
$ test $user2 = smith # test = smith
bash: test: =: unary operator expected
$ echo $?
2 (出错)
$
当表达式中使用变量时,最好将其用双引号括起(如“$var”)。这样,Shell进行变量替换后的字符串被双引号括起,作为一个单一的字符串传递给test命令。上例中,变量user1的字符串中含有空格,“$user1”被替换为一个带有空格的字符串“Tom Smith”,而$user1替换后成为两个字符串Tom和Smith。变量user2为空,“$user2”被替换为一个空串“”,而$user2则被替换为空。所以,空串或带空格的串在没有双引号的情况下都会导致test报错。
2. 数字测试表达式
例10.41 数字比较测试:
$ x1=5
$ test “$x1” -eq 5; echo $? # test “5” = 5
0 (真)
$
3. 文件测试表达式
例10.42 文件测试:
$ test -f /home/cherry/message; echo $? #检查指定的文件是否存在
0 (真)
$ gcc myprog.c 2>err #编译,错误信息记入err文件
$ test -s err && echo “Errors found” #检查编译错误文件是否为空
$
第2个test命令测试为真时,表示有编译错误发生,继续执行echo命令;否则不执行。
例10.43 目录测试:
$ cat dir_test
test -d $1 && echo “$1 is a directory ” && exit 0 #测试参数1是否是目录
echo “ $1 is not a directory” ; exit 1
$ dir_test proc
proc is a directory
$
如果test -d $1命令测试为真,则顺序执行第1行的后两个命令,以0状态退出;如果测试为假,则执行第2行命令,以1状态退出。
4. 逻辑测试表达式
例10.44 带有逻辑运算符的表达式测试:
$ test -f file -a ! -s file #测试file是否为空的普通文件
$ echo $?
1 (否)
$ read a b c
(输入2 7 5)
$ test \( $a -eq 0 -o $b -gt 5 \) -a $c -le 8 # ($a=0 or $b>5) and ($c<=8)?
$ echo $?
0 (是)
$ cat check_input #测试输入的数字是否有效
echo -n “Input a number(1-10): ”
read a
test $a -lt 1 -o $a -gt 10 && echo error! && exit 1
echo ok
exit 0
$ bash check_input
Input a number(1-10): 5
ok
$ bash check_input
Input a number(1-10): 15
error!
$
10.5 Shell控制结构
与C语言的控制结构语句类似,Shell提供了几个专门的内部命令来构造控制结构,用它们可以构造出任意的分支与循环。这些命令(有时也称为语句)可以分为以下几类:
● 分支结构:if、case。
● 循环结构:while、until、for。
● 循环控制:break、continue。
● 结束:return、exit。
10.5.1 条件与条件命令
控制结构需要根据某个条件作出控制转向的决策。在C语言中,条件是某表达式的取值,0为假,非0为真。在Shell语言中,条件是某命令的退出状态。当命令执行成功时,它返回一个0状态(即$?为0),此时条件为真;若命令失败,返回一个非0状态(即$?不为0),则此时条件为假。
用于进行条件判断的命令就称为条件命令。任何shell命令都具有退出状态,因而都可以作为条件命令使用。此外,Shell还提供了3个内部命令::、true和false。它们不作任何操作,只是返回一个特定的退出状态。:和true返回0;false返回非0。它们可作为恒真和恒假条件命令使用。
10.5.2 分支控制命令
分支控制命令用于控制程序在不同的条件取值下执行不同的流程。用于分支控制的命令有if和case,if命令用于两路分支控制,case命令用于多路分支控制。
1. if命令
if命令根据条件命令执行的结果决定后续命令的执行路径。if命令的一般形式为:
if 条件命令
then 命令列表1 #若$?为0,执行此分支
[ else 命令列表2 ] #否则,执行此分支
fi
if命令的执行过程是:首先执行条件命令,然后根据条件命令的退出状态作为条件决定后续的执行路径。若条件为真则执行命令列表1,否则执行命令列表2。
条件命令通常是一个test表达式命令,但也可以是任何其他的命令或命令列表。如果条件命令是一个命令列表,则Shell将依次执行其中的各个命令,并将最后一个命令的退出状态作为条件。命令列表可以是一个或多个命令,也可以是另一个if语句,从而形成嵌套if结构。另外,if结构可以没有else分支,但不能缺省then分支。
例10.45 判断参数1是否存在且是普通文件:
if [ -f $1 ]
then echo “$1 is an ordinary file.”
else echo “$1 is not an ordinary file or not exists.”
fi
例10.46 检查某用户是否登录,如果登录,则向他发即时信息:
echo -n “ Enter User Name: ”
read user
if who | grep $user
then echo “Don’t forget the meeting” | write $user
else echo “$user has not logged.”
fi
例10.46中的条件命令为who | grep命令,以grep命令的执行结果作为条件。若grep在who的输出中找到指定的用户名,则条件为真,表明该用户已登录;否则,条件为假,表明该用户未登录。当条件为真时,脚本利用write命令向用户发送即时消息(有关write命令的介绍见11.2.5小节)。
2. case命令
用case命令进行多路条件测试,结构更清晰。case命令的格式为:
case 测试字符串 in
模式1) 命令列表1;;
模式2) 命令列表2;;
...
模式n) 命令列表n;;
esac
case命令的执行过程是:先将测试字符串与各个模式字符串逐一比较,若发现了一个匹配的模式则执行该模式对应的命令列表。注意:若有多个匹配的模式时,只执行最前面的那一个分支。
例10.47 根据参数个数进行相应处理:
case $# in
0) DIR=“.” ;;
1) DIR=$1 ;;
*) echo “Usage: $0 [directory name]”; exit 1 ;;
esac
rm -i $DIR/*.bak
exit 0
该脚本的功能是清除指定目录下的所有.bak文件。若没有指定参数则清除当前目录;若带有1个参数则清除该参数指定的目录;若参数个数多于1个则提示命令的用法。
例10.48 按时间显示问候语:
hour= `date +%H`
case $hour in
08|09|10|11|12) echo “Good Morning!” ;;
13|14|15|16|17) echo “Good Afternoon!” ;;
18|19|20|21|22) echo “Good Evening!” ;;
*) echo “Hello!” ;;
esac
该脚本先用date命令求出当前的小时数,然后根据这个数字按时间段显示不同的问候语。注意:模式中的“|”表示“或”的意思,用于将多个模式合并到同一个分支;“*”表示“任意”,表示当前面没有匹配的模式时执行此分支。
10.5.3 循环控制命令
循环控制命令用于重复执行某个处理过程。Shell提供了3种循环控制结构,即for、while和until结构。它们的结构控制含义与C语言中for、while和do语句相同,只是形式上有所不同。另外,这些循环控制命令既可以用在脚本程序中作为循环控制语句,也可以以多行命令的方式在Shell下直接执行。
1. for命令
for命令常用于简单的、确定的循环处理。for命令的格式为:
for变量 [ in 字符串列表 ]
do
命令列表
done
for命令的执行过程是:定义一个变量,它依次取字符串列表中的各个字符串的值。对每次取值都执行命令列表,直到所有的字符串都处理完。当没有指定字符串列表时,默认指脚本的参数列表,即:for i 等同于for i in “[email protected]”。
例10.49 循环处理一组文件:
$ cat countlines
sum=0
for i in *.c #对当前目录下的每个.c文件做
do
lines=`wc -l $i`
sum=`expr $sum + $lines`
done
echo $sum
$ countlines
463
$
这是统计c源文件行数的脚本,它逐个求出当前目录下每个.c文件的行数,并将它们累加起来输出。
例10.50 循环处理参数列表:
$ cat lunch
for i in “[email protected]” #对参数行中的每个参数
do
echo “Time for lunch, I treat !” | mail $i
done
$ lunch bill fred joe
$
这是一个邀请同事午餐的脚本。执行该脚本时需带若干个用户名作为参数,脚本用mail命令向每个参数指定的用户发送一个邀请邮件(关于mail命令的介绍见第11.3.3小节)。
2. while命令
while命令的作用是进行有条件的循环控制,当条件为真时执行循环体命令列表,直到条件为假时结束。while命令常用于循环次数或循环处理的对象不够明确的循环过程。
while命令的格式为:
while 条件命令
do
命令列表
done
例10.51 向所有登录用户发即时消息:
$ cat inform
echo -n “Enter message :”
read MESSAGE #读入消息
set `who -q` #找出已登录用户,存入位置变量中
while [ $1 != “#” ] #对所有“#”前的位置变量
do
echo $MESSAGE | write $1 #发送即时消息
shift 1
done
$ who -q #输出已登录用户名与用户数
root zhao mary cherry cherry
# users=5
$ ./inform
Enter message : The network will go down at 1:00 pm.
$
该脚本先从标准输入接收消息,然后用who -q命令找出已登录的用户名,再用write命令向每个登录用户发送一个即时消息。该消息将出现在这些用户正使用的终端屏幕上。
3. until命令
until命令的作用与while命令类似,只不过是在条件不成立时执行循环体命令,直到条件成立。until命令常用于监视某事件的发生。
until命令的格式为:
until 条件命令
do
命令列表
done
例10.52 监视某用户是否登录:
$ cat watchfor
if [ ! $# .eq 1 ] #参数个数检查
then echo “Usage: watchfor user” ; exit 1
fi
until who | grep $1 #搜索已登录的用户,直到被监视的用
户登录
do
sleep 60
done
$ watchfor joe
joe tty1 Dec 24 09:46
$
该脚本运行时需要带一个用户名参数。脚本首先检查参数个数,如果不是1,则提示命令的用法并退出。如果检查通过,则每隔60秒在已登录的用户中搜索一次参数指定的用户,直到其登录。
10.5.4 退出循环命令
break和continue命令的作用与C语言中的break和continue语句相同,用于在必要时跳出循环。break用于终止整个循环。当执行break命令时,控制转移到循环体后(done之后)的命令执行。continue用于终止本轮循环,直接进入下一轮循环执行。当执行continue命令时,控制转移到循环体开始处(do之后)的命令执行。
另外,break和continue命令只能应用在for、while和until命令的循环体内。
例10.53 用break命令终止循环:
$ cat break_test
while : # always true
do
echo “Continue? [y/n]: ”
read replay
if [ $replay = n ]
then break
fi
done
$ break_test
Continue? [y/n]: y
Continue? [y/n]: y
Continue? [y/n]: n
$
该脚本中的循环条件为恒真,这是个无限循环结构,但在循环体中使用了break,使其可以在适当的时候(输入“n”字符时)退出循环。
例10.54 用continue命令跳过某轮循环:
$ cat continue_test
cd $1
for file in * #对当前目录下的所有文件
do
if [ -d $file ]
then continue
fi
ls -l $file
done
$ continue_test /home/cherry
-rwxr-xr-x 1 cherry faculty 564 Jan 31 08:59 memo
-rwxr-x--x 1 cherry faculty 91 Jan 04 17:05 routine
$
该脚本运行时将逐个检查参数1指定目录中的各个文件,略过子目录文件,列出其他文件的详细列表。
10.5.5 退出命令
exit命令是Shell的退出命令,用在Shell脚本中时表示退出脚本程序,返回到其父Shell。exit命令的格式是:exit [退出码],其中退出码是返回给父进程的退出状态值。如果程序中没有显式地使用exit命令退出,则脚本的退出状态将是其退出前执行的最后一条命令的退出状态。
例10.55 利用exit命令退出:
if [ ! -d $1 ]
then echo error && exit 1
else ls $1
fi
exit 0
该脚本接受一个目录名作为参数。当判断参数不是目录时报错退出,否则显示目录列表后正常退出。显式地给出退出码是一个好的编程习惯。
10.6 Shell程序综合举例
本节通过几个实用的例子展示Shell基本编程技术的综合应用。更复杂的应用需要涉及Shell高级编程的一些特性,读者可参阅专门的Shell编程教程。
例10.56 处理一批文件的程序procfile:
$ cat procfile
#!/bin/bash
#usage: procfile files
for i do
while true do
echo -n “$i: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: ”
read choice
case $choice in
e*) vi $i;;
v*) cat $i;;
r*) rm $i;;
n*) break;;
q*) exit 0;;
*) echo Illegal Option;;
esac
done
done
$ procfile file1 file2 file3
file1: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: n
file2: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: v
…(显示file2的内容)
file2: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: r
file2: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: n
file3: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: q
$
第1行以#!打头的注释行指示Shell应使用bash来执行此脚本。脚本执行时需要带若干个文件名作为运行参数。for循环用于依次处理参数中的各个文件。对每个文件的处理过程是一个while循环,它先列出可执行的操作的菜单,然后读取用户的输入,再通过case结构根据输入对文件进行指定的操作。当输入n时结束对这个文件的处理,进入下一个文件的处理过程。输入q时退出程序。
例10.57 一个求数字累加和的程序:
$ cat addall
#!/bin/bash
if [ $# = 0 ] then
echo “Usage: $0 number-list”
exit 1
fi
sum=0 # sum of numbers
count=$# # count of numbers
while [ $# != 0 ]
do
sum=`expr $sum + $1`
shift
done
# display final sum
echo “The sum of the given $count numbers is $sum.”
exit 0
$ addall
Usage: addall number-list
$ addall 1 4 9 16 25 36 49
The sum of the given 7 numbers is 140.
$
addall脚本以一系列整数为参数,对它们求和。脚本首先检查参数个数,如果没有参数则提示命令的用法并退出。 脚本通过while循环将参数表中的参数一个个累加到sum变量中,然后显示累加结果。
例10.58 一个脚本命令groups,用于求用户所在的用户组的名称:
$ cat /usr/bin/groups
#!/bin/sh
usage=“Usage:
$0 --help display this help and exit
$0 --version output version information and exit
$0 [user]… print the groups a user is in”
fail=0
case $# in
1) case “$1” in
--help ) echo “$usage” || fail=1; exit $fail;;
--version ) echo “groups (GNU coreutils) 4.5.3” || fail=1; exit $fail;;
* ) ;;
esac;;
* ) ;;
esac
if [ $# -eq 0 ]; then
id -Gn #求当前用户的组名
fail=$?
else
for name in “[email protected]”; do
groups=`id -Gn -- $name` #求$name用户的组名
status=$?
if test $status = 0; then
echo $name : $groups
else
fail=$status
fi
done
fi
exit $fail
$ groups --help
Usage:
groups --help display this help and exit
groups --version output version information and exit
groups [user]… print the groups a user is in
$ groups cherry zhao
cherry: faculty
zhao: guest
$
注意该脚本对于参数的判别。它允许带一个选项或0至多个用户名参数。对于--help选项,它显示帮助信息;对于--version选项,它显示版本信息;若是用户名参数,则调用id命令求出组名;没有参数和选项时它求出当前用户的用户组名。
例10.59 一个简单的文本文件打包程序:
$ cat bundle #打包程序
#bundle: group files into distribution package
echo “# To unbundle, bash this file”
for i do
echo “cat > $i << End of $i”
cat $i
echo “End of $i”
done
$ bundle file1 file2 file3 > bundlefile #打包3个文件
$ cat bundlefile #打好的包文件
# To unbundle, bash this file
cat > file1 << End of file1
…
…(file1的内容)
…
End of file1
cat > file2 << End of file2
…
…(file2的内容)
…
End of file2
cat > file3 << End of file3
…
…(file3的内容)
…
End of file3
$ cp bundlefile newdir/
$ cd newdir
$ bash bundlefile #在另一个目录中解包
$ ls
bundlefile file1 file2 file3
$
bundle脚本的功能是将参数指定的多个文本文件打包成一个文件以便发行。打包后的包文件具有自解包的功能。打包的方法是:将每个文件的内容前加上一行cat命令,尾部加一行结束标记,将多个文件存放在一个文件中。解包时,依次执行包文件中的cat命令,利用here文档机制将文件的内容复原出来。
习 题
9-1 什么是Shell脚本?写出执行一个Shell脚本的3种方法。
9-2 什么是Shell变量?如何定义Shell变量?如何引用Shell变量?
9-3 按照用途,Shell变量分为哪几类?各类的用途是什么?
9-4 哪个环境变量是用于保存命令的搜索路径的?如何修改搜索路径,使其包含“~”目录和“.”目录?这样的修改有什么作用?
9-5 根据引号的作用写出下列命令的输出:
echo who | wc -l
echo `who | wc -l`
echo ‘who | wc -l’
echo “who | wc -l”
9-6 根据引号的作用写出下列命令的输出:
echo ‘My logname is $LOGNAME’
echo “My logname is $LOGNAME”
9-7 如何用echo命令将字符串 /home/$user/* 显示出来?
9-8 解释以下命令有何不同:
find . -name ‘[A-H]’* -print find . -name ‘[A,H]’* -print
9-9 解释以下命令有何不同:
wc wc wc < wc wc > wc wc | wc
9-10 以下命令可完成什么任务?
cat > letter 2> save < memo
cat > letter < memo 2>&1
cat 2> save < memo | sort > letter
9-11 解释以下脚本的功能:
mail root << !!!
****** PROBLEM AGAIN ******
The midnight error has struck again!
All processing stopped.
!!!
9-12 编写一个Shell程序,它以立方体的边长作为参数,显示立方体的体积。
9-13 编写一个Shell程序,它删除当前目录下(包括各级子目录)所有长度为0的文件。
9-14 编写一个Shell程序,它将第2个参数及其后的各个参数指定的文件复制到第1个参数指定的目录中。要求对输入的参数做必要的检查。
9-15 编写一个Shell程序,它能够将指定目录及其子目录中的包含字符串root的文本文件找出来。
9-16 编写一个Shell程序,它以一个英文月份名作为参数,显示当年该月的日历。
9-17 修改例10.51中的inform脚本,使其只向不包括自己的所有登录用户发送即时消息。
9-18 根据例10.57中的addall脚本编写一个脚本,它带有若干个文件名作为运行参数,脚本的功能是统计这些文件的大小之和。