在 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 作者在當時做的一個決定,要分開放,沒什么特殊的原因,這樣說應該說服你了吧。
第二個問題沒有回答我,我就不再追問了,我猜答案是肯定的,否則干嘛實現那個功能。
文章列表