文章目录
- 前言
- 路径查找
- 软件构建自动化
前言
本篇,我们将进一步处理更复杂的工作。我们认为这里举出的例子都是一般用得到的工具,它们每一个都截然不同,且在大多数UNIX工具集里也没有。
在篇中的程序,包括命令行参数分析、在远程主机上运算、环境变量、工作记录、并行处理、使用eval
的运行时语句执行、草稿文件、Shell函数、用户定义初始文件,以及安全性议题考虑的范例。程序会运用Shell语言里的重要语句,并展现传统的UNIX Shell脚本编写风格。
路径查找
有些程序支持在目录路径上查找输入文件,有点像UNIX Shell查找以冒号隔开的目录列表,列在PATH
内,以找出可执行的程序。这对用户来说很方便,他只要提供较短的文件名,且不需要知道它们在文件系统里的位置。UNIX并为提及任何特殊命令或系统调用,可在查找路径下寻找文件,即使在很久以前有其他操作系统支持这种功能。幸好,要作路径查找不难,只要用对工具就行。
与其实现路径查找以寻找特定程序,不如做一个新的工具,以变量遍历名称为参数,而环境变量名称展开是预期的查找路径,后面接着零个或更多的文件模式,并报告匹配文件的位置。我们的程序将在其他需要路径查找支持的软件中,成为一般性工具(这也就是我们先前提到的“构建特定工具前,先想象”的原则)。
有时你得知道路径下的某个文件是不是不止一个,因为当路径下存有不同的版本时,你可能需要调整路径,控制要找到的版本。我们的程序会为用户提供一个命令行选项,以选择报告第一个找到的文件还是报告所有找到的文件。另外,根据用户要求提供一个可识别的版本编号,变成软件的标准实现,而且必须提供简短的在线帮助,让用户不需要在每次使用程序时,都重读程序的使用手册才能想起选项的名称。我们的程序当然也提供这样的功能。
我们先分析伪码程序,视为了注释与描述Shell程序代码的各个片段的顺序,以便说明。
我们从一般性的介绍性注释块开始,首行为识别程序的神奇行,/bin/sh
来执行脚本,注释块后,接的是程序行为的描述,并说明使用方法:
#! /bin/sh -
#
# 在查找路径下寻找一个或多个原始文件或文件模式,
# 查找路径由一个指定的环境变量所定义
#
# 标准输出产生的结果,通常是在查找路径下找到的每个文件之第一个实体的完整路径,
# 或是“filename:not found”的标准错误输出。
#
# 如果所有文件都找到,则退出码为0
# 否则,即为找不到的文件个数 - 非零值
# (Shell 的退出码限制为 125)。
#
#
# 语法:
# pathfind [--all] [--?] [--help] [--version] envar pattern(s)
#
# 选项 --all指的是寻找路径下的所有目录
# 而不是找到第一个就停止
在网络的环境下,安全性一直是必须慎重考虑的问题。其中有一种攻击Shell脚本的方式,是利用输入字段分隔字符:IFS
,它会影响Shell接下来对输入数据解释的方式。为避免此类型的攻击,部分Shell仅在脚本执行前,将IFS
重设为标准值;其他则导入该变量的一个外部设置。我们则是将自己做的预防操作放在脚本的第一步:
IFS='
很难在屏幕上或是显示的页面上看出位于引号里的内容:它们是具有三个字符的字符串,包括换行字符、空格、以及定位字符(tab)
。IFS的默认值为:空格,定位字符、换行符
,不过如果我们以这种方式编写,那些会自动修建空白的编辑器可能会将结尾的空白截去,让字符串的值减少成只有一个换行字符。比较好的方式应该是更严谨的使用转义字符,例如IFS="\040\t\n"
,可是Bourne Shell并不支持这样的转义字符。
在我们重新定义IFS时有一点请特别留意。当"$*"
展开以恢复命令行时,IFS值的第一个字符,会被当成字段分隔符。我们在这个脚本里不使用"$*"
,所以重新安排IFS内的字符不会有影响。
另一种常见的安全性攻击,则是欺骗软件,它执行非我们所预期的命令。为了阻断这种攻击,我们希望调用的程序是可信任的版本,而非潜伏在用户提供的查找路径下的欺骗程序,因此我们将PATH重设为一个最小值,以存储初始值供以后使用:
OLDPATH="$PATH"
PATH=/bin:/usr/bin
export PATH
export
语句是这里的关键:它可以确保所有子进程继承我们的安全查找路径。
程序代码接下来是5个以字母顺序排列的简短函数。
第一个函数为error()
函数,在标准错误输出上显示其参数,再调用一个函数(此部分稍后说明),不返回:
error()
{
echo "$@" 1>&2
usage_and_exit 1
}
第二个函数usage()
会写出简短信息,显示程序的使用方式,并返回给它的调用者。需留意的是:这个函数需要程序名称,但不是以直接编码模式,它是从变量PROGRAM
取得程序名称,这个变量设置为程序被调用的名称。这可以让安装程序在重新命名程序时,无须修改程序代码,这常发生在与已安装的程序名称产生冲突时,也即具同样名称但具有不同用途的时候。函数本身很简单:
usage()
{
echo "Usage: $PROGRAM [--all] [--?] [--help] [--version] envvar pattern(s)"
}
第三个函数usage_and_exit()
,会产生语法信息,并以它的单一参数所提供的状态码退出:
usage_and_exit()
{
usage
exit $1
}
第四个参数version()
是在标准输出上显示程序版本编号,并返回给它的调用者。如同usage()
,它是使用PROGRAM
取得程序名称:
version()
{
echo "$PROGRAM version $VERSION"
}
最后一个函数warnning()
会在标准错误上显示它的参数,并对变量EXITCODE
加1,可记录已发出的警告信息的数目,并返回给调用者:
warnning()
{
echo "$@" 1>&2
EXITCODE=`expr $$EXITCODE +1`
}
EXITCODE
的自增运算POSIX中可以使用EXITCODE=$((EXITCODE + 1))
,不过还是有相当多系统不认得该POSIX的这个用法。
即使程序很短,我们其实完全不用写函数,除了避免程序代码重复外,其实它可以隐藏不相关的细节,这是良好的程序实现方式:告知我们正要做什么,而不是说明我们要如何作。
这时我们已经到达运行时的第一个被执行的语句了。先初始化5个变量,以记录选项的选择、用户提供的环境变量名称、退出码、程序名称以及程序版本编号:
all=no
envvar=
EXITCODE=0
PROGRAM=`basename $0`
VERSION=1.0
在我们的程序里,将遵循小写(字母)变量为本地函数或主程序代码体所使用,而大写变量则被整个程序全局性地共享。这里给all变量一个字符串值,而非一个数字,是因为这样可以让程序更清楚,而对运行时资源消耗的影响也微乎其微。
接下来是大型代码块,是在所有的UNIX程序里典型的命令行参数解析:当我们有一个参数时(由参数计数值$#
决定,且必须大于零),会根据参数的字符串值来选择case
语句的程序块,处理该函数:
while test $# -gt 0
do
case $1 in
case
选择器在不同环境下可能有不同的解读方式。GNU程序风格鼓励使用长的、描述性的选项名称,而不是长久以来用于UNIX里的那套旧的、隐秘式的单一字符选项。后者简洁式的做法,在选项数很少且程序使用频繁的时候还让人可以接受,如果不是这种情况,描述性的名称会比较好,用户只需要提供足够的信息——不要和其他选项重复即可。然而,当有其他程序提供相同的选项时,则应避免这种简略用法,这么做才能让用户更容易了解程序,确保日后程序新版本加入新选项时,不会产生意外的结果。
针对--all
选项,我们只要通过把变量all重新设置为yes
来记录找到选项的事实即可:
--all | --al |--a | -all | -al| -a)
all=yes
;;
--version、--help同理
。
case
选择器-*)
会匹配剩下的所有选项:我们会在标准错误输出上报告非法选项,并调用usage()
函数,提醒用户用法为何,再立即以失败状态码(1)退出:
-*)
error "Unrecongnized option: $1"
;;
这里,匹配指出我们已经处理完所有的选项,所有可以退出循环。由于我们已处理完所有的情况,所以这里用终结关键字来结束case
语句:
*)
break
;;
esac
程序pathfind
的完整内容
#! /bin/sh -
#
# 在查找路径下寻找一个或多个原始文件或文件模式,
# 查找路径由一个指定的环境变量所定义
#
# 标准输出产生的结果,通常是在查找路径下找到的每个文件之第一个实体的完整路径,
# 或是“filename:not found”的标准错误输出。
#
# 如果所有文件都找到,则退出码为0
# 否则,即为找不到的文件个数 - 非零值
# (Shell 的退出码限制为 125)。
#
#
# 语法:
# pathfind [--all] [--?] [--help] [--version] envar pattern(s)
#
# 选项 --all指的是寻找路径下的所有目录
# 而不是找到第一个就停止
IFS=' '
OLDPATH="$PATH"
PATH=/bin:/usr/bin
export PATH
error()
{
echo "$@" 1>&2
usage_and_exit 1
}
usage()
{
echo "Usage: $PROGRAM [--all] [--?] [--help] [--veriosn] envvar pattern(s)"
}
usage_and_exit()
{
usage
exit $1
}
version()
{
echo "$PROGRAM version $VERSION"
}
warning()
{
echo "$@" 1>&2
EXITCODE=`expr $EXITCODE + 1`
}
all=no
envvar=
EXITCODE=0
PROGRAM=`basename $0`
VERSION=1.0
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
shift
done
envvar="$1"
test $# -gt 0 && shift
test "x$envvar" = "xPATH" && envvar=OLDPATH
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
# 限制退出状态是一般UNIX实现上的限制
test $EXITCODE -gt 125 && EXITCODE=125
exit $EXITCODE
注意:break 2指的是跳出这里的2层循环
。
测试案例:
pathfind
确实是一个很有用的练习。除了它是个方便的新工具程序,而标准的GNU、POSIX以及UNIX工具集里都没有之外,它还拥有所有大多数UNIX程序的主要组成部分:参数解析、选项处理、错误报告以及数据处理。我们还说明了消除几个著名安全漏洞的三个步骤:加入-
选项已终止起始的Shell命令行,立即设置IFS及PATH
。该程序代码的好处是可以再利用,只需要作一点点修改,例如:前置的注释标志,IFS与PATH的分配、5个辅助函数、处理参数的while与case语句,而且至少外部循环会遍历收集命令行上的文件。
作为一个练习,你可以开始考虑,是不是该为pathfind
的这些扩展做出一些改变:
-
将标准输出与标准错误输出的重定向存储到
/dev/null
,并加上--quiet
选项抑制所有输出,所以唯一会看到的便是指出是否找到匹配的退出码。这个好用的程序功能,在cmp
的-s
与grep
的-q
选项里已经有了。 -
加上
--trace
选项,将每个要测试的文件完整路径响应到标准错误输出。 -
加上
--test x
选项,让test的-f选项
可以置换为其他值,例如-h(文件为符号性连接)、-r(文件是可读取的)、-x(文件是可具执行的)
等。 -
让
pathfind
扮演过滤器功能:如果命令行上没有指定的文件名,则它应该自标准输入读取文件列表。这么做会对程序的架构与组织产生什么样的影响? -
修补所有你能找得到的安全漏洞,例如最新安全性公告所列的议题。
(有兴趣的同学,自己研究...哈哈哈
)
软件构建自动化
由于UNIX可运行在多种平台,因此在构建软件包时,常见的实现方式是从源代码开始安装,而非直接安装二进制包。大型的UNIX站点常由数个平台结合而成,对管理者而言,最冗长麻烦的工作就是将包安装在这些不同的系统上。而这正是自动化的好机会。很多软件开发人员,已直接采用GNU项目下所开发的软件包惯例,包括:
-
包以压缩存档文件
package-x.y.z.tar,gz(或package-x.y.z.tar.bz2)
的形式发布,文件解开后将出现在package-x.y.z
目录下。 -
顶层
configure
脚本通常是由GNU的autoconf
命令,通过configure.in或configure.ac
文件里的规则列表自动产生。执行该脚本时,有时得加上一些命令行选项,便能产生定制的C/C++头文件,通常叫做config.h、衍生自Makefile.in
(模板文件)的一个定制Makefile
,并且有时候还会有其他文件。 -
Makefile目标(target)的标准集已详述于《The GNU Coding Standards》中,有all(全部构建)、check(执行验证测试)、clean(删除不需要的中间文件)、distclean(恢复目录到它的原始发布),以及install(在本地系统上安装所有必需的文件)。
-
被安装的文件位于Makefile文件里prefix变量所定义的默认树状结构目录下,并且可在配置时使用
--prefix=dir
命令行选项进行设置,或是通过一个本地系统范围的定制文件提供。默认的prefix为/usr/local
,但无权限的用户可能得使用$HOME/local
,或是使用$HOME/``arch
/local更好。其中arch
是一条命令,它会显示定义平台的简短说明。GNU/Linux与Sun Solaris提供的是/bin/arch
。在其他平台下,安装自己的实作程序时,通常只是使用简单的Shell脚本包装(wrapper
),在搭配适当的echo
命令。
接下来的工作就是建立脚本,它被给定一个包列表,在目前系统下的许多标准位置之一,找到它们的源分发,将它们复制到远程主机列表中的每一个,在哪里解开它们,然后编译并使其成为合法可用状态。我们发现自动化安装步骤不是聪明的做法:构建日志必须先审慎地检查。
这个脚本必须让UNIX站点里的所有用户都能使用,所以我们不能在它里面内嵌有关特定主机的信息。我们假设用户已经提供了两个定制文件:directories——列出要查找包分发文件的位置,以及userhosts——列出用户名称、远程主机名称、远程构建目录以及特殊环境变量
。我们将这些及其他相关文件放在隐藏目录$HOME/.build
下,以降低混乱的程度。然而,因为在同一个站点内所有用户下的来源目录列表可能都相类似,所以我们包括一个合理的默认列表,就不再需要directories
文件。
有时候构建要能够只在一般构建主机的子集主机上完成,或是使用不再一般位置里的存档(archive)文件,因此,脚本要能够在命令行上设置这些值。
我们在这里开发的脚本可以这样调用:
$ build-all coreutils-5.2.1 gawk-3.1.4 #在所有主机上构建这两个包
$ build-all --on loaner.example.com gnupg-1.2.4 #在指定主机上构建包
$ build-all --source $HOME/work butter-0.3.7 #从非标准位置中构建包
这些命令其实做了很多事,下面我们大致列出,处理每个指定的软件包及在默认的或选定的构建主机上安装的步骤:
-
在本地文件系统下寻找包分发文件。
-
将分发文件复制到远程构建主机。
-
初始化远程主机上的登录连接。
-
切换到远程构建目录,并解开分发文件。
-
切换到包构建目录并设置、构建于测试包。
-
将初始化主机上的所有输出,分别为每个包与构建环境,记录在分开的日志文件中。
在远程主机上的构建以并行方式进行,所以安装执行所需要的总时间是以最慢的那台机器为准,而不是把所有单个时间求和。对于动辄安装百种以上不同环境系统的我们来说,幸好有build-all
程序,这也为包开发人员提供了一个不错的挑战。
完整程序如下:
#! /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'/^.*://' -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/[.]tgz$//' \
-e 's/[.]zip$//'`"
#如果我们在远程主机上看不到包文件,则复制过去
echo $SSH $SSHFLAGS $userhost "test -f $PARFILE"
if $SSH $SSHFLAGS $userhost "test -f $PARFILE"
then
parbaselocal=$PARFILE
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 || \
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 `$STRIPCOMMETS $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
# NB:如果此列表有改变, 则更新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.tgz "tar xfz" && return
find_file $srcdir/$subdir/$1.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="`$STRIPCOMMETS $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 | --configur | --configu | --config | --confi | \
--conf | --con | --co | \
-configure | -configur | -configu | -config | -confi | \
-conf | -con | -co )
shift
CONFIGUREFLAGS="$1"
;;
--environment | --environmen | --environme | --environm | --environ | \
--enviro | --envir | --envi | --env | --en | --e | \
-environment | -environmen | -environme | -environm | -environ | \
-enviro | -envir | -envi | -env | -en | -e )
shift
EXTRAENVIRONMENT="$1"
;;
--help | --hel | --he | --h | '--?' | -help | -hel | -he | -h )
usage_and_exit 0
;;
--logdirectory | --logdirector | --logdirecto | --logdirect | \
--logdirec | --logdire | --logdir | --logdi | --logd | --log | \
--lo | --l | \
-logdirectory | -logdirector | -logdirecto | -logdirect | \
-logdirec | -logdire | -logdir | -logdi | -logd | -log | -lo | -l )
shift
altlogdir="$1"
;;
--on | --o | -on | -o )
shift
userhosts="$userhosts $1"
;;
--source | --sourc | --sour | --sou | --so | --s | \
-source | -sourc | -sour | -sou | -so | -s )
shift
altsrcdirs="$altsrcdirs $1"
;;
--userhosts | --userhost | --userhos | --userho | --userh | \
--user | --use | --us | --u | \
-userhosts | -userhost | -userhos | -userho | -userh | \
-user | -use | -us | -u )
shift
set_userhosts $1
;;
--version | --versio | --versio | --versi | --vers | --ver | --ve | --v | \
-version | -versio | -versi | -vers | -ver | -ve | -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="`$STRIPCOMMETS $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"
continue
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
大家自行阅读就好啦,有问题评论区讨论或网上搜索即可!肯定是有点难度的,慢慢来