Shell脚本学习指南(七)——产生脚本


前言

本篇,我们将进一步处理更复杂的工作。我们认为这里举出的例子都是一般用得到的工具,它们每一个都截然不同,且在大多数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-sgrep-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  #从非标准位置中构建包

这些命令其实做了很多事,下面我们大致列出,处理每个指定的软件包及在默认的或选定的构建主机上安装的步骤:

  1. 在本地文件系统下寻找包分发文件。

  2. 将分发文件复制到远程构建主机。

  3. 初始化远程主机上的登录连接。

  4. 切换到远程构建目录,并解开分发文件。

  5. 切换到包构建目录并设置、构建于测试包。

  6. 将初始化主机上的所有输出,分别为每个包与构建环境,记录在分开的日志文件中。

在远程主机上的构建以并行方式进行,所以安装执行所需要的总时间是以最慢的那台机器为准,而不是把所有单个时间求和。对于动辄安装百种以上不同环境系统的我们来说,幸好有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

大家自行阅读就好啦,有问题评论区讨论或网上搜索即可!肯定是有点难度的,慢慢来

猜你喜欢

转载自blog.csdn.net/m0_56145255/article/details/128305360
今日推荐