Shell 就像编辑器一样:每个人都有自己喜欢的选择并极力为该选择辩护(还告诉您为什么应该使用该选择)。确实如此,shell 可提供不同的功能,但它们都实现了数十年前开发的核心理念。
我第一次使用现代 shell 是在二十世纪 80 年代,当时我正在 SunOS 上开发软件。当我了解了将一个程序的输出用作另一个程序的输入(甚至多次连环地使用)的能力后,我就有了一种简单且高效的方式来创建过滤器和转换。该核心理念提供了一种方式来构建一些简单工具,这些工具足够灵活,可与其他工具组合使用。通过这种方式,shell 不仅提供了一种与内核和设备交互的方式,还提供了现在作为软件开发中的常见设计模式的集成服务(比如管道和过滤器)。
让我们首先简单介绍一下现代 shell 的发展历史,然后探讨如今一些可用于 Linux 的外来的有用 shell。
shell 的发展历史
Shell(或命令行解释器)具有很长的历史了,但我们的讨论从第一个 UNIX® shell 开始。(贝尔实验室的)Ken Thompson 于 1971 年开发了第一个用于 UNIX 的 shell,名为 V6 shell。类似于它在 Multics 中的前身,这个 shell (/bin/sh) 是一个在内核外部执行的独立用户程序。globbing(参数扩展的模式匹配,比如 *.txt)等概念是在一个名为 glob的独立实用程序中实现的,就像用于评估条件表达式的 if 命令一样。这种独立性可保持 shell 很小,只需不到 900 行 C 源代码(请参见 参考资料 获取原始源代码的链接)。
该 shell 为重定向(<> 和 >>)和管道(| 或 ^)引入了一种紧凑的语法,这种语法已延续到现代 shell 中。您也可以找到对调用顺序命令(使用 ;)和异步命令(使用 &)的支持。
Thompson shell 缺少的是编写脚本的能力。它的唯一用途就是用作一个交互式 shell(命令解释器)来调用命令和查看结果。
1977 年以来的 UNIX shell
撇开 Thompson shell,我们开始将目光转移到 1977 年引入 Bourne shell 时的现代 shell。Bourne shell 由 Stephen Bourne 在 AT&T Bell Labs 为 V7 UNIX 创建,它在如今仍然是一个有用的 shell(在一些情况下还被用作默认的根 shell)。作者在研究 ALGOL68 编译器之后开发了 Bourne shell,所以您会发现它的语法比其他 shell 更加类似于 Algorithmic Language (ALGOL)。尽管使用 C 开发,源代码本身甚至使用了宏赋予它一种 ALGOL68 特色。
Bourne shell 有两个主要目标:用作一个命令解释器来交互式执行操作系统命令,以及用于编写脚本(编写可通过 shell 调用的可重用脚本)。 除了取代 Thompson shell,Bourne shell 还提供了相对于其前身的多项优势。Bourne 向脚本中引入了控制流、循环和变量,提供了一种更加强大的语言来与操作系统交互(包括交互式和非交互式)。该 shell 还允许您使用 shell 脚本作为过滤器,提供对处理信号的集成支持,但缺乏定义函数的能力。 最后,它整合了我们如今使用的许多功能,包括命令替换(使用反引号)和用于将保留的字符串文字嵌入到脚本中的 HERE 文档。
Bourne shell 不仅是向前发展的重要一步,也是众多衍生的 shell 的基础,其中许多 shell 如今应用在典型的 Linux 系统中。图 1 演示了重要 shell 的系列。Bourne shell 导致了 Korn shell (ksh)、Almquist shell (ash) 和流行的 Bourne Again Shell(或 Bash)的开发。在 Bourne shell 发布时,C shell (csh) 正在开发。图 1 显示了主要系列,但没有展示所有影响,也没有展示一些具有重要贡献的 shell。
图 1. 1977 年以来的 Linux shell
我们稍后将分析其中一些 shell,查看为它们的进步做出贡献的语言和功能示例。
基本 shell 架构
一种假想的 shell 的基本架构很简单(Bourne 的 shell 就是一个证据)。在图 2 中可以看到,基本架构看起来类似一个管道,其中会分析和解析输入,展开符号(使用各种方法,比如括号、波浪号、变量、参数扩展和替换,以及文件名生成),最终执行命令(使用 shell 内置的命令或外部命令)。
图 2. 假想 shell 的简单架构
在 参考资料 部分中,您可以找到一些链接来了解开源 Bash shell 的架构。
探索 Linux shell
现在让我们看看其中一些 shell,回顾它们所做的贡献并在每个 shell 中查看示例脚本。查看的内容包括 C shell、Korn shell 和 Bash。
Tenex C shell
C shell 是 Bill Joy 1978 年在加州大学伯克利分校攻读研究生期间为 Berkeley SoftwareDistribution (BSD) UNIX 系统开发的。5 年后,该 shell 引入了来自 Tenex 系统(在 DEC PDP 系统上很流行)的功能。Tenex 引入了文件名和命令完成功能,以及命令行编辑功能。Tenex C shell (tcsh) 仍然向后兼容 csh,但改进了它的整体交互式功能。tcsh 是 Ken Greer 在 Carnegie Mellon University 开发的。
该 C shell 的一个重要的设计目标是创建一种类似 C 语言的脚本语言。这是一个有用的目标,因为 C 语言是所使用的主要语言(此外,该操作系统也是主要使用 C 语言开发的)。
Bill Joy 在 C shell 中引用的一项有用功能是命令历史。此功能维护以前执行的命令的历史,允许用户检查并轻松选择之前的命令来执行。例如,键入命令 history 将显示以前执行的命令。可使用向上和向下箭头来选择命令,或者可以使用 !! 执行前一个命令。也可以引用以前的命令的参数,例如 !* 引用前一个命令的所有参数,其中 !$ 引用前一个命令的最后一个参数。
看一下 tcsh 脚本的一个简单示例(清单 1)。这段脚本获取一个参数(一个目录名称),输出该目录中的所有可执行文件以及找到的文件数量。我将在每个示例中重用此脚本设计来演示区别。
tcsh 脚本可分解为 3 个基本部分。首先,请注意,我使用了 shebang 或 hashbang 符号来将此文件声明为可由定义的 shell 可执行文件(在本例中为 tcsh 二进制文件)解释。这允许我以常规可执行文件的形式执行该文件,而不在它之前添加解释器二进制文件。它维护找到的可执行文件数量,所以我将此数量初始化为 0。
清单 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 shell
Korn shell (ksh) 由 David Korn 设计,是在与 Tenex C shell 相同的时期引入的。Korn shell 的一项最有趣的功能是,它除了向后兼容原始的 Bourne shell,还可用作脚本语言。
Korn shell 在 2000 年以开源形式发布(依据 Common Public License)以前一直是专用的软件。除了提供与 Bourne shell 强大的向后兼容性,Korn shell 还包含其他 shell 的功能(比如 csh 的历史功能)。该 shell 还提供了可在现代脚本语言(比如 Ruby 和 Python)中找到的一些更加高级的功能 — 例如,关联数组和浮点算法。Korn shell 可用于多种操作系统,包括 IBM® AIX® 和 HP-UX,致力于支持 Portable Operating System Interface for UNIX (POSIX) shell 语言标准。
Korn shell 是 Bourne shell 的衍生物,因此看上去更像 Bourne shell 和 Bash 而不是 C shell。让我们看一个查找可执行文件的 Korn shell 示例(清单 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 的相似性。在结构上,该脚本基本上是相同的,但在执行条件、表达式和迭代的方式上存在明显的区别。没有采用类似 C 的测试操作符,ksh 采用了典型的 Bourne 风格操作符(-eq、-ne 和 -lt 等)。
Korn shell 也有用一些与迭代相关的区别。在 Korn shell 中,使用了 for in 结构,使用命令替换来表示从命令 ls &#39;$1/*(表示指定子目录的内容)的标准输出创建的文件列表。
除了上面定义的其他功能,Korn 还支持别名功能(用于将一个词替换为用户定义的字符串)。Korn 还有其他许多功能默认已禁用(比如文件名称完成),但可由用户启用。
Bourne-Again Shell
Bourne-Again Shell(或 Bash)是一个开源 GNU 项目,旨在取代 Bourne shell。Bash 由 Brian Fox 开发,已成为世上最流行的 shell 之一(出现在 Linux、Darwin、Windows®、Cygwin、Novell、Haiku 等系统中)。顾名思义,Bash 是 Bourne shell 的一个超集,大部分 Bourne 脚本都可原封不动地执行。
除了支持脚本的向后兼容性,Bash 还整合了来自 Korn 和 C shell 的功能。您将找到命令历史、命令行编辑、一个目录栈(pushd 和popd)、许多有用的环境变量和命令完成等。
Bash 继续在发展,拥有许多新功能,支持正则表达式(类似于 Perl)和关联数组。尽管其中一些功能可能在其他脚本语言中不存在,但可以编写兼容其他语言的脚本。对于这一点,清单 3 中所示的示例脚本等同于 Korn shell 脚本(来自 清单 2),除了 shebang 区别 (/bin/bash)。
清单 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
这些 shell 之间的一个关键区别是它们的发布所依据的许可。您可能已猜到,Bash 是在 GNU 项目中开发的,是依据 GPL 发布的,而 csh、tcsh、zsh、ash 和 scsh 都是依据 BSD 或一种类似 BSD 的许可来发布的。Korn shell 可依据 Common Public License 使用。
外来的 shell
对于大胆的开发人员,可基于您的需要或爱好使用替代的 shell。Scheme shell (scsh) 提供了一种使用 Scheme(Lisp 语言的一种衍生物)的脚本环境。Pyshell 是对创建使用 Python 语言的类似脚本的一次尝试。最后,对于嵌入式系统,可以使用 BusyBox,它将一个 shell 和所有命令合并到一个二进制文件中,以简化其分发和管理。
清单 4 给出了以 Scheme shell (scsh) 编写的查找所有可执行文件的脚本。这段脚本可能看起来很奇怪,但它实现了与我们目前所提供的脚本类似的功能。这段脚本包含 3 个函数,直接使用可执行代码(在末尾)来测试参数数量。这段脚本真正强大之处在showfiles 函数内,它迭代一个列表(在 with-cwd 后构造),在列表的每个元素后调用 write-ln。这个列表通过迭代指定的目录并在其中过滤是可执行文件的文件来生成。
清单 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)))
结束语
早期 shell 的许多理念和大量接口在之后的 35 年几乎未变 — 这是对早期 shell 的原始作者的贡献的有力证明。在一个不断自我改造的行业中,shell 已大大改进,但没有发生重大变化。尽管存在过创建特殊 shell 的尝试,但 Bourne shell 的衍生物仍然是所使用的主要 shell。