Shell 指令碼程式設計陷阱
Shell 指令碼很棒,你可以非常輕鬆地寫出有用的東西來。甚至像是下面這個傻瓜式的命令:
# 用含有 Go 的詞彙起名字:
$ grep -i ^go /usr/share/dict/* | cut -d: -f2 | sort -R | head -n1
goldfish
如果用其他程式語言,就需要花費更多的腦力,用多行程式碼實現,比如用 Ruby 的話:
puts(Dir['/usr/share/dict/*-english'].map do |f|
File.open(f)
.readlines
.select { |l| l[0..1].downcase == 'go' }
end.flatten.sample.chomp)
Ruby 版本的程式碼雖然不是那麼長,也並不複雜。但是 shell 版是如此簡單,我甚至不用實際測試就可以確保它是正確的。而 Ruby 版的我就沒法確定它不會出錯了,必須得測試一下。而且它要長一倍,看起來也更復雜。
這就是人們使用 Shell 指令碼的原因,它簡單卻實用。下面是另一個例子:
curl https://nl.wikipedia.org/wiki/Lijst_van_Nederlandse_gemeenten |
grep '^<li><a href=' |
sed -r 's|<li><a href="/wiki/.+" title=".+">(.+)</a>.*</li>|\1|' |
grep -Ev '(^Tabel van|^Lijst van|Nederland)'
這個指令碼可以從維基百科上獲取荷蘭基層政權的列表。幾年前我寫了這個臨時的指令碼,用來快速生成一個資料庫,到現在它仍然可以正常執行,當時寫它並沒有花費我多少精力。但要用 Ruby 完成同樣的功能則會麻煩得多。
現在來說說 shell 的缺點吧。隨著程式碼量的增加,你的指令碼會變得越來越難以維護,但你也不會想用別的語言重寫一遍,因為你已經在這個 shell 版上花費了很多時間。
我把這種情況稱為“Shell 指令碼程式設計陷阱”,這是沉沒成本謬論的一種特例(LCTT 譯註:“沉沒成本謬論”是一個經濟學概念,可以簡單理解為,對已經投入的成本可能被浪費而念念不忘)。
實際上許多指令碼會增長到超出預期的大小,你經常會花費過多的時間來“修復某個 bug”,或者“新增一個小功能”。如此迴圈往復,讓人頭大。
如果你從一開始就使用 Python、Ruby 或是其他類似的語言來寫這個程式,你可能會在寫第一版的時候多花些時間,但以後維護起來就容易很多,bug 也肯定會少很多。
以我的 packman.vim 指令碼為例。它起初只包含一個簡單的用來遍歷所有目錄的 for
迴圈,外加一個 git pull
,但在這之後就剎不住車了,它現在有 200 行左右的程式碼,這肯定不能算是最複雜的指令碼,但假如我一上來就按計劃用 Go 來編寫它的話,那麼增加一些像“列印狀態”或者“從配置檔案裡克隆新的 git 庫”這樣的功能就會輕鬆很多;新增“並行克隆”的支援也幾乎不算個事兒了,而在 shell 指令碼里卻很難實現(儘管不是不可能)。事後看來,我本可以節省時間,並且獲得更好的結果。
出於類似的原因,我很後悔寫出了許多這樣的 shell 指令碼,而我在 2018 年的新年誓言就是不要再犯類似的錯誤了。
附錄:問題彙總
需要指出的是,shell 程式設計的確存在一些實際的限制。下面是一些例子:
- 在處理一些包含“空格”或者其他“特殊”字元的檔名時,需要特別注意細節。絕大多數指令碼都會犯錯,即使是那些經驗豐富的作者(比如我)編寫的指令碼,因為太容易寫錯了,只新增引號是不夠的。
- 有許多所謂“正確”和“錯誤”的做法。你應該用
which
還是command
?該用$@
還是$*
,是不是得加引號?你是該用cmd $arg
還是cmd "$arg"
?等等等等。 - 你沒法在變數裡儲存空位元組(0x00);shell 指令碼處理二進位制資料很麻煩。
- 雖然你可以非常快速地寫出有用的東西,但實現更復雜的演算法則要痛苦許多,即使用 ksh/zsh/bash 擴充套件也是如此。我上面那個解析 HTML 的指令碼臨時用用是可以的,但你真的不會想在生產環境中使用這種指令碼。
- 很難寫出跨平臺的通用型 shell 指令碼。
/bin/sh
可能是dash
或者bash
,不同的 shell 有不同的執行方式。外部工具如grep
、sed
等,不一定能支援同樣的引數。你能確定你的指令碼可以適用於 Linux、macOS 和 Windows 的所有版本嗎(無論是過去、現在還是將來)? - 除錯 shell 指令碼會很難,特別是你眼中的語法可能會很快變得記不清了,並不是所有人都熟悉 shell 程式設計的語境。
- 處理錯誤會很棘手(檢查
$?
或是set -e
),排查一些超過“出了個小錯”級別的複雜錯誤幾乎是不可能的。 - 除非你使用了
set -u
,變數未定義將不會報錯,而這會導致一些“搞笑事件”,比如rm -r ~/$undefined
會刪除使用者的整個家目錄(瞅瞅 Github 上的這個悲劇)。 - 所有東西都是字串。一些 shell 引入了陣列,能用,但是語法非常醜陋和費解。帶分數的數字運算仍然難以應付,並且依賴像
bc
或dc
這樣的外部工具($(( .. ))
這種方式只能對付一下整數)。
反饋
你可以發郵件到 martin@arp242.net,或者在 GitHub 上建立 issue 來向我反饋,提問等。
via: https://arp242.net/weblog/shell-scripting-trap.html
作者:Martin Tournoij 選題:lujun9972 譯者:jdh8383 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出
相關文章
- shell指令碼程式設計筆記指令碼程式設計筆記
- 7.shell指令碼程式設計指令碼程式設計
- 【學習】Linux Shell指令碼程式設計Linux指令碼程式設計
- Shell程式設計-01-Shell指令碼初步入門程式設計指令碼
- Linux Shell指令碼程式設計-基礎1Linux指令碼程式設計
- shell高效程式設計:shell指令碼從未如此美麗程式設計指令碼
- Linux Shell指令碼程式設計while語句案例Linux指令碼程式設計While
- Linux shell程式設計(一)shell指令碼中的變數詳解Linux程式設計指令碼變數
- Shell指令碼程式設計規範與變數(shell指令碼必須要知道的規矩!)指令碼程式設計變數
- 史上最全shell指令碼程式設計語法上冊指令碼程式設計
- shell指令碼程式設計學習筆記-運算子指令碼程式設計筆記
- shell指令碼程式設計學習筆記——變數指令碼程式設計筆記變數
- shell指令碼程式設計之選擇控制結構指令碼程式設計
- LINUX Shell指令碼程式設計例項詳解(一)上Linux指令碼程式設計
- 業務程式碼程式設計陷阱案例 - jaxenter程式設計
- Linux命令列與shell指令碼程式設計入門經驗Linux命令列指令碼程式設計
- Linux_day06_01_Shell指令碼程式設計_Bash基礎Linux指令碼程式設計
- 使用Python和Java呼叫Shell指令碼時的死鎖陷阱PythonJava指令碼
- 通用程式部署shell指令碼指令碼
- SELL 指令碼程式設計指令碼程式設計
- 程式碼上線的shell指令碼指令碼
- 好程式設計師Linux雲端計算教程分享Shell指令碼面試題程式設計師Linux指令碼面試題
- Shell 指令碼程式併發&程式數控制指令碼
- shell指令碼指令碼
- Shell程式設計程式設計
- 程式設計師的“能力陷阱”程式設計師
- Shell程式設計 --- Shell介紹程式設計
- 《Linux命令列與shell指令碼程式設計大全 第3版》Linux命令列---46Linux命令列指令碼程式設計
- shell指令碼案例指令碼
- 常用shell指令碼指令碼
- Linux Shell指令碼Linux指令碼
- shell指令碼(6)-shell陣列指令碼陣列
- shell程式設計五程式設計
- shell程式設計二程式設計
- Shell程式設計-shell變數1程式設計變數
- awk指令碼語言程式設計指南指令碼程式設計
- 用於管理應用程式得shell指令碼指令碼
- shell 指令碼加密 | shc指令碼加密