冷酷、锋利的边缘

作者:Todd Graham Lewis

Shell 范例被描述为(至少我这样认为)取自真正操作系统的最美妙的一些特性,并弯曲、扭曲、折叠、纺锤、并将其残害成极其迟钝和不完善的工具。当然,这些特性可以被弯曲、扭曲等等,并且仍然有效,这正是它们美妙之处的体现。

打开您的 Unix 工具箱(对于你们这些菜鸟来说是 /usr/bin),您将看到一套完整的工具,随时可以使用。正如一项基本技术的发现将人类历史的一个时代与另一个时代区分开来一样,Unix 下的重定向和作业控制创造了计算的黄金时代,这与 MS-DOS 的铁器时代苦工形成对比。由于能够简单地区分独立的、同时进行的过程,并在您自行决定时定向它们的输入和输出,因此您可以以多种方式使用这些工具来组装简单的 Unix 进程,这几乎没有限制。这种能力以及使用它的意愿构成了 shell 范例。

但是,权力所在之处也存在危险。单个进程在崩溃成一堆难以处理的意大利面条代码之前,可以承受多少 | &popen()?在我们简单的程序失控地冲向“内核崩溃:内存不足”的阴间之前,我们可以使用多少不同的编程环境?[很多——编者注]

本文将向您介绍如何在不同环境中混合和匹配可执行文件的 I/O 流。如果您正在编写 Perl 脚本,并且想要加入一点 grep 以获得更好的效果,请继续;这是可能的。最后,我们将讨论这些技术的局限性和智慧。

Shell

进程之间轻松通信的能力是 Unix 系统设计中固有的,因此“shell 范例”这个名称在某种程度上是一种用词不当。尽管如此,shell 是大多数人熟悉 I/O 重定向的环境,因此我们将从那里开始。正如我们稍后将看到的,所有这些设施都可以很容易地在 shell 提示符以外的地方重新创建。

在 shell 中有几种使用进程重定向的方法。例如,您可以获取一个进程的输出并将其定向到一个文件

cd ~; ls > /tmp/ls.file

或者,您可以将输出附加到现有文件

cd ~/bin; ls >> /tmp/ls.file

您还可以获取一个进程的输出并将其重定向为另一个进程的输入

cd ~; ls | grep lj.article

在大多数 shell 中,包括 Bourne 兼容的 bash 和 zsh,您可以将命令的输出集成到其他命令中。例如,如果您想要生成一个文件,并在文件名末尾附加昨天的日期,您可以执行以下操作

touch /usr/acct/atlanta/data.`
  date --date '1 day ago' +"%Y%m%d"
 `

这只是为我生成了一个名为 data.19960503 的文件。您得到的结果取决于您阅读Linux Journal的速度。这也取决于您正在运行哪个免费操作系统;FreeBSD 版本的 date 不提供 1 day ago 功能,因此如果您傻到不运行 Linux(或者如果您的雇主使用 FreeBSD),您将不得不获取并编译 gnu-date。)

C

当您需要一个已经作为 Unix 工具实现的功能,而您不想重新编码时,在 C 中包含外部命令就很好。例如,如果您需要对数据流进行排序或压缩输出文件,使用 sortgzip 而不是本地编码是完成任务的有效方法。在 C 中使用外部程序有两种方法:system()popen()

如果您在字符串中有大量数据,并且想要使用 sort 程序对其进行排序,您可以使用 popen() 调用 sort 程序,对数据进行排序,然后从程序中读回结果。如果您只想压缩一个文件,您可以使用更简单的 system() 函数。这两个函数对于 C 程序员来说都不陌生,但如果其中任何一个对来说不熟悉,请查阅 Linux 手册页,其中有关于它们的文档。如果您想要更多解释,您可以阅读 W. Richard Stevens 的Unix 环境高级编程

但是,如果您需要与您调用的程序交互,则可以使用 C 库来完成此操作,该库附带一个名为“Expect”的工具,稍后将在 Tcl 部分中介绍。

Perl 之母

虽然在 shell 中有许多不同的方法来操作进程 I/O,但在 Perl 中实际上只有一种方法:作为文件句柄。这实际上证明了 Perl 设计的美妙之处;向 Larry Wall 致敬,使其如此简单。

您可以从 Perl 中以几种不同的方式包含其他进程,所有方式都使用 open () 命令。例如,如果您想打开一个进程 bottle,Perl 脚本的输出应发送到该进程,您可以使用

open (BOTTLE, "| ~<bin/bottle"

来定向输出。同样,如果您想读取 bottle 的输入,您将执行大致相同的操作,在末尾添加管道符号 (|)

open (BOTTLE, "~<bin/bottle |")

在第一种情况下,您只能写入文件句柄 bottle,而在第二种情况下,您只能读取。

以这种方式打开的命令也可以变得很复杂。引号内的所有内容都在子 shell 中执行,因此以下任一命令都将起作用

open (BOTTLE, "cd ~; /bin/bottle |")
open (FIND, "cd /home/tlewis; find . -name $string -print |")

此时,许多人会问:“如果我想同时进行读取和写入怎么办?” 您无法使用 open () 命令执行此操作,因此 Perl 坏了吗?不,并非如此。您无法轻松打开双向管道这一事实是一个设计决策。正如 Unix FAQ 中解释的那样

尝试将输入和输出都管道传输到任意从属进程的问题在于,如果两个进程同时等待尚未生成的输入,则可能会发生死锁。

同样,可以使用 Expect 来完成此操作,我们稍后会看到。

一个简短的例子

#!/usr/bin/Perl
open (ACCT, "(cd /usr/acct/;".
  "for i in `ls | grep -v admin`; do; ".
  "cat $i/date.19960503; done) | sort |");
while (<ACCT>) {
     chop;
     ($A,$B,$C) = split;
     print "$C $A $B\n";
}

这将获取 /usr/acct/ 目录的有限子集中的数据,根据每个文件中每行的第一个条目对其进行排序,重新格式化数据并将其打印到标准输出。通过混合使用 Perl 和 shell 工具,这项工作变得容易得多。

Tcl/Tk

Tcl 是一种简单的脚本语言,设计为一种命令语言,可以轻松应用于各种 C 程序,以实现平滑的配置和用户交互。Tk 是一种从 Tcl 发展而来的语言,可以使用它来构建图形用户界面。人们通常将它们统称为 Tcl/Tk。

Tk 最近作为一种在 X-Windows 下构建图形界面的极其简单的方法而广受欢迎。如果您在使用任何最新的(自 1.3.60 起)开发内核时使用过 make xconfig,那么您就使用过 Tk。程序 Tkined(Linux 的网络管理工具)使用 Tk;它基于 Scotty,一个 Tcl 扩展,提供各种网络功能,例如访问 SNMP 数据。

根据其最初的设计目标,Tcl 允许您以相当直观的方式与外部进程交互。可以使用简单的 exec 命令在 Tcl 下执行简单命令。例如

exec ls | grep -v admin

返回的结果与之前的 Perl 示例完全相同,但将其打印到标准输出,很像 C 中的 system() 命令。

如果您希望与进程的输出进行交互或将信息定向到其输入,则需要将其与文件句柄关联,这与 Perl 中非常相似。这通过 open 命令完成,如

set g0 [open |sort r+]

这会打开命令 sort 以进行输入。您将在程序中的其他位置使用 puts 将数据发送到句柄 g0,然后使用 gets 从输出中读取数据。r+ 开关表示您可以同时将数据写入进程(要排序的数据)和从进程读取数据(排序后的数据)。如果您只希望将数据发送到标准输出,您将使用

set g0 [open |sort w]

让您可以写入该进程。

等等,您说,这意味着我可以同时从一个进程中读取和写入?是的,确实如此。Unix FAQ 不是说这是一件坏事吗?是的,它确实说了。如果您使用此功能构建相互锁定、自我供给的进程网络,那么您真的是在自找麻烦。如果您要这样做,请保持简单。

Expect

虽然这可能很危险,但人们对 Tcl 的这一功能如此狂热,以至于发明了 Tcl 的扩展 Expect,它本身就是一个编程环境,并且在某些用户中人气飙升至新的高度。

例如,ftp 是一个相当简单的程序。您通过命令行与本地程序进行交互,然后本地程序执行您的命令。由于这使用了简单的 Unix STDIN/STDOUT 交互方法,因此您可以编写 shell 脚本来 ftp 文件;我使用这样一个脚本从 Internet 自动检索 RFC。但是,像 telnet 这样的程序实际上不可能编写脚本,因为您不是将数据发送到程序本身,而是通过网络连接发送数据,以便在远程计算机上进行解释。因此,如果您需要维护大量路由器,并且配置或检查这些路由器的唯一方法是通过 telnet,那么您就麻烦了。

Expect 通过使用 Unix 的伪终端 (pseudo-tty) 机制解决了这个问题。使用 Expect,您可以编写程序与另一个程序之间的对话脚本,在其中您的程序可以智能地响应另一个程序。想想像 dip 或 chat 这样的拨号程序,只不过您可以编写与其他程序而不是调制解调器对话的脚本。

Expect 是程序间通信的最高境界,仅次于基于套接字或 sysV-ipc 的通信。(如果您不知道,请不要问。)虽然它最初是作为 Tcl 扩展开始的,但它也被呈现为 C 库;您可以从 C 程序或可以使用 C 库的其他环境(例如 Perl)访问其功能。

一帆风顺,但前方有礁石

在他的著作Tcl 和 Tk 工具包的引言中,John Ousterhout 提到,尽管 Tcl 最初被设计为一种简单的脚本语言,所有程序至少都有“一些新的 C 代码”,但它们为程序员提供的环境的简单性被证明太诱人了。“大多数 Tcl/Tk 用户从不编写任何 C 代码,”Ousterhout 写道,“大多数 Tcl/Tk 应用程序完全由 Tcl 脚本组成。”

这既是好事也是坏事,具体取决于您的标准是易用性还是效率/功能。为了回应 Tcl 的兴起,GNU 的杰出人物和都市传奇人物 Richard Stallman 以他一贯低调的方式发布了一篇 USENET 文章,题为“为什么你不应该使用 Tcl”:

Tcl 并非被设计为一种严肃的编程语言。它被设计为一种“脚本语言”,基于“脚本语言”不需要努力成为真正的编程语言的假设。因此,Tcl 不具备后者的能力。

以新的、非正统的,并且有些人会说危险的方式与其他程序交互的能力,是 Tcl 对某些人如此有吸引力,而对另一些人如此令人震惊的原因。这在使用非 shell 程序中的 Unix 工具时遇到的典型困境。

结论

这通常归结为时间问题。如果您试图让您的代码参加乡村集市,这些技术不会为您赢得蓝丝带。但是,如果您想在晚上 7 点之前完成它,以便您可以去集市,这些技术可能会奏效。

在家庭计算机中接近千兆次浮点运算速度芯片的时代,这里和那里浪费的几个周期不会杀死任何人,尤其是对于一个将运行一两次然后被丢弃的程序而言。将 shell 哲学扩展到开发工作也是一个有吸引力的选择——您可以使用这些技术快速拼凑出可用的程序,这对于时间紧迫的程序员来说很有吸引力。Tcl/Tk 是将 shell 哲学扩展到加速开发周期的完美示例。当然,这种方法的低效率是几乎所有关于 Tcl/Tk 优点的激烈辩论的原因。

无论是 Tcl、shell、Perl 还是 C,无论您选择哪种编程技术,通常都有一个选项,可以从中导入其他编程环境的工具供您使用。但是,如果 Richard Stallman 给您写了一封措辞严厉的信,批评您这样做,请不要说您没有被警告过。

Todd Graham Lewis (tlewis@mindspring.com) 已经凭借 Mindspring Enterprises(美国东南部最大的互联网服务提供商)取得了更大更好的成就。在那里,他从拥有花哨的“计算机科学”学位的工程师同事那里学到了很多东西。他想知道为什么不是每个人都像他一样通过玩他的 Linux 盒子来学习计算。

加载 Disqus 评论