第26章 Shell 编程


在DOS操作系统中,可以把多个DOS指令放在文件里作批处理。在Linux系统中也有类似的批处理命令,这些批处理命令在Linux中叫做Shell脚本(Shell Script )。 其功能已经和一般的高级语言不相上下。Shell脚本是以文本方式储存的,而非二进制文件。必须在Linux系统的Shell下解释执行。不同Shell的脚本大多会有一些差异,所以不能将写给A Shell的脚本用B Shell执行。在Linux系统中最常使用是Bourne Shell以及C Shell,所以本章结合这两个Shell的相同点和不同点来介绍Shell编程。

26.1 创建和运行Shell程序
26.1.1 创建Shell程序
26.1.2 运行Shell程序
26.2 使用Shell变量
26.2.1 给变量赋值
26.2.2 读取变量的值
26.2.3 位置变量和其他系统变量
26.2.4 引号的作用
26.3 数值运算命令
26.4 条件表达式
26.4.1 if 表达式
26.4.2 case 表达式
26.5 循环语句
26.5.1 for 语句
26.5.2 while 语句
26.5.3 until 语句
26.6 shift 语句
26.7 select 语句
26.8 repeat 语句
26.9 子函数


26.1 创建和运行Shell程序
26.1.1 创建Shell程序
使用VI等编辑器编辑,将要执行的Shell或Linux命令写入Shell程序文件。
例如,在一些较早版本的Linux/Unix系统更换光盘时,首先要把已经mount的光盘卸载,然后放入新的光盘,再用mount 命令挂载光盘。命令如下:
umount /dev/cdrom
mount -t iso9660 /dev/cdrom /cdrom
可以创建一个包含这两个命令的Shell程序文件remount,直接执行remount来更换光盘,而不必在每次更换光盘时
都重复执行这两个命令。
26.1.2 运行Shell程序
可以有四种方法:
1. 把Shell脚本的权限设置为可执行,在Shell提示符下直接执行。

可以使用下列命令更改Shell脚本的权限∶
chmod u+x filename 只有自己可以执行,其他人不能执行。
chmod ug+x filename 只有自己以及同一工作组的人可以执行,其他人不能执行。
chmod +x filename 所有人都可以执行。
 

Shell的指定:
1) 如果Shell脚本的第一个非空白字符不是“#”,则它会使用Bourne Shell。
2) 如果Shell脚本的第一个非空白字符是“#”,但不以“# !”开头时,则它会使用C Shell。
3) 如果Shell脚本以“#!”开头,则“# !”后面所跟的字符串就是所使用的Shell的绝对路径名。Bourne Shell的路径名称为/bin/sh ,而C Shell则为/bin/csh。
如:
1) 如使用Bourne Shell,可用以下方式:
#!/bin/sh
2) 如使用C Shell,可用以下方式:
#!/bin/csh
3) 如使用/etc/perl 作为Shell,可用以下方式:
#! /etc/perl
2. 在命令行中输入具体的Shell,其后跟随Shell脚本文件名。

例如,使用tcsh 执行上面的Shell脚本:
tcsh remount
3. 在pdksh 和bash下使用命令,或在tcsh下使用source 命令。

例如,在pdksh和bash下执行上面的Shell脚本:
.remount
在tcsh下执行上面的Shell脚本:
source remount
4. 使用命令替换。
如果想要使某个命令的输出成为另一个命令的参数时,就可以 使用这个方法。

我们将命令列于两个`号之间,命令执行后的输出结果代替这个命令以及两个`符号。例如:
str='Current directory is' `pwd`
echo $str
执行结果为:Current directory is /user1/whbian

26.2 使用Shell变量

26.2.1 给变量赋值
在pdksh 和bash中,给变量赋值的方法是一样的。例如,
想要把5赋给变量count,则使用如下的命令:
count=5 (注意,在等号的两边不能有空格)
在tcsh中,可以使用如下的命令:
set count = 5
赋值前无须事先定义变量类型。你可以使用同一个变量来存储字符串或整数。给字符串赋
值的方法和给整数赋值的方法一样。例如:
name=Garry (在pdksh和bash中)
set name = Garry (在tcsh中)

26.2.2 读取变量的值
可以使用$读取变量的值。例如,用如下的命令将count变量的内容输出到屏幕上:
echo $count
26.2.3 位置变量和其他系统变量
位置变量用来存储Shell程序后面所跟的参数。第一个参数存储在变量1中,第二个参数存
储在变量2中,依次类推。例如,你可以编写一个Shell程序reverse,执行过程中它有两
个变量。输出时,将两个变量的位置颠倒。Shell程序文件reverse的内容为:
#program reverse, prints the command line parameters out in reverse order
echo "$2" "$1"
在Shell下执行此Shell程序:reverse hello there
其输出为:there hello


其他系统变量:
有些系统变量可以赋予新值:
$HOME 用户自己的目录。
$ PATH 执行命令时所搜寻的目录。
$TZ 时区。
$MAILCHECK 每隔多少秒检查是否有新的邮件。
$PS1 在Shell命令行的提示符。
$PS2 当命令尚未打完时,Shell要求再输入时的提示符。
$MANPATHman 指令的搜寻路径。
有些变量在执行Shell程序时系统就设置好了,并且不能加以修改:
$# 存储Shell程序中命令行参数的个数。
$? 存储上一个执行命令的返回值。
$0 存储Shell程序的程序名。
$* 存储Shell程序的所有参数。
$@ 存储所有命令行输入的参数,分别表示为“$ 1”,“$ 2” . . . 。
$$ 存储Shell程序的PID。
$! 存储上一个后台执行命令的PID。


26.2.4 引号的作用
在Shell编程中,要区分不同的引号。单引号(‘ ’)、双引号(“”)和
反斜杠(\)都用作转义。
(1)这三者之中,双引号的功能最弱。当把字符串用双引号括起来时,Shell将忽略字符串
中的空格,但其他的字符都将继续起作用。双引号在将多于一个单词的字符串赋给一个
变量时尤其有用。例如,把字符串hello there赋给变量greeting时,应当使用下面的命令:
greeting="hello there" (在bash 和pdksh环境下)
set greeting = "hello there" (在tcsh环境下)
这两个命令将hello there作为一个单词存储在greeting变量中。如果没有双引号,bash和
pdksh将产生语法错,而tcsh则将hello赋给变量greeting 。
(2)单引号的功能则最强。当你把字符串用单引号括起来时,Shell将忽视所有单引号中的特
殊字符。例如,如果你想把登录时的用户名也包括在greeting变量中,应该使用下面的命
令:
greeting="hello there $LOGNAME" (在bash 和pdksh环境下)
set greeting="hello there $LOGNAME" (在tcsh环境下)
这将会把hello there root存储在变量greeting 中,如果你是以root 身份登录的话。但如果你
在上面使用单引号,则单引号将会忽略$符号的真正作用,而把字符串hello there $LOGNAME
存储在greeting 变量中。
(3)使用反斜杠是第三种使特殊字符发生转义的方法。反斜杠的功能和单引号一样,只是反
斜杠每次只能使一个字符发生转义,而不是使整个字符串发生转义。请看下面的例子:
greeting=hello\ there (在bash和pdksh环境下)
set greeting=hello\ there (在tcsh环境下)
在命令中,反斜杠使Shell忽略空格,从而将hello there作为一个单词赋予变量greeting。
当你想要将一个特殊的字符包含在一个字符串中时,反斜杠就会特别地有用。例如,你想
把一盒磁盘的价格$26.00赋予变量disk_price,则使用如下的命令:
disk_price = \$26.00 (在bash 和pdksh环境下)
set disk_price = \$26.00 (在tcsh环境下)

26.3 数值运算命令
使用expr命令处理数值运算:
expr expression
说明:
expression是由字符串以及运算符所组成的,每个字符串或是运算符之间必须用空格隔开。
: 字符串比较。比较的方式是以两字符串的第一个字母开始,以第二个字符串的
最后一个字母结束。如果相同,则输出第二个字串的字母个数,如果不同则返回0。
* 乘法
/ 除法
% 取余数
+ 加法
- 减法
< 小于
<= 小于等于
= 等于
!= 不等于
>= 大于等于
> 大于
& AND运算
| OR运算
注意当expression中含有*、(、)等符号时,必须在其前面加上\ ,以免被Shell解释成其他意义。
例如:
expr 2\*\(3+4\)
输出结果为14。
 

在bash和pdksh环境中,test命令用来测试条件表达式。其用法如下:
test expression
test命令可以和多种系统运算符一起使用。这些运算符可以分为四类:整数运算符、字符
串运算符、文件运算符和逻辑运算符。
1) 整数运算符
int1 -eq int2 如果int1 和int2相等,则返回真。
int1 -ge int2 如果int1 大于等于int2,则返回真。
int1 -gt int2 如果int1 大于int2,返回真。
int1 -le int2 如果int 1小于等于int 2,则返回真。
int1 -lt int2 如果int 1小于int2,则返回真。
int1 -ne int2 如果int1 不等于int2,则返回真。
2) 字符串运算符
str1 = str2 如果str1 和str2相同,则返回真。
str1 != str2 如果str1 和str2不相同,则返回真。
-n str 如果str 的长度大于零,则返回真。
-z str 如果str 的长度等于零,则返回真。
3) 文件运算符
-d filename 如果filename 为目录,则返回真。
-f filename 如果filename 为普通的文件,则返回真。
-r filename 如果filename 可读,则返回真。
-s filename 如果filename 的长度大于零,则返回真。
-w filename 如果filename 可写,则返回真。
-x filename 如果filename 可执行,则返回真。
4) 逻辑运算符
! expr 如果expr 为假,则返回真。
expr1 -a expr2 如果expr1 和expr2同时为真,则返回真。
expr1 -o expr2 如果expr1 或expr2有一个为真,则返回真。
tcsh中没有test命令,但它同样支持表达式。tcsh支持的表达式形式基本上和C语言一样。
这些表达式大多数用在if和while命令中。
 

tcsh表达式的运算符也分为整数运算符、字符串运算符、文件运算符和逻辑运算符四种。
1) 整数运算符
int1 <= int2 如果int 1小于等于int2,则返回真。
int1 >= int2 如果int1 大于等于int2,则返回真。
int1 < int2 如果int 1小于等于int2,则返回真。
int1 > int2 如果int1 大于int2,则返回真。
2) 字符串运算符
str1 == str2 如果str1 和str2相同,则返回真。
str1 != str2 如果str1 和str2不相同,则返回真。
3) 文件运算符
-r file 如果file可读,则返回真。
-w file 如果file可写,则返回真。
-x file 如果file可执行,则返回真。
-e file 如果file存在,则返回真。
-o file 如果当前用户拥有file ,则返回真。
-z file 如果file 长度为零,则返回真。
-f file 如果file 为普通文件,则返回真。
-d file 如果file 为目录,则返回真。
4) 逻辑运算符
exp1 || exp2 如果exp1 为真或exp2 为真,则返回真。
exp1 && exp2 如果exp1 和e x p 2同时为真,则返回真。
! exp 如果exp为假,则返回真。


26.4 条件表达式

26.4.1 if表达式
bash、pdksh和tcsh都支持嵌套的if...then...else表达式。bash和pdksh的if表达式如下:
if [ expression ]
then
commands
elif [ expression2 ]
then
commands
else
commands
fi
elif 和else在if表达式中均为可选部分。elif是else if的缩写。只有在if表达式和任何在它之
前的elif表达式都为假时,才执行elif。fi关键字表示if表达式的结束。
在tcsh中,if表达式有两种形式。第一种形式为:
if (expression1) then
commands
else if (expression2) then
commands
else
commands
endif
tcsh的第二种形式是第一种形式的简写。它只执行一个命令,如果表达式为真,则执行,
如果表达式为假,则不做任何事。其用法如下:
if (expression) command
 

下面是一个bash或pdksh环境下if表达式的例子。可以用来查看在当前目录下是否存在一个叫.profile的文件:
if [ -f .profile ]
then
echo "There is a .profile file in the current directory. "
else
echo "Could not find the .profile file."
fi
在tcsh环境下为:
#
if ( {-f .profile} ) then
echo "There is a .profile file in the current directory. "
else
echo "Could not find the .profile file."
endif
26.4.2 case 表达式
case表达式从几种情况中选择一种情况执行。case表达式可以使用带有通配符的字符串。
bash和pdksh的case表达式如下:
case string1 in
str1)
commands;;
str2)
commands;;
*)
commands;;
esac
在此,将string1和str1、str2比较。如果str1 和str2中的任何一个和strings1相符合,则它下
面的命令一直到两个分号( ; ; )将被执行。如果str1 和str2中没有和strings1相符合的,则星号(* )
下面的语句被执行。星号是缺省的case条件,因为它和任何字符串都匹配。
tcsh的选择语句称为开关语句。它和C语言的开关语句十分类似。
switch (string1)
case str1:
statements
breaksw
case str2:
statements
breaksw
default:
statements
breaksw
endsw
在此,string1和每一个case关键字后面的字符串相比较。如果任何一个字符串和string1相
匹配,则其后面的语句直到breaksw将被执行。如果没有任何一个字符串和string1匹配,则执
行default后面直到breaksw的语句。


下面是bash或pdksh环境下case表达式的一个例子。
它检查命令行的第一个参数是否为-i或e。如果是-i,则计算由第二个参数指定的文件中以
i开头的行数。如果是-e,则计算由第二个参数指定的文件中以e开头的行数。如果第一个参数
既不是-i也不是-e,则在屏幕上显示一条的错误信息。
case $1 in
-i)
count='grep ^i $2 | wc -l '
echo "The number of lines in $2 that start with an i is $count"
;;
-e)
count='grep ^e $2 | wc -l'
echo "The number of lines in $2 that start with an e is $count"
;;
*)
echo "That option is not recognized"
;;
esac
此例在tcsh 环境下为:
# remember that the first line must start with a # when using tcsh
switch ($1)
case -i | i:
set count = 'grep ^i $2 | wc -l'
echo "The number of lines in $2 that begin with i is $count"
breaksw
case -e | e:
set count = 'grep ^e $2 | wc -l'
echo "The number of lines in $2 that begin with e is $count"
breaksw
default:
echo "That option is not recognized"
breaksw
endsw


26.5 循环语句
Shell中提供了几种循环语句,最为常用的是for表达式。
26.5.1 for 语句
bash 和pdksh中有两种使用for语句的表达式。
第一种形式是:
for var1 in list
do
commands
done
在此形式时,对在list 中的每一项, for语句都执行一次。List可以是包括几个单词的、由
空格分隔开的变量,也可以是直接输入的几个值。每执行一次循环,var1都被赋予list中的当前
值,直到最后一个为止。
第二种形式是:
for var1
do
statements
done
使用这种形式时,对变量var1中的每一项,for语句都执行一次。此时,Shell程序假定变量
var1中包含Shell程序在命令行的所有位置参数。
一般情况下,此种方式也可以写成:
for var1 in "$@"
do
statements
done
在tcsh中,for循环语句叫做foreach。其形式如下:
foreach name (list)
commands
end
 

下面是一个在bash 或pdksh环境下的例子。
此程序读取每一个文件,把其中的内容转 换成大写字母,然后将结果存储在以.caps作为扩展名的同样名字的文件中。
for file
do
tr a-z A-Z <$file>$file.caps
done
在tcsh环境下,此例子可以写成:
#
foreach file ($*)
tr a-z A-Z <$file>$file.caps
end
26.5.2 while 语句
while 语句是另一种循环语句。当一个给定的条件为真时,则一直循环执行下面的语句直
到条件为假。在bash 和pdksh环境下,使用while 语句的表达式为:
while expression
do
statements
done
而在tcsh中,while 语句为:
while (expression)
statements
end
 

下面是在bash 和pdksh中while语句的一个例子。程序列出所带的所有参数,以及他们的位
置号。
count=1
while [-n "$*"]
do
echo "This is parameter number $count $1"
shift
count='expr $count + 1'
done
其中shift命令用来将命令行参数左移一个。
在tcsh中,此例子为:
#
set count = 1
while ( "$*" != "" )
echo "This is parameter number $count $1"
shift
set count = 'expr $count + 1'
end
 

26.5.3 until 语句
until 语句的作用和while语句基本一样,只是当给定的条件为假时,执行until语句。
until语句在bash和pdksh中的写法为:
until expression
do
commands
done
让我们用until语句重写上面的例子:
count = 1
until [ -z "$*" ]
do
echo "This is parameter number $count $1"
shift
count='expr $count + 1'
done
在应用中,until语句不是很常用,因为until语句可以用while语句重写。
 

26.6 shift 命令
bash、pdksh和tcsh都支持shift命令。shift命令用来将存储在位置参数中的当前值左移一
个位置。例如当前的位置参数是:
$1 = -r $2 = file1 $3 = file2
执行shift 命令:
shift
位置参数将会变为:
$1 = file1 $2 = file2
你也可以指定shift命令每次移动的位置个数。下面的例子将位置参数移动两个位置:
shift 2
下面是一个应用shift命令的例子。此程序有两个命令行参数,一个代表输入文件,另一个
代表输出文件。程序读取输入文件,将其中的内容转换成大写,并将结果存储在输出文件中。
while [ "$1" ]
do
if ["$1" = "-i"] then
infile = "$2"
shift 2
elif ["$1"="-o"]
then
outfile = "$2"
shift 2
else
echo "Program $0 does not recognize option $1"
fi
done
tr a-z A-Z <$infile>$outfile
 

26.7 select 语句
select 语句只存在于pdksh中,在bash或tcsh中没有相似的表达式。select语句自动生成一
个简单的文字菜单。其用法如下:
select menuitem [in list_of_items]
do
commands
done
其中方括号中是select语句的可选部分。
当select语句执行时, pdksh为在list_of_items中的每一项创建一个标有数字的菜单项。
list_of_items可以是包含几个条目的变量,就像choice1 choice2,或者是直接在命令中输入的选
择项,例如:
select menuitem in choice1 choice2 choice3
如果没有list_of_items,select语句则使用命令行的位置参数,就像for表达式一样。
一旦你选择了菜单项中的一个, select语句就选中的菜单项的数字值存储在变量menuitem
中。然后你可以利用do中的语句来执行选中的菜单项要执行的命令。
下面是select语句的一个例子。
select menuitem in pick1 pick2 pick3
do
echo "Are you sure you want to pick $menuitem"
read res
if [ $res = "y" -o $res = "Y" ]
then
break
fi
done
 

26.8 repeat 语句
repeat语句只存在于tcsh中,在pdksh和bash中没有相似的语句。repeat语句用来使一个单
一的语句执行指定的次数。repeat 语句如下:
repeat count command
下面给出repeat 语句的一个例子。它读取命令行后的一串数字,并根据数字在屏幕上分行
输出句号。
#
foreach num ($*)
repeat $num echo -n "."
echo ""
end
任何repeat 语句都可以用while或for语句重写。repeat语句只是更加方便而已。
 

26.9 子函数
Shell语言可以定义自己的函数,使用函数的最大好处就是程序更为清晰可读。
fname () {
shell commands
}
在pdksh中也可以使用如下的形式:
function fname {
shell commands
}
使用函数时,只须输入以下的命令:
fname [parm1 parm2 parm3 ...]
tcsh Shell中不支持函数。
你可以传递任何数目的参数给一个函数。函数将会把这些参数视为位置参数。请看下面的 例子。
此例子包括四个函数:upper()、lower()、print()和usage_error(),他们的任务分别是:将
文件转换成大写字母、将文件转换成小写字母、打印文件内容和显示出错信息。upper() 、
lower()、print()都可以有任意数目的参数。如果将此例子命名为convert,你可以在Shell提示
符下这样使用该程序:convert -u file1 file2 file3。
upper() {
shift
for i
do
tr a-z A-Z <$1>$1.out
rm $1
mv $1.out $1
shift
done;}
lower(){
shift
for i
do
tr A-Z a-z <$1 >$1.out
rm $1
mv $1.out $1
shift
done;}
print(){
shift
for i
do
lpr $1
shift
done;}
usage_error(){
echo "$1 syntax is $1 <option> <input files>"
echo ""
echo "where option is one of the following"
echo "p — to print frame files"
echo "u — to save as uppercase"
echo "l — to save as lowercase"; }
case $1
in
p | -p) print $@;;
u | -u) upper $@;;
l | -l) lower $@;;
*) usage_error $0;;
esac
 

虽然Shell语言功能强大而且简单易学,但你会发现在有些情况下,Shell语言无法解决你的
问题。这时,你可以选择Linux系统中的其他语言,例如C 和C++、gawk、以及Perl等。