Linux 外壳的演变之旅

Linux大全评论1.3K views阅读模式

外壳或外壳程序(shell)就像是编辑器:每个人都有自己的喜好,并且会为自己的选择进行强烈的辩护(并会告诉你为什么你应该换用)。诚然,一些外壳程序可以提供不同的功能,但它们都实现了几十年前就已经形成的核心理念。

关于现代的外壳程序,我的首次使用体验发生在1980年代,当时我正在SunOS上开发软件。一旦了解了把一个程序的输出用作另一个程序的输入(甚至可在命令链中多次这样做)这种能力之后,我就拥有了一种简单有效的创建过滤器和转换的方式。这一核心思想提供了一种构建简单工具的方式,这些工具足够灵活,能够以一种有益的组合来和其他工具一起使用。通过这种方式,外壳程序不仅提供了一种与内核和设备交换的方式,而且整合了多种服务(比如说管道和过滤器),这类服务现在在软件开发中已是常见的设计模式了。

我们先从现代外壳程序的简单历史开始,然后探讨Linux目前提供的一些有用的以及一些奇异的外壳程序。

外壳程序的历史

外壳程序——或称作命令行解释器——有着一个很长的历史,但这里的讨论从第一个 UNIX®外壳程序开始。(贝尔实验室的)Ken Thompson在1971年开发了名为V6 shell的首个UNIX外壳程序。与其在Multics上的前身相类似,这个外壳程序(/bin/sh)是一个独立的用户程序,在内核的外部执行。诸如通配符(参数扩展的模式匹配,比如说*.txt)一类的概念被放在一个名为glob的单独的实用程序中实现,就像是if命令计算条件表达式一样。这种分割维持了外壳程序的短小精悍,只有不到900行的C源代码(参阅参考资料获得到初始源代码的链接)。

外壳程序为重定向(<>和>>)和管道(|或^)引入了一种紧凑的语法,这些语法仍然在现代的外壳程序中使用。你也依然能够找到对调用顺序命令(使用;分隔)和异步命令(使用&分隔)的支持。

Thompson的外壳程序所缺少的是编写脚本的功能,它的唯一目就是作为一种交换式外壳(命令解释器)来调用命令然后查看结果。

自1977年以来的UNIX外壳程序

在Thompson外壳之后,我们从1977年的现代外壳程序来开始这一了解过程,Bourne外壳在这一年被引入。Bourne外壳是AT&T贝尔实验室的Stephen Bourne为V7 UNIX创建的,至今还保留了一个可用的外壳程序(在某些情况下,作为默认的根用户执行外壳(root shell))。该作者是在进行了ALGOL68编译器方面的工作之后才开发Bourne外壳的,所以你会发现其语法比其他外壳程序更类似于算法语言(Algorithmic Language ,ALGOL),而源代码本身,尽管是用C来开发的,甚至使用了宏来赋予它一种ALGOL68的味道。

Bourne外壳有两个主要的目的:作为一个命令解释器,以交互方式执行操作系统的命令;以及用来编写脚本(编写可通过外壳调用的可重用脚本)。除了取代Thompson外壳的功能之外,Bourne外壳还提供了一些超越其前任的优势。Bourne引入了控制流、循环和变量,提供了一种更函数化的语言来与操作系统交互(对话式的或是非对话式的都可以)。该外壳程序还允许你把外壳脚本当成过滤器使用,为处理信号提供集成的支持,不过其缺乏定义函数的功能。最后一点是,该外壳程序纳入了一些我们今天还在使用的功能,其中包括了命令替换(使用反引号),以及在脚本内部嵌入保留的串字面量的HERE文档。

Bourne外壳不仅是前进道路上的很重要的一步,而且是多种派生出来的外壳程序的基石,这些派生外壳中的许多今天仍然用在一些典型的Linux系统上。图1说明了一些重要的外壳程序的传承关系,Bourne外壳带来了Korn外壳(ksh)、Almquist外壳(ash)流行的Bourne Again Shell(或称Bash)的发展;而当Bourne外壳发布时,C shell外壳程序(csh)已在开发之中。图1说明了主要的传承关系,但并未包含了所有的影响,一些跨多个外壳的显著贡献在这里并未标注出来。

图1. 自1977年以来的Linux外壳

Linux 外壳的演变之旅

我们稍后会探讨其中的一些外壳程序,并例举出一些对它们的发展有贡献作用的语言和功能。

基本的外壳程序架构

设想中的外壳程序的基础架构很简单(已由Bourne外壳证明),正如你在图2中见到的那样,基本的架构看起来类似一个管道,其中的输入是分析和解析,接着是符号的扩充(使用各种各样的方法,比如说括号、波浪线、变量和参数的扩展和替换,以及文件名生成等),以及最后的命令执行(使用外壳内置的命令或是外部命令)。

图2. 假想外壳程序的简单架构

你可在参考资料一节找到找到有关链接,了解开源的Bash外壳的架构。

探讨Linux的外壳程序

现在我们来探讨一下几个这样的外壳程序,回顾它们所做出的贡献,并在每个外壳程序中检验一个脚本例子。要查看的外壳程序包括了C shell、Korn 外壳和Bash。

Tenex C shell外壳

1978年,当Bill Joy还是加州大学伯克利分校的在校学生时,他为Berkeley Software Distribution (BSD) UNIX系统开发了C shell。五年之后,该外壳引入了Tenex系统(在DEC PDP系统上很流行)上的功能。除了命令行编辑功能之外,Tenex还引入了文件名称和命令的补全功能。Tenex C shell(tcsh)保持了对csh的向后兼容,但提升了其整体的交互功能。tcsh是Ken Greer在卡内基 - 梅隆大学开发出来的。

C shell的一个主要设计目标是创建一种看上去类似于C语言的脚本语言,鉴于C当时是在用的主要语言(加之操作系统绝大部分都是使用C来开发的),所以这是一个很实用的目标。

Bill Joy带到C shell中的一个实用功能是命令的历史记录,这一功能维持之前执行过的命令的一个历史,并允许用户查看并轻松地选择前面的命令来执行。例如,输入命令history就会显示出之前执行过的命令,使用上下箭头按键来选择命令,或是使用!!来执行前面的一个命令。引用前一个命令的所有参数也是可以的,比如说,!*引用前一个命令的所有参数,而!$则是引用前一个命令的最后一个参数。

看一下一个简短的tcsh脚本例子(清单1),该脚本用到了一个参数(目录名称),给出该目录下的所有可执行文件和找到的文件的数目。我在每个例子中都重用了这一脚本,以此来说明一些不同之处。

该tcsh脚本被分成了三个基本的部分,首先,需要注意的是,我是使用了shebang或称作hashbang的符号(#!)来声明这一文件是可被外壳执行程序(在本例中是tcsh二进制执行文件)解释的,这就可以让我把该文件当成一个普通的可执行文件来执行,而不需要在它之前加上解释器的二进制文件名。脚本维持了一个找到的可执行文件的计数,所以我把这一计数初始化为零。

清单1. 用tcsh编写的查找所有可执行文件的脚本

#!/bin/tcsh
# find all executables

set count=0

# Test arguments
if ($#argv != 1) then
echo "Usage is $0

"
exit 1
endif

# Ensure argument is a directory
if (! -d $1) then
echo "$1 is not a directory."
exit 1
endif

# Iterate the directory, emit executable files
foreach filename ($1/*)
if (-x $filename) then
echo $filename
@ count = $count + 1
endif
end

echo
echo "$count executable files found."

exit 0

第一部分内容测试用户传递进来的参数,变量#argv代表了传递进来的参数个数(不包括命令名称自身)。你可以通过指定它们的索引来访问这些参数。例如,#1指向第一个参数(这是argv[1]的简写)。该脚本预期有一个参数,如果没有找到该参数的话,就发出一条错误消息,使用$0来表示在控制台中输入的命令(argv[0])。

第二部分内容确保传递进来的参数是一个目录, 如果参数是一个目录的话,运算符-d返回True。不过要注意的一点是,我先指定了一个!符号,其代表的意思是否定。通过这种方式,表达式要说的是,如果参数不是一个目录,则发出一条错误消息。

最后一部分内容遍历了目录中的文件,测试它们是否是可执行的。我使用了便捷的foreach这一遍历器,其遍历括号(本例中是一个目录)中的每个条目,然后在循环体中对每个条目进行检查,该步骤使用了运算符-x来检查文件是否是可执行的,如果是的话,输出该文件名称并且计数加一。在脚本的末尾,我输出可执行文件的数目。

Korn外壳

Korn外壳(Korn shell,ksh)由David Korn设计,其差不多是和Tenex C shell同一时期引入的。Korn外壳最吸引人的功能之一是被当成脚本语言使用,与此同时还向后兼容最初的Bourne外壳。

Korn外壳原来是专有软件,直到2000年的时候,它才(遵照通用公共许可协议)作为开源软件发布。除了提供很强的向后兼容Bourne外壳的功能之外,Korn外壳还包含了一些来自其他外壳的功能(比如说csh的历史记录功能)。该外壳还提供了一些更先进的功能,这些功能可以在诸如Ruby和Python一类的现代脚本语言中找到——比如说,关联数组和浮点运算。Korn外壳在许多操作系统上都是可用的,这些系统中就包括了IBM® AIX® and HP-UX;并且尽力去支持 Portable Operating System Interface for UNIX(POSIX)外壳语言的标准。

Korn外壳是从Bourne外壳派生而来的,因此其看上去更类似于Bourne外壳和Bash而不是C shell。我们来看一个Korn外壳的查找可执行文件的例子(清单2)。

清单2. 用ksh编写的查找所有可执行文件的脚本

#!/usr/bin/ksh
# find all executables

count=0

# Test arguments
if [ $# -ne 1 ] ; then
echo "Usage is $0

"
exit 1
fi

# Ensure argument is a directory
if [ ! -d "$1" ] ; then
echo "$1 is not a directory."
exit 1
fi

# Iterate the directory, emit executable files
for filename in "$1"/*
do
if [ -x "$filename" ] ; then
echo $filename
count=$((count+1))
fi
done

echo
echo "$count executable files found."

exit 0

在清单2中你首先会注意到的一件事情是,其和清单1相类似。就结构上来说,脚本几乎就是相同的,主要的不同体现在条件语句、表达式和遍历的执行方式上。ksh并未采用类C的测试运算符,其采用了典型的Bourne式的运算符(-eq、-ne、-lt等)。

Korn外壳在遍历方面也有些不同,在korn外壳中,所用的是for in结构,其使用了命令替换来表示文件列表,该文件列表通过命令ls '$1/*的标准输出来创建,而该命令则代表了指定名字的子目录中的内容。

除了前面明确了的其他功能之外,Korn还支持别名功能(使用用户定义的串来替代一个词)。Korn有许多其他功能在默认情况下是禁用的(比如说文件名称的补全),不过这些功能可由用户来启用。

Bourne-Again Shell外壳

Bourne-Again Shell,或称作Bash,是一个开源的GNU项目,其目标是取代Bourne外壳,Bash是由Brian Fox开发出来的,其已成为最常提供的外壳之一(在Linux、Darwin、Windows®、Cygwin、Novell、Haiku等等之上都有它的身影)。顾名思义,Bash是Bourne外壳的一个超集,大多数的Bourne脚本都可不做修改就能执行。

除了支持脚本的向后兼容之外,Bash还纳入了一些Korn和C shell的功能。像命令历史记录、命令行编辑、目录栈(pushd和popd)、许多有用的环境变量、命令补全等等这些功能你都会找得到。

Bash延续了演变过程,带来了一些新的功能,支持正则表达式(类似于Perl)和关联数组。尽管其中的一些功能并未出现在其他的脚本语言中,但还是可以编写出与其他语言兼容的脚本来。为了说明这一点,清单3给出了一个示例脚本,除了shebang符号(/bin/bash)不同之外,该脚本与(清单2中的)Korn外壳脚本是一样的。

清单3. 用Bash编写的查找所有可执行文件的脚本

#!/bin/bash
# find all executables

count=0

# Test arguments
if [ $# -ne 1 ] ; then
echo "Usage is $0

"
exit 1
fi

# Ensure argument is a directory
if [ ! -d "$1" ] ; then
echo "$1 is not a directory."
exit 1
fi

# Iterate the directory, emit executable files
for filename in "$1"/*
do
if [ -x "$filename" ] ; then
echo $filename
count=$((count+1))
fi
done

echo
echo "$count executable files found."

exit 0

这些外壳程序的一个重要的不同之处在于它们是遵循哪一个许可协议来发布的。Bash,正如你所料想的那样,是通过GNU项目来开发的,它的发布遵循GPL协议;不过,csh、tcsh、zsh、ash和scsh的发布都是遵循BSD或是类BSD许可协议的;而Korn外壳则是基于通用公共许可协议提供。

一些奇特的外壳程序

你可根据自己的需要或是喜好来使用一些风格大胆的备选外壳程序,就这一点来说,Scheme外壳(Scheme shell,scsh)提供了一种使用Scheme(一种派生自Lisp的语言)的脚本环境;Pyshell则试图创建一种使用Python语言的相似脚本。最后要提到的是嵌入式系统,有一个名为BusyBox的此类系统,其把一个外壳程序和所有的命令都纳入到一个二进制执行文件中,以此来简化自身的分发和管理。

清单4提供了Scheme外壳(scsh)中的查找所有可执行文件脚本的一个观摩版本,该脚本可能看起来有些异域风情,不过其实现了与迄今为止提供其他脚本相类似的功能。该脚本包含了三个函数和可直接执行的代码(在结尾部分)来检测参数的个数。该脚本的精髓所在是showfiles函数,该函数遍历一个列表(在执行with-cwd之后构造出来),跟在列表的每个元素后面调用write-In。这一列表是通过遍历指定名字的目录并过滤出可执行文件而生成的。

清单4. 用scsh编写的查找所有可执行文件的脚本

#!/usr/bin/scsh -s
!#

(define argc
(length command-line-arguments))

(define (write-ln x)
(display x) (newline))

(define (showfiles dir)
(for-each write-ln
(with-cwd dir
(filter file-executable? (directory-files "." #t)))))

(if (not (= argc 1))
(write-ln "Usage is fae.scsh dir")
(showfiles (argv 1)))

企鹅博客
  • 本文由 发表于 2020年9月19日 21:41:00
  • 转载请务必保留本文链接:https://www.qieseo.com/268042.html

发表评论