shell初级教程-备忘录2

3,SHELL脚本基本知识


对于不同的Unix和Linux,使用一段shell脚本将需要一些小小的改动才能运行通过。实际上shell的可迁移性不成问题,但是系统间命令的可迁移性存在差别。
在第二行注释中写入脚本名是一个好习惯。


    3.1 条件测试
写脚本时,有时要判断字符串是否相等,可能还要检查文件状态或是数字测试。基于这
些测试才能做进一步动作。Test命令用于测试字符串,文件状态和数字,也很适合于下一章将
提到的if,then,else条件结构。
expr命令测试和执行数值输出。使用最后退出状态命令$?可测知test和expr,二者均以0表正确,1表示返回错误。
测试文件状态
test一般有两种格式,即:
test condition 或 [ condition ]
使用方括号时,要注意在条件两边加上空格。
测试文件状态的条件表达式很多,但是最常用的如下:


- d 目录- s 文件长度大于0、非空
- f 正规文件- w 可写
- L 符号连接- u 文件有suid位设置
- r 可读- x 可执行
 
使用两种方法测试文件scores.txt是否可写并用最后退出状态测试是否成功。记住, 0表示成功,其他为失败。
                # ls -l scores.txt 
-rw-r--r-- 1 root root 0 Nov 21 12:15 scores.txt
# [ -w scores.txt ]
# echo $?
0
# test -w scores.txt 
# echo $?
0


        两种状态均返回0,可知文件scores.txt可写,现在测试其是否可执行:
# [ -x scores.txt ]
# echo $?
1


查看文件scores.txt权限列表,可知结果正如所料。
下面的例子测试是否存在appsbin目录
drwxr-xr-x  2 root root  4096 Nov 21 12:17 appsbin
# [ -d appsbin ]
# echo $?
0


目录appsbin果然存在。
测试文件权限是否设置了suid位
# ls -l xab
-rwsr-xr-x 1 root root 0 Nov 21 12:19 xab
# [ -u xab ]
# echo $?
0
从结果知道suid位已设置。


测试时使用逻辑操作符
测试文件状态是否为O K,但是有时要比较两个文件状态。shell提供三种逻辑操作完成此功能。
-a 逻辑与,操作符两边均为真,结果为真,否则为假。
-o 逻辑或,操作符两边一边为真,结果为真,否则为假。
! 逻辑否,条件为假,结果为真。
下面比较两个文件:
# ls -l results.txt scores.txt       
-rwxr-xr-x 1 root root 0 Nov 21 12:22 results.txt
-rw-r--r-- 1 root root 0 Nov 21 12:15 scores.txt


   # [ -w results.txt -a -w scores.txt ]
# echo $?
0


scores.txt不可执行,但results.txt可执行。
要测试文件results.txt是否可写、可执行:


# [ -w results.txt -a -x results.txt ]
# echo $?
0
结果为真。


        字符串测试
字符串测试是错误捕获很重要的一部分,特别在测试用户输入或比较变量时尤为重要。
字符串测试有5种格式。


test "string"
test string_operator "string"
test "string" string_operator "string"
[ string_operator string ]
[ string string_operator string]


这里,stringoperator可为:
==两个字符串相等。
!= 两个字符串不等。
-z 空串。
-n 非空串。
要测试环境变量EDITOR是否为空:


# [ -z $EDITOR ]
# echo $?
1
非空,取值是否是vim?
# [ $EDITOR == "vim" ]
# echo $?
0
是的,用echo命令反馈其值:
# echo $EDITOR
vim
测试变量tape与变量tape2是否相等:
# TAPE="/dev/rmt0"
# TAPE2="/dev/rmt1"
# [ "$TAPE" == "$TAPE2" ]
# echo $?
1
不相等。没有规定在设置变量时一定要用双引号,但在进行字符串比较时必须这样做。
测试变量tape与tape2是否不相等。
# [ "$TAPE" != "$TAPE2" ]  
# echo $?
0
是的,它们不相等。


测试数值
测试数值可以使用多操作符,一般格式如下:
"number"numericoperator"number"
或者
[ "number" numericoperator" number" ]
numericoperator可为:
    -eq 数值相等。
    -ne 数值不相等。
    -gt 第一个数大于第二个数。
    -lt 第一个数小于第二个数。
    -le 第一个数小于等于第二个数。
    -ge 第一个数大于等于第二个数。
下面的例子返回结果都一样。均为测试两个数是否相等(130是否等于130)。
# NUMBER=130
# [ "$NUMBER" -eq "130" ]
# echo $?
0
结果果然正确。
改变第二个数,结果返回失败,状态1(130不等于200)
# [ "$NUMBER" -eq "100" ]  
# echo $?
1
测试130是否大于100:
# [ "$NUMBER" -gt "100" ]  
# echo $?
0
当然。
也可以测试两个整数变量。下面测试变量sourcecount是否小于destcount:
# SOURCE_COUNT=13
# DEST_COUNT=15
# [ "$DEST_COUNT" -gt "$SOURCE_COUNT" ]
# echo $?
0
可以不必将整数值放入变量,直接用数字比较即可,但要加引号。
# [ "990" -le "995" ]
# echo $?
0
可以用逻辑操作符将两个测试表达式结合起来。仅需要用到一对方括号,而不能用两个,
否则将返回错误信息“ too many arg u m e n t s”。
# [ "990" -le "995" ] -a [ "123"  -gt "33" ]
-bash: [: too many arguments
下面例子测试两个表达式,如果都为真,结果为真,正确使用方式应为:
#[ "990" -le "995" -a "123"  -gt "33" ]      
[root@localhost ~]# echo $?
0


expr命令一般用于整数值,但也可用于字符串。一般格式为:
expr argument operator argument
expr也是一个手工命令行计数器。
# expr 10 + 10
  20
# expr 900 + 600
  1500
# expr 30 / 3
10
# expr 30 / 3 / 2
5
使用乘号时,必须用反斜线屏蔽其特定含义。因为shell可能会误解显示星号的意义。
# expr 30 \* 3
90

增量计数
expr在循环中用于增量计算。首先,循环初始化为0,然后循环值加1,反引号的用法意
即替代命令。最基本的一种是从(expr)命令接受输出并将之放入循环变量。
# LOOP=0
# LOOP=`expr $LOOP + 1`
# echo $LOOP
1

数值测试
可以用expr测试一个数。如果试图计算非整数,将返回错误。
# expr rr + 1
xpr: non-numeric argument
这里需要将一个值赋予变量(不管其内容如何),进行数值运算,并将输出导入dev/null,然后测试最后命令状态,如果为0,证明这是一个数,其他则表明为非数值。
# VALUE=12
# expr $VALUE + 10 > /dev/null 2>&1
# echo $?
0
这是一个数。
# VALUE=hello
# expr $VALUE + 10 > /dev/null 2>&1
# echo $?
3
这是一个非数值字符。
expr也可以返回其本身的退出状态,不幸的是返回值与系统最后退出命令刚好相反,成
功返回1,任何其他值为无效或错误。下面的例子测试两个字符串是否相等,这里字符串为"hello"和"hello"。 
# VALUE=hello
# expr $VALUE = "hello"
1
# echo $?
0
expr返回1。不要混淆了,这表明成功。现在检验其最后退出状态,返回0表示测试成功,"hello"确实等于"hello"。




模式匹配
expr也有模式匹配功能。可以使用expr通过指定冒号选项计算字符串中字符数。. *意即任何字符重复0次或多次。
        # VALUE=accounts.doc             
# expr $VALUE : '.*'
12
其匹配的方式是,expr 字符串 : 正则表达式


在expr中可以使用字符串匹配操作,这里使用模式. doc抽取文件附属名。
# expr $VALUE : '\(.*\).doc'
accounts


3.2,控制流结构
      所有功能脚本必须有能力进行判断,也必须有能力基于一定条件处理相关命令。本章讲述这方面的功能,在脚本中创建和应用控制结构.


 退出状态
 在书写正确脚本前,大概讲一下退出状态。任何命令进行时都将返回一个退出状态。如果要观察其退出状态,使用最后状态命令:
 $ echo $?
 主要有4种退出状态。前面已经讲到了两种,即最后命令退出状态$ ?和控制次序命令( $$,||)。其余两种是处理SHELL脚本或shell退出及相应退出状态或函数返回码。在第19章讲到函数时,也将提到其返回码。
   要退出当前进程,s h e l l提供命令e x i t,一般格式为:
   exit n
 其中,n为一数字。
   如果只在命令提示符下键入e x i t,假定没有在当前状态创建另一个s h e l l,将退出当前s h e l l。
   如果在脚本中键入e x i t,s h e l l将试图(通常是这样)返回上一个命令返回值。有许多退出脚本值,但其中相对于脚本和一般系统命令最重要的有两种,即:
 退出状态0 退出成功,无错误。
 退出状态1 退出失败,某处有错误。
   可以在shell脚本中加入自己的退出状态(它将退出脚本)。本书鼓励这样做,因为另一个shell脚本或返回函数可能要从shell脚本中抽取退出脚本。另外,相信加入脚本本身的退出脚本值是一种好的编程习惯。


 如果愿意,用户可以在一个用户输入错误后或一个不可覆盖错误后或正常地处理结束后
 退出脚本。
 注意从现在起,本书所有脚本都将加入注释行。注释行将解释脚本具体含义,帮助用户理解脚本。可以在任何地方加入注释行,因为其本身被解释器忽略。注释行应以#开头。




A,控制结构
  几乎所有的脚本里都有某种流控制结构,很少有例外。流控制是什么?假定有一个脚本包含下列几个命令:
#!/bin/sh
# make a directory
mkdir /home/dave/mydocs
# copy all doc files
cp *.docs /home/dave/docs
# delete all doc files
rm *.docs
        
上述脚本问题出在哪里?如果目录创建失败或目录创建成功文件拷贝失败,如何处理?里需要从不同的目录中拷贝不同的文件。必须在命令执行前或最后的命令退出前决定处理方法。s h e l l会提供一系列命令声明语句等补救措施来帮助你在命令成功或失败时,或需要处理一个命令清单时采取正确的动作。


        1,流控制
i f、t h e n、e l s e语句提供条件测试。测试可以基于各种条件。例如文件的权限、长度、数值或字符串的比较。这些测试返回值或者为真( 0),或者为假( 1)。基于此结果,可以进行相关操作。在讲到条件测试时已经涉及了一些测试语法。
c a s e语句允许匹配模式、单词或值。一旦模式或值匹配,就可以基于这个匹配条件作其他声明。
2,循环
循环或跳转是一系列命令的重复执行过程,本书提到了3种循环语句:
w
for 循环每次处理依次列表内信息,直至循环耗尽。
Until 循环此循环语句不常使用, u n t i l循环直至条件为真。条件部分在循环末尾部分。
While 循环w h i l e循环当条件为真时,循环执行,条件部分在循环头。流控制语句的任何循环均可嵌套使用,例如可以在一个f o r循环中嵌入另一个f o r循环。
现在开始讲解循环和控制流,并举一些脚本实例。
从现在起,脚本中e c h o语句使用L I N U X或B S D版本,也就是说使用e c h o方法echo -e -n,意即从e c h o结尾中下一行执行命令。应用于U N I X(系统V和B S D)的统一的e c h o命令参阅1 9章s h e l l函数。


if then else语句
i f语句测试条件,测试条件返回真( 0)或假(1)后,可相应执行一系列语句。i f语句结
构对错误检查非常有用。其格式为:
if 条件1
then 命令1
elif 条件2
then 命令2
else 命令3
f i
  让我们来具体讲解i f语句的各部分功能。
If 条件1 如果条件1为真
Then 那么
命令1 执行命令1
elif 条件2 如果条件1不成立
then 那么
命令2 执行命令2
else 如果条件1,2均不成立
命令3 那么执行命令3
fi 完成
i f语句必须以单词f i终止。在i f语句中漏写f i是最一般的错误。我自己有时也是这样。e l i f和e l s e为可选项,如果语句中没有否则部分,那么就不需要e l i f和e l s e部分。I f语句可以有许多e l i f部分。最常用的i f语句是if then fi结构。
if...then是最常见的条件判断语句,简而言之,就是符合某个条件判断的时候,就予以进行某项工作就是了,我们可以简单地这样子看:
if [ 条件判断表达式 ]; then
             当条件判断表达式成立时可以执行的命令
fi


较特别的是,如果有多个条件判断时,可以将多个判断写入一个中括号,还可以使用多个中括号隔开,而括号和括号之间,则以&&或||来隔开,这个要和命令行中的&&,||区别开来。




# cat iftest
#!/bin/sh
# iftest
# this is a comment line, all commment lines start with a #
if [ "10" -lt "12" ]
then    
    # yes 10 is less than 12
            echo "Yes, 10 is less than 12"
fi      


变量值测试
通过测试设置为接受用户输入的变量可以测知用户是否输入信息。下面的例子中测试用户键入r e t u r n键后变量n a m e是否包含任何信息。
# cat iftest2.sh
#!/bin/sh
# iftest2
echo -n "Enter your name :"
read NAME
# did the user just hit return ???
if [ "$NAME" == "" ]; then
       echo "You did not enter any information"
fi      
#./iftest2.sh
Enter your name :
You did not enter any information
    grep输出检查
不必拘泥于变量或数值测试,也可以测知系统命令是否成功返回。对g r e p使用i f语句找出g r e p是否成功返回信息。下面的例子中g r e p用于查看D a v e是否在数据文件d a t a . f i l e中,注意‘D a v e \ >’用于精确匹配。


[root@localhost ~]# cat grepif.sh 
#!/bin/sh
# grepif.sh
if grep 'Dave\>' data.file > /dev/null 2>&1
then
             echo "Great Dave is in the file"
else
             echo "No Dave is not in the file"
fi
  [root@localhost ~]# ./grepif.sh 
    No Dave is not in the file


用变量测试grep输出
正像前面看到的,可以用g r e p作字符串操作。下面的脚本中,用户输入一个名字列表,g r e p在变量中查找,要求其包含人名P e t e r。下面是具体的代码情况和运行信息。


[root@localhost ~]# cat grepstr.sh 
#!/bin/sh
# grepstr
    echo -n "Enter a list of names:"
read list
if echo $list | grep "Peter" > /dev/null 2>&1
then
              echo "Peter is here"
# could do some processing here...
else
                 echo "Peter's not in the list. No comment!"
      fi
  [root@localhost ~]# ./grepstr.sh 
Enter a list of names:John Louise Peter James
Peter is here
文件拷贝输出检查
下面测试文件拷贝是否正常,如果c p命令并没有拷贝文件m y f i l e到m y f i l e . b a k,则打印错误信息。注意错误信息中` basename $0`打印脚本名。如果脚本错误退出,一个好习惯是显示脚本名并将之定向到标准错误中。用户应该知道产生错误的脚本名
[root@localhost ~]# cat ifcp.sh 
#!/bin/sh
# ifcp.sh
if cp myfile myfile.bak; then
               echo "good copy"
else
               echo "`basename $0`: error could not copy the file" >&2
fi
[root@localhost ~]# ./ifcp.sh 
cp: cannot stat `myfile': No such file or directory
ifcp.sh: error could not copy the file
注意,文件可能没找到,系统也产生本身的错误信息,这类错误信息可能与输出混在一起。既然已经显示系统错误信息获知脚本失败,就没必要显示两次。要去除系统产生的错误和系统输出,只需简单的将标准错误和输出重定向即可。修改脚本为: >/dev/null 2>&1。


[root@localhost ~]# cat ifcp.sh 
#!/bin/sh
# ifcp.sh
if cp myfile myfile.bak > /dev/null 2>&1; then
                echo "good copy"
else
       echo "`basename $0`: error could not copy the file" >&2
fi
[root@localhost ~]# ./ifcp.sh 
ifcp.sh: error could not copy the file



上面当中>/dev/null表示任何标准输出都定向到那个无尽的“黑洞”/de/null中,然后2>&1表示错误输出也是到/dev/null中,&1表示前面的那个/dev/null,脚本运行时,所有输出包括错误重定向至系统垃圾堆。

当前目录测试
当运行一些管理脚本时,可能要在根目录下运行它,特别是移动某种全局文件或进行权限改变时。一个简单的测试可以获知是否运行在根目录下。下面脚本中变量D I R E C TO RY使用当前目录的命令替换操作,然后此变量值与" /"字符串比较( /为根目录)。如果变量值与字符串不等,则用户退出脚本,退出状态为1意味错误信息产生。
[root@localhost ~]# cat ifpwd.sh 
#!/bin/sh
# ifpwd.sh
DIRECTORY=`pwd`
# grab the current dirctory
if [ "$DIRECTORY" != "/" ]; then
               # is it the root directory ?
# no, the direct output to standard error, which is the screen
# by default.
       echo "You need to be in the root directory no $DIRECTORY to run
 this script" >&2
# exit with a value of 1, an error
exit 1
fi
# ./ifpwd.sh 
You need to be in the root directory no /root to run this script
文件权限测试
可以用i f语句测试文件权限,下面简单测试文件t e s t . t x t是否被设置到变量L O G N A M E,测试test.txt文件是否具有写的权限。下面的脚本先建立一个test.txt的空白文档,列出它的相关权限。然后执行脚本测试其是否可以写入,然后显示相关信息。
# touch test.txt
[root@localhost ~]# ls -l test.txt
--rw-r--r-- 1 root root 0 Nov 21 15:21 test.txt
[root@localhost ~]# chmod u+x ifwr.sh 
[root@localhost ~]# cat ifwr.sh 
#!/bin/sh
# ifwr.sh
LOGFILE=test.txt
echo $LOGFILE
if [ ! -w "$LOGFILE" ];    
then
    echo " You cannot write to $LOGFILE" >&2
       else
    echo " You can write to $LOGFILE" >&2
fi
[root@localhost ~]# ./ifwr.sh 
test.txt
You can write to test.txt


测试传递到脚本中的参数
 i f语句可用来测试传入脚本中参数的个数。使用特定变量$ #,表示调用参数的个数。可以测试所需参数个数与调用参数个数是否相等。以下测试确保脚本有三个参数。如果没有,则返回一个可用信息到标准错误,然后代码
退出并显示退出状态。如果参数数目等于3,则显示所有参数。


[root@localhost ~]# cat ifparam.sh 
#!/bin/sh
# ifparam
if [ $# -lt 3 ]; then
               # less than 3 parameters called, echo a usage message and exit
       # 如果少于三个参数则显示使用的信息,然后退出。
               echo "Usage: `basename $0`arg1 arg2 arg3" >&2
exit 1
fi
# good, received 3 params, let's echo them
# 好,现在接受了三个参数,让我们开始显示他们
echo "arg1: $1"
echo "arg2: $2"
echo "arg3: $3"
[root@localhost ~]# ./ifparam.sh cup medal
Usage: ifparam.sharg1 arg2 arg3
[root@localhost ~]# ./ifparam.sh cup medal trophy
  arg1: cup
arg2: medal
arg3: trophy
从上面的运行信息可以看出,如果只传入两个参数,则显示一可用信息,然后脚本退出。
只有正确传入了三个参数了,才显示所有的参数然后退出。
决定脚本是否为交互模式
有时需要知道脚本运行是交互模式(终端模式)还是非交互模式( c r o n或a t)。脚本也许
需要这个信息以决定从哪里取得输入以及输出到哪里,使用t e s t命令并带有- t选项很容易确认这一点。如果t e s t返回值为1,则为交互模式。假如我是在一个终端下运行下面这个脚本。
[root@localhost ~]# cat ifinteractive.sh 
#!/bin/sh
# ifinteractive.sh
if [ -t ]; then
               echo "We are interactive with a terminal"
else
               echo "We must be running from some background process probably
               cron or at"
fi
[root@localhost ~]# ./ifinteractive.sh 
We are interactive with a terminal
  简单的if else语句
下一个i f语句有可能是使用最广泛的:
  if 条件
t h e n
命令1
e l s e
命令2
    f i
  使用i f语句的e l s e部分可在条件测试为假时采取适当动作。

变量设置测试


下面的例子测试环境变量E D I TO R是否已设置。如果E D I TO R变量为空,将此信息通知用
户。如果已设置,在屏幕上显示编辑类型。
[root@localhost ~]# cat ifeditor.sh 
#!/bin/sh
# ifeditor.sh
if [ -z $EDITOR ]; then
               # the variable has not been set
               # 变量没有设置
               echo "Your EDITOR environment is not set"
else
               # let's see what it is
               # 如果设置了,让我们来看看它到底是什么
               echo "Using $EDITOR as the default editor"
fi
[root@localhost ~]# ./ifeditor.sh 
Your EDITOR environment is not set

将脚本参数传入系统命令
可以向脚本传递位置参数,然后测试变量。这里,如果用户在脚本名字后键入目录名,脚本将重设$ 1特殊变量为一更有意义的名字。即D I R E C TO RY。这里需测试目录是否为空,如果目录为空,ls -A将返回空,然后对此返回一信息。
# ifdirec.sh
# assigning $1 to DIRECTORY variable
DIRECTORY=$1
if [ "`ls -A $DIRECTORY`" == "" ]; then
       # if it's an empty string, then it's empty
       echo "$DIRECTORY is indeed empty"
else    
       # otherwise it is not
        echo "$DIRECTORY is not empty"
fi


null:命令用法
到目前为止,条件测试已经讲完了t h e n和e l s e部分,有时也许使用者并不关心条件为真或为假。
不幸的是i f语句各部分不能为空—一些语句已经可以这样做。为解决此问题, s h e l l提供了:空命令。空命令永远为真(也正是预想的那样)。回到前面的例子,如果目录为空,可以只在t h e n部分加入命令。
[root@localhost ~]# cat ifdirectory.sh 
#!/bin/sh
# ifdirectory.sh
DIRECTORY=$1
if [ "`ls -A $DIRECTORY`" == "" ]
then
               echo "$DIRECTORY is indeed empty"
else :
               # do nothing
fi
[root@localhost ~]# ./ifdirectory.sh testd
testd is indeed empty


测试目录创建结果
现在继续讨论目录,下面的脚本接受一个参数,并用之创建目录,然后参数被传入命令行,重设给变量D I R E C TO RY,最后测试变量是否为空。
if ["$DIRECTORY"=""]
也可以用
if[$# -lt 1]
来进行更普遍的参数测试。
如果字符串为空,返回一可用信息,脚本退出。如果目录已经存在,脚本从头至尾走一遍,什么也没做。创建前加入提示信息,如果键入Y或y,则创建目录,否则使用空命令表示不采取任何动作。使用最后命令状态测试创建是否成功执行,如果失败,返回相应信息。
[root@localhost ~]# cat ifmkdir.sh 
#!/bin/sh
# ifmkdir.sh
# parameter is passed as $1 but reassigned to DIRECTORY
DIRECTORY=$1
# is the string empty ??
if [ "$DIRECTORY" == "" ]
then
                echo "Usage :`basename $0` directory to create" >&2
                exit 1
fi
if [ -d $DIRECTORY ]
then : # do nothing
else
               echo "The directory does not exist"
               echo -n "Create it now? [y..n] :"
               read ANS
                if [ "$ANS" == "y" ] || [ "$ANS" == "Y" ]
                then
                                echo "creating now"
                                # create directory and send all output to /dev/null
                                mkdir $DIRECTORY > /dev/null 2>&1
                                if [ $? != 0 ]; then
                                                echo "Errors creating the directory $DIRECTORY" >&2
                                                exit 1
                                fi
               else : # do nothing
                fi
fi
[root@localhost ~]# ./ifmkdir.sh dt
The directory does not exist
Create it now? [y..n] :y
creating now




另一个拷贝实例
在另一个拷贝实例中,脚本传入两个参数(应该包含文件名),系统命令c p将$ 1拷入$ 2,输出至/ d e v / n u l l。如果命令成功,则仍使用空命令并且不采取任何动作。另一方面,如果失败,在脚本退出前要获知此信息
[root@localhost ~]# cat ifcp2.sh 
#!/bin/sh
# ifcp2.sh
if cp $1 $2 > /dev/null 2>&1
               # successful, great do nothing
then :
else
               # oh dear, show the user what files they were.
                echo "`basename $0`: ERROR failed to copy $1 to $2"
                exit 1
fi
[root@localhost ~]# ./ifcp2.sh myfile.lex myfile.lex.bak
ifcp2.sh: ERROR failed to copy myfile.lex to myfile.lex.bak
[root@localhost ~]# touch myfile.lex
[root@localhost ~]# ./ifcp2.sh myfile.lex myfile.lex.bak
上面展现了脚本运行成功和脚本运行失败的情况。
下面的脚本用s o r t命令将文件a c c o u n t s . q t r分类,并输出至系统垃圾堆。没人愿意观察屏幕上3 0 0行的分类页。成功之后不采取任何动作。如果失败,通知用户。
[root@localhost ~]# cat ifsort.sh 
#!/bin/sh
# ifsort
if sort accounts.qtr > /dev/null
                # sorted. Great
then :
else
               # better let the user know
                echo "`basename $0`: Oops..errors could not sort accounts.qtr"
fi
多个if语句
可能有时要嵌入i f语句。为此需注意i f和f i的相应匹配使用。


测试和设置环境变量
前面已经举例说明了如何测试环境变量E D I TO R是否被设置。现在如果未设置,则进一步为其赋值,脚本如下:
#!/bin/sh
# ifseted.sh
# is the EDITOR set?
if [ -z $EDITOR ]; then
       echo "Your EDITOR environment is not set"
       echo "I will assum you want to use vi .. OK"
       echo -n "Do you wish to change it now? [y..n] :"
       read ANS


        # check for an upper or lower case 'y'
        if [ "$ANS" == "Y" ] || [ "$ANS" == "y" ]; then
                echo "enter you  editor type :"
               read EIDTOR
               if [ -z $EDITOR ] || [ "$EDITOR" == "" ]; then
                       # if EDITOR not set and no value in variable EDITOR,
                       # then set it to vi
                       echo "No, editor entered, using vi as default"
                       EDITOR=vi
                       export EDITOR
               fi
               # got a value use it for EDITOR
               EDITOR=$EDITOR
               export EDITOR
               echo "setting $EDITOR"
        fi
else
       # user
       echo "Using vi as the default editor"
       EDITOR=vi
       export EDITOR
fi


脚本工作方式如下:首先检查是否设置了该变量,如果已经赋值,输出信息提示使用v i作为缺省编辑器。v i被设置为编辑器,然后脚本退出。如果未赋值,则提示用户,询问其是否要设置该值。检验用户输入是否为大写或小写y,输入为其他值时,脚本退出。如果输入Y或y,再提示输入编辑类型。使用$ E D I TO R =“”测试用户是否未赋值和未点击r e t u r n键。一种更有效的方法是使用-z $EDITO R方法,本文应用了这两种方法。如果测试失败,返回信息到屏幕,即使用v i做缺省编辑器,因而E D I TO R赋值为v i。如果用户输入了一个名字到变量E D I TO R,则使用它作为编辑器并马上让其起作用,即导出变量E D I TO R。


检测最后命令状态
前面将目录名传入脚本创建了一个目录,脚本然后提示用户是否应创建目录。下面的例子创建一个目录,并从当前目录将所有* . t x t文件拷入新目录。但是这段脚本中用最后状态命令检测了每一个脚本是否成功执行。如果命令失败则通知户。


#!/bin/sh
# ifmkdir2.sh
DIR_NAME=testdirec
# where are we?
THRER=`pwd`
# send all output to the system dustbin
mkdir $DIR_NAME > /dev/null 2>&1
# is it a directory ?
if [ -d $DIR_NAME ]; then
       # can we cd to the directory
       cd $DIR_NAME
       if [ $? == 0 ]; then
                # yes we can
                HERE=`pwd`
                cp $THERE/*.txt $HERE
       else
                echo "Cannot cd to $DIR_NAME" >&2
                exit 1
        fi
else
       echo "Cannnot create directory $DIR_NAME" >&2
       exit 1
fi

增加和检测整数值
下面的例子进行数值测试。脚本包含了一个计数集,用户将其赋予一个新值就可改变它。脚本然后将当前值1 0 0加入一个新值。工作流程如下:
用户输入一个新值改变其值,如果键入回车键,则不改变它,打印当前值,脚本退出。如果用户用y或Y响应新值,将提示用户输入增量。如果键入回车键,原值仍未变。键入一个增量,首先测试是否为数字,如果是,加入计数C O U N TO R中,最后显示新值。
#!/bin/sh
# ifcounter.sh
COUNTER=100
echo "Do you wish to change the counter value currently set at $COUNTER[y..n] :"
read ANS
if [ "$ANS" == "y" ] || [ "$ANS" == "Y" ]; then
       # yes user wants to change the value
       echo "Enter a sensible value "
       read VALUE
        # simple test to see if it's numeric, add any number to VALUE,
        # then check out return 
        # code
        expr $VALUE + 10 > /dev/null 2>&1
        STATUS=$?
        # check return code of expr
        if [ "$VALUE" == "" ] || [ "$STATUS" != "0" ]; then
                # send errors to standard error
                echo " You either entered nothing or a non-numeric " >&2
                echo " Sorry now exiting...counter stays at $COUNTER" >&2
                exit 1
        fi      
        # if we are here, then it's a number, so add it to COUNTER
       COUNTER=`expr $COUNTER + $VALUE`
        echo " Counter now set to $COUNTER"
else    
       # if we are here user just hit return instead of entering a number
       # or anssered n to the change a value prompt
       echo " Counter stays at $COUNTER"
fi      
下面是程序的运行结果:
[root@localhost ~]# ./ifcounter.sh 
Do you wish to change the counter value currently set at 100
       [y..n] :n
Counter stays at 100
[root@localhost ~]# ./ifcounter.sh 
Do you wish to change the counter value currently set at 100
        [y..n] :y
Enter a sensible value 
fdg
You either entered nothing or a non-numeric 
Sorry now exiting...counter stays at 100
[root@localhost ~]# ./ifcounter.sh 
Do you wish to change the counter value currently set at 100
       [y..n] :y
Enter a sensible value 
250
Counter now set to 350


   tty当前设置被保存,以便隐藏passwd域中字符,然后重新保存stty设置。
   如果用户ID和密码正确(密码是easypasswd),明亮I N VA L I D U S E R和I N VA L I D PA S S W D设置为n o表示有效用户或密码,然后执行测试,如果两个变量其中之一为y e s,缺省情况下,脚本退出用户。
   键入有效的I D和密码,用户将允许进入。这是一种登录脚本的基本框架。下面的例子中有效用户I D为root。
   提示:在实际中千万不要这么做,任何人都知道用root身份进行测试是很危险的。
#!/bin/sh
# ifpass.sh
# set the variables to false
  INVALID_USER=yes
  INVALID_PASSWD=yes
# save the current stty settings
  SAVEDSTTY=`stty -g`
  echo "You are logging into a sensitive area"
  echo -n "Enter your ID name :"
  read NAME
# hide the characters typed in
  stty -echo
  echo "Enter your password :"
  read PASSWORD
# back on again
  stty $SAVEDSTTY
  if [ "$NAME" == "root" ]; then
                # if a valid then set variable
          INVALID_USER=no
  fi      
          if [ "$PASSWORD" == "easypasswd" ]; then
# if valid password then set variable
          INVALID_PASSWD=no
 fi      
  if [ "$INVALID_USER" == "yes" -o "$INVALID_PASSWD" == "yes" ]; then
          echo "`basename $0 :` Sorry wrong password or userid"
         exit 1
 fi      
# if we get here then their ID and password are OK.
 echo "correct user id an password given"
  下面是对应两种不同情况的输出结果。
[root@localhost ~]# ./ifpass.sh 
You are logging into a sensitive area
Enter your ID name :root
Enter your password :
correct user id an password given
[root@localhost ~]# ./ifpass.sh 
You are logging into a sensitive area
Enter your ID name :root
Enter your password :
ifpass.sh Sorry wrong password or userid

elif用法
if then else语句的e l i f部分用于测试两个以上的条件.
使用elif进行多条件检测
使用一个简单的例子,测试输入脚本的用户名。脚本首先测试是否输入一个名字,如果没有,则什么也不做。如果输入了,则用e l i f测试是否匹配r o o t、l o u i s e或d a v e,如果不匹配其中任何一个,则打印该名字,通知用户不是r o o t、l o u i s e或d a v e。
#!/bin/sh
# ifelif.sh
echo  -n "enter your login name :"
read NAME
# no name entered do not carry on
if [ -z $NAME ] || [ "$NAME" == "" ]; then
       echo "You did not enter a name"
elif    
       # is the name root
       [ "$NAME" == "root" ]; then
       echo "Hello root"
  elif  
# or is it louise
       [ $NAME == "louise" ]; then
        echo "Hello louise"
  elif    
       # or is it dave
       [ "$NAME" == "dave" ]; then
       echo "Hello dave"
else    
# no it's somebody else
echo "You are not root or louise or dave but hi $NAME"
fi
  运行上述脚本,给出不同信息,得结果如下:                               
[root@localhost ~]# chmod +x ifelif.sh 
[root@localhost ~]# ./ifelif.sh 
enter your login name :dave
Hello dave
[root@localhost ~]# ./ifelif.sh 
enter your login name :
You did not enter a name
[root@localhost ~]# ./ifelif.sh 
enter your login name :Peter
You are not root or louise or dave but hi Peter
多文件位置检测
假定要定位一个用户登录文件,已知此文件在/ u s r / o p t s / a u d i t / l o g s或/ u s r / l o c a l / a u d i t / l o g s中,具体由其安装人决定。在定位此文件前,首先确保文件可读,此即脚本测试部分。如果未找件或文件不可读,则返回错误信息。脚本如下:
#!/bin/sh
# ifcataudit.sh
 # locations of the log file
    LOCAT_1=/usr/opts/audit/logs/audit.log
    LOCAT_2=/usr/local/audit/audit.logs if [ -r $LOCAT_1 ]; then
# if it is in this directory and is readable then cat is
        echo "Using LOCAT_1"
        cat $LOCAT_1
elif    
# else it then must be in this direcotory, and is it readable
     [ -r $LOCAT_2 ]
then    
     echo "Using LOCAT_2"
       cat $LOCAT_2
     else    
# not in any of the directories...
echo "`basename $0`: Sorry the audit file is not readable or cannot be
    localted." >&2
exit 1
fi      
运行上面脚本,如果文件在上述两个目录之一中并且可读,将可以找到它。如果不是,
返回错误并退出,下面结果失败,因为假想的文件并不存在
[root@localhost ~]# ./ifcataudit.sh 
ifcataudit.sh: Sorry the audit file is not readable or cannot be
localted.
       提示键入y或n
c a s e的一个有效用法是提示用户响应以决定是否继续进程。这里提示输入y以继续处理,n退出。如果用户输入Y、y或y e s,处理继续执行c a s e语句后面部分。如果用户输入N、n或n o或其他响应,用户退出脚本。
#!/bin/sh
# caseans.sh
echo -n "Do you wish to proceed [y..n] :"
      read ANS
case $ANS in
 y|Y|yes|Yes) echo "yes is selected"
 n|N) echo "no is selected"
       exit 0  # no error so only use exit 0 to terminate
       *) echo "`basename $0` : Unknow response" >&2
exit 1
esac    
             # if we are here then a y|Y|yes|Yes was selected only.
   
  运行脚本,输入无效响应,得结果:
      [root@localhost ~]# ./caseans.sh 
  Do you wish to proceed [y..n] :df
  caseans.sh : Unknow response
  给出有效响应:
  [root@localhost ~]# ./caseans.sh 
      Do you wish to proceed [y..n] :y
     yes is selected
    case与命令参数传递
    可以使用c a s e控制到脚本的参数传递。
  下面脚本中,测试特定变量$ #,它包含传递的参数个数,如果不等于1,退出并显示可用
   信息。
    然后c a s e语句捕获下列参数: p a s s w d、s t a r t、s t o p或h e l p,相对于每一种匹配模式执行进一步处理脚本。如果均不匹配,显示可用信息到标准错误输出。
#!/bin/sh
# caseparam.sh
  if [ $# != 1 ]; then
         echo "Usage:`basename $0`[start|stop|help]" >&2
 exit 1
  fi      
# assign the parameter to the variable OPT
  OPT=$1
  case $OPT in
   start) echo "starting.. `basename $0`"
# code here to start a process
   stop) echo "stopping.. `basename $0`"
# code here to stop a process
   help) 
# code here to display a help page
        *) echo "Usage:`basename $0`[start|stop|help]"
esac    
   运行脚本,输入无效参数。
[root@localhost ~]# ./caseparam.sh what
Usage:caseparam.sh[start|stop|help]
   入有效参数,结果为:
[root@localhost ~]# ./caseparam.sh stop
stopping.. caseparam.sh

捕获输入并执行空命令
不一定要在匹配模式后加入命令,如果你原本不想做什么,只是在进一步处理前过滤出意外响应,这样做是一种好办法。
如果要运行对应于一个会计部门的帐目报表,必须首先在决定运行报表的类型前确认用户输入一个有效的部门号,匹配所有可能值,其他值无效。用c a s e可以很容易实现上述功能。
下面的脚本中如果用户输入部门号不是2 3 4、4 5 3、6 5 5或4 5 4,用户退出并返回可用信息。一旦响应了用户的有效部门号,脚本应用同样的技术取得报表类型,在c a s e语句末尾显示有效的部门号和报表类型。脚本如下:

#!/bin/sh
# casevaild.sh
echo -n "Enter the account dept No: :"
read ACC
case $ACC in
       234);;
453);;
655);;
454);;
     *) echo "`basename $0`: Unknown dept No:" >&2
        echo "try ... 234,453,655,454"
exit 1
esac
# if we are here, then we have validated the dept no
echo "1 . post"
echo "2 . prior"
echo -n "Enter the type of report:"
read ACC_TYPE
case $ACC_TYPE in
      1) TYPE=post
      2) TYPE=prior;;
      *) echo "`basename $0`: Unknown account type." >&2
exit 1
esac
# if we are here the we are validated!
echo "now running report for dept $ACC for the type $TYPE"
# run the command reprot...
    下面是该脚本不同运行输入情况的显示结果。
 输入有效部门号:
 [root@localhost ~]# ./casevalid.sh 
 Enter the account dept No: :234
 1 . post
 2 . prior
 Enter the type of report:2
 now running report for dept 234 for the type prior
  输入无效部门号:
  [root@localhost ~]# ./casevalid.sh 
 Enter the account dept No: :432
 casevalid.sh: Unknown dept No:
 try ... 234,453,655,454
输入无效的报表类型:
  [root@localhost ~]# ./casevalid.sh 
 Enter the account dept No: :655
 1 . post
 2 . prior
 Enter the type of report:4
 casevalid.sh: Unknown account type.
    缺省变量值
  如果在读变量时输入回车键,不一定总是退出脚本。可以先测试是否已设置了变量,如果未设置,可以设置该值。下面的脚本中,要求用户输入运行报表日期。如果用户输入回车键,则使用缺省日期星期六,设置为变量w h e n的取值。 如果用户输入另外一天,这一天对于c a s e语句是运行的有效日期,即星期六、星期四、星期一。注意,这里结合使用了日期缩写作为捕获的可能有效日期。
 脚本如下:
#!/bin/sh
# caserep.sh
 echo "      Weekly Report"
 echo -n "What day do you want to run report [Saturday] :"
# if just a return is hit then except default which is Saturday
 read WHEN
 echo "validating .. ${WHEN:="Saturday"}"
 case $WHEN in
         Monday|MONDAY|mon)
  Sunday|SUNDAY|sun)
  Saturday|SATURDAY|sat)
    *)echo "Are you nuts! this report can only be run on " >&2
echo " on a Saturday, Sunday or Monday" >&2
exit 1
esac    
echo "Report to run on $WHEN"
    # command here to submitted actual report run
     对于正确输入:
  [root@localhost ~]# ./caserep.sh 
                  Weekly Report
 What day do you want to run report [Saturday] :
 validating .. Saturday
 Report to run on Saturday


     对于错误的输入:
[root@localhost ~]# ./caserep.sh 
                Weekly Report
What day do you want to run report [Saturday] :Tuesday
validating .. Tuesday
Are you nuts! this report can only be run on 
 on a Saturday, Sunday or Monday
可以推断出c a s e语句有时与if then else 语句功能相同,在某些条件下,这种假定是正确的。  


   f o r循环一般格式为:
  for 变量名i n列表
  d o
  命令1
  命令2⋯
  d o n e
  
  当变量值在列表里, f o r循环即执行一次所有命令,使用变量名访问列表中取值。命令可为任何有效的s h e l l命令和语句。变量名为任何单词。I n列表用法是可选的,如果不用它, f o r 循环使用命令行的位置参数。i n列表可以包含替换、字符串和文件名,下面看一些例子。
      还有一种常见的for循环的格式是:
    for ((初值; 循环条件; 执行步长))
  do
              执行的程序段
  done
  也就是括号中的内容是平常我们熟悉的C语言的风格。

简单的for循环
此例仅显示列表1 2 3 4 5,用变量名访问列表。
#!/bin/sh
# for_i.sh
for loop in 1 2 3 4 5
do
      echo $loop
done


    运行上述脚本,输出:

[root@localhost ~]# ./for_i.sh 
1
2
3
4
5

上面中的1 2 3 4 5序列也可以用下面的方式生成。seq 5或者echo {1..5}的方式。
   打印字符串列表
下面f o r循环中,列表包含字符串“ orange red blue grey”,命令为e c h o,变量名为l o o p, e c h o命令使用$ l o o p反馈出列表中所有取值,直至列表为空。
#!/bin/sh
# forlist.sh
for loop in "orange red blue grey"
do
     echo "$loop"
done
   运行脚本输出内容是:
orange red blue grey
   也可以在循环体中结合使用变量名和字符串。
This is the fruit $loop
   其输出结果是:
This is the fruit orange red blue grey
   对for循环使用ls命令
   这个循环执行l s命令,打印当前目录下所有文件。
#!/bin/sh
# forls.sh
for loop in `ls `
do
       echo $loop
done
    对for循环使用参数
 在f o r循环中省去i n列表选项时,它将接受命令行位置参数作为参数。实际上即指明:
for params in"$@"

for params in"$*"
下面的例子不使用i n列表选项, f o r循环查看特定参数$ @或$ *,以从命令行中取得参数。
#!/bin/sh
# forparam2
for params
do
      echo "You supplied $params as a command line option"
done
 下面的脚本包含i n"$ @",结果与上面的脚本相同。
  #!/bin/sh
  # forparam3
  for params in "$@"
  do
 echo "You supplied $params as a command line option"
  done

对上述脚本采取进一步动作。如果要查看一系列文件,可在f o r循环里使用f i n d命令,利用命令行参数,传递所有要查阅的文件。
#!/bin/sh
# forfind.sh
for loop
do
       find / -name $loop -print
done
脚本执行时,从命令行参数中取值并使用f i n d命令,这些取值形成- n a m e选项的参数值。


      使用for循环连接服务器
因为f o r循环可以处理列表中的取值,现设变量为网络服务器名称,并使用f o r循环连接每一服务器。
#!/bin/sh
# forping.sh
HOSTS="itserv dnssevr acctsmain ladpd ladwareA"
for loop in $HOSTS
do
  ping -c 2 $loop
done
使用for循环备份文件
可以用f o r循环备份所有文件,只需将变量作为c p命令的目标参数。这里有一变量. b a k,当在循环中使用c p命令时,它作为此命令目标文件名。列表命令为l s。
#!/bin/sh
# forbak.sh
suffix=".bak"
for loop in `ls `
do
       cp $loop $loop$suffix
done
多文件转换
匹配所有以L P S O开头文件并将其转换为大写。这里使用了l s和c a t命令。l s用于查询出相关文件, c a t用于将之管道输出至t r命令。目标文件扩展名为.U C,注意在f o r循环中使用l s命令时反引号的用法。
#!/bin/sh
# forUC.sh
for files in `ls LPSO*`
do
      cat $files | tr "[a-z]" "[A-Z]" > $file.UC
done
      多sed删除操作
下面的例子中, s e d用于删除所有空文件,并将输出导至以. H O L D . m v为扩展名的新文件中,m v将这些文件移至初始文件中。
#!/bin/sh
# forsed.sh
for files in `ls LPSO*`
do
       sed -e "/^$/d" $files >$files.HOLD
       mv $files.HOLD $files
done
循环计数
前面讨论e x p r时指出,循环时如果要加入计数,使用此命令。下面使用l s在f o r循环中列出文件及其数目。
#!/bin/sh
# forcount.sh
counter=0
for files in *
do
       # increment
       counter=`expr $counter + 1`
done
echo "There are $counter files in `pwd` we need to process"
脚本的输出结果是:
There are 87 files in /root we need to process
使用w c命令可得相同结果。
[root@localhost ~]# ls | wc -l
87


       for循环和本地文档
在f o r循环体中可使用任意命令。下面的例子中,一个变量包含所有当前登录用户。使用w h o命令并结合a w k语言可实现此功能。然后f o r循环循环每一用户,给其发送一个邮件,邮件信息部分用一个本地文档完成。
#!/bin/sh
# forbak.sh
suffix=".bak"
for loop in `ls `
do
    cp $loop $loop$suffix
done
多文件转换
匹配所有以L P S O开头文件并将其转换为大写。这里使用了l s和c a t命令。l s用于查询出相关文件, c a t用于将之管道输出至t r命令。目标文件扩展名为.U C,注意在f o r循环中使用l s命令时反引号的用法。
#!/bin/sh
# forUC.sh
for files in `ls LPSO*`
do
                 cat $files | tr "[a-z]" "[A-Z]" > $file.UC
done
多sed删除操作
下面的例子中, s e d用于删除所有空文件,并将输出导至以. H O L D . m v为扩展名的新文件中,m v将这些文件移至初始文件中。
#!/bin/sh
# forsed.sh
for files in `ls LPSO*`
do
        sed -e "/^$/d" $files >$files.HOLD
        mv $files.HOLD $files
done
循环计数
前面讨论e x p r时指出,循环时如果要加入计数,使用此命令。下面使用l s在f o r循环中列出文件及其数目。
#!/bin/sh
# forcount.sh
counter=0
for files in *
do
      # increment
       counter=`expr $counter + 1`
done
echo "There are $counter files in `pwd` we need to process"

脚本的输出结果是:
There are 87 files in /root we need to process
使用w c命令可得相同结果。
[root@localhost ~]# ls | wc -l
87


for循环和本地文档
在f o r循环体中可使用任意命令。下面的例子中,一个变量包含所有当前登录用户。使用w h o命令并结合a w k语言可实现此功能。然后f o r循环循环每一用户,给其发送一个邮件,邮件信息部分用一个本地文档完成。


#!/bin/sh
# formailit.sh
WHOS_ON=`who -u | awk '{print $1}'`
for user in $WHOS_ON
do
       mail $user << MAYDAY
       Dear Colleagues,
       It's my birthday today, see you down the
       club at 17:30 for a drink.
       See ya.
       $LOGNAME
       MAYDAY
       Done
    这个例子无法实现,可能原书上有错误,或者翻译有错误。
for循环嵌入
嵌入循环可以将一个f o r循环嵌在另一个f o r循环内:
for 变量名1 in列表1
d o
for 变量名2 in 列表2
d o
命令1
done
done
下面脚本即为嵌入f o r循环,这里有两个列表A P P S和S C R I P T S。第一个包含服务器上应用的路径,第二个为运行在每个应用上的管理脚本。对列表A P P S上的每一个应用,列表S C R I P T S里的脚本将被运行,脚本实际上为后台运行。脚本使用t e e命令在登录文件上放一条 目,因此输出到屏幕的同时也输出到一个文件。查看输出结果就可以看出嵌入f o r循环怎样使用列表S C R I P T S以执行列表A P P S上的处理。
#!/bin/sh
# audit_run.sh
APPS="/apps/accts /apps/claims /apps/stock /apps/serv"
SCRIPTS="audit.check report.run cleanup"
LOGFILE=audit.log
MY_DATE=`date +%H:%M" on "%d/%m%Y`
# outer loop
for loop in $APPS
do      
     # inner loop
       for loop2 in $SCRIPTS
       do      
            echo "system $loop now running $loop2 at $MY_DATE" | tee -a\
            $LOGFILE $loop $loop2 &
       done    
done    
程序的运行结果如下:
tee: /apps/accts: No such file or directory
tee: /apps/accts: No such file or directory
system /apps/accts now running report.run at 20:18 on 21/112010
tee: /apps/accts: No such file or directory
system /apps/accts now running cleanup at 20:18 on 21/112010
tee: /apps/claims: No such file or directory
system /apps/claims now running audit.check at 20:18 on 21/112010
tee: /apps/claims: No such file or directory
system /apps/claims now running report.run at 20:18 on 21/112010
tee: /apps/claims: No such file or directory
system /apps/claims now running cleanup at 20:18 on 21/112010
tee: /apps/stock: No such file or directory
system /apps/stock now running audit.check at 20:18 on 21/112010
tee: /apps/stock: No such file or directory
system /apps/stock now running report.run at 20:18 on 21/112010
tee: /apps/stock: No such file or directory
system /apps/stock now running cleanup at 20:18 on 21/112010
tee: /apps/serv: No such file or directory
system /apps/serv now running audit.check at 20:18 on 21/112010
[root@localhost ~]# system /apps/accts now running audit.check at 20:18 on 21/11201
tee: /apps/serv: No such file or directory
system /apps/serv now running report.run at 20:18 on 21/112010
tee: /apps/serv: No such file or directory
system /apps/serv now running cleanup at 20:18 on 21/112010
until循环
u n t i l循环执行一系列命令直至条件为真时停止。u n t i l循环与w h i l e循环在处理方式上刚好相反。一般w h i l e循环优于u n t i l循环,但在某些时候—也只是极少数情况下, u n t i l循环更加有用。
u n t i l循环格式为:
until 条件
命令1
d o n e
条件可为任意测试条件,测试发生在循环末尾,因此循环至少执行一次—请注意这一点。
下面是一些实例。
简单的until循环
   这段脚本不断的搜寻w h o命令中用户r o o t,变量I S - R O O T保存g r e p命令结果。如果找到了r o o t,循环结束,并向用户s i m o n发送邮件,通知他用户r o o t已经登录,注意这里s l e e p命令用法,它经常用于u n t i l循环中,因为必须让循环体内命令睡眠几秒钟再执行,否则会消耗大量系统资源。
#!/bin/sh
# until_who.sh
      IS_ROOT=`who | grep root`
until [ "$IS_ROOT" ]
do
       sleep 5
done    
echo "Watch it. roots in " | mail simon
     监视文件
下面例子中, u n t i l循环不断挂起做睡眠,直至文件/ t m p / m o n i t o r. l c k被删除。文件删除后,脚本进入正常处理过程。
#!/bin/sh
# until_lck.sh
LOCK_FILE=/tmp/process.LCK
until [ ! -f $LOCK_FILE ]
       do
       sleep 1
done    
echo "file deleted "
# normal processing now, file is present
     上述例子是使脚本与其他处理过程协调工作的一种方法。还有另外一种方法使脚本相通信。假定有另一段脚本p r o c e s s . m a i n用于搜集本地网络所有机器的信息并将之放入一个报表文件。
当脚本p r o c e s s . m a i n运行时,创建了一个L C K文件(锁文件),上面脚本必须接收 p r o c e s s . m a i n搜集的信息,但是如果p r o c e s s仍然在修改报表文件时试图处理该文件就不太好了。
为克服这些问题,脚本p r o c e s s . m a i n创建了一个L C K文件,当它完成时,就删除此文件。
上述脚本将挂起,等待L C K文件被删除,一旦L C K文件删除,上述脚本即可处理报表文件。
 


监视磁盘空间
u n t i l循环做监视条件也很有用。假定要监视文件系统容量,当它达到一定水平时通知超级用户。
下面的脚本监视文件系统/ l o g s,不断从变量$L O O K_O U T中抽取信息, $ L O O K _ O U T包含使用a w k和g r e p得到的/ l o g s容量。
如果容量达到9 0 %,触发命令部分,向超级用户发送邮件,脚本退出。必须退出,如果不退出,条件保持为真(例如,容量总是保持在9 0 %以上),将会不断的向超级用户发送邮件。
#!/bin/sh
# until_mon.sh
# get present column and strip off header row from df
LOOK_OUT=`df | grep /logs | awk '{print $5}' | sed 's/%//g'`
echo $LOOK_OUT
until [ "$LOOK_OUT -gt "90" ]
do
        echo "Filesystem..logs is nearly full" | mail root
exit 0
done


      while循环
w h i l e循环用于不断执行一系列命令,也用于从输入文件中读取数据,其格式为:
while 命令
d o
命令1
命令2
     d o n e
虽然通常只使用一个命令,但在w h i l e和d o之间可以放几个命令。命令通常用作测试条件。只有当命令的退出状态为0时,d o和d o n e之间命令才被执行,如果退出状态不是0,则循环终止。命令执行完毕,控制返回循环顶部,从头开始直至测试条件为假。
 
简单的while循环
以下是一个基本的w h i l e循环,测试条件是:如果C O U N T E R小于5,那么条件返回真。
C O U N T E R从0开始,每次循环处理时, C O U N T E R加1。
#!/bin/sh
# whilecount.sh
COUNTER=0
# does the counter = 5 ?
while [ $COUNTER -lt 5 ]
do
        # add ono to the counter
        COUNTER=`expr $COUNTER + 1`
        echo $COUNTER
     done 
 运行上述脚本,返回数字1到5,然后终止。
   
   使用while循环读键盘输入
 w h i l e循环可用于读取键盘信息。下面的例子中,输入信息被设置为变量F I L M,按< C t r l -D >结束循环。
#!/bin/sh
# whileread.sh
  echo "type <CTRL-D> to terminate"
  echo -n "Enter your most liked file :"
  while read FILE
  do
          echo "Yeah, great film the $FILE"
  done 
程序的运行结果如下:
type <CTRL-D> to terminate
Enter your most liked file :Sound of Music
Yeah, great film the Sound of Music
<CTRL-D>


用while循环从文件中读取数据
w h i l e循环最常用于从一个文件中读取数据,因此编写脚本可以处理这样的信息。
假定要从下面包含雇员名字、从属部门及其I D号的一个文件中读取信息。
names.txt
Louise Conrad:Accounts:ACC8987
Peter James:Payroll:PR489
Fred Terms:Customer:CUS012
James Lenod:Accounts:ACC887
Frank Pavely:Payroll:PR489


可以用一个变量保存每行数据,当不再有读取数据时条件为真。w h i l e循环使用输入重定向以保证从文件中读取数据。注意整行数据被设置为单变量$ L I N E .。
#!/bin/sh
# whileread.sh
while read LINE
do
       echo $LINE
done < names.txt


程序的运行结果如下:
[root@localhost ~]# ./whileread.sh 
Louise Conrad:Accounts:ACC8987
Peter James:Payroll:PR489
Fred Terms:Customer:CUS012
James Lenod:Accounts:ACC887
Frank Pavely:Payroll:PR489
使用IFS读文件
输出时要去除冒号域分隔符,可使用变量I F S。在改变它之前保存I F S的当前设置。然后在脚本执行完后恢复此设置。使用I F S可以将域分隔符改为冒号而不是空格或t a b键。这里有3个域需要加域分隔,即N A M E、D E P T和I D。为使输出看起来更清晰,对e c h o命令使用t a b键将域分隔得更开一些,脚本如下:
#!/bin/sh
# whilereadifs
# save the setting of IFS
SAVEDIFS=$IFS
# assign new separator to IFS
IFS=:
while read NAME DEPT ID
do
       echo -e "$NAME\t $DEPT\t $ID"
      done < names.txt
# restore the settings of IFS
IFS=$SAVEDIFS


脚本运行后的输出结果如下,结果清晰多了。
Louise Conrad    Accounts        ACC8987
Peter James      Payroll         PR489
Fred Terms       Customer        CUS012
James Lenod      Accounts        ACC887
Frank Pavely     Payroll         PR489
带有测试条件的文件处理
大部分w h i l e循环里都带有一些测试语句,以决定下一步的动作。这里从人员文件中读取数据,打印所有细节到一个保留文件中,直至发现James Lenod,脚本退出。测试前反馈的信息要确保“ James Lenod”加入保留文件中。
注意,所有变量在脚本顶端被设置完毕。这样当不得不对变量进行改动时可以节省时间和输入。所有编辑都放在脚本顶端,而不是混于整个脚本间。
#!/bin/sh
# whileread_file.sh
# initialise variables
SAVEDIFS=$IFS
IFS=:
HOLD_FILE=hold_file
NAME_MATCH="James Lenod"
INPUT_FILE=names.txt
# create a new HOLD_FILE each time, in case script is continuously run
>$HOLD_FILE
while read NAME DEPT ID
do
      # echo all information into holdfile with redirection
        echo $NAME $DEPT $ID >> $HOLD_FILE
# is it a match ???
 if [ "$NAME" == "$NAME_MATCH" ]; then
# yes then nice exit
 echo "all entries up to and including $NAME_MATCH are in $HOLD_FILE"
       exit 0
fi      
done < $INPUT_FILE
# restore IFS
IFS=$SAVEDIFS


还可以采取进一步动作,列出多少个雇员属于同一部门。这里保持同样的读方式。假定每个域都有一个变量名,然后在c a s e语句里用e x p r增加每行匹配脚本。任何发现的未知部门知识反馈到标准错误中,如果一个无效部门出现,没有必要退出。


#!/bin/sh
# whileread_cond.sh
# initialise variables
ACC_LOOP=0; CUS_LOOP=0; PAY_LOOP=0;
SAVEDIFS=$IFS
IFS=:
while read NAME DEPT ID
do
       # increment counter for each matched dept.
       case $DEPT in
              Accounts) ACC_LOOP=`expr $ACC_LOOP + 1`
              ACC="Accounts"
             Customer) CUS_LOOP=`expr $CUS_LOOP + 1`
             CUS="Customer"
             Payroll) PAY_LOOP=`expr $PAY_LOOP + 1`
             PAY="Payroll"
         *) echo "`basename $0`: Unknown department $DEPT" >&2
esac
done < names.txt
        IFS=$SAVEDIFS
echo "there are $ACC_LOOP employees assigned to $ACC dept"
      echo "there are $CUS_LOOP employees assigned to $CUS dept"
echo "there are $PAY_LOOP employees assigned to $PAY dept"


程序运行的结果如下所示:
[root@localhost ~]# ./whileread_cond.sh   
there are 2 employees assigned to Accounts dept
there are 1 employees assigned to Customer dept
there are 2 employees assigned to Payroll dept
[root@localhost ~]# vim whileread_cond.sh 


扫描文件行来进行数目统计
一个常用的任务是读一个文件,统计包含某些数值列的数值总和。下面的文件包含有部门S TAT和G I F T所卖的商品数量。
[root@localhost ~]# cat total.txt 
STAT    3444
GIFT     233
GIFT     252
GIFT     932
STAT     212
STAT     923
GIFT     129
现在的任务是要统计部门G I F T所卖的各种商品数量。使用e x p r保存统计和,看下面的e x p r语句。变量L O O P和TO TA L首先在循环外初始化为0,循环开始后, I T E M S加入TO TA L,第一次循环只包含第一种商品,但随着过程继续, I T E M S逐渐加入TO TA L。
下面的e x p r语句不断增加计数。
LOOP=0
TOTAL=0
...
while ...
do
TOTAL=`expr $TOTAL + $ITEMS`
ITEMS=`expr $ITEMS + 1`
done

使用e x p r语句时容易犯的一个错误是开始忘记初始化变量。
LOOP=0
TOTAL=0
如果真的忘了初始化,屏幕上将布满e x p r错误。
如果愿意,可以在循环内初始化循环变量。
TOTAL=`expr ${TOAL:=0} + ${ITEMS}`
上面一行如果变量TO TA L未赋值,将其初始化为0。这是在e x p r里初始化变量的第一个例子。另外在循环外要打印出最后总数。
#!/bin/sh
# total.sh
# init variables
LOOP=0
TOTAL=0
COUNT=0
echo "Items Dept"
echo "____________"
while read DEPT ITEMS
do
       # keep a count on total records read
       COUNT=`expr $COUNT + 1`
       if [ "$DEPT" == "GIFT" ]; then
              # keep a running total
               TOTAL=`expr $TOTAL + $ITEMS`
  ITEMS=`expr $ITEMS + 1`
              echo -e "$ITEMS\t$DEPT"
       fi      
       # echo $DEPT $ITEMS
done < total.txt
echo "============"
echo $TOTAL
echo "There were $COUNT entries altogether in the file"


    脚本的运行结果如下所示:
Items Dept
____________
234     GIFT
253     GIFT
933     GIFT
130     GIFT
============
1546
There were 7 entries altogether in the file
    每次读一对记录
有时可能希望每次处理两个记录,也许可从记录中进行不同域的比较。每次读两个记录很容易,就是要在第一个w h i l e语句之后将第二个读语句放在其后。使用这项技术时,不要忘不断进行检查,因为它实际上读了大量的记录。
[root@localhost ~]# cat record.txt 
record 1
record 2
record 3
record 4
record 5
record 6
每次读两个记录,下面的例子对记录并不做实际测试。
脚本如下:
#!/bin/sh
# readpair.sh
# first record
while read rec1
do
       # second record
       read rec2
      # further processing/testing goes here to test or compare both records
echo "This is record on of a pair :$rec1"
echo "This is record on of a pair :$rec2"
echo "----------------------------"
done < record.txt
  首先来检查确实读了很多记录,可以使用w c命令:
 [root@localhost ~]# cat record.txt | wc -l
    6
    共有6个记录,观察其输出:
  This is record on of a pair :record 1
  This is record on of a pair :record 2
  ----------------------------
      This is record on of a pair :record 3
  This is record on of a pair :record 4
  ----------------------------
  This is record on of a pair :record 5
  This is record on of a pair :record 6
  ----------------------------
   
   忽略#字符
    读文本文件时,可能要忽略或丢弃遇到的注释行,下面是一个典型的例子。
      假定要使用一般的w h i l e循环读一个配置文件,可拣选每一行,大部分都是实际操作语句。有时必须忽略以一定字符开头的行,这时需要用c a s e语句,因为#是一个特殊字符,最好首先 用反斜线屏蔽其特殊意义,在#符号后放一个星号*,指定*后可包含任意字符。
  配置文件如下:
  [root@localhost ~]# cat config
# THIS IS THE SUB SYSTEM AUDIT CONFIG FILE
# DO NOT EDIT!!!!.IT WORKS
#
# type of admin access
  AUDITSCH=full
# lanuch place of sub-systems
  AUDITSUB=/usr/opt/audit/sub
# serial hash number of product
  HASHSER=12890AB3
# END OF CONFIG FILE!!!


   忽略#符号的实现脚本如下:
#!/bin/sh
     # ignore_hash.sh
IT_FILE=config
if [ -s $INPUT_FILE ]; then
       while read LINE
     do      
                          case $LINE in
                   \#*);;
      # ignore any hash signs
                *) echo $LINE
    esac    
 done < $INPUT_FILE
 else    
        echo "`basename $0` : Sorry $INPUT_FILE does not exist or is empty"
exit 1
fi      
   程序的输出结果如下:
  AUDITSCH=full
 AUDITSUB=/usr/opt/audit/sub
 HASHSER=12890AB3

   处理格式化报表
读报表文件时,一个常用任务是将不想要的行剔除。以下是库存商品水平列表,我们感兴趣的是那些包含商品记录当前水平的列。
        [root@localhost ~]# cat order 
################ RE-ORDER REPORT  ##############
 ITEM           ORDERLEVEL              LEVEL
####################################################
 Pens            14                              12
   Pencils         15                              15
 Pads             7                               3
 Disks            3                               2
 Sharpeners       5                               1
####################################################
 我们的任务是读取其中取值,决定哪些商品应重排。如果重排,重排水平应为现在商品的两倍。输出应打印需要重排的每种商品数量及重排总数。
 我们已经知道可以忽略以某些字符开始的行,因此这里没有问题。首先读文件,忽略所有注释行和以‘ I T E M’开始的标注行。读取文件至一临时工作文件中,为确保不存在空行,用s e d删除空行,需要真正做的是过滤文本文件。脚本如下:
#!/bin/sh
# whileorder.sh
  INPUT_FILE=order
  HOLD=order.tmp
                   if [ -s $INPUT_FILE ]; then
         # zero the output file, we do not want to append!
         >$HOLD
while read LINE
do      
        case $LINE in
       \#*|ITEM*);; # ignore any # or the line with ITEM
                    *)
                   # redirect the output to a temp file
                   echo $LINE >>$HOLD
esac    
done <$INPUT_FILE
    # use to sed to delete any empty lines, if any
             sed -e '/^$/d' order.tmp >order.$
      mv order.$ order.tmp
 else    
      echo "`basename $0` : Sorry $INPUT_FILE does not exist or empty"
fi      
执行脚本后,输出结果为。
[root@localhost ~]# cat order.tmp 
Pens 14 12
Pencils 15 15
Pads 7 3
   Disks 3 2
  Sharpeners 5 1
现在要在另一个w h i l e循环中读取临时工作文件,使用e x p r对数字进行数值运算。
#!/bin/sh
# whileorder2
# init the variables
HOLD=order.tmp
RE_ORDER=0
ORDERS=0
STATIONERY_TOT=0
if [ -s $HOLD ]; then
       echo "========= STOCK RE_ORDER REPORT ========="
        while read ITEM RECORD LEVEL
        do
          # are we below the reorder level for this item ??
        if [ "$LEVEL" -lt "$RECORD" ];then
          # yes, do the new order amount
        NEW_ORDER=`expr $RECORD + $RECORD`
          # running total of orders
        ORDERS=`expr $ORDERS + 1`
         # running total of stock levels
       STATIONERY_TOT=`expr $STATIONERY_TOT + $LEVEL`
          echo "$ITEM need reordering to the amount $NEW_ORDER"
 fi
done <$HOLD
echo "$ORDERS new items need to be ordered"
echo "Our reorder total is $STATIONERY_TOT"
else
       echo "`basename $0` : Sorry $HOLD does not exists or is empty"
fi


以下为依据报表文件运行所得输出结果。
========= STOCK RE_ORDER REPORT =========
Pens need reordering to the amount 28
Pads need reordering to the amount 14
Disks need reordering to the amount 6
Sharpeners need reordering to the amount 10
4 new items need to be ordered
Our reorder total is 18

将两段脚本结合在一起很容易。实际上这本来是一个脚本,为讲解方便,才将其分成两个。

while循环和文件描述符
第5章查看文件描述符时,提到有必要用w h i l e循环将数据读入一个文件。使用文件描述符3和4,下面的脚本进行文件m y f i l e . t x t到m y f i l e . b a k的备份。注意,脚本开始测试文件是否存在,
如果不存在或没有数据,脚本立即终止。还有w h i l e循环用到了空命令(:),这是一个死循环, 因为n u l l永远返回真。尝试读至文件结尾将返回错误,那时脚本也终止执行。

#!/bin/sh
# copyfile.sh
FILENAME=myfile.txt
FILENAME_BAK=myfile.bak
if [ -s $FILENAME ]; then
      # open FILENAME for writing
      # open FILENAME for reading
          exec 4>$FILENAME_BAK
          exec 3<$FILENAME
      # loop forever until no more data and thus an error so we
      # are at end of file
        while:
       do      
              read LINE <&3
               if [ "$?" -ne 0 ]; then
# errors then close up
    exec 3<&-
    exec 4<&-
 fi      
# write to FILENAME_BAK
echo $LINE>&4
done    
else    
echo "`basename $0` : Sorry, $FILENAME is not present or is empty" >&2
fi      




    使用break和continue控制循环
有时需要基于某些准则退出循环或跳过循环步。s h e l l提供两个命令实现此功能。


break
b r e a k命令允许跳出循环。b r e a k通常在进行一些处理后退出循环或c a s e语句。如果是在
个嵌入循环里,可以指定跳出的循环个数。例如如果在两层循环内,用break 2刚好跳出整个循环。


跳出case语句
下面的例子中,脚本进入死循环直至用户输入数字大于5。要跳出这个循环,返回到s h e l l提示符下, b r e a k使用脚本如下:


#!/bin/sh
# breakout.sh
# while : means loop forever
while :
do
       echo -n "Enter any number [1..5] :"
       read ANS
       case $ANS in
               1|2|3|4|5) echo "great you entered a number 1 and 5"
               ;;
                     *) echo "Wrong number ... bye"
               break
               ;;
 esac    
 done 
continue
c o n t i n u e命令类似于b r e a k命令,只有一点重要差别,它不会跳出循环,只是跳过这个循
环步。


浏览文件行
下面是一个前面用过的人人文件列表,但是现在加入了一些头信息。
[root@localhost ~]# cat name2.txt 
-------LISTING OF PERSONNEL FILE-------
---- TAKEN AS AT 06/1999 -----
Louise Conrad:Accounts:ACC8987
Peter James:Payroll:PR489
Fred Terms:Customer:CUS012
James Lenod:Accounts:ACC887
Frank Pavely:Payroll:PR489
假定现在需要处理此文件,看过文件之后知道头两行并不包含个人信息,因此需要跳过这两行。也不需要处理雇员Peter James,这个人已经离开公司,但没有从人员文件中删除。对于头信息。只需简单计算所读行数,当行数大于2时开始处理,如果处理人员名字为Peter James,也将跳过此记录。脚本如下:
#!/bin/sh
# whilecontinue.sh
SAVEDIFS=$IFS
IFS=:
INPUT_FILE=name2.txt
NAME_HOLD="Peter James"
LINE_NO=0
if [ -s $INPUT_FILE ]; then
while read NAME DEPT ID
    do      
         LINE_NO=`expr $LINE_NO + 1`
         if [ "$LINE_NO" -le 2 ]; then
             # skip if the count is less than 2
         continue
         fi      
         if [ "$NAME" == "$NAME_HOLD" ]; then
             # skip if the name in NAME_HOLD is Peter James
            continue
        else    
echo "Now processing ... $NAME $DEPT $ID"
           # all the processing goes here
fi      
  done < $INPUT_FILE
  IFS=$SAVEDIFS
else    
   echo "`basename $0` : Sorry file not found or there is no data in the
    file" >&2
exit 1
fi      
运行上面的脚本,输出如下的内容
Now processing ... Louise Conrad Accounts ACC8987
Now processing ... Fred Terms Customer CUS012
Now processing ... James Lenod Accounts ACC887
Now processing ... Frank Pavely Payroll PR489




菜单
创建菜单时,在w h i l e循环里n u l l空命令很合适。h i l e加空命令n u l l意即无限循环,这正是一个菜单所具有的特性。当然除非用户选择退出或是一个有效选项。创建菜单只需用w h i l e循环和c a s e语句捕获用户输入的所有模式。如果输入无效,则报警,反馈错误信息,然后继续执行循环直到用户完成处理过程,选择退出选项。
菜单界面应是友好的,不应该让用户去猜做什么,主屏幕也应该带有主机名和日期,并伴随有运行此菜单的用户名。由于测试原因,所有选项使用的是系统命令。
下面是即将显示的菜单。
首先,使用命令替换设置日期,主机名和用户。日期格式为/ D D / M M / Y Y Y Y,参数格式为:
$ date +%d/%m/%y
3 2 / 0 5 / 1 9 9 9
对于主机名,使用h o s t n a m e - s选项只抽取主机名部分。主机名有时也包括了完全确认的域
名。当然如果要在屏幕上显示这些,那就更好了。
可以给变量一个更有意义的名字:
MYDATE=`date +%d/%m/%Y`
THIS_HOST=`hostname -s`
USER=`whoami`


对于w h i l e循环,只需将空命令直接放在w h i l e后,即为无限循环,格式为:
while :
d o
命令
d o n e
要注意实际屏幕显示,不要浪费时间使用大量的e c h o语句或不断地调整它们。这里使用
本地文档,在分界符后面接受输入,直至分界符被再次定位。格式为:
command << WORD
any input
WORD
此技术用于菜单屏幕,也将用于帮助屏幕。帮助屏幕不像这样复杂。
用c a s e语句控制用户选择。菜单选择有:
                       1 : List files in current directory
   2 : Use the vi editor
              3 : See who is on the system
              H : Help screen
                    Q : Exit Menu
c a s e语句应控制所有这些模式,不要忘了将大写与小写模式并列在一起,因为有时用户会关闭或打开CAPS LOCK键。因为菜单脚本不断循环,所以应该允许用户退出,如果用户选择Q或q键,脚本应退出,此时脚本带有0值。
如果用户选择无效,应发出警报并带有警告信息。虽然本章开始说过从现在开始一直使用L I N U X或BSD echo语句版本,这里必须使用系统V版本发出警报:
echo "\007 the bell ring"
用一个简单的e c h o和读语句锁屏直到用户点击回车键,这样任何信息或命令输出将可视。
也需要清屏,为此可使用t p u t命令(后面讨论t p u t),如果不这样做,使用c l e a r命令也可以。
              到此所有功能已经具备了,脚本如下:
#!/bin/sh
# menu.sh
# set the date, user and hostname up
MYDATE=`date +%d/%m/%Y`
THIS_HOST=`hostname -s`
USER=`whoami`
# loop forever !
while:
do
# clear the screen
input clear
# here documents starts here
cat <<MAYDAY
_________________________________________________________
User: $USER         Host:$THIS_HOST     Date:$MYDATE
_________________________________________________________
 1 : List files in current directory
 2 : Use the vi editor
 3 : See who is on the system
 H : Help screen
 Q : Exit Menu
        _________________________________________________________

       MAYDAY
 # here document finished
 echo -e -n "\tYour Choice [1,2,3,H,Q] >"
    read CHOICE
    case $CHOICE in
 1) ls
     ;;
 2) vi
 3) who
        H|h)
  # use a here document for the help screen
  cat <<MAYDAY
        This is the help screen, nothing here yet to help you!
MAYDAY
: ;;
     Q|q) exit 0
*) echo -e "\t\007unknown user response"
esac
      echo -e -n "\tHit the return key to continue"
     read DUMMY
done

你可能感兴趣的:(shell初级教程-备忘录2)