当前位置:首页 >> 脚本专栏

shell脚本学习指南[三](Arnold Robbins & Nelson H.F. Beebe著)

今天木有冷笑话,只有一个噩耗。噩耗是:今天木有冷笑话!!!
不要总想着冷笑话嘛,有点追求,听毛主席的话:好好学习,天天向上!

第七章输入输出、文件与命令执行

学C的应该了解标准输入输出和错误输出吧?感觉总打很多字进度太慢,所以一直在省略类似C的东西,也方便以后看这篇文章的人能够快速学完shell脚本(或者是快速看完这本书)。

读取行read命令是重要方式之一,它可以自标准输入读取行后,通过shell字段切割的功能(使用$IFS)进行切分,第一部分给第一个变量,第二部分给第二个,类推。如果切割单词多余变量,则剩下所有的给最后一个变量。如果输入行以反斜杠结尾,则read会丢弃反斜杠与换行符继续读取下行数据。它有一个选项 -r,它将忽视最后反斜杠当读入数据。使用read可能的一个错误是通过循环让read读取一个文件如:
while IFS=: read user pass uid gid fullname homedir shell < /etx/passwd ... 这个循环将一直下去并且每次read只读passwd的第一行。因为每次循环都重新打开了passwd文件读取内容。解决办法是: cat /etc/passwd | while IFS=: read ....... 这样通过管道解决读取文件问题。 这里有一个概念,文件描述符,一般这个文件描述符是由0-9这几个数字来描述的,所以传统上shell也就允许你最多打开十个文件。比如make 1> results 2> ERRS 。命令make的标准输出(文件描述符1)传给results,并将错误输出(文件描述符2)传给ERRS。设置完文件描述符后,如何引用呢?像这样:make > results 2>&1 。
1> results这里的1其实没必要,供输出重定向的默认文件描述符是标准输出,也就是文件描述符1,重定向 > results让文件描述符1作为文件results,接下来重定向2>&1有两部分,2>重定向文件描述2,也就是标准错误输出。而&1就是刚才我们的疑问,用来引用我们定义的文件描述符。特别注意:2>&1这样的四个字符一定要连着写。

再介绍一个用来改变shell本身I/O设置的exec命令。如果只有I/O重定向而没有任何参数时,exec会改变shell的文件描述符:

复制代码 代码如下:
exec 2> /tmp/$0.log #重定向shell本身的标准错误输出
exec 3< /some/file #打开新文件描述符3
...
read name rank serno <&3 #从该文件读取 exec 3>&- #关闭文件描述符3

上例展示了如何关闭文件描述符。
exec还有一个功能就是在当前shell下执行指定的程序。

书中对printf做了完整的介绍,这里就不再介绍了,就是C里边的那些东西。

shell中有两种与文件名相关的展开:第一个是波浪号展开(~ tilde expansion),另一个叫法较多如通配符展开式(wildcard expansion)、全局展开(globbing)或路径展开(pathname expansion)。

如果命令行字符第一个字符为波浪号或者变量指定的值里任何未被引号括起来的冒号之后的第一个字符为波浪号时,shell便会执行波浪号展开。波浪号展开的目的,是要将用户根目录的符号型表示方式,改为实际的目录路径。

shell环境下的通配符展开,有几个基本的通配符:? , * , [set] [!set] ,前俩略过,第三个是匹配出现中括号集合中的字符,第四个取反义。比如可以查找 *.html 就知道处所有类似的文件。另外有一点注意的,在linux下文件名里的.号没有任何特殊意义,匹配所有文件时只需用一个*即可,不用像windows下那样*.*。
习惯上,当执行通配符展开时,shell会忽略文件名开头为一个点号的文件。像这样的点号文件(dot files)通常用作程序配置文件或启动文件。

命令替换,书上写的概念很绕口,其实就是一个命令的用法或者写法,例如:
echo outer `echo inner1 \`echo inner2 \` `
这样输出结果就是 outer inner1 inner2 类似命令嵌套,从最内层开始执行。注意是反单引号,键盘1左边与波浪号同键那个。但是这样嵌套多了之后会非常难以阅读,就出现了新的语法:
$ echo outer $(echo inner1 $(echo inner2) inner1) outer
这样输出结果就是outer inner1 inner2 inner1 outer。这样清晰多了。

书中教了一个expr命令,从提到这个命令,到接下来的两段都在说这个命令不好用,并且可以是由$(( )) ,test替代。但是可以了解一下,作用就是计算之后跟着的一个表达式比如:expr 1 + 1 。这里注意加号两边的空格,是必要的,书里貌似没说,郁闷半天才发现必须添加空格。。- -!

这里又提了引用,就是说用来防止shell将某些东西解释成你不想要的意义,比如你就是有就想要*,而不是需要一个通配符,这时候你需要转义(\) ,或者是单引号引起来(单引号引起来的内容转义符号也无效),或是双引号。混用的时候请小心。

书中详细说了一下命令的执行顺与,感觉很有必要细看一下,全都摘录一下。
shell从标准输入或脚本中读取的每一行成为管道;它包含了一个或多个命令,这些命令被零或多个管道字符隔开。事实上还有很多特殊符号可用来分隔单个的命令:分号;、管道|、&、逻辑AND&&、还有逻辑OR||。对于每一个读取的管道,shell都会将命令分割,为管道设置I/O,并且对每一个命令依次执行下面操作:
1、将命令分割成token,是以固定的一组meta字符分隔,有空格、制表符、换行字符、;、(、)、<、>、|、和&。token的种类包括单词、关键字、输出入重定向器,以及分号。这是微妙的,但是变量、命令还有算符替换,都可以在shell执行token认定的时候被执行。这就是为什么先前所举例子vi ~$user/.profile中波浪符号可以展开像预期的那样工作。
2、检查每个命令的第一个token,看看是否它是不带有引号或反斜杠的关键字。如果它是一个开放的关键字(if 或者 { (之类的),则这个命令其实是一个复合命令。shell为复合命令进行内部的设置,读取下一条命令,并再次启动进程。如果关键字非复合命令的开始符号(例如,它是控制结构的中间部分,像then、else或do,或是结尾部分,例如fi,done或逻辑运算符),则shell会发出语法错误的信号。
3、将每个命令的第一个单词与别名列表对照检查。如果匹配,它便代替别名的定义,并回到步骤1;否则,进行步骤4(别名是给交互式shell使用)。回到步骤1,允许让关键字的别名被定义:例如alias aslongas=while or alias procedure=function。注意,shell不会执行递归的别名展开,反而当别名展开为相同的命令时它会知道,并停止潜在的递归操作。可以通过引用要保护的单词的任何部分而禁止别名展开。
4、如果波浪号字符出现在单词的开头处,则将 波浪号替换成用户的跟目录$HOME,将~user替换成user的根目录。
波浪号替换会发生在下面的位置:
* 在命令行里,作为单词的第一个未引用字符
* 在变量赋值中的=之后以及变量赋值中的任何:之后
* 形式${ varibale op word } 的变量替换里的word部分
5、将任何开头为$符号的表达式,执行参数(变量)替换。
6、将任何形式为$(string)或 `string`的表达式,执行命令替换。
7、执行形式$((string))的算术表达式。
8、从参数、命令与算术替换中取出结果行的部分,再一次将它们切分为单词。这次它使用$IFS里的字符作为定界符,而不是使用步骤1的那组meta字符。通常,在IFS里连续多个重复的输入字符是作为单一定界符,这是你所期待的。这只有对空白字符而言是真的。对非空白字符,则不是这样的。举例说,当读取以冒号分隔字段的/etc/passwd文件时,两个连续冒号所界定的是一个空子段。
9、对于*、?以及一对[...]的任何出现次数,都执行文件名生成的操作,也就是通配符展开。
10、使用第一个单词作为一个命令,遵循查找次序,也就是,先作为个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,以及最后作为查找$PATH找到的第一个文件。
11、在完成I/O重定向与其他同类型事项之后,执行命令。

shell程序碰到一句命令,都会执行上边的一次流程。比如:
复制代码 代码如下:
$ mkdir /tmp/x
$ cd /tmp/x
$ touch f1 f2
$ f=f y="a b"
$ echo ~+/${f}[12] $y $(echo cmd subst) $((3+2)) > out

命令一开始会根据shell语法分割token,最重要一点是I/O重定向 > out 在这里是被识别的,并存储供稍后使用。最后这句echo被分为5个token,分别是: echo ,~+/${f}[12] , $y , $(echo cmd subst) ,$((3 + 2))这5个部分。
然后检查第一个单词echo是否为关键字,例如if或for,这里不是所以命令不变继续处理。
检查第一个单词依然是echo 是否为别名,这里不是,继续执行。
扫描所有单词是否需要波浪号展开,本例中,~+为ksh93与bash的扩展,等同于$PWD,也就是当前的目录。token 2将被修改变成如下:
echo /tmp/x/${f}[12] $y $(echo cmd subst) $((3 + 2))
下一步变量展开:token2与token3被修改变成:
echo /tmp/x/f[12] a b $(echo cmd subst) $((3 + 2))
再来要处理的是命令替换。注意,这里可递归引用列表里的所有步骤!在次例中,因为我们试图让所有的东西容易理解,因此命令替换修改了token4,结果如下:
echo /tmp/x/f[12] a b cmd subst $((3 + 2))
现在执行算术替换,结果如下:
echo /tmp/x/f[12] a b cmd subst 5
前面所有的展开产生的结果,都将再一次被扫描,看看是否有#IFS字符,如果有则它们是作为分隔符,产生额外的单词。
最后的替换阶段是通配符展开变化如下:
echo /tmp/x/f1 /tmp/x/f2 a b cmd subst 5
这时,shell已经准备好要执行最后的命令了。它会去寻找echo。正好ksh93与bash里的echo都已内建到shell中。
shell实际执行命令。首先执行>out的重定向,再调用内部的echo版本。这一系列完成了这句语句的执行。

eval语句是再告知shell取出eval的参数,并再执行它们一次,是他们经过整个命令行的处理步骤。看一个例子:

复制代码 代码如下:
listpage="ls | more"
$listpage

运行之后你会发现shell把|与more看作ls的参数,而不是直接产生一页页的文件列表。这是由于在shell执行变量时,管道字符出现在步骤5,也就是在它确实寻找管道字符之后(在步骤1).变量的展开一直要到步骤8才进行解析。结果,shell把 | 与more看作ls的参数,使得ls会试图在当前目录下寻找名为|与more的文件。

现在,想想eval $listpage吧,在shell到达最后一步时,会执行带有ls、|与more参数的eval,这会让shell回到步骤1,具有一行包括了这些参数的命令。在步骤1发现|后,将该行分割为两个命令:ls 和more。每个要被处理的命令都以一般方式执行,最后的结果是在当前目录下分页的文件列表。

还有两个其他的结构,有时也很有用,subShell与代码块。
subShell是一群被括在圆括号里的命令,这些命令会在另外的进程中执行。当你需要让一小组的命令在不同的目录下执行时,这些命令会在另外的进程中执行。如:
tar -cf - . | (cd /newdir; tar -xpf - )
左边tar产生当前目录打包文件,将它传送给标准输出。右边的cd命令会先切换到新目录,也就是让打包文件在此目录下解开。然后,右边的tar将从打包文件里解开文件,请注意,执行此管道的shell并未更改它的目录。

代码块概念上与subShell雷同,只不过它不会建立新进程。代码块用花括号括起。且会对主脚本造成影响(比如当前目录)。一般花括号被视为关键字,即它们只有出现在命令的第一个符号时被识别。实际上,这表示你必须将结束花括号放置在换行符或分号之后。

shell有很多命令,之前说过,特殊内建命令与一般内建命令的差别在于shell查找要执行的命令时,会先查找特殊内建命令再找shell函数,接下来才是一般内建命令。最后是$PATH路径内的外部命令。这种查找顺序让定义shell函数以扩展或覆盖一般shell内建命令成为可能。举例说你希望shell的提示号能包含当前目录路径的最后一个组成部分。最简单的实现方式,就是在每次改变目录时,都让shell改变PS1.你可以写一个自己专用的函数如下:

复制代码 代码如下:
# chdir ---改变目录时更新PS1的个人函数
chdir () {
cd "$@" #实际更改目录
x=$(pwd) #取得当前目录的名称
PS1="${x##*/}\$ " #截断前面的组成部分后,指定给PS1
}

这么做有个问题,你必须在shell下输入chdir而不是cd,这样你可以自己写一个名为cd的函数,然后shell会先找到你的cd函数,而不是一般内建函数cd。但是这样又会有问题,shell函数如何真正访问cd命令,这里函数内cd会再此调用你写的cd函数导致递归出现。这时候我们需要转义策略,使用内建命令command来告诉shell要避开函数的查找直接访问真正的命令。

#cd --改变目录时更新PS1的私人版
cd(){
command cd "$@"
x=$(pwd)
PS1="${x##*/}\$ "
}

第八章产生脚本


详细讲了一下set命令。。。

这一章详解了两个好用脚本的实现过程,这两个脚本详解内容揉杂在注释里给出。

脚本一:功能是在给出的路径下查找目标路径

复制代码 代码如下:
#! /bin/sh -
#
# 标准输出所产生的结果,通常是查找路径下找到的每一个文件之第一个实体的完成路径,
# 或是“filename: not found ”的标准错误输出。
#
# 如果所有文件都找到,则退出码为0,
# 否则,即为找不到的文件个数(非0)
# shell的退出码限制为125

# 语法:
#      pathfind [--all] [--?] [--help] [--version] envvar pattern(s)
#
# 使用--all选项时,在路径下的每一个目录都会被查找,
# 而非停在第一个找到者。
# 所有脚本的头部说明脚本功能是必不可少的,对人阅读很有用。

#在网络的环境下,安全性一直是必须慎重考虑的问题。其中有一种攻击shell脚本  #的方式,是利用输入字段分隔字符:IFS,它会影响shell接下来对输入数据解释的
#方式。为避免此类的攻击,部分shell仅在脚本执行前,将IFS重设为标准值;其他
#则导入该变量的一个外部设置。很难在屏幕上看出来,单引号内包含一个换行一
#个空格和一个制表符,这是IFS的默认值。也可以使用转义\040\t\n,但bourne
#shell不支持这一的转义。重新定义IFS时有一点要特别留意,当"$*"展开以回复命
#令行时,IFS值的第一个字符,会被当成字段分隔符。这里不使用$*,不受影响
IFS='
  '
#另一种常见的安全性攻击,则是欺骗软件,它执行非我们所预期的命令。为了阻断
#这种攻击,我们希望调用的程序是可信任的版本,而非潜伏在用户提供的查找路径
#下的欺骗程序,因此我们将PATH设最小值,存储初始值供以后使用。exprot语句
#是这里的关键么,它可以确保所有子进程继承我们的安全查找路径。
OLDPATH="$PATH"

PATH=/bin:/usr/bin
export PATH
#错误输出函数
error(){
 echo "$@" 1>&2
 usage_and_exit 1
}
#简短的一个信息提示函数,$PROGRAM稍后会赋值为命令名
usage(){
 echo "Usage: $PROGRAM [--all] [--?] [--help] [--version] envvar pattern(s)"
}
#提供信息和状态码退出
usage_and_exit(){
 usage
 exit $1
}
#提供用户版本号
version(){
 echo "$PROGRAM version $VERSION"
}
#给出警告信息并在状态码上加1,记录警告次数
warning(){
 echo "$@" 1>&2
 EXITCODE=`expr $EXITCODE + 1`
}
#按大写全局,小写局部命名规则,初始化需要用的变量
all=no
envvar=
EXITCODE=0
#basename会截去参数最后一个斜杠之前字串,返回剩下的部分
PROGRAM=`basename $0`
VERSION=1.0
#接下来这块就是linux经典的命令行参数解析部分了,不多解释,需要注意的是?号
#是通配符,所以筛选选项的时候防止展开加上单引号。
while test $# -gt 0
do
 case $1 in
 --all | --al | --a | -all | -al | -a )
  all=yes

 --help | --hel | --he | --h | '--?' | -help | -hel | -he | -h | '-?' )
  usage_and_exit 0

 --version | --versio | --versi | --vers | --ver | --ve | --v | \
 -version | -versio | -versi | -vers | -ver | -ve | -v )
  version
  exit 0

 -* )
  error "Unrecognized option: $1"

 * )
  break

#这里我小纠结了一下esac命令,没搞清楚在这干嘛的,也没这个命令说明,仔细看
#看才发现是case逆序写法,是case的结束标志,就像if结束标志fi一样
 esac
 shift
done
#下面我们要处理除选项外的参数了,我们可以用"$@"来取得,但是避免将他们存
#储在变量内如files="$@",因为文件名中如果有空格将无法正确被处理。
envvar="$1"
test $# -gt 0 && shift
#因为有可能用户提供的环境变量是PATH,为安全性考虑会重设,这是我们检测该变
#量,并适当更新envvar,开头的x是为了避免开展当成test的选项。
test "x$envvar" = "xPATH" && envvar=OLDPATH

#下边这句虽然很段,但是最棘手部分:使用shell的eval语句。我们envvar里已经拥
#有了环境变量的名称,可以"$envvar"取得,但我们现在要的是它的展开,我们也
#想要冒号分隔符转换成一般空白分隔符。如果MYPATH为用户所提供的名称,我们
#便会构建参数字符串'${'"$envvar"'}',也是shell展开为'${MYPATH}'的等同物。两边
#的单引号是为了避免它更进一步展开,该字串传给eval,它会将其视为两个参数:
#echo与${MYPATH}。eval在环境下寻找MYPATH,假设找到就执行展开命令,并输
#出,通过管道传给tr命令将冒号转换为空格,最后将转化值给dirpath,错误信心隐藏
#输给/dev/null
dirpath=`eval echo '${'"$envvar"'}' 2>/dev/null | tr : ' ' `

#为错误情况进行健全检测
if test -z "$envvar"
then
 error Environment variable missing or empty
elif test "x$dirpath" = "x$envvar"
then
 error "Broken sh on this platform: cannot expand $envvar"
elif test -z "$dirpath"
then
 error Empty directory search path
elif test $# -eq 0
then
 exit 0
fi

#接下来三重循环,外层处理参数文件或模式,中层循环处理查找路径下的目录,内
#层循环匹配单一目录下的文件。
for pattern in "$@"
do
 result=
 for dir in $dirpath
 do
  for file in $dir/$pattern
  do
   if test -f "$file"
   then
    result="$file"
    echo $result
    test "$all" = "no" && break 2
   fi
  done
 done
 test -z "$result" && warning "$pattern: not found"
done

#限制退出状态是一般linux实现上的限制
test $EXITCODE -gt 125 && EXITCODE=125
exit $EXITCODE

这里作者给留了课后作业:增添一个功能,不单单只能匹配文件,也能匹配其他东西比如:符号性连接文件,可读取文件或者可执行文件之类的,需要test -x选项来进行匹配,本人完成的如下:

复制代码 代码如下:
#变量初始化的地方增添一个test选项变量,默认为f
testopt=f

#选项解析的位置增添test选项并检测合法性
        --test | --tes | --te | --t | -test | -tes | -te | -t )
                echo $2
                if echo $2 | grep -e "^[bcdefgGhkLOPrSstuwx]\{1\}$"
                then
                        testopt=$2
                        shift
                else
                        error "Unrecognized --test option: $2"
                fi

 
#最后循环test匹配位置改为:
  if test -"$testopt" "$file"

欧了,课后作业完成,安全性还没经验,有老师批改作业木有?

下边给了第二个脚本,是软件构建自动化,代码灰长长。。。这个打代码都码的头疼了,跳过吧,有兴趣童鞋自己搞定。我是不求甚解的先赶进度了,回来再搞。

复制代码 代码如下:
#! /bin/sh -
# 在一台或多台构建主机上,并行构建一个或多个包
#
# 语法:
# build-all [ --? ]
#    [ --all "..." ]
#    [ --check "..." ]
#    [ --configure "..." ]
#    [ --environment "..." ]
#    [ --help ]
#    [ --logdirectory dir ]
#    [ --on "[user@]host[:dir][,envfile] ..." ]
#    [ --source "dir..." ]
#    [ --userhosts "file(s)" ]
#    [ --version ]
#    package(s)
#
# 可选用的初始化文件:
# $HOME/.build/directories list of source directories
# $HOME/.build/userhosts  list of [user@]host[:dir][,envfile]

IFS='
  '

PATH=/usr/local/bin:/bin:/usr/bin
export PATH

UMASK=002
umask $UMASK

build_one(){
#语法:
# build_one [user@]host[:build-directory][,envfile]
 arg="`eval echo $1`"

 userhost="`echo $arg | sed -e 's/:.*$//'`"

 user="`echo $userhost | sed -e s'/@.*$//'`"
 test "$user" = "$userhost" && user=$USER

 host="`echo $userhost | sed -e s'/^[^@]@//'`"

 envfile="`echo $arg | sed -e 's/^[^,]*,//'`"
 test "$envfile" = "$arg" && envfile=/dev/null

 builddir="`echo $arg | sed -e s'/^[^,]*,//'`"
 test "$builddir" = "$arg" && builddir=/tmp

 parbase=`basename $PARFILE`
 #NB:如果这些模式被更换过,则更新find_package()
 package="`echo $parbase | \
  sed -e 's/[.]jar$//' \
           -e 's/[.]tar[.]bz2$//' \
           -e 's/[.]tar[.]gz$//' \
           -e 's/[.]tar[.]Z$//' \
           -e 's/[.]tar$//' \
           -e 's/[.]taz$//' \
           -e 's/[.]zip$//'`"
 #如果我们在远程主机上看不到包文件,则复制过去
 echo $SSH $SSHFLAGS $userhost "test -f $PARFILE"
 if $SSH $SSHFLAGS $userhost "test -f $PARFILE"
 then
  parbaselocal=$PARFLE
 else
  parbaselocal=$parbase
  echo $SCP $PARFILE $userhost:$builddir
  $SCP $PARFILE $userhost:$builddir
 fi
 #在远程主机上解开存档文件、构建,以及后台执行方式检查它
 sleep 1  #为了保证唯一的日志文件名
 now="`date $DATEFLAGS`"
 logfile="$package.$host.$now.log"
 nice $SSH $SSHFLAGS $userhost "
  echo '==================================================' ;
  test -f $BUILDBEGIN && . $BUILDBEGIN || \
   test -f $BUILDBEGIN && source $BUILDBEGIN || \
    true ;
  echo 'Package:   $package' ;
  echo 'Archive:   $PARFILE' ;
  echo 'Date:   $now' ;
  echo 'Local user:  $USER' ;
  echo 'Local host:  `hostname`' ;
  echo 'Local log directory: $LOGDIR' ;
  echo 'Local log file:  $logfile' ;
  echo 'Remote user:  $user' ;
  echo 'Remote host:  $host' ;
  echo 'Remote directory:  $builddir' ;
  printf 'Remote date:  ' ;
  date $DATEFLAGS ;
  printf 'Remote uname:  ' ;
  uname -a || true ;
  printf 'Remote gcc version: ' ;
  gcc --version | head -n 1 || echo ;
  printf 'Remote g++ version: ' ;
  g++ --version | head -n 1 || echo ;
  echo 'Configure environment: `$STRIPCOMMENTS $envfile | \
   $JOINLINES`' ;
  echo 'Extra environment:  $EXTRAENVIRONMENT' ;
  echo 'Configure directory: $CONFIGUREDIR' ;
  echo 'Configure flags:  $CONFIGUREFLAGS' ;
  echo 'Make all targets:  $ALLTARGETS' ;
  echo 'Make check targets: $CHECKTARGETS' ;
  echo 'Disk free report for $builddir/$package:' ;
  df $builddir | $INDENT ;
  echo 'Environment:' ;
  env | env LC_ALL=C sort | $INDENT ;
  echo '==============================================' ;
  umask $UMASK ;
  cd $builddir || exit 1 ;
  /bin/rm -rf $builddir/$package ;
  $PAR $parbaselocal ;
  test "$parbase" = "$parbaselocal" && /bin/rm -f $parbase ;
  cd $package/$CONFIGUREDIR || exit 1 ;
  test -f configure && \
   chmod a+x configure && \
    env `$STRIPCOMMENTS $envfile | $JOINLINES` \
     $EXTRAENVIRONMENT \
     nice time ./configure $CONFIGUREFLAGS ;
  nice time make $ALLTARGETS && nice time make $CHECKTARGETS ;
  echo '===============================================' ;
  echo 'Disk free report for $builddir/$package:' ;
  df $builddir | $INDENT ;
  printf 'Remote date: ' ;
  date $DATEFLAGS ;
  cd ;
  test -f $BUILDEND && . $BUILDEND || \
   test -f $BUILDEND && source $BUILDEND || \
    true;
  echo '===============================================' ;
 " < /dev/null > "$LOGDIR/$logfile" 2>&1 &
}

error(){
 echo "$@" 1>&2
 usage_and_exit 1
}

find_file(){
#语法:
# find_file file program-and-args
#如果找到,返回0,如果找不到返回1
 if test -r "$1"
 then
  PAR="$2"
  PARFILE="$1"
  return 0
 else
  return 1
 fi
}

find_package(){
#语法:
# find_package package-x.y.z
 base=`echo "$1" | sed -e 's/[-_][.]*[0-9].*$//'`
 PAR=
 PARFILE=
 for srcdir in $SRCDIRS
 do
  test "$srcdir" = "." && srcdir="`pwd`"
  for subdir in "$base" ""
  do
  #如果此列表有改变,则更新build_one()内的包设置
  find_file $srcdir/$subdir/$1.tar.gz "tar xfz" && return
  find_file $srcdir/$subdir/$1.tar.Z "tar xfz" && return
  find_file $srcdir/$subdir/$1.tar "tar xf" && return
  find_file $srcdir/$subdir/$1.tar.bz2 "tar xfj" && return
  find_file $srcdir/$subdir/$1.tar.tgz "tar xfz" && return
  find_file $srcdir/$subdir/$1.tar.zip "unzip -q" && return
  find_file $srcdir/$subdir/$1.jar "jar xf" && return
  done
 done
}

set_userhosts(){
#语法:
# set_userhosts file(s)
 for u in "$@"
 do
  if test -r "$u"
  then
   ALTUSERHOSTS="$ALTUSERHOSTS $u"
  elif test -r "$BUILDHOME/$u"
  then
   ALTUSERHOSTS="$ALTUSERHOSTS $BUILDHOME/$u"
  else
   error "File not found: $u"
  fi
 done
}

usage(){
cat <<EOF
 Usage:
  $PROGRAM [ --? ]
    [ --all "..." ]
    [ --check "..." ]
    [ --configure "..." ]
    [ --environment "..." ]
    [ --help ]
    [ --logdirectory dir ]
    [ --on "[user@]host[:dir][,envfile] ..." ]
    [ --source "dir ..." ]
    [ --userhosts "file(s)" ]
    [ --version ]
    package(s)
EOF
}

usage_and_exit(){
 usage
 exit $1
}

version(){
 echo "$PROGRAM version $VERSION"
}

warning(){
 echo "$@" 1>&2
 EXITCODE=`expr $EXITCODE + 1 `
}

ALLTARGETS=
altlogdir=
altsrcdirs=
ALTUSERHOSTS=
BUILDBEGIN=./.build/begin
BUILDEND=./.build/end
BUILDHOME=$HOME/.build
CHECKTARGETS=check
CONFIGUREDIR=.
CONFIGUREFLAGS=
DATEFLAGS="+%Y.%m.%d.%H.%M.%S"
EXITCODE=0
EXTRAENVIRONMENT=
INDENT="awk '{ print \"\t\t\t\" \$0 }'"
JOINLINES="tr '\n' '\040'"
LOGDIR=
PROGRAM=`basename $0`
SCP=scp
SSH=ssh
SSHFLAGS=${SSHFLAGS--x}
STRIPCOMMENTS='sed -e s/#.*$//'
userhosts=
VERSION=1.0

#默认的初始化文件
defaultdirectories=$BUILDHOME/directories
defaultuserhosts=$BUILDHOME/userhosts

#要寻找包分发的位置列表,如果用户未提供个人化列表,则使用默认列表:
SRCDIRS="`$STRIPCOMMENTS $defaultdirectories 2> /dev/null`"
test -z "$SRCDIRS" && \
 SRCDIRS=".
  /usr/local/src
  /usr/local/gnu/src
  $HOME/src
  $HOME/gnu/src
  /tmp
  /usr/tmp
  /var/tmp"
while test $# -gt 0
do
 case $1 in
 --all | --al | --a | -all | -al | -a )
  shift
  ALLTARGETS="$1"

 --cd | -cd )
  shift
  CONFIGUREDIR="$1"

 --check | --chec | --che | --ch | -check | -chec | -che | -ch )
  shift
  CHECKTARGETS="$1"

 --configure | --conf | --co | -configure | -conf | -co )
  shift
  CONFIGUREFLAGS="$1"

 --environment | --environ | -- envir | --e | -environment | \
  -environ | -envir | -e )
  shift
  EXTRAENVIRONMENT="$1"

 --help | --h | '--?' | -help | -h | '-?' )
  usage_and_exit 0

 --logdirectory | --log | --l | -logdirectory | -log | -l )
  shift
  altlogdir="$1"

 --on | --o | -on | -o )
  shift
  userhosts="$userhosts $1"

 --source | --s | -source | -s )
  shift
  altsrcdirs="$altsrcdirs $1"

 --userhosts | --u | -userhosts | -u )
  shift
  set_userhosts $1

 --version | --v | -version | -v )
  version
  exit 0

 -* )
  error "Unrecognized option: $1"

 * )
  break

 esac
 shift
done

#寻找适当的邮件客户端程序
for MAIL in /bin/mailx /usr/bin/mailx /usr/sbin/mailx /usr/ucb/mailx \
  /bin/mail /usr/bin/mail
do
 test -x $MAIL && break
done
test -x $MAIL || error "Cannot find mail client"

#命令行来源目录优先于默认值
SRCDIRS="$altsrcdirs $SRCDIRS"

if test -n "$userhosts"
then
 test -n "$ALTUSERHOSTS" &&
   userhosts="$userhosts `$STRIPCOMMENTS $ALTUSERHOSTS 2> /dev/null`"
else
 test -z "$ALTUSERHOSTS" && ALTUSERHOSTS="$defaultuserhosts"
 userhosts="`$STRIPCOMMENTS $ALTUSERHOSTS 2> /dev/null`"
fi

#检查是否要执行某些操作
test -z "$userhosts" && usage_and_exit 1

for p in "$@"
do
 find_package "$p"

 if test -z "$PARFILE"
 then
  warning "Cannot find package file $p"
 fi

 LOGDIR="$altlogdir"
 if test -z "$LOGDIR" -o ! -d "$LOGDIR" -o ! -w "$LOGDIR"
 then
  for LOGDIR in "`dirname $PARFILE`/logs/$p" \
  $BUILDHOME/logs/$p /usr/tmp /var/tmp /tmp
  do
   test -d "$LOGDIR" || mkdir -p "LOGDIR" 2> /dev/null
   test -d "$LOGDIR" -a -w "$LOGDIR" && break
  done
 fi

 msg="Check build logs for $p in `hostname`:$LOGDIR"
 echo "$msg"
 echo "$msg" | $MAIL -s "$msg" $USER 2> /dev/null

 for u in $userhosts
 do
  build_one $u
 done
done

#将退出状态限制为一般unix实际做法
test $EXITCODE -gt 125 && EXITCODE=125

exit $EXITCODE

个人原创,转载请注明:三江小渡