Bash 為何要發明 shopt 命令

紫雲飛發表於2015-10-27

在 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 作者在當時做的一個決定,要分開放,沒什麼特殊的原因,這樣說應該說服你了吧。

第二個問題沒有回答我,我就不再追問了,我猜答案是肯定的,否則幹嘛實現那個功能。 

相關文章