红联Linux门户
Linux帮助

标准化您的 UNIX 命令行工具

发布时间:2006-10-13 00:17:44来源:红联作者:smallwl
  本文介绍用于标准化界面以简化在不同 UNIX® 系统之间移动的方法。如果您管理多种 UNIX 系统(特别是在异构环境中),则最艰巨的任务可能是在不同环境之间切换并执行不同的任务,同时还必须考虑系统之间的所有差异。本文并不介绍特定的差异,而是研究能够提供兼容层(或包装)以支持一致环境的方法。
关于本系列
典型的 UNIX® 管理员拥有一套经常用于辅助管理过程的关键实用工具、诀窍和系统。存在各种用于简化不同过程的关键实用工具、命令行链和脚本。其中一些工具来自于操作系统,而大部分的诀窍则来源于长期的经验积累和减轻系统管理员工作压力的要求。本系列文章主要专注于最大限度地利用各种 UNIX 环境中可用的工具,包括简化异构环境中的管理任务的方法。
差异和问题
如果您使用多种 UNIX 主机,特别是每种主机都支持不同的 UNIX 风格(Berkeley Software Distribution (BSD)、UNIX System Release 4 (VSVR4) 等)或版本,您也许发现自己要花大量的时间来检查和确定自己所在的主机类型,以便能够适应系统操作方式的变化。
例如,ps 命令在基于 BSD 和基于 SVR4 的 UNIX 主机上,分别需要不同的命令行选项来获得大致相同的信息(有关更多细节,请参阅 系统管理员工具包: 进程管理技巧)。平台之间还存在更广泛的差异。有时,这种差异是命令名称发生了更改;Linux® 提供 adduser 命令,而 Solaris 则提供 useradd 命令。
就标准化而言,有多种方法可供您采用。
您可以选择对主要平台(例如 Solaris)进行标准化,并在其他平台上提供等效命令的包装以匹配 Solaris 标准。
也可以选择对为所使用的任务提供最佳组合的命令集进行标准化,挑选您喜欢的命令并为特定平台上不存在的命令构建包装。
您可以创建自己的一套执行特定任务的脚本(包括您自己用于 ls、ps 等常用工具的替代脚本),以便它们生成您想要的信息。这样做有点危险,原因是它意味着您可能从未使用原始命令,从而可能在您的脚本不可用时导致潜在的问题。
如何具体实现各个命令的包装以提供一个兼容或唯一的层,这取决于您是尝试简单地为功能相同的替代命令提供一个公认名称,还是需要构建一个或多个命令的包装以获得等效的结果。可能的解决方案有三种:
别名----这种解决方案仅在某些外壳中受支持----别名提供了将给定的字符串展开为特定命令的简单方法。
外壳函数----大多数现代外壳都支持这种解决方案----外壳函数使您能够创建更复杂的序列,但是由于它们作为内置函数运行,在差异相当小时可能更为实用。
外壳脚本----当您要构建的包装特别复杂时,更好的解决方案是使用外壳脚本,您可以代替原始命令调用这些脚本。使用外壳脚本,您可以更创造性地处理替代,甚至为另一个命令提供完全由外壳脚本驱动的替代。
让我们研究一下每种可能的解决方案和一些可通过此方法来进行模拟的示例命令。
使用别名
别名在 Korn (ksh)、Bourne-Again SHell (bash)、TENEX C shell (tcsh) 和 Z shell (zsh) 外壳中受支持,当您希望设置命令的特定选项,同时仍然支持其他选项时,别名提供了也许是最简单的方法。顾名思义,您可以将一个命令用作另一个命令的别名,或者为带有附加选项的同一个命令提供别名。别名从您键入的内容展开为其展开形式。
例如,一个常用的别名是 ll,它调用等效的 ls -l(ll 通常称为长清单 (long listing))。每当用户键入 ll,就会直接将其替换为展开形式,因此:$ ll a* 在执行前展开为:$ ls -l a*。
命令行选项也仍然有效,换句话说,$ ll -a 展开为:$ ls -l -a。
还可以为现有命令设置别名;假设将 -F 选项添加到所有 ls 命令,这样,$ ls 将展开为:$ ls -F。
要设置别名,请使用内置的外壳 alias 语句,并在引号中指定所需的展开形式。例如,要设置前面详细描述的 ll 的展开形式,可使用:$ alias ll='ls -l'。
别名在以下情况下最为有用:您希望使用 base 命令并容易地指定附加选项,同时仍然允许设置特定于平台的选项。
一个很好的例子就是 ps 命令,它在基于 SVR4 和基于 BSD 的 UNIX 主机上是不同的。在本系列的第一篇文章中,请参阅 系统管理员工具包: 进程管理技巧 ----这篇文章解释了如何使用 ps 的选项来获得相似的清单。您可以结合别名使用那些选项,而不会影响您指定附加选项的能力。例如,在 BSD 上,您将如清单 1 所示指定别名。
清单 1. 在 BSD 上指定别名
$ alias ps='ps -o pid,ppid,command'
而在 SVR4 主机上,您将如清单 2 所示创建别名。
清单 2. 在 SVR4 上指定别名
$ alias ps='ps -opid,ppid,cmd
现在,在这两个系统对 ps 的不同操作方式的限制下,您获得了 ps 产生的标准输出。和前面一样,您可以继续添加更多选项;例如,在安装了该别名的任一个平台上请求所有进程,添加 -A 选项就是这样一种情况。这会在 BSD(在此示例中为 Mac OS X)上产生类似于清单 3 的输出。
文章评论

共有 3 条评论

  1. smallwl 于 2006-10-13 00:19:37发表:

    该脚本的关键是 foreach 循环,它遍历所提供的命令行参数(在 $* 中提供)。对于每个选项,case 语句会尝试识别该选项----使用短格式或长格式并设置一个变量。命令行开关为 $1。如果该选项后面正常地跟着一个值(例如,用户 ID),您可以将 $2 当作该值来进行访问,并使用它将该值赋于某个变量。
    识别出某个选项后,shift 语句从 $* 变量列表中移动一个位置(若指定了数字,则移动指定数目的位置),以便已经识别出的命令行参数在循环的下一次迭代中不再在 $* 变量中。
    识别并提取出可能的参数以后,您所需做的就是构建新的选项来提供给最终要使用的命令。由于 useradd/adduser 都支持短格式的参数,所以可在此基础上构建新的命令选项字符串。这是通过检查对应的变量是否已设置并将该选项添加到命令行来实现的。请注意双引号的使用,它确保了原始命令中引用的参数被保留并得到正确识别。
    将该脚本安装在支持任一种原始命令的平台上以后,您现在可以添加用户并指定所要的选项,包括对参数进行混合和匹配(请参见清单 9)。
    清单 9. 添加用户
    $ adduser.sh --homedir /etc -g wheel --shell /bin/bash -c "New user" mcbrown
    同样的基本原理也可以用于构建其他命令的包装,甚至更改参数名称和选项,或者提供等效的表达式。
    如果希望用原始名称安装该脚本----例如,adduser----并将其放在某个目录中(例如,/usr/local/compat),您必须确保该目录在 PATH 中出现在实际命令的目录之前。下面是假设将兼容性脚本放在 /usr/local/compat 目录中的一个例子:$ PATH=/usr/local/compat:$PATH。
    使用单个源
    无论您是使用多个脚本还是单个配置脚本/别名来支持统一的环境,您也许都希望使用单独一组脚本来支持系统。因此,设置新系统以使用标准化脚本(无论它们是独立脚本还是安装外壳函数和别名)非常简单,只需将它们复制到新系统即可。
    通过使用命令行工具和外壳流控制(如 if 或 case)的组合,您可以使用单个源来选择各种要使用的选项。有两个工具在这种情况下很有用:一个工具识别主机(如 hostname 或 uname),另一个工具识别平台 (uname)。
    uname 产生的缺省输出是基本操作系统名称,如 Linux 或 Solaris。例如,可以按照前一部分中的 ps 示例,将该命令与 case 语句结合使用以选择正确的别名,如清单 10 所示。
    清单 10. uname 的输出
    UNAME='uname'
    case "$UNAME" in
    FreeBSD|NetBSD|Darwin)
    alias ps='ps -o pid,ppid,command'
    break
    ;;
    Solaris|Linux)
    alias ps='ps -o pid,ppid,cmd'
    break
    ;;
    esac
    也可以在脚本中使用同样的基本过程来选择特定的序列。
    在使用内联外壳函数时,与在每次使用函数时才做出决定相比,使用类似如此的包装来选择正确的函数定义通常更容易,因为这样做会更加高效。
    总结
    规范化环境对于简化管理大有帮助。它减轻了您的负担,帮助您确定所在的系统类型,以及哪个命令和/或选项集最适合于获取所需信息或执行所需操作。为每个命令选择正确的机制完全取决于该命令和您要尝试达到的目的。
    在您希望调用命令行选项的单个命令上,最好使用别名机制。内联函数最适合于您希望容易地将其嵌入当前脚本环境的更复杂操作和序列,而完整的单独脚本则最适合于麻烦的多步骤操作,或您希望为命令(或选项)提供支持而不更改外壳环境的场合。
    尽管有这些明显的优点,但是务必要记住,如果将自己过于屏蔽在基础的系统之外,当发生故障而您无法访问脚本时,您可能处于无准备的状态----您应该寻求扩展和扩充,而不是替代。

  2. smallwl 于 2006-10-13 00:19:02发表:

    清单 7. 定义一个执行与 killall 相同的信号发送功能的函数
    function killall()
    {
    ps -ef|grep $1|awk '{ print $2; }'|xargs kill -9
    }
    请注意,该函数的 awk 部分中的 $2 不会展开,因为您已经对 awk 脚本定义使用了单引号,这样阻止了展开,并且在此示例中会挑选第二列。
    与别名一样,指定外壳函数的最佳位置是在外壳的初始化脚本中。函数的局限性在于,它们依赖外壳提供支持能力,而这并不总是可能或可用。
    虽然可以随心所欲地使内联外壳函数变得任意长,但在许多情况下,外壳函数并不理想。例如,在模拟更复杂的命令或提供命令包装的超长序列中,您需要分析选项并提供本地化的等效命令,此时内联函数就没有多大用处了。在这种情况下,外壳脚本可能更为适合。
    使用脚本
    构建一致环境的最容易和最兼容的方法,是创建可用作实际命令的包装的外壳脚本,这样考虑了您希望支持的各种选项和设置。
    例如,useradd 和 adduser 命令在设置参数(如用户 ID 或组成员资格)时支持同样的单字母命令行选项,因此 Linux 上的 $ adduser -u 1000 -G sales,marketing mcbrown 等效于 Solaris 上的 $ useradd -u 1000 -G sales,marketing mcbrown。
    然而,Linux 版本还支持扩展命令选项,例如,--uid 和 --groups 等效于上面的命令行选项。这些扩展选项在 Solaris 上不受支持,但是,如果创建一个名为 adduser 的外壳脚本,您就可以模拟 Linux 版本,然后用适当的选项运行实际的 Solaris useradd 命令。
    清单 8 是用作 adduser 或 useradd 命令的包装的示例外壳脚本。
    清单 8. 用作包装的示例外壳脚本
    #!/bin/bash
    # -*- shell-script -*-
    for i in $*
    do
    case $i in
    --uid|-u) OPT_UID=$2; shift 2;;
    --groups|-G) OPT_GROUPS=$2; shift 2;;
    --gid|-g) OPT_GROUP=$2; shift 2;;
    --home-dir|-d) OPT_HOMEDIR=$2; shift 2;;
    --shell|-s) OPT_SHELL=$2;shift 2;;
    --non-unique|-o) OPT_NONUNIQUE=1;shift 2;;
    --comment|-c) OPT_COMMENT=$2;shift 2;;
    esac
    done
    OPTS=""
    if [ -n "$OPT_$HOMEDIR" ]
    then
    OPTS="$OPTS -d $OPT_HOMEDIR"
    fi
    if [ -n "$GROUP" ]
    then
    OPTS="$OPTS -g $OPT_GROUP"
    fi
    if [ -n "$OPT_GROUPS" ]
    then
    OPTS="$OPTS -G $OPT_GROUPS"
    fi
    if [ -n "$OPT_SHELL" ]
    then
    OPTS="$OPTS -s $OPT_SHELL"
    fi
    if [ -n "$OPT_UID" ]
    then
    OPTS="$OPTS -u $OPT_UID"
    fi
    if [ -n "$OPT_COMMENT" ]
    then
    OPTS="$OPTS -c \"$OPT_COMMENT\""
    fi
    if [ -n "$OPT_NOUNIQUE" ]
    then
    OPTS="$OPTS -o"
    fi
    CMD=adduser
    UNAME=`uname`
    case $UNAME in
    Solaris) CMD=useradd;break;;
    esac
    $CMD $OPTS $*

  3. smallwl 于 2006-10-13 00:18:25发表:

    清单 3. 在 BSD 上使用 -A 选项
    $ ps -A
    PID PPID COMMAND
    1 0 /sbin/launchd
    23 1 /sbin/dynamic_pager -F /private/var/vm/swapfile
    27 1 kextd
    32 1 /usr/sbin/KernelEventAgent
    33 1 /usr/sbin/mDNSResponder -launchdaemon
    34 1 /usr/sbin/netinfod -s local
    35 1 /usr/sbin/syslogd
    36 1 /usr/sbin/cron
    37 1 /usr/sbin/configd
    38 1 /usr/sbin/coreaudiod
    39 1 /usr/sbin/diskarbitrationd
    ...

    SVR4 系统(Gentoo Linux 主机)会显示同样的列,如清单 4 所示。
    清单 4. 在 SVR4 上使用 -A 选项
    $ ps -A
    PID PPID CMD
    1 0 init [3]
    2 1 [migration/0]
    3 1 [ksoftirqd/0]
    4 1 [watchdog/0]
    5 1 [migration/1]
    6 1 [ksoftirqd/1]
    7 1 [watchdog/1]
    8 1 [events/0]
    9 1 [events/1]
    10 1 [khelper]
    11 1 [kthread]
    14 11 [kblockd/0]
    15 11 [kblockd/1]
    16 11 [kacpid]
    ...
    另一个选项或多或少地镜像了本文其他地方给出的脚本和函数解决方案。该选项是为给定命令的特定输出创建别名,这些别名采用同一方法来提供相同的格式化输出。同样以 ps 为例,您可以创建别名 ps-all 来输出所有进程列表,并根据需要为每种平台设置相应的展开形式。
    设置这些别名的最佳位置是在登录期间执行的外壳初始化脚本中,例如 .ksh、.profile 或 .bashrc。您可以在这些脚本中执行同样的系统检查,以验证要启用哪些别名。如果希望提供适用于所有用户的全局解决方案,则应将别名定义放在公开可用的文件中(例如放在 /etc or /usr/local 中),并设置用户初始化脚本以获得别名定义来源。
    别名机制最适合于您希望设置单个命令的命令行选项的情况,虽然也可以使用它们来将给定的命令展开为一组命令或管道。这样削弱了为展开形式中除最后一个命令以外的其他任何命令指定附加参数的能力。对于处理此类包装,外壳中的内联函数可能更为适合。
    使用内联外壳函数
    大多数外壳都支持函数,这些函数本质上是微型脚本,您可以在其中放置命令和其他外壳脚本元素以执行特定的任务。由于它们是主外壳定义中的函数,因此使用起来方便快捷,同时仍然支持许多完整外壳脚本所具有的相同功能,如命令行参数。
    对于支持别名无法在其中工作的某些命令和组合,对命令行参数的支持非常关键。例如,killall 命令最基本的功能是终止所有与特定字符串匹配的命令。该命令并非在所有平台上都可用,但是一旦您了解了它,就会希望在其他环境中使用它。
    在 Solaris 上,killall 命令存在,但是将其用作关闭过程的一部分以终止所有进程。设想在 Solaris 主机上意外调用 killall 命令以关闭所有 Apache 进程,没想到却实际上关闭了系统!
    提供替代----在所有主机上使用相同的名称或使用不同的名称----可以实现按名称终止进程的预期结果,并消除不希望的和可能代价高昂的错误,同时扩展本身并不支持该选项的系统的功能。
    该命令的关键部分是能够识别正在运行的进程,提取与给定字符串匹配的进程,并使用 kill 命令将 KILL 信号发送到每个匹配进程。在命令行上,您可以通过一系列管道实现等效的功能(使用 KILL 信号),如清单 5 所示。
    清单 5. 提供 killall 命令的替代
    $ ps -ef|grep gcc|awk '{ print $2; }'|xargs kill -9

    该命令的关键部分是提供给 grep(在此示例中为 gcc)的字符串和 ps 输出中包含所需进程 ID 的列。上面的例子对 Solaris 主机和大多数 SVR4 UNIX 变种有效。
    别名在此示例中无法工作,因为您希望能够插入命令中的信息不在结尾;别名所实现的是一种展开方法。然而,内联外壳函数正好适合这种情况。
    在支持 Bourne 语法(bash 和 zsh)的外壳中,您可以使用清单 6 所示的以下语法来定义函数。
    清单 6. 定义函数
    function NAME()
    {
    # do stuff here
    }
    调用函数时,函数参数作为 $1、$2 等形式来提供,就像在典型的外壳脚本中一样。因此,您可以定义一个函数,使其执行与 killall 相同的基于字符串的信号发送功能(请参见清单 7)。