Bash 为何要发明 shopt 命令

在 Bash 中,有两个内置命令用来控制 Bash 的各种可配置行为的开关(打开或关闭),这些开关称之为选项(option)。其中一个命令是 set,set 命令有三种功能:显示所有的变量和函数;修改 Bash 的位置参数;控制 Bash 的第一套选项。可见 set 命令完全违背了“一个命令只干一件事”的 UNIX 哲学。另外一个命令是 shopt,从名字(shell options 的缩写)就可以看出,它的功能是控制 Bash 的另一套选项。那么问题就来了,为啥要用两套选项?

在回答为什么之前,我们先看看两者的不同点:

1. set 命令是 POSIX 规范,shopt 不是

set 命令是 Bash 从 sh 继承来的,而且它和它的大多数选项一起都是在 POSIX 规范中的。而 shopt 是 Bash 在 2.0 版本时新增的,别的 Shell 没有这个命令。

$ set -o | wc -l

27

$ shopt | wc -l

47

在我电脑上的 Bash 4.4 beta 中,set 一共有 27 个选项,shopt 一共有 47 个选项。

2. set 命令和 shopt 命令分别对应两个不同的环境变量

在 Bash 1.* 时代,用 set 命令开启的选项只能在当前 Shell 进程中生效,没有办法通过环境变量传递给它的子进程 Shell,从 Bash 2.0 开始,新增了一个只读变量 SHELLOPTS,只要把它设置成环境变量,它就能把在当前 Shell 中打开的选项传递给子进程 Shell。

$ echo $SHELLOPTS

braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor

$ set -o noglob

$ echo $SHELLOPTS

braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor:noglob

$ echo *

*

$ export SHELLOPTS

$ bash -c 'echo *'

*

上面的例子演示了:在当前 Shell 中打开了 noglob 选项,然后 SHELLOPTS 变量的值会自动同步(所有开启的选项名用冒号 join 成的字符串),但这个变量默认并不是环境变量,需要手动 export 一下,然后子进程 Shell 会获取到这个环境变量的值,解析之后,打开这些继承来的选项。为了演示 Bash 的确有这个解析过程,可以这么玩:

$ env SHELLOPTS=foo bash

bash: foo: invalid option name

值得注意的是,虽然 shopt 命令和 SHELLOPTS 变量是同时实现的(Bash 2.0),而且它俩的名字看起来也的确像是有对应关系似的,然而并没有。shopt 命令一直没有一个像 set 命令之于 SHELLOPTS 的东西,直到 Bash 4.1,才有了 BASHOPTS 变量,它的功能和 SHELLOPTS 一样,用来把 shopt 命令打开的选项传递给子进程 Shell,这里就不具体演示了。

3. shopt 也可以控制 set 的选项,反之则不行

shopt 命令有个 -o 选项,这个选项的功能就是用来查看或修改原本用 set 控制的那套选项,比如我们随便选个 set 的选项 noglob:

$ shopt -s noglob

bash: shopt: noglob: invalid shell option name

$ shopt -so noglob

$ shopt noglob

bash: shopt: noglob: invalid shell option name

$ shopt -o noglob

noglob         on

不加 -o 控制自己的一套选项,加上 -o 控制 set 控制的那套选项。可见在控制 Bash 的选项这个功能上,shopt 命令完全可以代替 set 命令。

4. 为什么要发明 shopt

在了解了这两个命令之后,我不禁要问:为什么要发明一个新的命令?要知道,清楚的记住哪个选项属于哪个命令是很难的,比如我问你 noglob 和 nullglob 哪个是 set 选项哪个是 shopt 选项,没几个人能记得。为什么不像 zsh 一样让 set 管理所有的选项呢:

$ zsh -c 'set -o | wc -l'

176

我自己猜测了很久:是不是 set 命令的短选项不够了?但我又看到不是所有的 set 长选项都有对应的短选项。是不是 Bash 作者在当时决定以后把 POSIX 规定的选项放一个地方,把其它 Bash 私有的选项放另一个地方,况且 set 命令已经很复杂了,所以发明了个新命令?然后我又发现很多 set 的选项都不在 POSIX 规范里,比如 onecmd、pipefail、history 和 errtrace 等。由于这些猜测说服不了我的好奇心,于是我在 help-bash 上询问了 Bash 作者,毕竟这是 20 年前的事了,除了他谁还可能知道 http://lists.gnu.org/archive/html/help-bash/2015-10/msg00008.html

在邮件里,我咨询了两个问题,一个就是“为什么不让 set 控制所有的选项,为什么要发明 shopt”;另外一个是“给 shopt -o 参数是不是意味着 Bash 的实现者鼓励人们用新的 shopt 命令而不是旧的 set 命令来控制 Bash 选项”。

第一个问题的答案比较复杂,总结一下就是:作者的出发点的确是为了让 set 控制“那些在 POSIX 规范里的选项”,以及“那些从 sh 继承来的,但不在 POSIX 规范里的选项(比如上面提到的 onecmd)”,以及“那些为了兼容性,从 ksh 引入的,但不在 POSIX 规范里的选项(比如上面提到的 pipefail)”;让 shopt 控制那些 Bash 私有的选项。但由于历史原因,20 年以后,现在看来,这两个出发点显得都不是那么有说服力:现在的 set 选项里存在着既不是从 sh 继承的,又不是从 ksh 学来的,又不在 POSIX 规范中的选项,比如 history 和 errtrace 等,Bash 作者解释说,history 是他希望 POSIX 规范能采纳(然而目前并没有),所以他先实现在了 Bash 里, errtrace(-E) 选项是因为他为了和 errexit(-e)对应起来,所以实现了,他还说如果再来一次的话,他会把 errtrace 放在 shopt 里。至于 shopt 里放着的选项是不是都是 Bash 私有的,也并不是,ksh 和 zsh 也从这些选项里引入了一些到自己的 set 选项里。除了上面我提到名字的选项,邮件里还讲了另外一些不符合一般规律的选项,很复杂,看了也记不住。总之,两个命令的两套选项显得杂乱无章,毫无规律,是历史原因。读到这里,也许有些好奇心强的朋友还想问:难道把 Bash 的私有选项也放 set 里不行吗,不行吗,不行吗!是行,这只是 Bash 作者在当时做的一个决定,要分开放,没什么特殊的原因,这样说应该说服你了吧。

第二个问题没有回答我,我就不再追问了,我猜答案是肯定的,否则干嘛实现那个功能。 

posted @ 2015-10-27 15:29 紫云飞 阅读(...) 评论(...) 编辑 收藏