1、bash 编程入门Shell Script(bash)简介众所皆知地,UNIX 上以小工具著名,利用许多简单的小工具,来完成原本需要大量软体开发的工作,这一点特色,使得 UNIX 成为许多人心目中理想的系统平台。 在众多的小工具中,Shell Script 算得上是最基本、最强大、运用最广泛的一个。它运用围之广,不但从系统启动、程式编译、定期作业、上网连线,甚至安装整个 Linux系统,都可以用它来完成。 因为 Shell Script 是利用您平日在使用的一些指令,将之组合起来,成为一个“ 程式“ 。如果您平日某些序列的指令下得特别频繁,便可以将这些指令组合起来,成为另一个新的指令。这样,不
2、但可以简化并加速操作速度,甚至还可以干脆自动定期执行,大大简化系统管理工作。 *Bash(GNU Bourne-Again SHell)是许多 Linux 平台的内定 Shell,事实上,还有许多传统 UNIX 上用的 Shell,像 tcsh、csh 、ash、bsh、ksh 等等,Shell Script 大致都类同,当您学会一种 Shell 以后,其它的 Shell 会很快就上手,大多数的时候,一个 Shell Script 通常可以在很多种 Shell 上使用。 这里我介绍您 bash 的使用方法。事实上,当您“man bash“时,就可以看到 bash 的说明书,不过对许多人来说,这
3、份说明书犹如“无字天书“ 一样难懂。这份文件,主要资料来源为“man bash“,我加上一些实际日常的应用例来说明。希望这样能让那些始终不得其门而入的人们,多多少少能有点概念。 教学例子“Hello world“ Shell Script 照传统程式教学例,这一节介绍 Shell Script 的“Hello World“ 如何撰写。 *#!/bin/sh # Filename : hello echo “Hello world!“ *大家应该会注意到第一行的“#!/bin/sh“。在 UNIX 下,所有的可执行 Script,不管是那一种语言,其开头都是“#!“,例如 Perl 是“#!/u
4、sr/bin/perl“,tcl/tk 是“#!/usr/bin/wish“,看您要执行的 Script 程式位置在那里。您也可以用“#! /bin/bash“、“#!/bin/tcsh“ 等等,来指定使用特定的 Shell。 echo 是个 bash 的内建指令。 *接下来,执行 hello 这个 script: 要执行一个 Script 的方式有很多种。 *第一种 : 将 hello 这个档案的权限设定为可执行。 foxmanfoxman bash# chmod 755 hello 执行 foxmanfoxman bash# ./hello hello world *第二种 : 使用 ba
5、sh 内建指令“source“或“.“。 foxmanfoxman bash# source hello hello world 或 foxmanfoxman bash# . hello hello world *第三种 : 直接使用 sh/bash/tcsh 指令来执行。 foxmanfoxman bash# sh hello hello world 或 foxmanfoxman bash# bash hello hello world *Bash 执行选项 *-c string : 读取 string 来当命令。 -i : 互动介面。 -s : 由 stdin 读取命令。 - : 取消往后
6、选项的读取。 -norc : 不要读/.bashrc 来执行。 -noprofile : 不要读 /etc/profile、/.bash_profile、/.bash_login、/.profile 等等来执行。 -rcfile filename : 执行 filename,而非/.bashrc -version : 显示版本。 -quiet : 启动时不要哩唆。 -login : 确保 bash 是个 login shell。 -nobraceexpansion : 不要用 curly brace expansion(符号展开)。 -nolineediting : 不用 readline 来
7、读取命令列。 -posix : 改采 Posix 1003.2 标准。 用于自动备份的 Shell Script一个用于自动备份的 Shell Script我们先前提到,可利用 Shell Script 搭配 crond 来作定期的工作。要作定期性的工作,在 UNIX 上,就是与 crond 的搭配运用。 *首先我们先来研究如何对系统进行备份。 要对系统进行备份,不外乎便是利用一些压缩工具。在许多 UNIX 系统上,tar及 gzip 是 de facto 的资料交换标准。我们经常可以看见一些 tar.gz 或 tgz 档,这些档案,被称为 tarball。当然了,您也可以用 bzip2、zi
8、p 等等压缩工具来进行压缩,不必限定于gzip。但 tar 配合 gzip 是最普遍的,也是最方便的方式。 要将我们想要的资料压缩起来,进行备份,可以结合 tar 及 gzip 一起进行。方式有很多种,最常用的指令是以下这一种: tar -c file/dir . | gzip -9 xxxx.tar.gz 您也可以分开来做: tar -r file/dir . -f xxxx.tar gzip -9 xxxx.tar 或 tar -r file/dir . -f xxxx.tar gzip -9 xxxx.tar.gz *在解过 Linux 下档案备份的基本知识后,我们来写一个将档案备份的
9、Script。 #!/bin/sh # Filename : backup DIRS=“/etc /var /your_directories_or_files“ BACKUP=“/tmp/backup.tgz“ tar -c $DIRS | gzip -9 $BACKUP 其中 DIRS 放的是您要备份的档案及目录,BACKUP 是您的备份档。可不要将/tmp 放进 DIRS 中,那样做,您是在做备份的备份,可能将您的硬碟塞爆。 *接下来测试 foxmanfoxman bash# chmod 755 backup foxmanfoxman bash# ./backup 执行完成后在/tmp
10、就会有一个 backup.tgz,里面储存了您重要的资料。您可用 gzip -dc /tmp/backup.tgz | tar -vt 或 tar vtfz /tmp/backup.tgz 来看看里面的档案列表。 要解开时,可用以下指令来完成复原: gzip -dc /tmp/backup.tgz | tar -xv 或 tar xvfz /tmp/backup.tgz 备份通常是仅备份系统通常最重要的部份,/etc 可说是不可缺少的一部份。另外,看您系统中有那些重要的资料需要备份。通常来说,您没有必要备份 /bin、/sbin 、/usr/bin 、/usr/sbin、/usr/X11R6/
11、bin 等等这些执行档目录。只要备份您重要的档案即可,别把整个硬碟备份,那是蛮呆的动作。 *如果您有许多台机器,可利用其中一台任务较轻的内部网路主机,做为主要备份主机。将所有机器都自动执行备份,然后利用 NFS/Coda/Samba 等网路档案系统,将备份的资料放到该备份机器中,该机器则定时收取备份资料,然后您再由该机器中进行一次备份。 这里是整个系统备份方案的图示。 在您进行之前,先解一下,系统中那些是要备份的,那些是不需要的。 *新的 backup#!/bin/sh HOSTNAME=hostname DIRS=“/etc /var /your_important_directory“ B
12、ACKUP=“/tmp/$HOSTNAME.tgz“ NFS=“/mnt/nfs“ tar -c $DIRS | gzip -9 $BACKUP mv -f $BACKUP $NFS *备份主机内的 Script : collect_backup#!/bin/sh NFS=“/mnt/nfs“ BACKUP=“/backup“ mv -f $NFS/*.tgz $BACKUP 在此,您不能够将所有备份都直接放在/mnt/nfs,这是危险的。万一任一台机器不小心将/mnt/nfs 所有内容删除,那么备份就会消失。因此,您需要将/mnt/nfs 移到一个只有该备份主机可存取的目录中。 *当这些个别
13、的 Script 都测试好以后,接下来我们将他们放到 crontab 里面。找到您的 crontab,它的位置可能在/var/spool/cron/crontabs/root、/etc/crontab、/var/cron/tabs/root。 在 crontab 中选择以下之一加入(看您定期的时间): Slackware : /var/spool/cron/crontabs/root01 * * * * /full_backup_script_path/backup 1 /dev/null 2 /dev/null # 每小时( 太过火一点) 30 16 * * * /full_backup_s
14、cript_path/backup 1 /dev/null 2 /dev/null # 每日16:30,下班前备份 30 16 * * 0 /full_backup_script_path/backup 1 /dev/null 2 /dev/null # 每周一16:30 0 5 1 * * /full_backup_script_path/backup 1 /dev/null 2 /dev/null # 每月一号 5:0 RedHat/Debian : /etc/crontabRedHat 可直接将 backup 放入/etc/cron.hourly, /etc/cron.daily, /e
15、tc/cron.weekly, /etc/cron.monthly。或采用如上加入/etc/crontab 的方式: 有关 crontab 的用法,可查“man 5 crontab“,在此不详述。 备份主机的设定类同。 注意: 所有机器不要同时进行备份,否则网路会大塞车。备份主机收取备份的时间要设为最后,否则会收不到备份资料。您可以在实作后,将时间间隔调整一下。 *看看,两个小小不到三行的 Shell Script,配合 cron 这个定时工具。可以让原本需要耗时多个小时的人工备份工作,简化到不到十分钟。善用您的想像力,多加一点变化,可你让您的生活变得轻松异常,快乐悠哉。档案系统检查系统安全一
16、向是大多数电脑用户关心的事,在 UNIX 系统中,最重视的事,即系统中有没有“木马 “(Trojan horse)。不管 Trojan horse 如何放进来的,有一点始终会不变,即被放置木马的档案,其档案日期一定会被改变,甚至会有其它的状态改变。此外,许多状况下,系统会多出一些不知名的档案。因此,平日检查整个档案系统的状态是否有被改变,将所有状态有改变的档案,以及目前有那些程式正在执行,自动报告给系统管理员,是个避免坐上“木马 “的良方。 *#!/bin/sh # Filename : whatever_you_name_it DIRS=“/etc /home /bin /sbin /usr
17、/bin /usr/sbin /usr/local /var /your_directory“ ADMIN=““ FROM=““ # 写入 Sendmail 的标头 echo “Subject: $HOSTNAME filesystem check“ /tmp/today.mail echo “From: $FROM“ /tmp/today.mail echo “To: $ADMIN“ /tmp/today.mail echo “This is filesystem report comes from $HOSTNAME“ /tmp/today.mail # 报告目前正在执行的程式 ps ax
18、f /tmp/today.mail # 档案系统检查 echo “File System Check“ /tmp/today.mail ls -alR $DIRS | gzip -9 /tmp/today.gz zdiff /tmp/today.gz /tmp/yesterday.gz /tmp/today.mail mv -f /tmp/today.gz /tmp/yesterday.gz # 寄出信件 sendmail -t然后把它放到一个不显眼的地方去,让别人找不到。 把它加入 crontab 中。 30 7 * * * /full_check_script_path/whatever_
19、you_name_it 1 /dev/null 2 /dev/null #上班前检查 有些档案是固定会更动的,像/var/log/messages、/var/log/syslog、/dev/ttyX 等等,不要太大惊小怪。控制圈 for演示了几个简单的 Shell Script,相信您应该对 Shell Script 有点概念了。现在我们开始来仔细研究一些较高等的 Shell Script 写作。一些进一步的说明,例如“$“、 “、“、“1“、“2“ 符号的使用,会在稍后解释。 *for name in word; do list ; done控制圈。 word 是一序列的字, for 会将
20、word 中的个别字展开,然后设定到 name 上面。list是一序列的工作。如果in word;省略掉,那么 name 将会被设定为 Script 后面所加的参数。*例一: #!/bin/sh for i in a b c d e f ; do echo $i done 它将会显示出 a 到 f。 *例二: 另一种用法, A-Z#!/bin/sh WORD=“a b c d e f g h i j l m n o p q r s t u v w x y z“ for i in $WORD ; do echo $i done 这个 Script 将会显示 a 到 z。 *例三 : 修改副档名如
21、果您有许多的.txt 档想要改名成.doc 档,您不需要一个一个来。 #!/bin/sh FILES=ls /txt/*.txt for txt in $FILES ; do doc=echo $txt | sed “s/.txt/.doc/“ mv $txt $doc done 这样可以将*.txt 档修改成*.doc 档。 *例四 : meow#!/bin/sh # Filename : meow for i ; do cat $i done 当您输入“meow file1 file2 .“时,其作用就跟“cat file1 file2 .“一样。 *例五 : listbin #!/bin
22、/sh # Filename : listbin for i in /bin/* ; do echo $i done 当您输入“listbin“ 时,其作用就跟“ls /bin/*“一样。 *例六 : /etc/rc.d/rc 拿一个实际的例来说,Red Hat 的/etc/rc.d/rc 的启动程式中的一个片断。 for i in /etc/rc.d/rc$runlevel.d/S*; do # Check if the script is there. ! -f $i ; . esaccase/esac 的标准用法大致如下: case $arg in pattern | sample) #
23、 arg in pattern or sample ; pattern1) # arg in pattern1 ; *) #default ; esac arg 是您所引入的参数,如果 arg 内容符合 pattern 项目的话,那么便会执行pattern 以下的程式码,而该段程式码则以两个 -More-(28%) 分号“;“做结尾。 可以注意到“case“及“esac“是对称的,如果记不起来的话,把“case“ 颠倒过来即可。*例一 : paranoia#!/bin/sh case in start | begin) echo “start something“ ; stop | end)
24、echo “stop something“ ; *) echo “Ignorant“ ; esac 执行foxmanfoxman bash# chmod 755 paranoia foxmanfoxman bash# ./paranoia Ignorant foxmanfoxman bash# ./paranoia start start something foxmanfoxman bash# ./paranoia begin start something foxmanfoxman bash# ./paranoia stop stop something foxmanfoxman bash
25、# ./paranoia end stop something *例二 : inetpanel许多的 daemon 都会附上一个管理用的 Shell Script,像 BIND 就附上 ndc,Apache就附上 apachectl。这些管理程式都是用 sh ell script 来写的,以下示一个管理 inetd 的 shell script。 #!/bin/sh case in start | begin | commence) /usr/sbin/inetd ; stop | end | destroy) killall inetd ; restart | again) killall
26、-HUP inetd ; *) echo “usage: inetpanel start | begin | commence | stop | end | destory | restart | again“ ; esac *例三 : 判断系统有时候,您所写的 Script 可能会跨越好几种平台,如 Linux、FreeBSD、Solaris 等等,而各平台之间,多多少少都有不同之处,有时候需要判断目前正在那一种平台上执行。此时,我们可以利用 uname 来找出系统资讯。 #!/bin/sh SYSTEM=uname -s case $SYSTEM in Linux) echo “My sy
27、stem is Linux“ echo “Do Linux stuff here.“ ; FreeBSD) echo “My system is FreeBSD“ echo “Do FreeBSD stuff here.“ ; *) echo “Unknown system : $SYSTEM“ echo “I dont what to do.“ ; esac 流程控制 selectselect name in word; do list ; doneselect 顾名思义就是在 word 中选择一项。与 for 相同,如果in word;省略,将会使用 Script 后面所加的参数。 例:#
28、!/bin/sh WORD=“a b c“ select i in $WORD ; do case $i in a) echo “I am A“ ; b) echo “I am B“ ; c) echo “I am C“ ; *) break; ; esac done 执行结果foxmanfoxman bash# ./select_demo 1) a 2) b 3) c #? 1 I am A 1) a 2) b 3) c #? 2 I am B 1) a 2) b 3) c #? 3 I am C 1) a 2) b 3) c #? 4 返回状态 Exit在继续下去之前,我们必须要切入另一个
29、话题,即返回状态值 - Exit Status。因为if/while/until 都迁涉到了使用 Exit Status 来控制程式流程的问题。 *许多人都知道,在许多语言中(C/C+/Perl) ,都有一个 exit 的函数,甚至连 Bash自己都有个 exit 的内建命令。而 exit 后面所带的数字,便是返回状态值 - Exit Status。 返回状态值可以使得程式与程式之间,利用 Shell script 来结合的可能性大增,利用小程式,透过 Shell script,来完成很杂的工作。 在 shell 中,返回值为零表示成功(True) ,非零值为失败 (False)。 *举例来说
30、,以下这个两个小程式 yes/no 分别会返回 0/1(成功/失败): /* yes.c */ void main(void) exit(0); /* no.c */ void main(void) exit(1); 那么以下这个“YES“ 的 shell script 便会显示“YES“。 #!/bin/sh # YES if yes ; then echo “YES“ fi 而“NO“不会显示任何东西。 #!/bin/sh # NO if no ; then echo “YES“ fi *test express express 在 Shell script 中,test express/
31、 express 这个语法被大量地使用,它是个非常实用的指令。由于它的返回值即 Exit Status,经常被运用在 if/while/until 的场合中。而在后面,我们也会大量运用到,在进入介绍 if/while/until 之前,有必要先解一下。 其返回值为 0(True)或 1(False),要看表述(express) 的结果为何。 express 格式 -b file : 当档案存在并且属性是 Block special(通常是/dev/xxx)时,返回 True。 -c file : 当档案存在并且属性是 character special(通常是/dev/xxx)时,返回 Tru
32、e。 -d file : 当档案存在并且属性是目录时,返回 True。 -e file : 当档案存在时,返回 True。 -f file : 当档案存在并且是正常档案时,返回 True。 -g file : 当档案存在并且是 set-group-id 时,返回 True。 -k file : 当档案存在并且有“sticky“ bit 被设定时,返回 True。 -L file : 当档案存在并且是 symbolic link 时,返回 True。 -p file : 当档案存在并且是 name pipe 时,返回 True。 -r file : 当档案存在并且可读取时,返回 True。 -s
33、 file : 当档案存在并且档案大小大于零时,返回 True。 -S file : 当档案存在并且是 socket 时,返回 True。 -t fd : 当 fd 被开启为 terminal 时,返回 True。 -u file : 当档案存在并且 set-user-id bit 被设定时,返回 True。 -w file : 当档案存在并且可写入时,返回 True。 -x file : 当档案存在并且可执行时,返回 True。 -O file : 当档案存在并且是被执行的 user id 所拥有时,返回 True。 -G file : 当档案存在并且是被执行的 group id 所拥有时,
34、返回 True。 file1 -nt file2 : 当 file1 比 file2 新时( 根据修改时间),返回 True。 file1 -ot file2 : 当 file1 比 file2 旧时( 根据修改时间),返回 True。 file1 -ef file2 : 当 file1 与 file2 有相同的 device 及 inode number 时,返回 True。 -z string : 当 string 的长度为零时,返回 True。 -n string : 当 string 的长度不为零时,返回 True。 string1 = string2 : string1 与 stri
35、ng2 相等时,返回 True。 string1 != string2 : string1 与 string2 不相等时,返回 True。 ! express : express 为 False 时,返回 True。 expr1 -a expr2 : expr1 及 expr2 为 True。 expr1 -o expr2 : expr1 或 expr2 其中之一为 True。 arg1 OP arg2 : OP 是-eqequal、-nenot-equal、-ltless-than 、-leless-than-or-equal 、-gtgreater-than 、-gegreater-tha
36、n-or-equal 的其中之一。 *在 Bash 中,当错误发生在致命信号时,bash 会返回 128+signal number 做为返回值。如果找不到命令,将会返回 127。如果命令找到了,但该命令是不可执行的,将返回126。除此以外,Bash 本身会返回最后一个指令的返回值。若是执行中发生错误,将会返回一个非零的值。 Fatal Signal : 128 + signo Cant not find command : 127 Cant not execute : 126 Shell script successfully executed : return the last comma
37、nd exit status Fatal during execution : return non-zero流程控制 ifif list then list elif list then list . else list fi几种可能的写法 *第一种 if list then do something here fi 当 list 表述返回值为 True(0)时,将会执行“do something here“。 例一 : 当我们要执行一个命令或程式之前,有时候需要检查该命令是否存在,然后才执行。 if -x /sbin/quotaon ; then echo “Turning on Quot
38、a for root filesystem“ /sbin/quotaon / fi 例二 : 当我们将某个档案做为设定档时,可先检查是否存在,然后将该档案设定值载入。 # Filename : /etc/ppp/settings PHONE=1-800-COLLECT #!/bin/sh # Filename : phonebill if -f /etc/ppp/settings ; then source /etc/ppp/settings echo $PHONE fi 执行 foxmanfoxman ppp# ./phonebill 1-800-COLLECT *第二种 if list t
39、hen do something here else do something else here fi 例三 : Hostname #!/bin/sh if -f /etc/HOSTNAME ; then HOSTNAME=cat /etc/HOSTNAME else HOSTNAME=localhost fi *第三种 if list then do something here elif list then do another thing here fi 例四 : 如果某个设定档允许有好几个位置的话,例如 crontab,可利用 if then elif fi 来找寻。 #!/bin/
40、sh if -f /etc/crontab ; then CRONTAB=“/etc/crontab“ elif -f /var/spool/cron/crontabs/root ; then CRONTAB=“/var/spool/cron/crontabs/root“ elif -f /var/cron/tabs/root ; then CRONTAB=“/var/cron/tabs/root“ fi export CRONTAB *第四种 if list then do something here elif list then do another thing here else do
41、 something else here fi 例五 : 我们可利用 uname 来判断目前系统,并分别做各系统状况不同的事。 #!/bin/sh SYSTEM=uname -s if $SYSTEM = “Linux“ ; then echo “Linux“ elif $SYSTEM = “FreeBSD“ ; then echo “FreeBSD“ elif $SYSTEM = “Solaris“ ; then echo “Solaris“ else echo “What?“ fi 控制圈 while/untilwhile list do list done当 list 为 True 时,
42、该圈会不停地执行。 例一 : 无限回圈写法 #!/bin/sh while : ; do echo “do something forever here“ sleep 5 done 例二 : 强迫把 pppd 杀掉。 #!/bin/sh while -f /var/run/ppp0.pid ; do killall pppd done *until list do list done当 list 为 False(non-zero)时,该圈会不停地执行。 例一 : 等待 pppd 上线。 #!/bin/sh until -f /var/run/ppp0.pid ; do sleep 1 done
43、 参数与变数在继续下去介绍 function 之前,我们必须停下来介绍“参数与变数“ 。 *参数(Parameters)是用来储存“值“ 的资料型态,有点像是一般语言中的变数。它可以是个名称(name)、数字(number)、或者是以下所列出来一些特殊符号(Special Parameters)。在 shell 中,变数是由 name 形式的参数所构成的。 *在前面的许多例中,我们事实上已经看到许多参数的运用。要设定一个 Parameter实际很简单: name=value 例如说: MYHOST=“foxman“ 而要使用它时,则是加个“$“符号。 echo $MYHOST *位置参数(Po
44、sitional Parameters) *所谓的位置参数便是 0,1,2,3,4,5,6,7,8,9.。使用时,用,.。 位置参数是当 script 被载入时,后面所附加的参数。是本身,则为第一个参数,为第二个,依此类推。而当 Positional Parameters 被 function 所使用时,它们会被暂时取代(下一节会介绍 function)。 例如以下这个 script: #!/bin/sh # Filename : position echo echo 执行时: foxmanfoxman bash# ./position abc ./position abc 当位置参数超过两位
45、数时,有特别的方法来展开,称为 Expansion。 *特殊参数(Speical Parameters) 这些符号,非常不人性,对新手来说很困扰。但上手后,会觉得方便无比,有些如果您看不懂的话,就-算了,不用浪费太多时间在上面。 * 星号 将 Positional Parameters 合成一个参数,其间隔为 IFS 内定参数的第一个字元( 见内建变数一节)。 例: #!/bin/sh # starsig echo $* 执行: foxmanfoxman bash# starsig a b c d e f g a b c d e f g * at 符号 与*星号类同。不同之处在于不参照 IFS。 例: #!/bin/sh # atsig echo $ 执行: foxmanfoxman bash# atsig a b c d e f g a b c d e f g *# 井字号 展开 Positional parameters 的数量。 例: #!/bin/sh # poundsig echo $# 执行 foxmanfox