cheatsheet from http://man.ddvip.com/os/freebsd_book_chs/ch24.htm
身为 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 } |
函式有几个要注意的地方:
- 在使用函式之前一定要先定义它,也就是在一个 Shell Script 中,一定要先写函式的内容,在档案最后再写会呼叫函式的程序部份。
- 在 Shell Script 中的变量全部都是全域变量 (Global),所以在函式中的变量也会影响函式外的其它部份。
- 命令列输入的参数在 Shell Script 中是以 $1, $2....来读取,但是这些参数并不会在函式中出现。所以必须使用传递参数的方式来将要在函式中使用的变量传给该函式。传递的方法和在命令列中使用 Shell Script 的方式一样,例如:name arg1 arg2..。传进函式的变数会以 $1,$2... 来储存,这和命令列传给 Shell Script 的参数名称一样但内容不同。
范例:
$ 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",并记录在指定的档案中。