Zsh 開發指南(第一篇 變數和語句)

陌辭寒發表於2017-09-15

導讀

網上關於 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/zsh

local files=("${(f)$(md5sum **/*(.D))}")
local files_to_delete=()
local -A md5s

for 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\tdef\ng"

# 輸出變數,也可以使用 print
% echo $str1
abcde

# 簡單的數值計算
% 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 good
good
# (( 裡邊可以使用與(&&)或(||)非(!)操作符,同 c 系列語言
% ((num == 1 || num == 2)) && echo good

# 比較字串
% str=abc
# 比較字串要用 [[,內側要有空格,[[ 的具體用法之後會講到
# 這裡雙等號可以替換成單等號,可以根據自己的習慣選用
# 本系列文章統一使用雙等號,因為和 (( )) 一致,並且使用雙等號的常用程式語言更多些
# $str 兩側不需要加雙引號,即使 str 未定義或者 $str 中含空格和特殊符號
% [[ $str == abc ]] && echo good
good
# 可以和空字串 "" 比較,未定義的字串和空字串比較結果為真
# [[ 裡也可以用 && || !
% [[ $str == "" || $str == 123 ]] && echo good複製程式碼

語句

稍微瞭解下簡單變數的使用後,快速進入語句部分。

zsh 支援多種風格的語法,包括經典的 posix shell (bash 的語法和它類似,但有一些擴充套件,可以歸為一類)的,以及 csh 風格的等等。但 posix shell 的語法並不好用,我們沒必要一定使用這個。我只選用一種我認為最方便簡潔的語法,沒有 fithendodoneesacin 等的關鍵字(雖然其中某些關鍵字其他程式語言也有,但基本用法都各異,而且容易混淆),也不需要多餘的分號。如果不確定語法是否符合預期,可以定義一個函式然後使用 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 用於直接進入下一次迴圈。所有的迴圈語句中都可以使用 breakcontinue,下邊不再贅述。

# 樣例 死迴圈
 while (( 1 )) {
    echo good
}複製程式碼

untilwhile 相反,不滿足條件時執行,一旦滿足則停止,其他的用法和 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/zsh…

付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活定價,歡迎諮詢,微信 ly50247。

相關文章