第二十四章 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 |
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 |
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",并记录在指定的档案中。