Username: Password:

标准化您的 UNIX 命令行工具
来源:IBM作者:IBM 发布时间:2008-04-07 02:56:57

本文介绍用于标准化界面以简化在不同 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 标准。
  • 也能够选择对为所使用的任务提供最好组合的命令集进行标准化,挑选您喜欢的命令并为特定平台上不存在的命令构建包装。
  • 您能够创建自己的一套执行特定任务的脚本(包括您自己用于 lsps 等常用工具的替代脚本),以便他们生成您想要的信息。这样做有点危险,原因是他意味着您可能从未使用原始命令,从而可能在您的脚本不可用时导致潜在的问题。

怎样具体实现各个命令的包装以提供一个兼容或唯一的层,这取决于您是尝试简单地为功能相同的替代命令提供一个公认名称,还是需要构建一个或多个命令的包装以获得等效的结果。可能的解决方案有三种:

  • 别名——这种解决方案仅在某些外壳中受支持——别名提供了将给定的字符串展开为特定命令的简单方法。
  • 外壳函数——大多数现代外壳都支持这种解决方案——外壳函数使您能够创建更复杂的序列,但是由于他们作为内置函数运行,在差异相当小时可能更为实用。
  • 外壳脚本——当您要构建的包装特别复杂时,更好的解决方案是使用外壳脚本,您能够代替原始命令调用这些脚本。使用外壳脚本,您能够更创造性地处理替代,甚至为另一个命令提供完全由外壳脚本驱动的替代。

让我们研究一下每种可能的解决方案和一些可通过此方法来进行模拟的示例命令。

使用别名

别名在 Korn (ksh)、Bourne-Again SHell (bash)、TENEX C shell (tcsh) 和 Z shell (zsh) 外壳中受支持,当您希望配置命令的特定选项,同时仍然支持其他选项时,别名提供了也许是最简单的方法。顾名思义,您能够将一个命令用作另一个命令的别名,或为带有附加选项的同一个命令提供别名。别名从您键入的内容展开为其展开形式。

例如,一个常用的别名是 ll,他调用等效的 ls -lll 通常称为长清单 (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. 在 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)。


清单 7. 定义一个执行和 killall 相同的信号发送功能的函数

function killall()
            {
            ps -ef|grep $1|awk ’{ print $2; }’|xargs kill -9
            }
            

请注意,该函数的 awk 部分中的 $2 不会展开,因为您已对 awk 脚本定义使用了单引号,这样阻止了展开,并且在此示例中会挑选第二列。

和别名相同,指定外壳函数的最好位置是在外壳的初始化脚本中。函数的局限性在于,他们依赖外壳提供支持能力,而这并不总是可能或可用。

虽然能够随心所欲地使内联外壳函数变得任意长,但在许多情况下,外壳函数并不理想。例如,在模拟更复杂的命令或提供命令包装的超长序列中,您需要分析选项并提供本地化的等效命令,此时内联函数就没有多大用处了。在这种情况下,外壳脚本可能更为适合。

使用脚本

构建一致环境的最容易和最兼容的方法,是创建可用作实际命令的包装的外壳脚本,这样考虑了您希望支持的各种选项和配置。

例如,useraddadduser 命令在配置参数(如用户 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 是用作 adduseruseradd 命令的包装的示例外壳脚本。


清单 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 $*
            

该脚本的关键是 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

使用单个源

无论您是使用多个脚本还是单个配置脚本/别名来支持统一的环境,您也许都希望使用单独一组脚本来支持系统。因此,配置新系统以使用标准化脚本(无论他们是单独脚本还是安装外壳函数和别名)很简单,只需将他们复制到新系统即可。

通过使用命令行工具和外壳流控制(如 ifcase)的组合,您能够使用单个源来选择各种要使用的选项。有两个工具在这种情况下很有用:一个工具识别主机(如 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
            

也能够在脚本中使用同样的基本过程来选择特定的序列。

在使用内联外壳函数时,和在每次使用函数时才做出决定相比,使用类似如此的包装来选择正确的函数定义通常更容易,因为这样做会更加高效。

总结

规范化环境对于简化管理大有帮助。他减轻了您的负担,帮助您确定所在的系统类型,连同哪个命令和/或选项集最适合于获取所需信息或执行所需操作。为每个命令选择正确的机制完全取决于该命令和您要尝试达到的目的。

在您希望调用命令行选项的单个命令上,最好使用别名机制。内联函数最适合于您希望容易地将其嵌入当前脚本环境的更复杂操作和序列,而完整的单独脚本则最适合于麻烦的多步骤操作,或您希望为命令(或选项)提供支持而不更改外壳环境的场合。

尽管有这些明显的长处,但是务必要记住,假如将自己过于屏蔽在基础的系统之外,当发生故障而您无法访问脚本时,您可能处于无准备的状态——您应该寻求扩展和扩充,而不是替代。

 

喜欢本文,那就收藏到:

    Del.icio.us Google书签 Digg Live Bookmark Technorati Furl Yahoo书签 Facebook 百度搜藏 新浪ViVi 365Key网摘 天极网摘 和讯网摘 博拉网 POCO网摘 添加到饭否 QQ书签 Digbuzz我挖网
相关评论  我也要评论
还没有关于此文章的相关评论!
  • 昵称: (为空则显示guest)
  • 评论分数: ★ ★ ★★★ ★★★★ ★★★★★
  • 评论内容:(不能超过250字,需审核后才会公布,请自觉遵守互联网相关政策法规。
  • 导航
    赞助商
    文章类别
    订阅