什么是 GNU:Bash—GNU Shell

作者:Chet Ramey
历史记录

bash 和 readline 库共同提供对先前输入的命令列表(命令历史记录)的访问。bash 提供了变量($HISTFILE$HISTSIZE$HISTCONTROL)以及 history 和 fc 内建命令来操作历史记录列表。$HISTFILE 的值指定了 bash 在退出时写入命令历史记录的文件,以及在启动时从中读取历史记录的文件。$HISTSIZE 用于限制历史记录中保存的命令数量。$HISTCONTROL 提供了一种粗略的方式来控制哪些命令保存在历史记录列表中:值为 ignorespace 表示不保存以空格开头的命令;值为 ignoredups 表示不保存与上次保存的命令相同的命令。在早期版本的 bash 中,$HISTCONTROL 被命名为 $history_control;为了向后兼容,仍然接受旧名称。history 命令可以读取或写入包含历史记录列表的文件,并显示当前列表内容。fc 内建命令,采用自 POSIX.2 和 Korn Shell,允许显示和重新执行历史记录列表中的命令,并可选择编辑。readline 库提供了一组命令来搜索历史记录列表,以查找当前输入行的一部分或用户键入的字符串。最后,历史记录库通常直接合并到 readline 库中,实现了一种类似于 csh 的历史记录调用、扩展和重新执行先前命令的功能(“bang history”,之所以如此称呼是因为感叹号引入了历史记录替换)

$ echo a b c d e
a b c d e
$ !! f g h i
echo a b c d e f g h i
a b c d e f g h i
$ !-2
echo a b c d e
a b c d e
$ echo !-2:1-4
echo a b c d
a b c d

命令历史记录仅在 shell 为交互式时保存,因此 shell 脚本无法使用它。

新的 Shell 变量

bash 解释许多方便的变量,以使生活更轻松。这些变量包括 FIGNORE,它是一组文件名后缀,用于标识在完成文件名时要排除的文件;HOSTTYPE,它自动设置为描述 bash 当前正在执行的硬件类型的字符串;command_oriented_history,它指示 bash 将多行命令(例如 while 或 for 循环)的所有行保存在单个历史记录条目中,从而方便重新编辑;以及 IGNOREEOF,其值指示交互式 shell 在退出前将读取的连续 EOF 字符数—这是一种防止意外退出的简单方法。auto_resume 变量改变了 shell 处理简单命令名称的方式:如果作业控制处于活动状态并且设置了此变量,则不带重定向的单字简单命令会导致 shell 首先查找并重新启动具有该名称的挂起作业,然后再启动新进程。

花括号展开

由于 sh 没有提供生成共享公共前缀或后缀的任意字符串的便捷方法(路径名展开要求文件名存在),因此 bash 实现了花括号展开,这是一种从 csh 借鉴的功能。花括号展开类似于路径名展开,但生成的字符串不必对应于现有文件。花括号表达式由一个可选的前导码、一对包含一系列逗号分隔字符串的花括号和一个可选的后导码组成。前导码会添加到花括号内每个字符串的前面,然后后导码会添加到每个结果字符串的后面

$ echo a{d,c,b}e
ade ace abe
进程替换

在可以支持的系统上,bash 提供了一种称为进程替换的功能。进程替换类似于命令替换,因为它的规范包括要执行的命令,但 shell 不会收集命令的输出并将其插入到命令行中。相反,bash 打开一个管道连接到该命令,该命令在后台运行。shell 使用命名管道 (FIFO) 或 /dev/fd 方法命名打开的文件,以将进程替换扩展为在打开时连接到管道的文件名。此文件名成为扩展的结果。进程替换可用于比较应用程序的两个不同版本的输出,作为回归测试的一部分

$ cmp <\>(old_prog) <(new_prog)
提示符自定义

bash 提供的更受欢迎的交互式功能之一是自定义提示符的能力。主提示符 $PS1 和辅助提示符 $PS2 在显示之前都会被展开。当提示符字符串展开时,会执行参数和变量展开,因此任何 shell 变量都可以放入提示符中(例如,$SHLVL,它指示当前 shell 的嵌套深度)。bash 特殊解释提示符字符串中以反斜杠开头的字符。其中一些反斜杠转义符会被替换为当前时间、日期、当前工作目录、用户名以及正在输入的命令的命令号或历史记录号。甚至有一个反斜杠转义符,用于使 shell 在以 root 用户身份运行时通过使用 su 命令来更改其提示符。在打印每个主提示符之前,bash 会展开变量 $PROMPT_COMMAND,如果它有一个值,则将展开后的值作为命令执行,从而允许额外的提示符自定义。例如,以下赋值会导致当前用户、当前主机、时间、当前工作目录的最后一个组成部分、shell 嵌套级别以及当前命令的历史记录号嵌入到主提示符中

$ PS1='\u@\h [     ] \W($SHLVL:\!)\$ `
chet@odin [21:03:44] documentation(2:636)$ cd ..
chet@odin [21:03:54] src(2:637)$

被赋值的字符串用单引号括起来,以便如果导出它,子 shell 将更新 $SHLVL 的值

chet@odin [21:17:35] src(2:638)$ export PS1
chet@odin [21:17:40] src(2:639)$ bash
chet@odin [21:17:46] src(3:696)$

当以普通用户身份运行时,\$ 转义符显示为“$”,但当以 root 用户身份运行时,则显示为“#”。

文件系统视图

自从 Berkeley 在 4.2 BSD 中引入符号链接以来,它们最令人恼火的属性之一是使用 cd 时“扭曲”到文件系统的完全不同的区域,以及由此产生的“cd ..”的非直观行为。Unix 内核以物理方式处理符号链接。当内核转换路径名(其中一个组件是符号链接)时,它会在处理链接时替换路径名的全部或部分。如果符号链接的内容以斜杠开头,则内核将完全替换路径名;否则,链接内容将替换当前组件。在任何一种情况下,符号链接都是可见的。如果链接值是绝对路径名,则用户会发现自己处于文件系统的完全不同的部分。

bash 提供了文件系统的逻辑视图。在此默认模式下,命令和文件名补全以及更改当前工作目录的内建命令(如 cd 和 pushd)会透明地跟踪符号链接,就好像它们是目录一样。$PWD 变量(它保存了 shell 对当前工作目录的想法)取决于用于到达该目录的路径,而不是其在本地文件系统层次结构中的物理位置。例如

$ cd /usr/local/bin
$ echo $PWD
/usr/local/bin
$ pwd
/usr/local/bin
$ /bin/pwd
/net/share/sun4/local/bin
$ cd ..
$ pwd
/usr/local
$ /bin/pwd
/net/share/sun4/local

当然,当不理解 shell 文件系统逻辑概念的程序以不同方式解释“..”时,就会出现一个问题。当 bash 根据与其物理位置不对应的逻辑层次结构完成包含“..”的文件名时,通常会发生这种情况。对于那些发现这很麻烦的用户,可以使用相应的文件系统物理视图

$ cd /usr/local/bin
$ pwd
/usr/local/bin
$ set -o physical
$ pwd
/net/share/sun4/local/bin
国际化

bash 1.13 版本中最显著的改进之一是更改为“八位清洁”。以前的版本使用字符的第八位来标记它们在执行单词展开时是否被引用。虽然这不会影响大多数用户(他们大多数只使用七位 ASCII 字符),但有些人觉得这很局限。从 1.13 版本开始,bash 实施了一种不同的引用机制,该机制不会更改字符的第八位。这使得 bash 可以操作名称中带有“怪异”字符的文件,但这无助于用户输入这些名称,因此 1.13 版本引入了对 readline 的更改,使其也成为八位清洁的。存在一些选项,可以强制 readline 不对第八位设置的字符赋予特殊意义(默认行为是将这些字符转换为元前缀键序列),并输出这些字符而不转换为元前缀序列。这些更改,以及将键盘映射扩展到完整的八位,使 readline 能够与大多数 ISO-8859 字符集系列一起工作,这些字符集被许多欧洲国家/地区使用。

POSIX 模式

尽管 bash 旨在符合 POSIX.2 标准,但在某些领域,默认行为与该标准不兼容。对于希望在严格的 POSIX.2 环境中运行的用户,bash 实现了 POSIX 模式。当此模式处于活动状态时,bash 会修改其与 POSIX.2 不同的默认操作以符合标准。当 bash 使用“-o posix”选项启动或执行 set -o posix 时,将进入 POSIX 模式。为了与其他尝试符合 POSIX.2 标准的 GNU 软件兼容,当 bash 启动或在执行期间分配值时,如果变量 $POSIX_PEDANTIC 或 $POSIXLY_CORRECT 中的任何一个被设置,bash 也会进入 POSIX 模式。例如,当 bash 在 POSIX 模式下启动时,它会加载 $ENV 值指定的文件,而不是“正常”的启动文件。

未来计划

bash 的下一个版本 1.14 将引入几个功能,并且正在考虑在未来的版本中引入许多功能。本节将简要详细介绍计划在 1.14 版本中推出的新功能,并描述可能在更高版本中出现的功能。

bash-1.14 中提供的新功能回答了几个最常见的增强功能请求。最值得注意的是,有一种机制可以在提示符中包含不可见字符序列,例如那些导致终端以不同颜色或突出模式打印字符的序列。在早期版本中,没有什么可以阻止使用这些序列,但 readline 重新显示算法假定每个字符都占据物理屏幕空间,并且会过早地换行。

Readline 有一些新变量、几个新的可绑定命令和一些额外的 emacs 模式默认键绑定。已经实现了一种新的历史记录搜索模式:在此模式下,readline 会在历史记录中搜索以当前行开头和光标之间的字符开头的行。现有的 readline 增量搜索命令不再多次匹配相同的行。文件名补全现在会展开目录名中的变量。历史记录展开功能现在几乎完全与 csh 兼容:已添加了缺失的修饰符,并且历史记录替换已得到扩展。

前面描述为在未来版本中出现的一些功能,例如 set -o posix 和 $POSIX_PEDANTIC,已在 1.14 版本中出现。有一个新的 shell 变量 OSTYPE,bash 为其分配一个值,该值标识它正在运行的 Unix 版本(非常适合将特定于架构的二进制目录放入 $PATH 中)。两个变量已重命名:$HISTCONTROL 替换 $history_control,而 $HOSTFILE 替换 $hostname_completion_file。在这两种情况下,为了向后兼容,都接受旧名称。ksh select 结构(允许生成简单菜单)已实现。已向现有变量添加了新功能:$auto_resume 现在可以取值 exact 或 substring,并且 $HISTCONTROL 理解值 ignoreboth,它结合了两个先前可接受的值。dirs 内建命令已获得打印目录堆栈特定成员的选项。强制文件系统物理视图的 $nolinks 变量已被 set 内建命令的 -P 选项取代(等效于 set -o physical);为了向后兼容,保留了该变量。包含在 $BASH_VERSION 中的版本字符串现在包括补丁级别以及“构建版本”的指示。一些很少使用的功能已被删除:exit 的 bye 同义词和 $NO_PROMPT_VARS 变量已消失。现在有一个有组织的测试套件,可以在构建新版本的 bash 时作为回归测试运行。

文档已得到彻底修改:readline 库上有一个新的手册页,并且 info 文件已更新以反映当前版本。与往常一样,已修复尽可能多的错误,尽管肯定还有一些错误存在。

我希望在以后的 bash 版本中包含一些功能。其中一些功能基于其他 shell 中已经完成的工作。

除了简单变量之外,bash 的未来版本还将包括一维数组,使用 ksh 数组实现作为模型。ksh 语法的添加,例如 varname=( ... ) 以将单词列表直接分配给数组,以及允许 read 内建命令将值列表直接读取到数组中的机制,将是理想的。考虑到这些扩展,ksh

“set -A”语法可能不值得支持(-A 选项将值列表分配给数组,但这是一个相当特殊的特殊情况)。

一些 shell 包括一种可编程单词补全方法,用户可以基于每个命令指定在尝试补全时如何处理命令的参数:作为文件名、主机名、可执行文件等等。当前 bash 实现的其他方面可以保持原样;现有的启发式方法仍然有效。只有在补全简单命令的参数时,可编程补全才会生效。

如果能够让用户更精细地控制哪些命令保存到历史记录列表中,那也很好。一种建议是使用一个变量,暂定名为 HISTIGNORE,它将包含以冒号分隔的命令列表。在应用 $HISTCONTROL 的限制后,以这些命令开头的行将不会被放入历史记录列表中。在指定 $HISTIGNORE 的内容时,shell 模式匹配功能也可能可用。

较新的 shell(如 wksh,也称为 dtksh)提供的一件事是动态加载代码,将其他内建命令的实现加载到正在运行的 shell 中。这个新的内建命令将接受一个对象文件或共享库,其中实现了内建命令的“主体”(对于熟悉 bash 内部结构的人来说是 xxx_builtin())和一个结构,其中包含新命令的名称、在调用新内建命令时要调用的函数(可能在指定为参数的共享对象中定义)以及 help 命令要打印的文档(也可能存在于共享对象中)。它将管理扩展内建命令内部表的细节。

还需要一些其他内建命令:两个是 POSIX.2 getconf 命令,它打印 POSIX.2 定义的系统配置变量的值,以及 disown 内建命令,它使运行作业控制处于活动状态的 shell“忘记”其内部作业表中的一个或多个后台作业。例如,使用 getconf,用户可以检索 $PATH 的值,保证找到所有 POSIX 标准实用程序,或者找出包含指定目录的文件系统中文件名可以有多长。

这些功能都没有实现时间表,也没有包含它们的具体计划。如果任何人对这些建议有任何意见,请随时给我发送电子邮件。

反思和经验教训

在 bash 开发过程中重复最多的教训是,Bourne Shell 中存在一些黑暗的角落,并且人们会使用所有这些角落。在 Bourne shell 的原始描述中,引用和 shell 语法都规范得很差且不完整;随后的描述并没有多大帮助。Bourne 在描述随第七版 Unix 发行的 shell 的论文中提出的语法与实际情况相差甚远,以至于它不允许命令 who|wc。事实上,正如 Tom Duff 所说

“没有人真正知道 Bourne shell 的语法是什么。即使检查源代码也几乎没有帮助。”1

POSIX.2 标准包含一个 yacc 语法,该语法接近于捕获 Bourne shell 的行为,但它不允许 sh 接受的一些构造而没有报错—并且那里存在一些使用它们的脚本。bash 花费了几个版本和几个错误报告才实现了 sh 兼容的引用,并且仍然有一些 bash 标记为语法错误的“合法”sh 构造。完全的 sh 兼容性是一个难题。

shell 比我希望的更大更慢,尽管当前版本比以前更快。

readline 库可以进行大量的重写。

手写的解析器来替换当前 yacc 生成的解析器可能会导致速度提升,并且会解决一个明显的难题:shell 可以在输入时解析“$(...)”构造中的命令,而不是在构造展开时报告错误。

与往常一样,有一些糟粕与精华并存。需要清理重复的功能区域。在几种情况下,bash 特殊处理变量以启用以另一种方式提供的功能($notify 与 set -o notify 和 $nolinks 与

set -o physical,例如);应该删除对变量名的特殊处理。还有一些东西可以删除;$allow_null_glob_expansion 和 $glob_dot_filenames 变量的价值尤其值得怀疑。既然已经实现了 POSIX 强制要求的 $((...)) 构造,那么 $[...]$ 算术评估语法就显得多余了,可以删除。如果 help 内建命令输出的文本是 shell 外部的,而不是编译到 shell 中的,那就太好了。由 $command_oriented_history 启用的行为(导致 shell 尝试将多行命令的所有行保存在单个历史记录条目中)应该成为默认行为,并且应该删除该变量。

可用性

与所有其他 GNU 软件一样,bash 可以通过匿名 FTP 从 prep.ai.mit.edu:/pub/gnu 和其他 GNU 软件镜像站点获得。当前版本在 bash-1.13.5.tar.gz 目录中。使用 archie 查找最近的存档站点。最新版本始终可以通过 FTP 从 bash.CWRU.Edu:/pub/dist 获得。bash 文档可以通过 FTP 从 bash.CWRU.Edu:/pub/bash 获得。

自由软件基金会出售包含 bash 的磁带和 CD-ROM;发送电子邮件至 gnu@prep.ai.mit.edu 或致电 +1-617-876-3296 了解更多信息。bash 也随附于多个版本的 Unix 兼容系统。它作为 /bin/sh 和 /bin/bash 包含在多个 Linux 发行版中,并作为 BSDI 的 BSD/386 和 FreeBSD 中的贡献软件。

结论

bash 是 sh 的 достойный 继任者。它具有足够的便携性,可以在几乎所有版本的 Unix(从 4.3 BSD 到 SVR4.2)以及多个类 Unix 工作站上运行。它足够健壮,可以取代大多数这些系统上的 sh,并提供更多功能。它有数千名常规用户,他们的反馈帮助使其变得像今天这样出色—这证明了自由软件的好处。

1 Tom Duff,“Rc-A Shell for Plan 9 and UNIX systems”,1990 年夏季 EUUG 会议论文集,伦敦,1990 年 7 月,第 21-33 页

2 BSD/386 是 Berkeley Software Design, Inc. 的商标。

Chet Ramey (chet@po.cwru.edu) 是凯斯西储大学的程序员,也是自由软件基金会的志愿者。

加载 Disqus 评论