shell编程经验总结

来源:互联网 发布:网页游戏源码架设教程 编辑:程序博客网 时间:2024/04/30 01:08

一、代码重用

在其他语言中的代码重用的方法大家应该都很熟悉了,我就不在此献丑了。在shell编程中我想大多数人是不熟悉的(如果很熟就当我没说J)。一般情况如果想使用别人的一段代码,或者一个函数(或者直接使用脚本),目前可能大多数人都会直接拷贝源代码到自己的脚本中。这样有什么坏处我就不说了。其实我们在编写脚本时只要简单的改变一下编程习惯就能很容易的让你的程序被大家使用,不再废话,直接看下面的代码。

################################################################################这是一个简单判断用户输入的字符串是否是ip地址的函数,如果你写脚本会在乎健壮性和#安全性的话,应该会对对用户输入的ip参数进行有效性判断,所以应该会经常用到。###############################################################################is_ip(){       if [ $# -ne 1 ]       then       {              echo "Usage:$0 string"              return 1       }       fi       typeset ip=$1       #判断是否是以 . 作为分割的四个字段,并且每个字段都是 0-255 之间的数,我们即认为是 ip 地址。       typeset ipflag       ipflag=`echo ${ip} | awk -F. '{ if ( NF==4 && ( $1 >= 0 && $1 < 256 ) && ( $2 >= 0 && $2 < 256 ) &&  ( $3 >= 0 && $3 < 256 ) && ( $4 >= 0 && $4 < 256 )  ) print "ok" }'`       #是 ip 返回 0       if [ "-${ipflag}" = "-ok" ]       then       {              return 0       }       fi       return 1}

然后保存到tool.inc文件中(inc是我自己的习惯而定的后缀名,大家不喜欢可以随便叫,然后在自己的脚本中用.即可把此程My.sh

#!/bin/bashmain(){       typeset userip=$1       is_ip ${userip}       if [ $? –ne 0 ]       {              echo “invalid ip address ${userip}return 1       }       fi       # do my other process       return 0}#需要用到的函数, 此处也可以使用相对路径. /home/away/common/tools.inc#执行主函数main “$@exit $?

现的好处其他的我就不多说,只说一两个,这样的重用是进程内的重用,不像直接使用别人的脚本,是进程外的(因为每次使用都要生产一个进程,会需要很多开销的)。如果一个for循环少一点一千次,这个性能影响会很大,并且被调用的是个脚本,进程id($$)是一直变的,也不好跟踪。
第二个好处就是实现简单的shell编程的函数重定义和跨平台,具体方法,因为时间关系以后再详细描述.

二、重定义和跨平台

我在Shell编程经验总结的第一篇文章中提到在shell编程中把执行语句封装成函数,并通过引入的方式实现代码重用的一个好处是实现函数重定义和跨平台,函数重定义有什么好处呢?还有有些人可能觉得shell本身就是跨平台的,为什么还存在跨平台的问题呢?下面将详细描述.
你写shell脚本的时候有没有写过和系统命令同名的函数?没有的话没有关系,可以马上试一下.
我想把ls改成 ls –l的效果(你可以直接用别名J),我在此就不考虑别名的情况了.看函数的实现.下面是tools.inc的内容.

#tools.incls(){       ls -l}

下面是test.sh的内容.

#!/bin/bash. /home/away/common/tools.incecho "begin"lsecho "end"

好,执行一下test.sh脚本,你会看到什么?程序core dump了,为什么
away@DXX-3900:~/test$ test.sh
begin
Segmentation fault
因为自身递归调用导致的,说明什么,你的ls是调用了你自己定义的ls函数.而不是系统的.
好了,现在我们把tools.inc添加一点内容变成

#tools.inc#first lsls(){       ls -l}#second lsls(){        echo "my ls 2"}

然后在和上面一样执行一下test.sh.
away@DXX-3900:~/test$ t.sh
begin
my ls 2
end
现在没有问题了,为什么?后面定义的ls函数覆盖了第一个定义的ls函数
现在应该明白了函数重定义了吧,有什么好处呢?具体好处就是实现了函数重定义(好像是废话哦,呵呵).其实有没有好处地球人都知道.我就不多说了.
上面知道了shell函数的重定义,现在就应该说说跨平台了.shell需要跨平台是因为各个unix/linux系统提供的命令的参数和输出结果不一致带来的问题.
比如:我想用shell编写一个从系统中获取系统中剩余空间的函数(此处只考虑已经创建文件系统的,裸设备不考虑在内),你可以用df –m 然后awk处理一下,取出第四个字段相加一下就可以了(linux下),在一般的linux下也许可以,但是在hp-unix下就不行了,hp下的df命令输出的格式是完全不一样的.到是bdf命令和df比较像.,然后你为了支持多个平台你的代码中就需要如下写:

OS_TYPE=`uname -s`if [ "-${OS_TYPE}" = "-Linux" ]then{        echo "in linux os"        #df -k        #此处省略获取磁盘剩余空间的代码}elif [ "-${OS_TYPE}" = "-HP-UX" ]then{        echo "in hp-unix"        #bdf -k        #此处省略获取磁盘剩余空间的代码}elif [ "-${OS_TYPE}" = "-SunOS" ]then{        echo "in sun os"        #df -k        #此处省略获取磁盘剩余空间的代码}elif [ "-${OS_TYPE}" = "-AIX" ]then{        echo "in aix os"        #df -k        #此处省略获取磁盘剩余空间的代码}fi

你也可以把磁盘信息获取部分的代码封装成函数,你需要定义四个不同名的函数. get_disk_info_linux get_disk_info_hp get_disk_info_sun get_disk_info_ibm然后在if elif fi中调用这些函数. 如果系统中很多地方用到的系统命令名称或者因版本导致输出格式不一致的话,你将很快发现程序中到处充满了可恶的if elif fi之类的语句.还有可能这个get_disk_info*的函数在linux和sun,ibm平台实现都一样,而只有hp的实现不一样时,又有不一样的if语句.这样看起来代码就太恶心了.有没有办法解决呢?有,就是利用上面讲到的函数重定义.
我们可以创建下面几个文件tools.inc tools_linux.inc tools_hp.inc tools_sun.inc tools_ibm.inc, 然后在主脚本test.sh中实现如下:

#test.shOS_TYPE=`uname -s`#这个tools.inc存放各个平台实现最大相同(这个地方后面会特别解释一下,呵呵)的函数. /home/away/common/tools.incif [ "-${OS_TYPE}" = "-Linux" ]then{. /home/away/common/ tools_linux.inc}elif [ "-${OS_TYPE}" = "-HP-UX" ]then{. /home/away/common/tools_hp.inc}elif [ "-${OS_TYPE}" = "-SunOS" ]then{. /home/away/common/tools_sun.inc}elif [ "-${OS_TYPE}" = "-AIX" ]then{. /home/away/common/tools_ibm.inc}fimain(){}main “$@

tools.inc的实现如下:

#tools.incget_disk_info(){       echo "common get_disk_info function"       #df -k    #此处省略获取磁盘剩余空间的代码}

tools_hp.inc实现如下:

#tools_hp.incget_disk_info(){       echo "hp get_disk_info function"       #bdf -k    #此处省略获取磁盘剩余空间的代码}

然后你就可以在相应的平台中写需要的函数了,如果这个函数的实现在最多的平台实现相同的话可以把这个函数写在tools.inc而相应的平台就不必再实现了,如果hp平台的实现不一样就可以在tools_hp.inc中实现这个函数,函数明都叫一样的比如上面的get_disk_info().这个就有点像c++中的虚函数的特性.tools.inc作为父类,而具体平台的有选择的进行了重新实现.
这样实现以后你会发现只有一个地方需要if fi之类的语句引入相应平台的文件,所有的函数实现放在具体的平台文件中.说到里面废话也说了差不多了,不知道对大家有没有什么帮助,虽然我们基本都用linux平台,但是也可能遇上因为linux版本不一样导致的问题,也可以通过这个方法解决.

三、编程习惯

在上篇Shell编程经验总结之-重定义和跨平台一文中提到的内容对于大型shell系统的构建,以及平台命令存在差异的问题都能很好的解决,同样也可以简单的实现多语言的支持,你一定想到了只要把语言信息定义成资源文件作为变量引入到系统中即可.如果你没有构建过大型系统对于以上好处你不一定能体会到。
好了,如果你开始因为我的影响慢慢改变,开始了习惯新的方法来组织你的shell脚本,那么你会遇到很多新的问题。现在我就从一些细小的地方讲述一下shell编程应该注意的一些习惯。
首先,从脚本的入口讲起.建议每个sh文件(即用户一般直接执行的)都有一个main()函数,然后在main函数里面统一写执行主体.然后再在shell中调用main函数.如下:

#!/bin/bash#######################################################function:#input:#output:#return:######################################################main(){       #根据需要进行参数处理       get_my_log_all       return $?}######################################################. /home/away/common/tools.inccd ${0%/*}#echo `pwd`main "$@"exit $?

上述代码定义了一个main函数,然后引入我们需要的功能函数,存放在tools.inc中,这些都已经很熟悉了,接下来的内容 cd ${0%/*} 可能很多人没见过。干什么用的呢?如果你对shell还算熟悉的话,应该知道这是进入本脚步所在的目录,即把当前的工作目录设置成本脚本所在的目录。

${}相关的知识可以通过man bash (随便找个shell都可以)查询到,下面我通过一些例子说明 ${} 的一些特异功能:假设我们定义了一个路径变量为:file=/dir1/dir2/dir3/my.file.txt我们可以用 ${ } 分別替换获得不同的值:${file#*/}:拿掉第一条 / 及其左边的字串:dir1/dir2/dir3/my.file.txt${file##*/}:拿掉最后一条 / 及其左边的字串:my.file.txt${file#*.}:拿掉第一个 . 及其左边的字串:file.txt${file##*.}:拿掉最后一个 . 及其左边的字串:txt${file%/*}:拿掉最后条 / 及其右边的字串:/dir1/dir2/dir3${file%%/*}:拿掉第一条 / 及其右边的字串:(空值)${file%.*}:拿掉最后一个 . 及其右边的字串:/dir1/dir2/dir3/my.file${file%%.*}:拿掉第一个 . 及其右边的字串:/dir1/dir2/dir3/my

简单来说就是
两个#(或者%)表示最大匹配
一个#(或者%)表示最小匹配
#从左边开始匹配,%从右边开始匹配
但是找到匹配的被切掉.
这样做有什么好处呢?第一,你始终能通过pwd准确的知道你脚本所在的绝对路径(如果系统中其他地方需要的话可以用全局变量保存起来)。第二可以避免一个因为工作目录的问题而导致你的系统不能正常运行的问题。举个简单的例子说明一下。
你可能会有一个目录结构如下的系统:
bin/test.sh
cfg/test.cfg
一个可执行文件test.sh 一个配置文件test.cfg,test.sh要用到此配置文件中的配置信息。
Test.sh的内容如下:

#!/bin/bash#具体代码省略……#你因为不知道系统所在的绝对路径而使用相对路径访问配置文件。cat ../cfg/test.cfg | grep …… #后面进行一些其他操作,都省略……

上面的代码看起来是没什么问题,并且如果你平时都进入test.sh所在的bin目录执行次脚本你不会发现有任何问题。但是有一天你心血来潮,不想cd到bin目录执行此脚本。而是在bin所在的父目录执行bin/test.sh时,你就会惊讶,天哪,我的程序以前都运行的很好的, 为什么现在不行了呢?你可能还在抱怨谁动了我的奶酪。其实是你自己没有好好构建这个系统。因为当前工作目录是bin的父目录,而cat ../cfg/test.cfg当然不是你想要的那个文件了,一般是不会存在的,除非有巧合。

再往下看程序 main "$@"exit $?。这个exit $?我相信大家都很清楚就不多说了(注:bash不支持在脚本中直接return,只能在函数中进行return,而ksh支持在脚本中也可以return)。现在主要说说main “$@”(如果你了解$@和$*的区别,你可以跳过这一段不看,不过我认识的大多数人是不知道这二者的区别的)你在我的文章和脚本中应该看到过很多次了,我每次都会这样写的,把用户输入的参数原样传递给main函数进行处理。很多人都知道$*代表着所有的位置参数,并且要引用所有参数时一般会用$*,大多数情况这个是能运行良好的,但是如果你给出的参数有一个是有空格分开的字符串,但是你又想把他作为一个参数,你会用””引起来,举例如下:test.sh内容#!/bin/bash……#!/bin/bashmain(){        if [ $# -ne 3 ]        then        {                echo $#                echo "Usage:$0 prara1 para2 para3"                return 1        }        fi        echo "ok"}main $*exit $?

那么执行参数如下时你会得到这样的结果:
away@DXX-3900:~/test./test.shabcd4Usage:./testprara1para2para3*的缺点,你可以在程序中把#和1 23 4shell@解决这个问题,他能把原有的包含在””里面的参数继续包含在””,但是必须在$@外面加””,再传递给接受此参数列表的函数。
以下是一些常用的shell特殊内置变量:

       $@ 所有的位置参数,在””中能把包含在””中的字符串仍然作为一个整体       $* 所有的位置参数,默认以空格作为分割       $? 上一个命令的返回值       $# 位置参数的个数       $$ 本进程的进程id       $! 最近的一个后台进程的id       $0 本进程名       $1 ~ $9 各个位置参数
   讲完了脚本的整体架构,现在我们进入到函数里面,根据shell的特点讲述函数编写注意的一些事项。

顺便说一下编码风格方面的问题,其他语言的编码风格的资料也很多,但是shell因为有自己的特点我在此基础上补充一些。
第一,为了你程序的健壮性和安全性,请检查参数个数符合你的要求并尽量检查各个参数的有效性。
第二,用有意义的局部变量保存位置参数,不要直接使用位置参数,不然你的程序不但晦涩难懂,对于以后的扩展和修改也是非常不利的。

作为一个程序员,特别是为公司开发的系统让别人无法理解和很难维护都是不可接受的,shell中的技巧又特别多,建议尽量少用,如果不能不用也最好给出简明的注释,以备后人维护。具体的要求可以根据具体情况调整,比如你是给自己用的可以先简单的给出,如果给大家用的,在接口上至少能给出比较清晰明了的注释说明,在一些很晦涩的代码上最好能给出详细的说明。
例如下面的代码一眼能看出具备那些功能sed的水平可以说是很不错了:)

echo "dfsdi4563462342fj[123]"| sed -n 's/[0-9][0-9]*([^0-9a-zA-Z]*)[0-9][0-9]*([^0-9a-zA-Z]*)[0-9][0-9]*([^0-9a-zA-Z]*)$/010203/p'|sed -n 's/[0-9][0-9]*([^0-9a-zA-Z]*)[0-9][0-9]*([^0-9a-zA-Z]*)$/0102/p'|sed -n 's/[0-9][0-9]*([^0-9a-zA-Z]*)$/01/p'

其他建议如下:
命名:
1 shell变量和函数名都基本统一用有意义的单词的小写加下划线的形式
2 全局的变量和常量用大写,用下划线进行连接
注释:
如果是真的给自己看的,没有时间写注释是可以原谅的,但是写给大家用的请尽量说明程序的用途

0 0
原创粉丝点击