《Linux命令、编辑器与Shell编程》读书笔记12-函数和脚本

来源:互联网 发布:淘宝收件人名字大全 编辑:程序博客网 时间:2024/06/06 16:34

控制Shell脚本执行顺序

条件测试和信号捕获

条件测试主要包括用户输入、条件命令执行完后的返回状态、变量值、文件状态及系统发送的信号等。

退出状态

Linux系统中的命令、脚本或者程序,在执行完退出后,都存在退出状态(0:执行成功,1:执行失败),这个值保存在预定义变量$?中。编写脚本时也可以自定义退出状态,比如在每个可能的退出点增加一条设置退出状态的语句,就可以根据自定义的退出状态值得知脚本是运行到什么地方出的错。

【举例】

#检查向脚本传递的参数个数是否为0,如果为0则给出错误提示,不为0则输出参数

#!/bin/bash

function usage()

{

echo "Error:Must have a parameter."

   echo "$0 parameter..."

   exit 1

}

if [ $# = 0 ]

   then

   usage

fi

echo $1

echo $2

exit 2

*上文中,将异常退出的状态值置为1,也就是赋给了变量$#,将正常退出的状态值置为2.

* function usage()表示定义了一个名为usage的函数。Shell中的函数也没有数据类型,且只在调用它的脚本范围内生效。

测试状态

文件状态测试

使用方括号将要测试的目标括起来,系统会按照要求判断这个文件,并将结果(是:0,否:1)返回给预定义变量$?,之后使用echo $?就可以得知结果了;命令格式如下

[ -command  target_name  ]

使用”man  [“查看command可用的选项如下(仅翻译常用的):

-b FILE

      FILE exists and 是否是一个块设备文件

-c FILE

      FILE exists and is 字符设备文件

-d FILE

      是否是一个目录

-e FILE

      文件是否存在

-f FILE

      是否是一个常规文件

-g FILE

      FILE exists and is set-group-ID

-G FILE

      FILE exists and is owned by the effective group ID

-h FILE

      是否是一个超链接(same as -L)

-k FILE

      FILE exists and has itsstickybit set(待学习)

-L FILE

      是否是一个超链接 (same as -h)

 

-O FILE

      FILE exists and is owned by the effective user ID

-p FILE

      FILE exists and is a named pipe

-r FILE

      是否具备读权限

-s FILE

      FILE exists and has a size greater than zero

-S FILE

      FILE exists and is a socket

-t FD file descriptor FD is opened on a terminal

-u FILE

      是否具有SUID权限

-w FILE

      是否具备写权限

-x FILE

      是否具备可执行权限

FILE1 -ef FILE2

      FILE1 and FILE2 have the same device and inode numbers

FILE1 -nt FILE2

      FILE1 is newer (modification date) than FILE2

FILE1 -ot FILE2

      FILE1 is older than FILE2

*注:这个命令如果要放到if里用的话,就不用再加额外的方括号了

变量状态测试

测试即将使用的变量是否被定义过:

[ -z $PARMETER_NAME ]

返回0表示该变量未被使用,1表示已经使用。

字符串和数值状态测试

可以用来判断用户的输入是否符合要求的格式或值,比如:

判断两个字符串是否相同:

[ “abc” = “ABC” ]

判断变量的值是否为空:

[ -n “$PARA” ]

其它数值比较格式:

STRING1 = STRING2

      the strings are equal

STRING1 != STRING2

       the strings are not equal

INTEGER1 -eq INTEGER2

       INTEGER1 is equal to INTEGER2

INTEGER1 -ge INTEGER2

      INTEGER1 is greater than or equal to INTEGER2

INTEGER1 -gt INTEGER2

      INTEGER1 is greater than INTEGER2

INTEGER1 -le INTEGER2

      INTEGER1 is less than or equal to INTEGER2

INTEGER1 -lt INTEGER2

      INTEGER1 is less than INTEGER2

INTEGER1 -ne INTEGER2

      INTEGER1 is not equal to INTEGER2

系统进行数值测试时,会自动把两边转换为数值后在进行运算。

逻辑操作符

将上面任意两个表达式用下列连接选项组合起来,就组成了逻辑表达式:

-a:与,两边都为0时总结果为0

-o:或,任意一边为0,总结果为0

!:非,总结过时反得。

例如,测试/etc/passwd文件是否可读写:

[ -r /etc/passwd-a -w /etc/passwd ]

echo $?

0

捕获系统信号

这个是相对于用户发出的操作信号而言的。有时为了避免正在运行的脚本因用户误操作而异常退出,编写脚本时可以通过捕捉系统信号来决定脚本合适停止执行。

系统信号说明

1) SIGHUP

本信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一session内的各个作业,这时它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

2) SIGINT

程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

3) SIGQUIT

SIGINT类似,但由QUIT字符(通常是Ctrl-/)来控制.进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。

4) SIGILL

执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段.堆栈溢出时也有可能产生这个信号。

5) SIGTRAP

由断点指令或其它trap指令产生.debugger使用。

6) SIGABRT

调用abort函数生成的信号。

7) SIGBUS

非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)

8) SIGFPE

在发生致命的算术运算错误时发出.不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。

9) SIGKILL

用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

10) SIGUSR1

留给用户使用

11) SIGSEGV

试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

12) SIGUSR2

留给用户使用

13) SIGPIPE

管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

14) SIGALRM

时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

15) SIGTERM

程序结束(terminate)信号,SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL

17) SIGCHLD

子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)

18) SIGCONT

让一个停止(stopped)的进程继续执行.本信号不能被阻塞.可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作.例如,重新显示提示符

19) SIGSTOP

停止(stopped)进程的执行.注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行.本信号不能被阻塞,处理或忽略.

20) SIGTSTP

停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

21) SIGTTIN

当后台作业要从用户终端读数据时,该作业中的所有进程会收到SIGTTIN信号.缺省时这些进程会停止执行.

22) SIGTTOU

类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

23) SIGURG

有”紧急”数据或out-of-band数据到达socket时产生.

24) SIGXCPU

超过CPU时间资源限制.这个限制可以由getrlimit/setrlimit来读取/改变。

25) SIGXFSZ

当进程企图扩大文件以至于超过文件大小资源限制。

26) SIGVTALRM

虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

27) SIGPROF

类似于SIGALRM/SIGVTALRM,但包括该进程用的CPU时间以及系统调用的时间.

28) SIGWINCH

窗口大小改变时发出.

29) SIGIO

文件描述符准备就绪, 可以开始进行输入/输出操作.

30) SIGPWR

Power failure

31) SIGSYS

非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP

不能恢复至默认动作的信号有:SIGILL,SIGTRAP

默认会导致进程流产的信号:

SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

默认会导致进程退出的信号:

SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM

默认会导致进程停止的信号:

SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

默认进程忽略的信号:

SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIOSVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

举例

捕捉系统信号用trap命令,下面这个脚本开始执行后会sleep60秒,当用户按下ctrl+c后,脚本使用trap命令调用自定义的trap2函数输出提示:

#!/bin/bash

trap"trap2" 2

function trap2()

{

        echo "You press CTRL+C."

        echo "Exiting process, pleasewait..."

        exit 1

}

sleep 60

条件判断语句if

格式一:

if 条件表达式

         then

         语句1

         语句2

         …

fi

这里的条件表达式就是上面提到的条件测试表达式。

格式二:

if 条件表达式

then

         语句块1

         …

else

         语句块2

         …

fi

Shell里的语句块不用像其他语言、必须用{}括起来。

格式三:

if 条件测试表达式1

then

         语句块1

elif 条件测试表达式2

then

         语句块2

elif 条件测试表达式4

then

         语句块4

else

         语句块5

fi

这种格式的执行流程如下:

 

例一:判断文件状态及类型

判断某个文件的状态及类型,通过脚本参数将文件路径传入脚本:

#!/bin/bashfunction test_file(){        FILE_NAME=$1        if [ ! -e $FILE_NAME ]                then                echo "file not exist."                return 1        fi        if [ -d $FILE_NAME ]        then                echo $FILE_NAME": Directory."                return 0        elif [ -c $FILE_NAME ]        then                echo $FILE_NAME":Character device file."                return 0        elif [ -L $FILE_NAME ]        then                echo $FILE_NAME":Link file."                return 0        elif [ -b $FILE_NAME ]        then                echo $FILE_NAME":Block device file."                return 0        elif [ -f $FILE_NAME ]        then                echo $FILE_NAME":Regular file."                return 0        else                echo $FILE_NAME":Unkown type."                return 1        fi}function usage(){        echo "Error:Must have a file path following the script name."        echo "Usage: "$0" file_name"        exit 1}if [ $# = 0 ]        then        usage        exit 1fiif MESSAGE=`test_file $1`        then        echo $MESSAGE        exit 0else        echo $MESSAGE        exit 1fi

例二:多if嵌套

判断用户输入的脚本参数是一个目录还是文件,如果是则提示用户是否删除,读取用户输入后、做相应的操作。

#!/bin/bashfunction usage(){        echo "Error: Must have a file path following the script name."        echo "Usage: "$0" file name"        exit 1}if [ $# = 0 ]        then        usage        exit 1fiif [ -d $1 ]        then        echo -n "Are you sure to delete this directory?[y|n]"        read ANS_del        if [ $ANS_del = "y" ]                then                rm -rf $1 &>/dev/null        else                exit 1        fielse        echo -n "Are you sure to delete this file?[y|n]"        read ANS_del    if [ $ANS_del = "y" ]        then        rm -rf $1 &>/dev/null    else        exit 1    fifi

多条件判断语句case

基本格式如下:

case  变量名 in

         模式1)

                   语句块1

                   ;;

         模式2)

                   语句块1

                   ;;

         ……

                   ;;

esac

它的执行流程和if-elif相同,但是缺陷是只能对具体的某几个数值进行判断,而无法进行变量范围判断。

例一:判断用户的输入

#!/bin/bash

while true

        do

                echo -n "Please enter yesor no.[yes|no]"

                read ANS

                case "$ANS" in

                        y|Y|yes|Yes)

                                echo "Youenter yes."

                                                                    #下面可以添加想要执行的命令语句

… …

                                break

                                ;;

                        n|N|no|No)

                                echo "Youenter no."

                                                                    #下面可以添加想要执行的命令语句

… …

                                break

                                ;;

                        *)

                                echo"Please enter yes or no."

                                continue

                                ;;

                esac

        done

#下面可以添加想要执行的收尾语句

… …

*注:case判断中,每一个判断模式下面一定要记住加break语句,否则有可能是代码陷入死循环。

 

循环语句for

基本格式:

for  步进变量名 in 变量列表

do

         语句块

done

这里的变量列表既可以是一个集合,也可以是一个数组。

for语句会自动从将变量列表中的值依次传递给步进变量,直到所有变量都使用完为止。

例:输出循环的次数和步进变量值

#!/bin/bash

I=1

J="1 2 3 45"

for LOOP in $J

        do

                echo "Loop:" $I

                echo "LOOP="$LOOP

                I=`expr $I + 1`

done

exit 0

 

循环语句while

基本格式:

while  条件判断语句(为真)

         do

                   语句块

done

例子可以参考下面until的例子。

循环语句until

基本格式:

until  条件判断表达式(为假)

         do

                   语句块

done

该语句的执行流程按照书本上说如下:

但在我的机器(SLSE11_SP3,内核版本:3.0.76-0.11)上还是会先判断条件、然后才执行语句块。建议能用forwhile代替的循环、还是不要用这个语句。容易产生歧义。

可以看出,它是在判断条件为假时才执行语句块,当条件为真、则跳出循环,执行done后面的语句。

例:使用until语句监控某个分区的使用率,当使用率大于90%则向root用户告警并退出脚本,否则就一直循环执行监控语句。每次监控启动间隔600秒。

#!/bin/bashfunction usage(){echo"Error:Must have a parameter."    echo "$0 parameter..."    exit 1}if [ $# = 0 ]    then    usagefiUSED=`df -k | grep $1 | awk '{print $5}'|awk -F% '{print $1}'`until ["$USED" -gt "90" ]        do        echo "disk:" $1        echo "Used:" $USED        if [ $USED -gt "90" ]        then                echo "disk:" $1 >/tmp/disk.tmp                echo "Used:" $USED>> /tmp/disk.tmp                mail -s "diskwarning!" root </tmp/disk.tmp                rm -rf /tmp/disk.tmp                echo "Has notifiedroot."        fi        sleep 600done#如果使用率大于90%,until里面的语句将不会被执行,所以外面还要再写下echo"disk:" $1echo"Used:" $USEDecho"disk:" $1 > /tmp/disk.tmpecho"Used:" $USED >> /tmp/disk.tmpmail -s "diskwarning!" root </tmp/disk.tmprm -rf/tmp/disk.tmpecho "Hasnotified root."unset USED


*注:使用非root用户执行该脚本时,我的机器报了这个错:

df: `/root/.gvfs':Permission denied

按照网上的方法,使用root用户进入/root目录卸载掉这个文件后问题解决了:umount  .gvfs

breakcontinue控制循环

break:当循环满足某一条件时,使用该语句跳出循环;

如果使用了循环嵌套,则可以在break后面加上数字指定要跳出的循环层数。

continue:结束当前循环、继续进行下一次循环(如果还满足循环条件的话)

函数和脚本参数

其实上面的例子中,已经用到了shell函数和脚本参数。

Shell函数

定义函数

function  function_name()

{

         语句块

}

这里注意函数名function_name的命名,不要使用系统函数,尽量能体现该函数的功能。推荐驼峰命名和下划线的组合。

向函数传递参数

函数能够接收参数的前提是函数中引用了位置变量$1~$9;向函数传参时、直接在函数名后面加上要传递的参数、用空格分隔即可。比如:

命名一个脚本名为call_function.sh

#!/bin/bashfunction Add_A(){        B=` expr $1 + $2 `}Add_A  $1  $2echo$1"+"$2"="$B#执行./call_function.sh 3234  53213234+5321=8555

 

返回函数执行状态

系统会把函数最后一句执行的状态作为该函数的执行状态自动返回给位置变量#?,但也可以手动通过return语句设置函数的返回值。

系统执行函数遇到return时,无论后面有没有待执行的语句,都会立即设置函数返回值并退出当前函数。

return:用函数最后一句的执行状态作为函数的返回值、并立即退出函数。

return 0:返回函数执行成功、并立即退出函数。

return 1:返回函数执行失败、并立即退出函数。

调用函数文件

顾名思义,可以将某个函数写进一个固定的脚本文件,从而让其他脚本随时调用该函数(前提是执行用户对该函数文件有读和执行的权限)

我们可以把检查脚本执行时是否带参数的函数写到下列文件usage.sh

#!/bin/bashfunction usage(){    if [ $# = 0 ]    then    echo "Error: Must have a file pathfollowing the script name."    echo "Usage: "$0" number1number2"    exit 1    fi}#然后在上面的"call_function.sh"脚本中调用这一函数:#!/bin/bashfunction Add_A(){        B=` expr $1 + $2 `}#调用函数所在文件../usage.sh#调用文件中的函数usageAdd_A $1 $2echo$1"+"$2"="$B#不带参数执行call_function.sh./call_function.shError: Must have afile path following the script name.Usage:./call_function.sh number1 number2


 

*调用函数所在文件时要注意,上面的作法只适用于被调文件和执行文件在同一目录下的情况,否则要用相对路径或者决定路径表示

向脚本传递参数

当向脚本传递的参数大于9个时,需要使用shift命令。

(个人感觉很少有脚本会用到9个参数,这里简单说下shift实现的原理,就不举例了)

假设向脚本传递了10个参数,分别为:

1       2       3        4        5       6       7       8       9       0

$1     $2     $3     $4     $5     $6     $7     $8     $9     (暂无变量)

这里直接用$1~$9引用对应的值1~9没有问题,但如果想用第十个变量值0,就需要执行一次shift命令,执行后,值与变量名的对应关系就会发生变化,如下:

2       3       4        5       6       7       8        9       0

$1     $2     $3     $4     $5     $6     $7     $8     $9

这时$9对应的就是0,而1这个值就没有办法再被引用了。同一脚本中,没使用一次shift,对应的值就会“往前”移动一位,直到所有位置变量对应的值都变为空。

 

0 0