第二十四章 Shell Script

身为 UNIX 系统管理者除了要熟悉 UNIX 指令外,我们最好学会几种 scripts 语言,例如 shell script 或 perl。学会 script 语言后,我们就可以将日常的系统管理工作写成一支执行档,如此一来,在管理系统时就可以更加灵活。

Shell script 是最基本的 script 语言,它是一堆 UNIX 指令的集合。本章将介绍 Shell script 的基本功能及语法,期望读者可以经由学习 Shell scripts 让使用 UNIX 系统时可以更加得心应手。

24.1 概论

Shell Script 是一个类似 MS Windows 中 .bat 档的东西,简单的说,Shell Script 就是将一堆 shell 中的指令放在一个文字文件中来执行。因此,为了能写出一个 shell Script,你必须先对 UNIX 指令有初步的认识。身为一个 UNIX 系统的管理者,一定要会使用 shell script 来使管理工作更加容易。

一般我们会将 Shell Script 的扩展名命名为 .sh,但并非一定要这么做,这样做只是为了要让我们更容易管理这些档案。在介绍如何 Shell Script 的内容之前,我们先来看如何写出一个 Shell Script 并执行它。假设我们要写一个名为 test.sh 的 Shell Script,首先用你习惯使用的文字编辑软件来开一个文件名为 test.sh 内容如下:

#!/bin/sh
echo Hello world!!

第一行是必需的,用来定义你要使用的 shell。这里我们定义要使用的是 Bourne Shell,其所在路径是 /bin/sh。在 UNIX 系统中有许多不同的 Shell 可以使用,而每个 Shell 的特性及用法都有些许的不同。因此,在写 Shell Script 时,我们会针对 Bourne Shell (sh) 来写,因为 sh 是所有 UNIX 系统中都会有的 Shell。就算你执行 Shell Script 时的环境不是使用 sh,只要加上第一行 #!/bin/sh 就可以在执行此 Shell Script 时使用 sh。而第二行的 echo 代表列出一个字符串,我们常使用它来输出信息。将 test.sh 存盘后,我们就可以用下列其中一种方式执行它:

1. 转向输入

$ sh < test.sh

2. 如果要输入参数的话,第一种方式便不适用,可以改用这种方法。<arguments> 就是我们要输入的参数,在上面的 test.sh 中并不需要输入参数:

$ sh test.sh <arguments>

3.你也可以改变 test.sh 的权限,将它变成可以独立执行的档案,这样就可以只打 test.sh 来执行它:

$ chmod a+x test.sh

$ ./test.sh

在 Shell Script 中,你们可以使用 # 为批注,在 # 后面的字符串都将被视为批注而被式忽略。而分号 ; 则代表新的一行,例如打 ls;ls -d 代表二个指令。另外,我们可以使用变量、流程控制、甚至是副函式来使程序更加灵活。以下的各章节我们会详细加以说明。

24.2 变量的使用

24.2.1 变量的使用

我们知道 Shell Script 是使用一堆指令拼凑而成,为了方便说明及练习起见,我们不使用编辑档案的方式来执行,而改以在命令列中打我们要的指令。首先,先打 sh 来进入 Bourne Shell。

% sh
$

在打了 sh 之后,会进入 Bourne Shell,其一般使用者的提示字符为 $。以下各指令开头的 $ 表示提示字符,而 $ 之后的粗体字才是我们输入的字符串。

在 Shell Script 中,所有的变量都视为字符串,因此并不需要在定义变量前先定义变量类型。在 Shell 中定义和使用变量时有些许的差异。例如,我们定义一个变量 color 并令它的值为 red,接着使用 echo 来印出变量 color 的值:

$ color=red
$ echo $color
red

在这里,以 color=red 来定义变量 color 的值为 red,并以 echo $color 来印出 color 这一个变数。在定义变量时,不必加 $,但是在使用它时,必须加上 $。请注意,在等号的二边不可以有空白,否则将出现错误 ,系统会误以为你要执行一个指令

我们再介绍一个范例:

$ docpath=/home/td/src/doc
$ echo $docpath
/home/td/src/doc
$ ls $docpath
abc.txt abc2.txt semmt.doc
$ ls $docpaht/*.txt
abc.txt abc2.txt

这里我们定义了变量 docpath 的值为 /home/td/src/doc,并印出它。接着我们使用 ls 这个指令来印出变量 docpath 目录中所有档案。 再以 ls $docpath/*.txt 来印出 /home/td/src/doc/ 目录下所有扩展名为 .txt 的档案。

我们再来看一个例子,说明如何使用变量来定义变量:

$ tmppath=/tmp
$ tmpfile=$tmppath/abc.txt
$ echo $tmpfile
/tmp/abc.txt

另外,我们也可以使用指令输出成为变量,请注意这里使用的二个 ` 是位于键盘左上角的 ` ,在 shell script 中,使用 ` 包起来的代表执行该指令:

$ now=`date`
$ echo $now
Mon Jan 14 09:30:14 CST 2002

如果在变量之后有其它字符串时,要使用下列方式来使用变量:

$ light=dark
$ echo ${light}blue
darkblue
$ echo "$light"blue
darkblue

这里双引号中的字将会被程序解读,如果是使用单引号将直接印出 $light 而非 dark。

经由上面几个简单的例子,相信您对变量的使用已有初步的认识。另外有一些我们必须注意的事情:

$ color=blue
$ echo $color
blue
$ echo "$color"
blue
$ echo '$color'
$color
$ echo \$color
$color
$ echo one two three
one two three
$ echo "one two three"
one two three

我们可以看到上面各个执行结果不大相同。在 Shell Script 中,双引号 " 内容中的特殊字符不会被忽略,而单引号中的所有特殊字符将被忽略。另外,\ 之后的一个字符将被视为普通字符串。

如果您希望使用者能在程序执行到一半时输入一个变量的值,您可以使用 read 这个指令。请看以下的范例:

#!/bin/sh
printf "Please input your name:"
read Name
echo "Your name is $Name"

由于 echo 指令内定会自动换行,所以我们使用 printf 这个指令来输出字符串。我们将上述内容存成档案 input.sh,接着使用下列指令来执行:

$ sh input.sh
Please input your name:Alex
Your name is Alex

您可以看到变量 Name 已被设为您所输入的字符串了。

24.2.2 程序会自动定义的变量

在执行 Shell Script 时,程序会自动产生一些变量:

变量名称 说明
$? 表示上一个指令的离开状况,一般指令正常离开会传回 0。不正常离开则会传回 1、2 等数值。
$$ 这一个 shell 的 process ID number
$! 最后一个在背景执行的程序的 process number
$- 这个参数包含了传递给 shell 旗标 (flag)。
$1 代表第一个参数,$2 则为第二个参数,依此类推。而 $0 为这个 shell script 的档名。
$# 执行时,给这个 Shell Script 参数的个数
$* 包含所有输入的参数,$@ 即代表 $1, $2,....直到所有参数结束。$* 将所有参数无间隔的连在一起,存成一个单一的参数。也就是说 $* 代表了 "$1 $2 $3..."。
$@ 包含所有输入的参数,$@ 即代表 $1, $2,....直到所有参数结束。$@ 用将所有参数以空白为间隔,存在 $@ 中。也就是说 $@ 代表了 "$1" "$2" "$3"....。

以下我们举几个例子来说明:

$ ls -d /home
/home
$ echo $?
0
$ ls /home/aaa/bb/ccc
/home/aaa/bb/cc: No such file or directory
$ echo $?
2
$ echo $?
0

上面例子中的第一行是 ls,我们可以看到存在一个目录 /home,接者 echo $? 时,出现 0 表示上一次的命令正常结束。接着我们 ls 一个不存在的目录,再看 $? 这个变量变成 2,表示上一次执行离开的结果不正常。最后一个 echo $? 所得到的结果是 0,因为上一次执行 echo 正常显示 2。

如果写一个文件名为 abc.sh,内容如下:

#!/bin/sh
echo $#: $1 $2 $3 $4 $5 $6 $7 $8 $9
echo $@

接着以下列指令来执行该档案:

$ chmod a+x abc.sh
$ ./abc.sh a "b c d" e f
4:a b c d e f
a b c d e f

上面最后二行即为执行结果。我们可以看到 $# 即为参数的个数,而 $1, $2, $3...分别代表了输入的参数 "a", "b c d", "e", "f",而最后的 $@ 则是所有参数。

24.2.3 系统内定的标准变量

你可以使用 set 这个指令来看目前系统中内定了哪些参数。一般而言会有 $HOME, $SHELL, $USER, $PATH 等。

$ echo $HOME
/home/jack
$ echo $PATH
/usr/bin:/usr/sbin:/bin

24.2.4 空变量的处理

如果程序执行时,有一个变量的值尚未被给定,你可以利用下列方式来设定对于这种情形提出警告:

$ echo $number one
one
$ set -u
$ echo $number one
sh: ERROR: number: Parameter not set

set -u 之后,如果变量尚未设定,则会提出警告。你也可以利用下列的方式来处理一些空变量及变量的代换:

表达式 说明
${var:-word} 如果变量 var 尚未设定或是 null,则将使用 word 这个值,但不改变 var 变量的内容。
${var:=word} 如果变量 var 尚未设定或是 null,则变量 var 的内容将等于 word 这个字符串,并使用这个新的值。
${var:?word} 如果变量 var 已经设定了,而且不是 null,则将使用变量 var。否则则印出 word 这个字符串,并强制离开程序。我们可以设定一个字符串 "Parameter null or not set" 来在变量未设定时印出,并终止程序。
${var:+word} 如果变量 var 已经设定了,而且不是 null,则以 word 这个字符串取代它,否则就不取代。

我们以下面的例子来说明:

$ echo $name Wang
Wang
$ echo ${name:-Jack} Wang
Jack Wang
$ echo $name Wang
Wang

上面的例子中,变数 $name 并未被取代,而下面的例子中,$name 将被取代:

$ echo $name Wang
Wang
$ echo ${name:=Jack} Wang
Jack Wang
$ echo $name Wang
Jack Wang

24.3 运算符号

24.3.1 四则运算

在 shell 中的四则运算必须使用 expr 这个指令来辅助。因为这是一个指令,所以如果要将结果指定给变量,必须使用 ` 包起来。请注意,在 + - * / 的二边都有空白,如果没有空白将产生错误

$ expr 5 -2
3
$ sum=`expr 5 + 10`
$ echo $sum
15
$ sum=`expr $sum / 3`
$ echo $sum
5

还有一个要特别注意的是乘号 * 在用 expr 运算时,不可只写 *。因为 * 有其它意义,所以要使用 \* 来代表。另外,也可以用 % 来求余数。

$ count=`expr 5 \* 3`
$ echo $count
$ echo `expr $count % 3`
5

我们再列出更多使用 expr 指令的方式,下列表中为可以放在指令 expr 之后的表达式。有的符号有特殊意义,必须以 \ 将它的特殊意义去除,例如 \*,否则必须用单引号将它括起来,如 '*':

类别 语法 说明
条件判断 expr1 \| expr2 如果 expr1 不是零或 null 则传回 expr1,否则传回 expr2。
expr1 \& expr2 如果 expr1 及 expr2 都不为零或 null,则传回 expr1,否则传回 0。
四则运算 expr1 + expr2 传回 expr1 加 expr2 后的值。
expr1 - expr2 传回 expr1 减 expr2 后的值。
expr1\* expr2 传回 expr1 乘 expr2 后的值。
expr1 / expr2 传回 expr1 除 expr2 后的值。
expr1 % expr2 传回 expr1 除 expr2 的余数。
大小判断 expr1 \> expr2 如果 expr1 大于 expr2 则传回 1,否则传回 0。如果 expr1 及 expr2 都是数字,则是以数字大小判断,否则是以文字判断。以下皆同。
expr1 \< expr2 如果 expr1 小于 expr2 则传回 1,否则传回 0。
expr1 = expr2 如果 expr1 等于 expr2 则传回 1,否则传回 0。
expr1 != expr2 如果 expr1 不等于 expr2 则传回 1,否则传回 0。
expr1 \>= expr2 如果 expr1 大于或等于 expr2 则传回 1,否则传回 0。
expr1 \<= expr2 如果 expr1 小于或等于 expr2 则传回 1,否则传回 0。
文字处理 expr1 : expr2 比较一固定字符串,即 regular expression。可以使用下列字符来辅助:

. 匹配一个字符。

$ 找字符串的结尾。

[list] 找符合 list 中的任何字符串。

* 找寻 0 个或一个以上在 * 之前的字。

\( \) 传回括号中所匹配的字符串。

我们针对比较复杂的文字处理部份再加以举例:

$ tty
ttyp0
$ expr `tty` : ".*\(..\)\$"
p0
$ expr `tty` : '.*\(..\)$'
p0

上面执行 tty 的结果是 ttyp0,而在 expr 中,在 : 右侧的表达式中,先找 .* 表示0个或一个以上任何字符,传回之后在结尾 ($) 时的二个字符 \(..\)。在第一个 expr 的式子中,因为使用双引号,所以在 $ 之前要用一个 \ 来去除 $ 的特殊意义,而第二个 expr 是使用单引号,在单引号内的字都失去了特殊意义,所以在 $ 之前不必加 \。

除了使用 expr 外,我们还可以使用下列这种特殊语法:

$ a=10
$ b=5
$ c=$((${a}+${b}))
$ echo $c
15
$ c=$((${a}*${b}))
$ echo $c
50

我们可以使用 $(()) 将表达式放在括号中,即可达到运算的功能。

24.3.2 简单的条件判断

最简单的条件判断是以 && 及 || 这二个符号来表示。

$ ls /home && echo found
found
$ ls /dev/aaaa && echo found
ls: /dev/aaaa: No such file or directory
$ ls -d /home || echo not found
/home
$ ls /dev/aaaa && echo not found
ls: /dev/aaaa: No such file or directory
条件式 说明
a && b 如果 a 是真,则执行 b。如果 a 是假,则不执行 b。
a || b 如果 a 是假,则执行 b。如果 a 是真,则不执行 b。

24.3.3 以 test 来比较字符串及数字

我们说过 Shell Script 是一堆指令的组合,所以在比较字符串及数字时一样是经由系统指令来达成。这里我们使用 test 及 [ 来做运算,运算所传回的结果是真 (true) 或假 ( false)。我们可以将它应用在条件判断上。test 和 [ 都是一个指令,我们可以使用 test 并在其后加上下表中的参数来判断真假。或者也可以使用 [ 表达式 ] 来替代 test,要注意的是 [ ] 中的空白间隔。

表达式 说明
-n str1 如果字符串 str1 的长度大于 0 则传回 true。
-z str1 如果字符串 str1 的长度等于 0 则传回 true。
str1 如果字符串 str1 不是 null 则传回 true。
str1 = str2 如果 str1 等于 str2 则传回 true。等号二边有空白。
str1 != str2 如果 str1 不等于 str2 则传回 true。!= 的二边有空白。
a -eq b Equal,等于。a 等于 b 则传回真 (true)。
a -ne b Not equal,不等于。a 不等于 b 则传回真 (true)。
a -gt b Grwater than,大于。a 大于 b 则传回真 (true)。
a -lt b Less Than,小于。a 小于 b 则传回真 (true)。
a -ge b Greater or equal,大于或等于。a 大于或等于 b 则传回真 (true)。
a -le b Less or equal,小于或等于。a 小于或等于 b 则传回真 (true)。

我们举例来说明:

$ test 5 -eq 5 && echo true
true
$ test abc!=cde && echo true
ture
$ [ 6 -lt 10 ] && echo true
ture

$ pwd
/home
$ echo $HOME
/home/jack
$ [ $HOME = `pwd` ] || echo Not home now
Not home now

24.3.4 以 test 来处理档案

我们也可以使用 test 及 [ 来判断一个档案的类型。下表中为其参数:

用法 说明
-d file 如果 file 为目录则传回真(true)。
-f file 如果 file 是一般的档案则传回真(true)。
-L file 如果 file 是连结档则传回真(true)。
-b file 如果 file 是区块特别档则传回真(true)。
-c file 如果 file 是字符特别文件则传回真(true)。
-u file 如果file 的 SUID 己设定则传回真(true)。
-g file 如果file 的 SGID 己设定则传回真(true)。
-k file 如果file 的 sticky bit 己设定则传回真(true)。
-s file 如果 file 的档案长度大于 0 则传回真(true)。
-r file 如果 file 可以读则传回真(true)。
-w file 如果 file 可以写则传回真(true)。
-x file 如果 file 可以执行则传回真(true)。

我们举例来说明:

$ [ -d /bin ] && echo /bin is a directory
/bin is a directory
$ test -r /etc/motd && echo /etc/motd is readable
/etc/motd is readable

第一个指令测试 /bin 是否存在,而且是一个目录,如果是则执行 echo 传回一个字符串。第二个指令是测试 /etc/motd 是否可以被读取,如果是则执行 echo 传回一个字符串。

24.4 内建指令

在 Shell 中有一些内建的指令,这些内建的指令如流程控制及 cd 等指令是 Shell 中的必备元素。另外还有一些为了提高执行效率的指令,如 test、echo 等。有的内建指令在系统中也有同样名称不同版本的相同指令,但是如 test、echo 等在执行时会伪装成是在 /bin 中的指令。

在写 shell script 时,要注意指令是否存在。下列即为常见的内建指令:

指令 说明
exit 离开程序,如果在 exit 之后有加上数字,表示传回值,如:exit 0。在 UNIX 系统下,当程序正常结束,会传回一个值 0,如果不正常结束则会传回一个非 0 的数字。
. file dot 指令,在 shell 中可以使用 "." 来呼叫一个外部档案,例如 . /etc/rc.conf 或 . .profile。注意 . 和其后的指令中间有空白。
echo 印出一个字符串。如果要使用非 shell 内建的 echo 则打 /bin/echo 来使用。
pwd 显示目前所在目录。
read var ... 从标准输入 (通常是键盘) 读入一行,然后将第一个字指派给跟在 read 之后的第一个参数,第二个字给第二个参数,依此类推,直到最后将所有字给最后一个参数。如果只有一个参数则将整行都给第一个参数。
readonly [var..] readonly 这个指令如果没有加参数则显示目前只读的变量。如果有加变量的话,则将该变量设定为只读。
return [n] 离开所在函式,如果在其后有加数字的话,则传回该数字。和 exit 一样,这个指令可以传回该函式的执行结果,0 表示正常结束。
set 将 $1 到 $n 设定为其参数的字。例如:

$ date
Mon Jan 21 11:19 CST 2002
$ set `date`
$ echo $4
11:19

wait [n] 等待在执行程序 (PID) 为 n 的背景程序结束,如果没有加参数 n 则等待所有背景程序结束。
exec command 执行一个外部程序,通常用于要改变到另一个 shell 或是执行不同的使用者者接口,如:

exec /usr/local/bin/startkde

export [var] 设定环境变量,如果没有参数则印出新的环境变量。
eval command 把参数当成 shell 命令来执行,如:

$ a=c; b=m; c=d; cmd=date
$ eval $`echo $a$b$c`
Mon Jan 21 11:19 CST 2002

24.5 流程控制

24.5.1 if 的条件判断

基本语法:

if condition-list
      then list
elif condition-list
      then list
else list
fi

范例一:

#!/bin/sh
if test -r /etc/motd
      then cat /etc/motd
else  echo "There is not motd or file is not readable"
fi

说明:上面这一个程序是检查 /etc/motd 这个档案是否可以读,如果可以则印出该档案,否则印出档案不可读。

范例二:

$ ee test.sh
#!/bin/sh
if [ $1 -gt 5 ]
      then echo " $1 is bigger then 5"
elif [ $1 -ge 0 ]
      then echo " $1 is between 5 and 0. "
else echo "$1 is less then 0."
fi
$ chmod a+x test.sh
$ ./test.sh 3
3 is between 5 and 0.

说明:这里我们建立一个档名为 test.sh 的档案,以指令 cat test.sh 来看它的内容。接着执行 ./test.sh 3,表示输入一个参数 3。test.sh 档案的内容表示依输入的参数判断参数大于 5 或介于 5 和 0 的中间,或者是小于 0。

24.5.2 while 及 until 循环

基本语法:

while condition-list
   do list
   done

until condition-list
   do list
   done

范例一:

#!/bin/sh
i=1
while [ $i -le 5 ]
   do
       echo $i
       i=`expr $i + 1`
   done

说明:首先令变量 i=1,接着在循环中当 i 小于等于 5 时就印出 i 的值,每印一次 i 就加 1。直到 i 大于 5 才停止。

范例二:

#!/bin/sh
i=1
until [ $i -gt 5 ]
   do
       echo $i
       i=`expr $i + 1`
   done

说明:首先令变量 i=1,接着循环会判断,一直执行到 i 大于 5 才停止。每跑一次循环就印出 i 的值,每印一次 i 就加 1。注意 while 和 until 的判断式中,一个是 -le ,一个是 -gt。

24.5.3 for 循环

基本语法:

for name in word1 word2 …
   do do-list
   done

for name
   do do-list
   done

范例一:

$ ee color1.sh
#!/bin/sh
for color in blue red green
    do
       echo $color
    done
$ chmod a+x color1.sh
$ ./color1.sh
blue
red
green

说明:这个档案 color1.sh 中,会在每一次循环中将关键词 in 后面的字符串分配给变量 color,然后印出变量 color。关键词 in 让我们可以依序设定一些值并指派给变量,然而,我们也可以不使用关键词 in。如果没有关键词 in ,程序会自动读取输入的参数,并依序指派给 for 之后的变量。请看范例二。

范例二:

$ ee color2.sh
#!/bin/sh
for color
    do
       echo $color
    done
$ chmod a+x color2.sh
$ ./color2.sh black green yellow
black
green
yellow

说明:在 color2.sh 这个档中,for 循环没有使用 in 这个关键词。但我们在执行它时输入三个参数,循环会自动将输入的参数指派给 for 之后的变量 color,并印出它。

24.5.4 case 判断

基本语法:

case word in
    pattern1) list1 ;;
    pattern2) list2 ;;
    …
esac

范例:

$ ee num.sh
for num
do
   case $num in
       0|1|2|3)       echo $num is between 0~3;;
       4|5|6|7)       echo $num is between 4~7;;
       8|9)           echo $num is 8 or 9;;
       *)             echo $num is not on my list;;
    esac
done
$ chmod a+x num.sh
$ ./num.sh 3 8 a
3 is between 0~3
8 is 8 or 9
a is not on my list

说明:这个程序是用来判断输入的参数大小。for 循环会将每一个输入的参数指定给变量 num,而在 case 中,判断变量 num 的内容符合哪一个条件,同一个条件中的每个字用 | 分开。如果未符上面的条件则一定会符合最后一个条件 * 。每一个要执行的 list 是以 ;; 做结尾,如果有多行 list,只要在最后一行加上一个 ;; 即可。

24.6 函式的运用

在 Shell Script 中也可以使用函式 (function) 来使用程序模块化。

基本语法:

name ( )
{
    statement
}

函式有几个要注意的地方:

范例:

$ ee test.sh
#! /bin/sh
ERRLOG=$1
ok ( )
{
      read ans
      case $ans in
           [yY]*) return 0;;
           *) return 1;;
      esac
}
errexit ( )
{
      echo $1
      date >> $ERRLOG
      echo $1 >> $ERRLOG
      exit
}
echo -n  "Test errexit function [y/n] "
ok && errexit "Testing the errexit function"
echo Normal termination
$ chmod a+x test.sh
$ ./test.sh err.log

说明:

这个程序中有二个函式:errexit 及 ok。第一行定义要将 log 档存在传给这个 Shell Script 的第一个参数。接着是二个函式,之后印出一行字,echo -n 表示印出字后游标不换行。然后再执行 ok 这个函式,如果 ok 函式执行成功则再执行 errexit 函式,并传给 errexit 函式一个字符串,最后再印出一个字符串。

在 ok 函式中,使用 read 指令来读入一个参数并指派给变数 ans。接着判断使用者输入的值是否为 Y 或 y,如果是则传回 1 代表没有成功执行,如果不是则传回 0 代表成功执行函式 ok。

如果 ok 函式传回 1 便不会执行 errexit 函式。如果是 0 则在 errexit 函式中,会先印出要传给 errexit 的参数 " Testing the errexit function",并记录在指定的档案中。