Zsh 開發指南(第一篇 變數和語句)
導讀
網上關於 zsh 的文章有很多,但其中超過 95% 的文章講如何使用和配置,寫如何用 zsh 編程的文章很少,能找到的多數也是隻言片語,不成系統。國外有幾本講 zsh 的書,其中也有很多內容是配置、使用、編寫補全腳本等等,對編程有用的篇幅佔比並不多,而且比較零散不便於查詢。至於官方文檔?那是讓即使有多年編程經驗的開發者也會抓狂的神奇存在。可讀性極差,而且基本沒有例子,不熟悉文檔結構和內容的話,很難找到自己想要的東西。但內容覆蓋很全面,洋洋洒洒近 500 頁,耐心去看總會找到的。還有一份官方「入門」文檔,上次更新時間是 2002 年,也要 300 多頁,至於可讀性,比官網文檔要稍微好一些吧,還是有一定的參考價值的。官網上還有一些鏈接,裡邊內容比較零散,也可以看看。
很多人在 zsh 中用 bash 語法寫腳本,雖然也可以正常運行,但這樣無法利用 zsh 的眾多優秀特性,還是非常遺憾的。熟悉下 zsh 下獨有的特性,對寫腳本的幫助是很大的。
本系列文章無關 zsh 的安裝、使用、配置(如果需要配置文件,可以參考我的 .zshrc,裡邊有比較詳細的注釋),更無 oh-my-zsh 相關內容,安裝 zsh 後無需配置即可開始學習編寫腳本。讀者不需要有 bash 的基礎(最好了解一些),但需要接觸過任何一門編程語言,對編程的一些基礎概念要有了解。
為什麼用 zsh 寫腳本
很多人對 zsh 的了解停留在界面漂亮、主題多、插件多、補全強等等,而對 zsh 的語言特性了解並不多。因為 zsh 基本兼容 bash,不少人使用 bash 語法寫 zsh 腳本,或者偶爾使用一些 zsh 特有的小技巧,很難體會出 zsh 作為一門編程語言的強大之處。
另外有些人認為 bash 幾乎在所有類 Unix 系統都有默認安裝,而 zsh 往往要自己安裝,為了通用性而用 bash 寫腳本比較好。這個說法也有一定的道理,但並不是對所有開發者來說都有影響。如果是開源軟體的開發者,為了避免潔癖用戶因為不想安裝他用不到的 zsh 而不使用自己的軟體,而避免使用 zsh,是有一定道理的(但現在 zsh 的用戶量也有一定的積累了)。除此之外,自己平時寫腳本、公司內部使用等多數場景,都是不需要考慮這個因素的。
如果在公司使用,還涉及其他因素。
第一個是 zsh 的部署成本。但因為多數情況都需要部署其他軟體,甚至自己的腳本可以和 zsh 打包部署(去掉用不到的文件後的 zsh 只有 1M 多),所以基本不成問題。而且如果使用系統默認的 bash 的話,還涉及版本不同導致的問題,比如不同系統的 bash 版本不一樣,或者系統升級後,bash 的升級導致之前的腳本掛掉等等。所以即使使用 bash,最好也是統一部署或者自帶一個特定的版本,而不是使用系統默認的,以減少不必要的麻煩。
第二個就是非常重要的學習成本。因為會寫 bash 的人很多,但會寫 zsh 的比較少,如果只有自己會寫,那麼和別人合作會出問題。但 zsh 的學習成本並沒有那麼大,尤其是對會 bash 開發者來說,要大致看懂 zsh 腳本基本只需要幾十分鐘的學習,而編寫的話,循序漸進也是很自然的事情,而且想不起來的時候還可以用 bash 的語法寫。所以學習成本沒有那麼可觀。
第三個是使用 zsh 開發的好處。如果 zsh 和 bash 相比,沒有明顯的好處,為什麼要學習和使用它呢?那麼就要從 bash 痛點講起了。我想經常寫 bash 腳本的人,很少有人會舉大拇指說 bash 真好用啊。相反,我曾經多次聽某些開發者說我寫過一個超過 2000(或者其他行數)行的 shell(bash)腳本。但幾乎沒有人會認為寫一個超過 2000 行的 Python 腳本是一件多麼特別的事情。蹩腳的語法(幾乎所有從任何其他語言遷移過來的開發者,都要重新熟悉和習慣它的語法)、嚴重依賴外部命令(因為文件系統錯誤等問題,掛掉一個外部命令,腳本就休克了。命令版本不同會有用法上的微秒差別,調試測試困難。頻繁起新進程性能低下)、功能孱弱蹩腳(很多需要頻繁使用的功能不全面或者不好用,比如字元串處理和數組的用法)等等,讓很多開發者非常頭疼,其中有些人甚至主張禁止使用 shell 腳本,一律改用 Python 等等,但 Python 並非適用所有場景,而且也有另外的一些問題,這樣做也是因噎廢食。Zsh 並非將這些問題全部解決了,但和 bash 相比,有很大的改善。比如 zsh 支持多種風格的語法,開發者很容易找到親切感;對外部命令的依賴比 bash 要輕很多,多數常用的功能不需要使用外部命令,性能更好,調試也更加方便;功能上和 bash 相比也有比較大的提升,處理不那麼複雜的場景已經比較夠用了。
有人可能會說,不如「一步到位」,使用 Powershell。Powershell 的確比 Python 更適合作為一種 shell 腳本語言,但使用它的話會有其他問題。
首先 Powershell 的學習成本是絕對要比 zsh 高的,如果想省點事,這並不是好的選擇。
其次 Linux 下的 Powershell 目前還是 beta 版,以後會不會有很多人用也很難說,如果很少有人用,那麼生態環境就成問題。比如遇到問題後找不到解決辦法,配套的軟體和庫不完善等等。
再次 Powershell 解釋器的啟動速度非常感人,在我的機器上,Windows 下的 Powershell 空腳本要執行將近 200 毫秒,Linux 下的要更長一些(我只在 WSL 里安裝試用過,時間翻了幾倍),而 zsh 的話,在 Linux 下不超過 5 毫秒,在 WSL 下也不超過 20 毫秒。如果寫一個簡單的腳本,運行時都要卡一下,是非常影響體驗的。
最後如果平時就使用 Powershell 作為交互 shell,那麼雖然腳本的啟動時間問題有所緩解,但用戶體驗會差很多,而且以後也很難提升上來,很容易得不償失。
Zsh 腳本樣例
可以通過一個例子直觀感受下用 zsh 寫的腳本。這是一個刪除當前目錄以及所有子目錄下重複文件的腳本,通過 md5 判斷文件是否相同(不嚴謹)。熟悉 bash 的讀者可以嘗試用 bash 完成相同的功能,然後對比一下代碼(我之前寫過一個 bash 版本的,不貼上來了),就能比較直觀地感受到 bash 和 zsh 的區別了。
#!/bin/zshlocal files=("${(f)$(md5sum **/*(.D))}")local files_to_delete=()local -A md5sfor i ($files) { local md5=$i[1,32] if (($+md5s[$md5])) { files_to_delete+=($i[35,-1]) } else { md5s[$md5]=1 }}(($#files_to_delete)) && rm -v $files_to_delete
為什麼要使用 shell 腳本語言
對於沒有接觸過 shell 腳本的開發者或者用戶來說,有一個更重要的問題,我為什麼要學習和使用 shell 腳本呢?
那麼要從 shell 腳本的使用場景說起。Shell 是一種和計算機系統交互的文本界面(CLI),簡單說就是輸入命令後返回結果(也有比較複雜的操作)。CLI 在某些場景要比圖形界面(GUI)方便和高效很多,是不可取代的(即使有一天語音識別取代了文本輸入,CLI 也會換湯不換藥地繼續存在)。那麼使用 CLI 就必須約定好指令格式,而 shell 腳本就是一種用於 CLI 交互的指令格式。
因為這個比較特別的場景,shell 腳本有一些與其他編程語言不同的特點。一個很重要的特點,shell 腳本要比較簡潔,容易輸入。如果發送一條簡單指令就要打幾十個字元,那恐怕誰也無法接受。而為了達到可以接受的簡潔程度,shell 腳本的語法,往往比其他編程語言的更加怪異。
有人可能會說,這搞混了兩個事情。在 CLI 輸入命令和寫腳本文件然後執行命令是兩回事,不需要使用同一種語言,而只是在 CLI 交互中,通常是沒有必要寫複雜邏輯的,也就是說 shell 腳本基本沒有必要學習。
是兩回事不假,但二者並不是不相關的。比如有人這麼想後,決定在 shell 里只使用最簡單的命令,不學習較為複雜的語法,如果需要寫腳本,就用 Python 之類的語言寫。那麼有什麼問題嗎?
Python 是為通用的場景設計的,雖然也能處理 shell 腳本所做的事情,但往往要寫出多幾倍甚至幾十倍(如果對 Python 也不甚了解的話)的代碼出來。而很多時候,shell 腳本做的是一次性工作,運行完就直接刪除,或者直接在一行敲完,回車即可,這樣的場景用 Python 寫成本要高出很多。而且並不是一個 Python 初學者就能用 Python 實現 shell 腳本的功能的,甚至熟練的 Python 開發者也很可能一時想不好怎麼實現某個用 shell 腳本能很容易實現的功能。Shell 腳本的很多工作是和字元串和目錄文件打交道,特點是要實現的功能複雜多樣,沒有固定模式,無論用什麼語言寫,都不容易。Python 自帶的字元串和目錄文件等類庫功能非常基礎,基本只能實現功能很單一的操作,稍微複雜點的功能都需要自己寫。如果去找某些功能複雜的第三方庫,那就會涉及一堆問題,比如同樣有學習和部署成本,可能因為用戶少所以有 bug 未被發現,可能已經沒有人維護了,Python 的語法決定庫怎麼寫都不能讓語法太簡潔等等。
而初步熟悉一門 shell 腳本只需要幾十分鐘,用多了自然就熟悉了,成本收益的權衡不言而喻。
格式約定
文中行首的 % 代表 zsh 的命令提示符(類似 bash 的 $,這個是可以自由定義的,具體是什麼不重要),行首的 > 代表此行是換行後的輸入內容,以 # 開頭的為注釋(非 root 用戶的命令提示符,本系列文章不需要 root 用戶),其餘的是命令的輸出內容。另外某些地方會貼成段的 zsh 代碼,那樣就省略開頭的 %,比較容易分辨。
一個樣例:
# 前兩行是輸入內容,第三行是輸出內容% echo "Hello > World"Hello World
本系列文章使用的 zsh 版本是 5.4.1(寫這篇文章時的最新版本),代碼在老版本中可能運行不了或者結果有出入,盡量使用最新版本。
下面直接進入正題。
變數
接觸一門新的編程語言,運行完 Hello World 後,首先要了解的基本就是如何定義和使用變數了。有了變數後可以比較變數內容,進而可以接觸條件、循環、分支等語句,繼而了解函數的用法,更高級的數據結構的使用,更多庫函數,等等。這樣就大概了解了一門面向過程的語言的基本用法,剩下的可以等到用的時候再查手冊。
所以這一篇講最基本的變數和語句。
zsh 有 5 種變數:整數、浮點數(bash 不支持)、字元串、數組、哈希表(或者叫關聯數組或者字典,本系列文章統一使用「哈希表」這一名詞),另外還有一些其他語言少有的東西,比如 alias(但主要是交互時使用,編程時基本用不到)。此篇只涉及整數、浮點數、字元串,並且不涉及數值計算和字元串處理等內容。
變數定義
Zsh 的變數多數情況不需要提前聲明或者指定類型,可以直接賦值和使用(但哈希表是一個例外)。
# 等號兩端不能有空格% num1=123% num2=123.456% str1=abcde# 如果字元串中包含空格等特殊字元,需要加引號% str2=abc def# 也可以用雙引號,但和單引號有區別,比如雙引號里可以使用變數,而單引號不可以% str3="abc def $num1"# 在字元串中可以使用轉義字元,單雙引號均可% str4="abc def
g"# 輸出變數,也可以使用 print% echo $str1abcde# 簡單的數值計算% num3=$(($num1 + $num2))# (( 中的變數名可以不用 $% num3=$((num1 + num2))# 簡單的字元串操作% str=abcdef# 2 和 4 都是字元在數組的位置,從 1 開始數,逗號兩邊不能有空格% echo $str[2,4]bcd# -1 是最後一個字元% echo $str[4,-1]def
變數比較
# 比較數值% num=123# (( )) 用於數值比較等操作,如果為真返回 0,否則返回 1# && 後邊的語句在前邊的語句為真時才執行# 注意這裡只能使用雙等號來比較% ((num == 123)) && echo goodgood# (( 裡邊可以使用與(&&)或(||)非(!)操作符,同 c 系列語言% ((num == 1 || num == 2)) && echo good# 比較字元串% str=abc# 比較字元串要用 [[,內側要有空格,[[ 的具體用法之後會講到# 這裡雙等號可以替換成單等號,可以根據自己的習慣選用# 本系列文章統一使用雙等號,因為和 (( )) 一致,並且使用雙等號的常用編程語言更多些# $str 兩側不需要加雙引號,即使 str 未定義或者 $str 中含空格和特殊符號% [[ $str == abc ]] && echo goodgood# 可以和空字元串 "" 比較,未定義的字元串和空字元串比較結果為真# [[ 里也可以用 && || !% [[ $str == "" || $str == 123 ]] && echo good
語句
稍微了解下簡單變數的使用後,快速進入語句部分。
zsh 支持多種風格的語法,包括經典的 posix shell (bash 的語法和它類似,但有一些擴展,可以歸為一類)的,以及 csh 風格的等等。但 posix shell 的語法並不好用,我們沒必要一定使用這個。我只選用一種我認為最方便簡潔的語法,沒有 fi、then、do、done、esac、in 等的關鍵字(雖然其中某些關鍵字其他編程語言也有,但基本用法都各異,而且容易混淆),也不需要多餘的分號。如果不確定語法是否符合預期,可以定義一個函數然後使用 which 查看,內容會被轉化成原始(posix shell 風格)的樣子。熟悉 bash 並且喜歡使用 bash 語法的讀者可以跳過這部分內容,語法的不同並不影響後續內容的閱讀,繼續使用 bash 風格語法寫 zsh 也是沒有問題的。
條件語句
# 格式if [[ ]] {} elif {} else {}
大括弧也可以另起一行,本系列文章統一使用這種風格,縮進為 4 個空格。注意 elif不可寫作 else if。
[[ ]] 用於比較字元串、判斷文件等,功能比較複雜多樣,這裡先使用最基礎的用法。注意盡量不要用 [[ ]] 比較數值,因為不留神的話,數值會被轉化成字元串來比較,沒有任何錯誤提示,但結果可能不符合預期,導致不必要的麻煩。
# 樣例if [[ "$str" == "name" || "$str" == "value" ]] { echo "$str"}
(( )) 用於比較數值,裡邊可以調用各種數值相關的函數,格式類似 c 語言,變數前的 $ 可省略。
# 格式if (( )) {}# 樣例if ((num > 3 && num + 3 < 10)) { echo $num}
{ } 用於在當前 shell 運行命令並且判斷運行結果。
# 格式if { } {}# 樣例if {grep sd1 /etc/fstab} { echo good}
( ) 用於在子 shell 運行命令並且判斷運行結果,用法和 {} 類似,不再舉例。
# 格式if ( ) {}
這幾種括弧可以一起使用,這樣可以同時判斷字元串、數值、文件、命令結果等等。最好不要混合使用 && ||,會導致可讀性變差和容易出錯。
# 格式if [[ ]] && (( )) && { } {}
循環語句
# 格式while [[ ]] { break/continue}
和 if 一樣,這裡的 [[ ]] 可以替換成其他幾種括弧,功能也是一樣的,不再依次舉例。break 用於結束循環,continue 用於直接進入下一次循環。所有的循環語句中都可以使用 break 和 continue,下邊不再贅述。
# 樣例 死循環 while (( 1 )) { echo good}
until 和 while 相反,不滿足條件時運行,一旦滿足則停止,其他的用法和 while 相同,不再舉例。
# 格式until [[ ]] {}
for 循環主要用於枚舉,這裡的括弧是 for 的特有用法,不是在子 shell 執行。括弧內是字元串(可放多個,空格隔開)、數組(可放多個)或者哈希表(可放多個,哈希表是枚舉值而不是鍵)。i 是用於枚舉內容的變數名,變數名隨意。
# 格式for i ( ) {}# 樣例for i (aa bb cc) { echo $i}# 枚舉當前目錄的 txt 文件for i (*.txt) { echo $i}# 枚舉數組array=(aa bb cc)for i ($array) { echo $i}
經典的 c 風格 for 循環。
# 格式for (( ; ; )) {}# 樣例for ((i=0; i < 10; i++)) { echo $i}
這個樣例只是舉例,實際上多數情況不需要使用這種 for 循環,可以這樣。
# 樣例,{1..10} 可以生成一個 1 到 10 的數組for i ({1..10}) { echo $i}
repeat 語句用於循環固定次數,n 是一個整數或者內容為整數的變數。
# 格式repeat n {}# 樣例repeat 5 { echo good}
分支語句
分支邏輯用 if 也可以實現,但 case 更適合這種場景,並且功能更強大。
# 格式 + 樣例case $i { (a) echo 1 ;; (b) echo 2 # 繼續執行下一個 ;& (c) echo 3 # 繼續向下匹配 ;| (c) echo 33 ;; (d) echo 4 ;; (*) echo other ;;}
;; 代表結束 case 語句,;& 代表繼續執行緊接著的下一個匹配的語句(不再進行匹配),;| 代表繼續往下匹配看是否有滿足條件的分支。
用戶輸入選擇語句
select 語句是用於根據用戶的選擇決定分支的語句,語法和 for 語句差不多,如果不 break,會循環讓用戶選擇。
# 格式select i ( ) {}# 樣例select i (aa bb cc) { echo $i}
輸出是這樣的。
1) aa 2) bb 3) cc?#
按上邊的數字加回車來選擇。
異常處理語句
# 格式{ 語句 1} always { 語句 2}
如果語句 1 執行出錯,則執行語句 2。
簡化的條件語句
if 語句的簡化版,在只有一個分支的情況下更簡潔,功能和 if 語句類似,不贅述。
格式:[[ ]] || {}[[ ]] && {}
最好不要連續混合使用 && ||,比如。
aa && bb || cc && dd
容易導致邏輯錯誤或者誤解,可以用 { } 把語句包含起來。
aa && { bb || { cc && dd } }
比較複雜的判斷還是用 if 可讀寫更好,&& || 通常只適用於簡單的場景。
總結
本篇簡單介紹了變數和語句的使用方法。變數部分只涉及了最基礎常用的部分,後續文章會詳細介紹。語句部分已經覆蓋了所有需要使用的語句,實際上這些語句都不只有這一種語法,但本系列文章統一使用這個語法。但涉及到的幾種括弧的用法比較複雜,之後的文章也會詳細介紹。
本文不再更新,全系列文章在此更新維護:github.com/goreliu/zshguide
付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活定價,歡迎諮詢,微信 ly50247。
推薦閱讀: