当完成了许多重要的功能改进时,对 shell 兼容性问题也做了大量的修正。您看,keychain 1.0 需要 bash ,而以后的版本则改为可以使用任何 sh 兼容的 shell。这一更改使得 keychain 跳出固有的框架,可以在包括 Linux、BSD、Solaris、IRIX 和 AIX 以及其它 UNIX 平台的几乎所有 UNIX 系统上运行。转至 sh 并与常规 UNIX 兼容,这已经是困难重重了,而同时它也经过了大量的学习经验。创建运行在所有这些平台上的单个脚本事实上是非常棘手的,主要因为我根本无权访问这些操作系统中的大多数系统!要感谢的是,全球范围内的 keychain 用户这样做了,并且许多人在识别兼容性问题以及提交补丁程序来解决它们等方面提供了非常大的帮助。
事实上,有两类兼容性问题必须解决。首先,我需要确信 keychain 只使用所有 sh 实现下完全支持的内置件、表达式和操作符,包括所有流行的免费和商业 UNIX sh shell、 zsh (以 sh 兼容的模式)和 bash 版本 1 和 2。这里是用户提交的应用到 keychain 源码中的一些 shell 兼容的修正:
由于较早的 sh shell 不支持 ~ 约定来引用用户的主目录,因此将使用 ~ 的行更改为使用 $HOME 来代替:
清单 3. 使之成为 $HOME
hostname=`uname -n` pidf=${HOME}/.ssh-agent-${hostname} cshpidf=${HOME}/.ssh-agent-csh-${hostname}
接着,所有对 source 的引用都更改成 . ,以确保与纯 NetBSD 的 /bin/sh 兼容,因为它根本不支持 source 命令:
清单 4. 迎合 NetBSD
if [ -f $pidf ] then . $pidf else SSH_AGENT_PID="NULL" fi
按照这个方法,我还应用了一些与性能相关的好的修正。一位聪明的 shell 脚本编写者告诉我不要通过输入 touch foo 来“更新”文件的修改日期,您可以这样做:
清单 5. 更新文件的修改日期
> foo
通过使用内置的 shell 语法,而不是使用外部二进制文件,这样避免了使用 fork() ,而脚本却变得更加有效。 > foo 应使用任何兼容 sh 的 shell;但是,好象 ash 并不支持它。对大多数人来说这不应是个问题,因为 ash 更象是急救磁盘类型的 shell,而不是人们每天都要使用的程序。
获取在多个 UNIX 操作系统下运行的脚本不单单需要坚持纯粹的 sh 语法。请记住,大多数脚本还要调用诸如 grep 、 awk 、 ps 和其它命令的外部命令,而且必须尽可能以与标准相符的方法来调用这些命令。例如,包含在大多数 UNIX 版本中的 echo 能识别 -e 选项,而 Solaris 中的 echo 却不能识别 ― 当使用它时,它只把 -e 打印到标准输出(stdout)。因此为了处理 Solaris, keychain 现在自动检测 echo -e 是否起作用:
清单 6. 寻找 Solaris
if [ -z "`echo -e`" ] then E="-e" fi
上面的代码中,如果支持 -e ,那么将 E 设置为 -e 。然后,可以按如下所示调用 echo:
清单 7. 更好的 echo
echo $E Usage: ${CYAN}${0}${OFF} [ ${GREEN}options${OFF} ] ${CYAN}sshkey${OFF} ...
通过使用 echo $E ,而不是 echo -e ,可以根据需要动态地启用或禁用 -e 选项。
可能最重要的兼容性修正涉及到更改 keychain 如何检测当前正在运行的 ssh-agent 的进程的方法。以前,我使用 pidof 命令来这样做,但是由于有几个系统没有 pidof ,所以不得不抛弃这个方案。实际上, pidof 无论如何都不是最佳的解决方案,因为它列出系统上运行的 所有 ssh-agent 进程,而不管用户是谁,但我们实际上感兴趣的是当前有效的 UID 所拥有的所有 ssh-agent 进程。
所以,为抽取所需的进程标识,我们不使用 pidof ,而是转向将 ps 输出通过管道输送到 grep 和 awk 上。 这是一个用户提交的修正:
清单 8. 管道比 pidof 好
mypids=`ps uxw | grep ssh-agent | grep -v grep | awk '{print $2}'`
上面的管线将 mypids 变量设置为当前用户拥有的所有 ssh-agent 进程的值。 grep -v grep 命令是管线的一部分,这样确保 grep ssh-agent 进程不会成为我们的 PID 列表中的一部分。
这种方法从概念上来说非常好,但是因为 ps 选项未在各类 BSD 和 System V 的 UNIX 派生系统上标准化,所以使用 ps 开启了一个全新的尚未解决的难题。这里是一个示例:虽然 ps uxw 在 Linux 下起作用,而在 IRIX 下不起作用。 ps -u username -f 在 Linux、IRIX 和 Solaris 下起作用,而在只理解 BSD 样式的 ps 选项的 BSD 下不起作用。为了解决这个问题,在执行 ps 管线之前, keychain 会自动检测当前系统的 ps 是使用 BSD 语法或还是 System V 语法:
清单 9. 检测 BSD 还是 System V
psopts="FAIL" ps uxw >/dev/null 2>&1 if [ $? -eq 0 ] then psopts="uxw" else ps -u `whoami` -f >/dev/null 2>&1 if [ $? -eq 0 ] then psopts="-u `whoami` -f" fi fi if [ "$psopts" = "FAIL" ] then echo $0: unable to use \"ps\" to scan for ssh-agent processes. Report KeyChain version and echo system configuration to drobbins@gentoo.org. exit 1 fi mypids=`ps $psopts 2>/dev/null | grep "[s]sh-agent" | awk '{print $2}'` > /dev/null 2>&1
为了确保我们能同时使用 System V 和 BSD 样式的 ps 命令,脚本试���运行 ps uxw ,而丢弃任何输出。如果这个命令的错误码为零,那么我们知道 ps uxw 正常工作,并且我们正确地设置了 psopts 值。但是,如果 ps uxw 返回一个非零的错误码(指出我们需要使用 BSD 样式的选项),那么我们试着运行 ps -u `whoami` -f ,并再次丢弃了任何输出。此时,我们有希望发现可以使用的 ps 是 BSD 的变体还是 System V 的变体。如果我们不知道答案,那么打印出错误并退出。但是很有可能这两个 ps 命令中的一个工作正常,在这样的情况下,执行上面代码段的最后一行,即 ps 管线。通过紧跟在 ps 后面使用 $psopts 变量扩展,我们能将正确的选项传送给 ps 命令。
ps 管线还包含一个 grep ,它的确是一个宝物,是 Hans Peter Verne 好心发给我的。 请注意 grep -v grep 不再是管线的一部分;实际上它已经被除去并且 grep "ssh-agent" 已经改为 grep "[s]sh-agent" 。这样一个 grep 命令最后执行与 grep ssh-agent | grep -v grep 相同的操作;您知道为什么吗?
清单 10. 简洁的 grep 诀窍
mypids=`ps $psopts 2>/dev/null | grep "[s]sh-agent" | awk '{print $2}'` > /dev/null 2>&1
是不是有点困惑?如果您确定 grep "ssh-agent" 和 grep "[s]sh-agent" 应匹配完全相同的文本行的话,那么您是正确的。所以当 ps 的输出通过管道输送给它们时,为什么它们生成不同的结果呢?这里是它的工作原理:当使用 grep "[s]sh-agent" 时,您更改了 grep 命令在 ps 进程列表中显示的方式。通过这样做,防止 grep 与它本身相匹配,因为 [s]sh-agent 字符串与 [s]sh-agent 正则表达式不匹配。那样不是很完美吗?如果您仍不太明白,请用一下 grep ,您很快就会明白了。