分类:
2008-08-01 02:28:01
背景:这是一篇我参考很多资料后为原公司编写的coding style说明,个人感觉代码风格很重要,希望大家也形成自己的coding style。
“这篇简短的文章描述了Linux内核首选的编码风格。编码风格是很个人化的东西,我不会把自己的观点强加给任何人。但是,Linux内核的代码毕竟是我必须有能力维护的,因此我宁愿它的编码风格是我喜欢的。请至少考虑一下这一点。”
摘自Linux Kernel Coding Style -- Torvalds Linus
众所周知,现在的中国,是一个自由、开放的国度,而我们系统部,更是秉承这一“Open”的精神,为保障我们的系统能正常的、稳定的、高效的为用户提供服务,我们使用了各种开源界软件,这些开源软件对于我们系统的发展,是功不可没的。Shell就是开源软件星空中最灿烂的星星之一。在我们维护系统、排查故障、优化性能的过程中,无一不和shell紧密的接触。而shell编程,则是提高我们工作效率的有效方法之一。
在我们现有的系统中,存在着我们编写的很多shell脚本,但由于脚本是由不同的同事进行编写的,个人的编码风格也不尽相同,在阅读别人编写的脚本时,编码风格相差太大就会影响到我们的工作效率,例如以下的几种典型情况,就会影响到我们工作的效率了:
1. 公司以及个人的发展,必然会有老同事的离职,新同事的就职,那么,老同事所编写和维护的脚本,就会交给新同事来维护和发展。
2. 对于其他同事维护的脚本,也经常会有不同的同事来进行维护。
3. 在进行相对规模较大的脚本的开发时,经常会是几个人一起进行开发,不同编码风格会影响各个模块的合成以及之后的测试调优过程。
因此,我们迫切的需要各位同事的shell编码风格能尽量的相近,基于这样的情况,我们编写了《脚本编程编码风格》这篇文档。
编码规则设定之初,必定有许多不完善,我们期待后来者将其完善。本文档主要分为以下几个部分:
1. 脚本版式
2. 命名规则
3. 目录结构
4. VI的配置
5. 其他事项
本文档参考了以下资料:
Linux Kernel Ccoding Style -- Torvalds Linus
写脚本也是一门艺术,良好清晰的版式,能让人一目了然,让阅读者有清晰的逻辑。
Tab(制表符)通常为8个字符,Linus在他的《Linux Kernel Coding Style》里这样叙述到:“有些叛逆主张试图把缩进变成4个(甚至是2个!)字符的长度,这就好象试图把PI定义成3是一样的。……有些人说8个字符大小的缩进导致代码太偏右了,并且在一个80字符宽的终端屏幕上看着很不舒服。对这个问题的回答是:如果你有超过3个级别的缩进,你就有点犯糊涂了,应当修改你的程序。”
对于这样描述,我的看法是,Kernel是用C语言来编写的,C语言的语句不像shell,shell编程中,即使一个判断语句,都可能因为使用了管道和命令反译而变得很长,因此,我们推荐Tab的字符数为4个字符。
Shell中大括号可以用在变量的保护,命令的组合等地方,但是这里我们主要描述在函数里的用法,我们推荐函数的边界大括号单独占一行,缩进与函数名一致,函数体内缩进一次,例如:
func_exam() { commands … } |
空行起着分隔程序段落的作用。空行不会浪费内存,所以不要舍不得用空行。空格的使用使得程序看起来很“块状”,逻辑结构更清晰。我们建议:
1. 每个函数定义结束后要加空行。
2. 逻辑块之间要有空行分隔。
3. 逻辑紧密的语句之间不加空格。
空格的使用情况较多,也不好归纳,下面仅仅举几个例子来说明:
1. 定义变量时空格用于美化视图。
firstVar=value1 secondVar=value2 thirdVar=value3 |
2. 管道、重定向前后建议加空格。
command1 | command2 > /dev/null 2>&1 |
3. 运算符、判断符前后建议加空格,有语法限制的除外。
if [[ -d dir1 ]] then … fi |
对于较长的表达式,为了结构紧凑可以适当去掉一些空格,例如在for循环中。
1. 函数
函数建议的规范如下:
func_exam() { commands … } |
2. 选择
选择包括if和case两种,当然还有select,但它是一个和case结合使用的循环,用得比较少。
if condition then commands … fi if condition then commands1 … else commands2 … fi if condition1 then commands1 … elif condition2 then commands2 … else commands3 … fi |
3. 循环
循环主要使用while和for。
while condition do commands … done for var in value_list do commands … done for ( init var ; condition ; change var ) do commands … done |
没有一种命名规则是所有人都赞同的,在这里,我们只是把常用的命名规则罗列出来,然后从中找出适合shell编程和便于理解的规则作为我们的命名规则。当然,我们不会只推荐一种,我们会推荐一两种,然后希望这一两种成为我们的规范。
比较著名的命名规则首推匈牙利命名法,这种命名方法是由Microsoft程序员查尔斯·西蒙尼(Charles Simonyi) 提出的。其主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”。匈牙利命名法关键是:标识符的名字以一个或者多个小写字母开头作为前缀;前缀之后的是首字母大写的一个单词或多个单词组合,该单词要指明变量的用途。例如:lpszStr, 表示指向一个以'\0'结尾的字符串(sz)的长指针(lp)变量。
骆驼(Camel)命名法近年来越来越流行,在许多新的函数库和Java这样的平台下使用得当相多。骆驼命名法,正如它的名称所表示的那样,指的是混合使用大小写字母来构成标识符的名字。其中第一个单词首字母小写,余下的单词首字母大写。例如:printEmployeePaychecks(),函数名中每一个逻辑断点都有一个大写字母来标记。
帕斯卡(Pascal)命名法与骆驼命名法类似。只不过骆驼命名法是第一个单词首字母小写,而帕斯卡命名法则是第一个单词首字母大写。例如:DisplayInfo()和UserName都是采用了帕斯卡命名法。
在C#中,以帕斯卡命名法和骆驼命名法居多。事实上,很多程序设计者在实际命名时会将骆驼命名法和帕斯卡结合使用,例如变量名采用骆驼命名法,而函数采用帕斯卡命名法。
另一种流行的命名规则称为下划线命名法。
下划线法是随着C语言的出现流行起来的,在UNIX/LIUNX这样的环境,以及GNU代码中使用非常普遍。
对于以上的几种命名规则,我们认为比较对变量或函数进行描述的是骆驼命名法和下划线命名法,因此,我们推荐使用这两种命名方面。
函数的命名推荐使用下划线分割小写字母的方式。命名结构形如:
1. 设备名_操作名():表达的意思是要对什么设备进行什么操作,例如array_handle()表示对一个数组进行操作。
2. 操作名():一些简单的操作,可以直接用操作名来做函数名,只要不和系统命令冲突,例如email()表示发送email。
为区别于函数,变量的命名方法我们推荐骆驼命名法,因为unix/linux衍生于C。命名时尽量用有意义的单词,单词所写能表达意思的尽量用单词缩写。例如progRoot表示程序的根目录,logDir表示日志文件的目录。
常用的缩写词有:
原词 |
缩写 |
原词 |
缩写 |
原词 |
缩写 |
addition |
add |
answer |
ans |
array |
arr |
average |
avg |
buffer |
buf/buff |
check |
chk |
count |
cnt |
column |
col |
control |
ctrl/ctl |
define |
def |
delete |
del |
distination |
dist |
display |
disp |
division |
div |
environment |
env |
error |
err |
frequency |
freq |
header |
hdr |
index |
idx |
image |
img |
increment |
inc |
initialize |
init |
length |
len |
memory |
mem |
middle |
mid |
make |
mk |
message |
msg |
number |
num |
operator |
optr |
packet |
pkt |
position |
pos |
previous |
pre/prev |
pointer |
ptr |
record |
rcd |
receive |
recv |
result |
res |
return |
ret |
source |
src |
stack |
stk |
string |
str |
subtraction |
sub |
table |
tab |
temporary |
tmp/temp |
total |
tot |
time stamp |
ts |
value |
val |
variable |
var |
|
|
Shell编写过程中,常量和变量的区分不是太清晰,因此,其命名规则可以参照变量命名法。但如果有需要特别注意的,建议用下划线分割大写字母的方法。例如,GLOBAL_VAR1。
函数中可以使用local定义仅在函数内部使用的局部变量,为区别于全局变量,建议在局部变量之前加下划线,例如local _funcLocalVar。
为便于管理,我们建议稍具规模的脚本使用如下的目录结构:
progName/ :以脚本名作为根目录名 |-- conf :配置文件目录 |-- data :源/目的数据目录 |-- logs :日志存放目录 |-- modules :子模块目录 |-- run :脚本运行临时文件目录 `-- tools :第三方工具目录 |
若有子模块,子模块也使用相同的目录结构。
这里只讨论我们常用的vi配置。下面的配置使得vi能通过脚本后缀名来使用不同的语法高亮,自动缩进等功能。
set nocompatible " get out of horrible vi-compatible mode,不使用vi兼容模式 filetype on " detect the type of file,检测文件类型 filetype plugin on " load filetype plugins,加载文件类型插件 set background=dark " we are using a dark background,使用暗的背景 syntax on " syntax highlighting on,语法高亮 set nobackup " do not make backup file,不备份文件 set ruler " Always show current positions along the bottom,显示光标当前位置 set number " turn on line numbers,显示行数 set backspace=2 " make backspace work normal, set nohlsearch " do not highlight searched for phrases,搜索时不高亮关键词 set incsearch " BUT do highlight as you type you search phrase,但在输入关键词时高亮 set ai " autoindent,自动缩进 set si " smartindent,智能缩进 set cindent " do c-style indenting,进行c风格的缩进 set tabstop=4 " tab spacing (settings below are just to unify it),Tab占4个字符 set softtabstop=4 " unify,与Tab统一 set shiftwidth=4 " unify,与Tab统一 set noexpandtab " real tabs please!,不实用扩展的Tab set smarttab " use tabs at the start of a line, spaces elsewhere,除行开头,其余的Tab用空格替换 |
注释是必须的,否则,你可能会忘记一个星期前写的一个脚本是用来干嘛的。但过量的注释会使得脚本看起来很复杂,也很臃肿。
注释应该放在使用之前,如果是变量的话,建议与变量处于同一行,使得结构更紧凑。函数的注释也应该放在函数体之前,并且要对函数进行简单描述和如何使用等。
例如:
变量注释:
progRoot=/var/PROGRAM/purge_cache # Program's running path. log=$protRoot/logs/purge_cache.log # Log file name. servList=$progRoot/serv.list # List all servers' ip. servListRo=$progRoot/run/.serv.list.ro # Standard servers' file. |
函数的注释:
# Export an array to a file, line by line. # Usage : array_2_file ARRAY_NAME TO_FILE_NAME array_2_file() { commands … } |
脚本的整体结构,建议做成如下形式:
#!/bin/bash # #脚本名、作者、时间和相关描述信息 # #函数定义区 … #脚本主体 #定义目录、日志等变量 #主要逻辑区 … #结束 |
一旦写的脚本达到一定数量,我们就会慢慢感觉到,有很多脚本语句是重复出现的,比如,发送email、清空临时文件、日志打包等,这时候,我们就可以考虑把经常需要做的工作集中到一个函数中来处理,下次要再使用的时候,只需要给函数传递一定的参数就可以了。
借用C语言中接口和实现的定义:
一个函数/模块由两部分组成:接口和实现。接口指明模块要做什么,它声明了该模块可用的标识符、类型和例程;实现指明模块是如何完成其接口声明的目标的。客户调用程序是使用某个模块的一段代码。客户调用程序导入接口;而实现导出接口。客户调用程序只需要了解接口即可。接口与实现只需一次编写和调试就可多次使用,这就是可重用性。
接口只需要指明客户调用程序可使用的标识符即可,应尽可能地隐藏一些无关的表示细节和算法,这样客户调用程序可以不必依赖于特定的实现细节,即减少了它们之间的耦合。接口在头(.h)文件中声明,该文件声明了客户调用程序可以使用的宏、类型、数据结构、变量以及例程。用户通过预处理指令#include导入接口。
实现导出接口。它定义了必要的变量和函数,以提供接口所规定的功能。一个实现揭示了表示的细节和接口给出的特定行为的算法。理想情况是:客户调用程序根本不需要了解这些细节。实现是由一个或多个定义(.c)文件提供的。一个实现必须提供其导出的接口所指定的功能。实现应包含接口的.h文件,以保证它的定义与接口的声明是一致的。
尽管上述的接口和实现的定义是基于C语言编程的,我们仍然可以从中得到启发。我们可以在函数化/模块化之后,在脚本中使用source(或点“.“命令)来将函数引如脚本,然后就可以在脚本中自由使用了。
下面是一个简单的脚本,用途是监控磁盘容量的变化,并报警,在此贴出来,权当做此次规范的实例。
#!/bin/bash # script: diskUsage.sh # author: Micheas Liao # date: Tue Jul 31 15:12:43 CST 2007 # function # sendMail # Usage: sendMail sendMail() { env MAILRC=/dev/null chartset=C from=support@pconline.com.cn smtp=61.145.113.31 \ nail -n -s "NAS/netApp diskUsage Report from $hostName($ip)" bb@pconline.com.cn < $log.tmp } export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin export LANG=C # main progRoot=/var/PROGRAM/diskUsage runDir=$progRoot/run logDir=$progRoot/logs log=$logDir/diskUsage.log dfLog=$logDir/df.log dfTmp=$runDir/df.tmp mailTag=$run/.mail.tag ipList="192.168.200.3 192.168.200.4 192.168.200.5" warnCode=90 hostName=$(hostname -s) ip=$(hostname -i) cd $progRoot [[ ! -d $runDir ]] && mkdir $runDir [[ ! -d $logDir ]] && mkdir $logDir [[ ! -f $log ]] && touch $log [[ ! -f $log.tmp ]] && touch $log.tmp [[ ! -f $dfLog ]] && touch $dfLog # check ip for _ip in $ipList do _pingOpt="-c 4 -i 0.5" ping $_pingOpt $_ip > /dev/null 2>&1 if [[ $? != "0" ]] then sleep 5 ping $_pingOpt $_ip > /dev/null 2>&1 if [[ $? != "0" ]] then date +%c >> $log.tmp echo " ---------- Ping NAS/netApp: $_ip failed!!! Pls check!!! Maybe NOT JUST this one!!! ---------- " >> $log.tmp sendMail cat $log.tmp >> $log ; rm -fr $log.tmp exit 1 fi fi done # check disk usage dfOpt="-Pk -t nfs" df $dfOpt > $dfTmp ( date +%c ; echo "----------" ; cat $dfTmp ; echo "----------" ) >> $dfLog cat $dfTmp | grep "%" | while read line do [[ "x$line" = "x" ]] && continue fileSys=$(echo $line | awk '{print $1}') useBlks=$(echo $line | awk '{print $3}') usePctg=$(echo $line | awk '{split($5,tmp,/%/);print tmp[1]*100}') mountPt=$(echo $line | awk '{print $6}') if (( usePctg >= warnCode*100 )) then touch $mailTag fi fmtName=$(echo $fileSys | sed -e 's@:@@' -e 's@/@.@g') [[ ! -f $logDir/$fmtName ]] && touch $logDir/$fmtName ymdHM=$(date +%Y%m%d%H%M) echo $ymdHM $useBlks $usePctg >> $logDir/$fmtName done if [[ -e $mailTag ]] then date +%c >> $log.tmp echo " ---------- NAS/netApp disk usage: WARNING " >> $log.tmp cat $dfTmp | awk '{printf "%s\n%-25s%-25s%-15s%-10s%s\n\n",$1,$2,$3,$4,$5,$6}' >> $log.tmp echo "----------" >> $log.tmp sendMail rm -fr $mailTag $log.tmp $dfTmp fi rm -f $mailTag # end |