用純 Bash 編寫的程序有多複雜?
經過一些非常快速的研究,Bash 似乎是一種圖靈完備的語言。
我想知道,為什麼 Bash 幾乎只用於編寫相對簡單的腳本?由於 Linux 附帶了 Bash shell,因此您可以在沒有任何外部解釋器或編譯器的情況下執行 shell 腳本,這是其他流行電腦語言所必需的。這是一個巨大的優勢,在某些情況下可以彌補語言本身的平庸。
那麼,此類程序的複雜程度是否有限制?純 Bash 是用來編寫複雜程序的嗎?是否可以在純 Bash 中編寫文件壓縮器/解壓縮器?編譯器?一個簡單的影片遊戲?
是不是因為調試工具非常有限就這麼少用?
看來 Bash 是一種圖靈完備的語言
圖靈完備性的概念與在大型程式語言中有用的許多其他概念完全分開:可用性、表達性、可理解性、速度等。
如果我們只需要圖靈完備性,我們就根本沒有任何程式語言*,*甚至沒有彙編語言。因為我們的 CPU 也是圖靈完備的,所以電腦程序員都只會寫機器程式碼。
為什麼 Bash 幾乎專門用於編寫相對簡單的腳本?
大型、複雜的 shell 腳本——例如
configure
GNU Autoconf 輸出的腳本——是非典型的,原因有很多:許多系統,尤其是較舊的系統,在技術上確實在系統某處具有與 POSIX 兼容的外殼,但它可能不在可預測的位置,例如
/bin/sh
. 如果您正在編寫一個 shell 腳本並且它必須在許多不同的系統上執行,那麼您如何編寫shebang 行?一種選擇是繼續使用/bin/sh
,但選擇將自己限制在 POSIX 之前的 Bourne shell 方言中,以防它在這樣的系統上執行。Pre-POSIX Bourne shell 甚至沒有內置算術;你必須呼籲
expr
或bc
完成這項工作。即使使用 POSIX shell,您也會錯過自1990 年代初期Perl 首次流行以來我們期望在 Unix 腳本語言中找到的關聯數組和其他功能。
這一歷史事實意味著有幾十年的傳統,即忽略現代 Bourne 家族 shell 腳本解釋器中的許多強大功能,純粹是因為你不能指望它們無處不在。
事實上,這種情況一直持續到今天:Bash直到第 4 版才獲得關聯數組,但您可能會驚訝於仍有多少系統仍在使用基於 Bash 3。Apple 在 2017 年仍將 Bash 3 與 macOS 一起發布——顯然是為了許可的原因——而 Unix/Linux 伺服器通常在很長一段時間內都在生產中執行,但幾乎沒有受到任何影響,因此您可能有一個仍在執行 Bash 3 的穩定舊系統,例如 CentOS 5 機器。如果您的環境中有這樣的系統,則不能在必須在其上執行的 shell 腳本中使用關聯數組。
如果您對這個問題的回答是您只為“現代”系統編寫 shell 腳本,那麼您必須應對大多數 Unix shell 的最後一個公共參考點是POSIX shell 標準這一事實,因為它是於 1989 年推出。基於該標準有許多不同的外殼,但它們都在不同程度上偏離了該標準。再次採用關聯數組,
bash
,zsh
和ksh93
都具有該功能,但存在多個實現不兼容的地方。那麼,你的選擇是只使用 Bash,或者只使用 Zsh,或者只使用ksh93
.如果您對這個問題的回答是“所以只安裝 Bash 4”或
ksh93
,或者其他什麼,那麼為什麼不“只”安裝 Perl 或 Python 或 Ruby 呢?這在很多情況下是不可接受的;預設值很重要。 2. Bourne 家族的 shell 腳本語言都不支持模組。在 shell 腳本中最接近模組系統的是
.
命令——也就是source
更現代的 Bourne shell 變體——相對於適當的模組系統,它在多個級別上都失敗了,其中最基本的是命名空間。無論使用哪種程式語言,當一個較大的整體程序中的任何單個文件超過幾千行時,人類的理解就會開始下降。我們將大型程序構造成許多文件的真正原因是我們最多可以將它們的內容抽象為一兩句話。文件 A 是命令行解析器,文件 B 是網路 I/O 泵,文件 C 是庫 Z 和程序其餘部分之間的 shim,等等。當您將許多文件組裝成單個程序的唯一方法是文本包含時,您限制了程序可以合理增長的大小。
為了比較,就像C 程式語言沒有連結器,只有
#include
語句一樣。這樣的 C-lite 方言不需要諸如extern
or之類的關鍵字static
。這些功能的存在是為了實現模組化。 3. POSIX沒有定義將變數範圍限定為單個 shell 腳本函式的方法,更不用說文件了。這有效地使所有變數全域化,這再次損害了模組化和可組合性。
在 post-POSIX shell 中有解決這個問題的方法——當然是
bash
在.ksh93``zsh
您可以在 GNU Autoconf 宏編寫的樣式指南中看到這一點的影響,他們建議您在變數名稱前加上宏本身的名稱,這導致變數名稱非常長,純粹是為了將衝突的機會減少到可接受的附近零。
在這個分數上,甚至 C 也好一英里。不僅大多數 C 程序主要使用函式局部變數編寫,C 還支持塊作用域,允許單個函式中的多個塊重用變數名稱而不會交叉污染。 4. Shell 程式語言沒有標準庫。
可以爭辯說 shell 腳本語言的標準庫是 的內容
PATH
,但這只是說要完成任何重要的事情,shell 腳本必須呼叫另一個完整的程序,可能是用更強大的語言編寫的首先。也沒有像 Perl 的CPAN那樣廣泛使用的 shell 實用程序庫存檔。如果沒有大量可用的第三方實用程式碼庫,程序員必須手動編寫更多程式碼,因此她的工作效率會降低。
即使忽略大多數 shell 腳本依賴於通常用 C 編寫的外部程序來完成任何有用的事情這一事實,所有這些
pipe()
→fork()
→exec()
呼叫鏈都會產生成本。與其他作業系統上的IPC和程序啟動相比,這種模式在 Unix 上相當有效,但在這裡它有效地取代了您在另一種腳本語言中使用子常式呼叫所做的事情,這仍然更加有效。這嚴重限制了 shell 腳本執行速度的上限。 5. Shell 腳本幾乎沒有通過並行執行來提高其性能的內置能力。Bourne shell 有
&
,wait
和管道為此,但這主要只對編寫多個程序有用,而不是實現 CPU 或 I/O 並行性。您不可能僅使用 shell 腳本來固定核心或使 RAID 陣列飽和,如果這樣做,您可能會在其他語言中獲得更高的性能。管道尤其是通過並行執行提高性能的弱方法。它只允許兩個程序並行執行,並且在任何給定時間點,兩個程序中的一個都可能在與另一個之間的 I/O 上被阻塞。
xargs -P
有一些後來的方法可以解決這個問題,例如GNUparallel
,但這只是轉移到上面的第 4 點。由於實際上沒有充分利用多處理器系統的內置能力,shell 腳本總是比用可以使用系統中所有處理器的語言編寫的程序慢。再次以 GNU Autoconf
configure
腳本為例,將系統中的核心數量增加一倍對於提高其執行速度幾乎無濟於事。 6. Shell 腳本語言沒有指針或引用。這可以防止你做一堆用其他程式語言輕鬆完成的事情。
一方面,無法間接引用程序記憶體中的另一個資料結構意味著您受限於內置資料結構。你的 shell 可能有關聯數組,但它們是如何實現的?有幾種可能性,每種都有不同的權衡:紅黑樹、AVL 樹和雜湊表是最常見的,但還有其他的。如果你需要一組不同的權衡,你會被卡住,因為沒有引用,你就沒有辦法手動滾動許多類型的高級資料結構。你被你得到的東西困住了。
或者,您可能需要一個資料結構,該結構甚至沒有足夠的替代方案內置到您的 shell 腳本解釋器中,例如有向無環圖,您可能需要它來建模依賴圖。我已經程式了幾十年,我能想到的在 shell 腳本中做到這一點的唯一方法就是濫用文件系統,使用符號連結作為虛假引用。當您僅依賴圖靈完備性時,您會得到這種解決方案,它無法告訴您解決方案是否優雅、快速或易於理解。
高級資料結構只是指針和引用的一種用途。它們還有大量其他應用程序,這在 Bourne 家族的 shell 腳本語言中根本無法輕鬆完成。
我可以繼續說下去,但我認為你明白了這一點。簡單地說,Unix 類型系統有許多更強大的程式語言。
這是一個巨大的優勢,在某些情況下可以彌補語言本身的平庸。
當然,這正是 GNU Autoconf 使用 Bourne shell 腳本語言家族的一個特意限制子集作為其
configure
腳本輸出的原因:因此它的configure
腳本幾乎可以在任何地方執行。您可能找不到比 GNU Autoconf 的開發人員更多地相信使用高度可移植的 Bourne shell 方言編寫實用程序的人,但他們自己的創作主要是用 Perl 編寫的,加上一些
m4
.腳本; 只有 Autoconf 的輸出是純 Bourne shell 腳本。如果這不引出“無處不在的伯恩”概念有多有用的問題,我不知道會是什麼。那麼,此類程序的複雜程度是否有限制?
從技術上講,不,正如您的圖靈完整性觀察所暗示的那樣。
但這與說任意大的 shell 腳本編寫起來很愉快、易於調試或執行速度快是不一樣的。
是否可以在純 bash 中編寫文件壓縮器/解壓縮器?
“純” Bash,沒有任何對
PATH
? 壓縮器可能使用echo
和十六進制轉義序列是可行的,但這樣做會相當痛苦。由於無法在 shell 中處理二進制數據,解壓縮器可能無法以這種方式編寫。您最終會要求od
將二進制數據轉換為文本格式,這是 shell 處理數據的本機方式。一旦你開始談論按照預期的方式使用 shell 腳本,作為驅動
PATH
.完全沒有限制。通過呼叫其他程序來獲得所有功能的 shell 腳本的PATH
執行速度不如用更強大的語言編寫的單體程序,但它確實可以執行。這就是重點。如果您需要一個程序快速執行,或者如果它需要本身功能強大而不是從其他人那裡借用功能,那麼您不要在 shell 中編寫它。
一個簡單的影片遊戲?
這是shell 中的俄羅斯方塊。如果您去尋找其他此類游戲,則可以使用。
只有非常有限的調試工具
在支持大型程式所需的功能列表中,我會將調試工具支持排在第 20 位。
printf()
與正確的調試器相比,許多程序員對調試的依賴程度要高得多,無論使用哪種語言。在 shell 中,你有
echo
和set -x
,它們一起足以調試很多問題。