bash和zsh的差異
技術標籤:SHELL
Zsh**Zsh中,萬用字元展開失敗是一個語法錯誤。而在Bash中則不是。**和Bash,究竟有何不一樣
已經有很多人寫過相似“為何Zsh比Bash好”“為何Zsh比* shell好”的文章了,講解如何配置Zsh或折騰各類oh-my-zsh主題的教程也是一搜一大籮,可是卻極少看到Zsh和Bash這兩個Shell做為指令碼語言時的具體差別比較。那麼,這裡就是一篇,從語言特性的角度上簡單整理了二者一些細微的不相容之處,供編寫可移植Shell指令碼時參考。(僅僅是從我本身過去的經驗教訓中總結出來的,因此應該也是不徹底的。)git
開始以前:理解Zsh的模擬模式(emulation mode)
一種流行的說法是,Zsh是與Bash相容的。這種說法既對,也不對,由於Zsh自己做為一種指令碼語言,是與Bash不相容的。符合Bash規範的指令碼沒法保證被Zsh直譯器正確執行。可是,Zsh實現中包含了一個屌炸天的模擬模式(emulation mode),支援對兩種主流的Bourne衍生版shell(bash、ksh)和C shell的模擬(csh的支援並不完整)。在Bash的模擬模式下,可使用與Bash相同的語法和命令集合,從而達到近乎徹底相容的目的。為了啟用對Bash的模擬,須要顯式執行:程式設計師
$ emulate bash
等效於:github
$ emulate sh
Zsh是不會根據檔案開頭的shebang(如#!/bin/sh
#!/bin/bash
)自動採起相容模式來解釋指令碼的,所以,要讓Zsh解釋執行一個其餘Shell的指令碼,你仍然必須手動emulate sh
或者emulate ksh
,告訴Zsh對何種Shell進行模擬。正則表示式
那麼,Zsh究竟在什麼時候可以***自動***模擬某種Shell呢?shell
對於現在的絕大部分GNU/Linux(Debian系除外)和Mac OS X使用者來講,系統預設的/bin/sh
指向的是bash
:程式設計
$ file /bin/sh
/bin/sh: symbolic link to `bash'
不妨試試用zsh
來取代bash
做為系統的/bin/sh
:陣列
# ln -sf /bin/zsh /bin/sh
全部的Bash指令碼仍然可以正確執行,由於Zsh在做為/bin/sh
存在時,可以自動採起其相應的相容模式(emulate sh
)來執行命令。也許正是由於這個理由,Grml直接選擇了Zsh做為它的/bin/sh
,對現有的Bash指令碼能作到近乎完美的相容。安全
無關主題:關於/bin/sh
和shebang的可移植性
說到/bin/sh
,就不得不提一下,在Zsh的語境下,sh
指的是大多數GNU/Linux發行版上/bin/sh
預設指向的bash
,或者至少是一個Bash的子集(若並不是所有GNU Bash的最新特性都被實現的話),而非指POSIX shell。所以,Zsh中的emulate sh
能夠被用來對Bash指令碼進行模擬。bash
眾所周知,Debian的預設/bin/sh
是 dash(Debian Almquist shell),這是一個純粹POSIX shell相容的實現,基本上你要的bash和ksh裡的那些高階特性它都沒有。*“若是你在一個#!/bin/sh
指令碼中用到了非POSIX shell的東西,說明你的指令碼寫得是錯的,不關咱們發行版的事情。”*Debian開發者們在把預設的/bin/sh
換成dash
,致使一些指令碼出錯時這樣宣稱道。固然,咱們應該繼續偽裝與POSIX shell標準保持相容是一件重要的事情,即便如今你們都已經用上了更高階的shell。
由於有非GNU的Unix,和Debian GNU/Linux這類發行版的存在,你不可以假設系統的/bin/sh
老是GNU Bash,也不該該把#!/bin/sh
用做一個Bash指令碼的shebang(——除非你願意放棄你手頭Shell的高階特性,寫只與POSIX shell相容的指令碼)。若是想要這個指令碼可以被方便地移植的話,應指定其依賴的具體Shell直譯器:
#!/usr/bin/env bash
這樣系統才可以老是使用正確的Shell來執行指令碼。
(固然,顯式地呼叫bash
命令來執行指令碼,shebang怎樣寫就無所謂了)
echo
命令 / 字串轉義
~~Zsh比之於Bash,可能最容易被注意到的一點不一樣是,~~Zsh中的echo
和printf
是內建的命令。
$ which echo
echo: shell built-in command
$ which printf
printf: shell built-in command
Bash中的echo
和printf
一樣是內建命令:
$ type echo
echo is a shell builtin
$ type printf
echo is a shell builtin
感謝讀者提醒,在Bash中不能經過which
來肯定一個命令是否為外部命令,由於which
自己並非Bash中的內建命令。which
在Zsh中是一個內建命令。
Zsh內建的echo
命令,與咱們之前在GNU Bash中常見的echo
命令,使用方式是***不相容***的。
首先,請看Bash:
$ echo \
$ echo \\\\
咱們知道,由於這裡傳遞給echo
的只是一個字串(容許使用反斜槓\
轉義),因此不加引號與加上雙引號是等價的。Bash輸出了咱們預想中的結果:每兩個連續的\
轉義成一個\
字元輸出,最終2個變1個,4個變2個。沒有任何驚奇之處。
你能猜到Zsh的輸出結果麼?
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
$ echo \
$ echo \\\
(゜Д゜*)
解釋稍後。
咱們還知道,要想避免一個字串被反斜槓轉義,能夠把它放進單引號。正如咱們在Bash中所清楚看到的這樣,全部的反斜槓都照原樣輸出:
$ echo '\\'
\
$ echo '\\\\'
\\\
再一次,你能猜到Zsh的輸出結果麼?
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
$ echo '\\'
$ echo '\\\\'
\
((((((゜Д゜*))))))))))))
這個解釋是這樣的:在前一種不加引號(或者加了雙引號)的情形下,傳遞給echo
內部命令的字串將首先被轉義,echo \\
中的\\
被轉義成\
,echo \\\\
中的\\\\
被轉義成\\
。而後,在echo
這個內部命令輸出到終端的時候,它還要把這個東西再轉義一遍,一個單獨的\
無法轉義,因此仍然是做為\
輸出;連續的\\
被轉義成\
,因此輸出就是\
。所以,echo \\
和echo \\\\
的輸出相同,都是\
。
為了讓Zsh中echo
的輸出不被轉義,須要顯式地指明-E
選項:
$ echo -E \
$ echo -E \\\\
因而,咱們也就知道在後一種加單引號的情形下,如何獲得與原字串徹底相同的輸出了:
$ echo -E '\\'
\
$ echo -E '\\\\'
\\\
而Bash的echo
預設就是不對輸出進行轉義的,若要獲得轉義的效果,需顯式地指定-e
選項。Bash和Zsh中echo
命令用法的不相容,在這裡體現出來了。
變數的自動分字(word splitting)
在Bash中,你能夠經過呼叫外部命令echo
輸出一個字串:
echo $text
咱們知道,Bash會對傳遞給命令的字串進行分字(根據空格或換行符),而後做為多個引數傳給echo
。固然,做為分隔符的換行,在最終輸出時就被抹掉了。因而,更好的習慣是把變數名放在雙引號中,把它做為一個字串傳遞,這樣就能夠保留文字中的換行符,將其原樣輸出。
echo "$text"
在Zsh中,你不須要經過雙引號來告訴直譯器“$text
是一個字串”。直譯器不會把它轉換成一個由空格或者\n
分隔的引數列表或者別的什麼。因此,沒有Bash中的trick,直接echo $text
就能夠保留換行符。可是,如前一節所說,咱們須要一個多餘的工做來保證輸出的是未轉義的原始文字,那就是-E
選項:
echo -E $text
從這裡咱們看到,Zsh中的變數在傳遞給命令時是不會被自動切分紅words而後以多個引數的形式存在的。它仍然保持為一個量。這是它與傳統的Bourne衍生shell(ksh、bash)的一個重要不相容之處。這是Zsh的特性,而不是一個bug。
萬用字元展開(globbing)
萬用字元展開(globbing)也許是Unix shell中最為實用化的功能之一。比起正則表示式,它的功能至關有限,不過它的確能知足大部分時候的需求:依據固定的字首或字尾匹配檔案。須要更復雜模式的時候實際上是不多見的,至少在檔案的命名和查詢上。
Bash和Zsh對萬用字元展開的處理方式有何不一樣呢?舉個例子,假如咱們想要列舉出當前目錄下全部的.markdown
檔案,但實際上又不存在這樣的檔案。在Zsh中:(注意到這裡使用了內建的echo
,由於咱們暫時還不想用到外部的系統命令)
$ echo *.markdown
zsh: no matches found: *.markdown
Bash中:
$ echo *.markdown
*.markdown
Zsh由於萬用字元展開失敗而報錯;而Bash在萬用字元展開失敗時,會放棄把它做為萬用字元展開、直接把它當作字面量返回。看起來,Zsh的處理方式更優雅,由於這樣你就能夠知道這個萬用字元確實沒法展開;而在Bash中,你很難知道到底是不存在這樣的檔案,仍是存在一個檔名為'*.markdown'
的檔案。
接下來就是不那麼和諧的方面了。
在Zsh中,用ls
檢視固然仍是報錯:
$ ls *.markdown
zsh: no matches found: *.markdown
Bash,這時候呼叫ls
也會報錯。由於當前目錄下沒有.markdown
字尾的檔案,萬用字元展開失敗後變成字面的'*.markdown'
,這個檔案天然也不可能存在,因此外部命令ls
報錯:
$ ls *.markdown
ls: cannot access *.markdown: No such file or directory
一樣是錯誤,差異在哪裡?對於Zsh,這是一個語言級別的錯誤;對於Bash,這是一個外部命令執行的錯誤。這件差異很重要,由於它意味著後者能夠被輕易地catch,而前者不能。
想象一個常見的指令式程式設計語言,Java或者Python。你能夠用try…catch或相似的語言結構來捕獲執行時的異常,比較優雅地處理沒法預料的錯誤。Shell固然沒有通用的異常機制,可是,你能夠經過檢測某一段命令的返回值來模擬捕獲執行時的錯誤。例如,在Bash裡能夠這樣:
$ if ls *.markdown &>/dev/null; then :; else echo $?; fi
2
因而,在萬用字元展開失敗的情形下,咱們也能輕易地把外部命令的錯誤輸出重定向到/dev/null
,而後根據返回的錯誤碼執行後續的操做。
不過在Zsh中,這個來自Zsh直譯器自身的錯誤輸出卻沒法被重定向:
$ if ls *.markdown &>/dev/null; then :; else echo $?; fi
zsh: no matches found: *.markdown
1
大部分時候,咱們並不想看到這些醜陋多餘的錯誤輸出,咱們指望程式能徹底捕獲這些錯誤,而後完成它該完成的工做。但這也許是一種正常的行為。理由是,在程式語言裡,syntax error通常是沒法簡單地由使用者在執行階段自行catch的,這個報錯工做將直接由直譯器來完成。除非,固然,除非咱們用了邪惡的eval
。
$ if eval "ls *.markdown" &>/dev/null; then :; else echo $?; fi
1
Eval is evil. 但在Zsh中捕獲這樣的錯誤,彷佛沒有更好的辦法了。必須這麼作的緣由就是:Zsh中,萬用字元展開失敗是一個語法錯誤。而在Bash中則不是。
基於上述理由,依賴於Bash中萬用字元匹配失敗而直接把"*"
看成字面量傳遞給命令的寫法,在Zsh中是沒法正常執行的。例如,在Bash中你能夠:(雖然在大部分狀況下*能用*,但顯然不加引號是不科學的)
$ find /usr/share/git -name *.el
由於Zsh不會在glob擴充套件失敗後自動把"*"
當成字面量,而是直接報錯終止執行,因此在Zsh中你必須給"*.el"
加上引號,來避免這種擴充套件:
$ find /usr/share/git -name "*.el"
字串比較
在Bash中判斷兩個字串是否相等:
[ "$foo" = "$bar" ]
或與之等效的(現代程式語言中更常見的==
比較運算子):
[ "$foo" == "$bar" ]
注意等號左右必須加空格,變數名必定要放在雙引號中。(寫過Shell的都知道這些規則的重要性)
在條件判斷的語法上,Zsh基本和Bash相同,沒有什麼改進。除了它的直譯器想得太多,以致於不當心把==
當作了一個別的東西:
$ [ foo == bar ]; echo $?
zsh: = not found
要想使用咱們最喜歡的==
,只有把它用引號給保護起來,不讓直譯器作多餘的解析:
$ [ foo "==" bar ]; echo $?
1
因此,為了少打幾個字元,仍是老老實實用更省事的=
吧。
陣列
一樣用一個簡單的例子來講明。Bash:
array=(alpha bravo charlie delta)
echo $array
echo ${array[*]}
echo ${#array[*]}
for ((i=0; i < ${#array[*]}; i++)); do
echo ${array[$i]}
done
輸出:
alpha
alpha bravo charlie delta
4
alpha
bravo
charlie
delta
很容易看到,Bash的陣列下標是從0開始的。$array
取得的其實是陣列的第一個元素的值,也就是${array[0]}
(這些行為和C有點像)。要想取得整個陣列的值,必須使用${array[*]}
或${array[@]}
,所以,獲取陣列的長度可使用${#array[*]}
。在Bash中,必須記得在訪問陣列元素時給整個陣列名連同下標加上花括號,好比,${array[*]}
不能寫成$array[*]
,不然直譯器會首先把$array
看成一個變數來處理。
再來看這段Zsh:
array=(alpha bravo charlie delta)
echo $array
echo $array[*]
echo $#array
for ((i=1; i <= $#array[*]; i++)); do
echo $array[$i]
done
輸出:
alpha bravo charlie delta
alpha bravo charlie delta
4
alpha
bravo
charlie
delta
在Zsh中,$array
和$array[*]
同樣,能夠用來取得整個陣列的值。所以獲取陣列的長度可直接用$#array
。
Zsh的預設陣列下標是從1而不是0開始的,這點更像C shell。(雖然一直沒法理解一個名字叫C的shell為什麼會採用1做為陣列下標開始這種奇葩設定)
最後,Zsh不須要藉助花括號來訪問陣列元素,所以Bash中必需的花括號都被略去了。
關聯陣列
Bash 4.0+和Zsh中都提供了對相似AWK關聯陣列的支援。
declare -A array
array[mort]=foo
和普通的陣列同樣,在Bash中,必須顯式地藉助花括號來訪問一個數組元素:
echo ${array[mort]}
而Zsh中則沒有必要:
echo $array[mort]
說到這裡,咱們注意到Zsh有一個不一樣尋常的特性:支援使用方括號進行更復雜的globbing,array[mort]
這樣的寫法事實上會形成二義性:到底是取array
這個關聯陣列以mort
為key的元素值呢,仍是以萬用字元展開的方式匹配當前目錄下以"array"
開頭,以"m"
、"o"
、"r"
或"t"
任一字元結尾的檔名呢?
在array[mort]=
做為命令開始的狀況下,不存在歧義,這是一個對關聯陣列的賦值操做。在前面帶有$
的狀況下,Zsh會自動把$array[mort]
識別成取關聯陣列的值,這也沒有太大問題。問題出在它存在於命令中間,卻又不帶$
的狀況,好比:
read -r -d '' array[mort] << 'EOF'
hello world
EOF
咱們的本意是把這個heredoc賦值給array[mort]
陣列元素。在Bash中,這是徹底合法的。然而,在Zsh中,直譯器會首先試圖對"array[mort]"
這個模式進行glob展開,若是當前目錄下沒有符合該模式的檔案,固然就會報出一個語法錯誤:
zsh: no matches found: array[mort]
這是一件很傻的事情,為了讓這段指令碼可以被Zsh直譯器正確執行,咱們須要把array[mort]
放在引號中以防止被展開:
read -r -d '' 'array[mort]' << 'EOF'
hello world
EOF
這是Zsh在擴充套件了一些強大功能的同時帶來的不便之處(或者說破壞了現有指令碼相容性的安全隱患,又或者是讓直譯器混亂的pitfalls)。
順便說一句,用Rake構建過專案的Rails程式設計師都知道,有些時候須要在命令列下經過方括號給rake
傳遞引數值,如:
$ rake seeder:seed[100]
Zsh這個對方括號展開的特性確實很不方便。若是不想每次都用單引號把引數括起來,能夠徹底禁止Zsh對某條命令後面的引數進行glob擴充套件:(~/.zshrc
)
alias rake="noglob rake"
嗯,對於rake
命令來講,glob擴充套件基本是沒有用的。你能夠關掉它。
分號與空語句
雖然有點無聊,但仍是想提一下:Bash不容許語句塊中使用空語句,最小化的語句是一個noop命令(:
);而Zsh容許空語句。
剛開始寫Bash的時候,老是記不得何時該加分號何時不應加。好比
if [ 1 ]
then
:
fi
若是放在一行裡寫,應該是
if [ 1 ]; then :; fi
then
後面是不能接分號的,若是寫成
if [ 1 ]; then; :; fi
就會報錯:
bash: syntax error near unexpected token `;'
解釋是:then
表示一個程式碼段的開始,fi
表示結束,這中間的內容必須是若干行命令,或者以分號;
結尾的放在同一行內的多條命令。咱們知道在傳統的shell中,分號自己並非一條命令,空字串也不是一條命令,所以,then
後面緊接著的分號就會帶來一條語法錯誤。(有些時候對某個“語言特性”的所謂解釋只是為了掩飾設計者在一開始犯的錯誤,因此就此打住)
在Zsh中,上述兩種寫法都合法。由於它容許只包含一個分號的空命令。
$ ;
固然,由於分號只是一個語句分隔符,因此沒有也是能夠的。這種寫法在Zsh中合法:(then
的語句塊為空)
if [ 1 ]; then fi
第二彈
其實只是先挖個坑而已。我也不知道有沒有時間寫,暫且記上。
Zsh vs. Bash:不徹底對比解析(2)
- 別名,函式定義和做用域
- 協程序(coprocess)
- 重定向
- 訊號和陷阱(trap)
Linux伺服器上zsh和bash的對比
使用預設指令列模式(bash shell)的管理員可能想仔細看看zshell或是zsh。因為它於bash類似,功能又有所增強,zsh在Linux社群得到了關注。那麼zsh有什麼不一樣之處呢?本文就列出在Linux伺服器上zsh和bash的資料形式的對比。
-
做者:Mark 譯來源:TechTarget中國| 2011-06-13 14:03
移動端 收藏 分享
使用預設指令列模式(bash shell)的管理員可能想仔細看看zshell或是zsh。因為它於bash類似,功能又有所增強,zsh在Linux社群得到了關注。
那麼zsh有什麼不一樣之處呢?首先,zsh在感受和功能上都和bash類似。可是一些加強功能讓zsh變成一個有趣的選擇。下面是一臺Linux伺服器上zsh和bash的資料形式的對比:
Zsh加強功能:標籤完成和拼寫錯誤修正
用過bash標籤完成的管理員會發現zsh中的增長功能使人印象深入。這些功能包括選單中現有的自動完成命令選項,該選單能夠經過使用箭頭鍵滾動。舉例來講,鍵入如下命令將提供可能命令列標記的列表:
$ ls -
或是
$ rm -
選擇要取消的特定程式,程式列表就和取消命令一塊兒可用了。
另外一個功能在內建頁面程式中,它提供到less命令的快捷方式。要訪問它,輸入:
$<filename
這和在命令列上執行less檔名同樣。
對笨拙的打字員來講,拼寫錯誤修正功能可用了。例如,若是你輸入了一條錯誤命令,zsh會提示修正:
$ lls
zsh: 要將 'lls’修改成 ‘ls’ [nyae]嗎?
要修改它,輸入y,命令就更正為ls,接著命令就準備運行了。
其它選項也很實用。輸入n拒絕命令修正,輸入a中斷命令,輸入e跳轉到命令列進行編輯。這個自動修正功能也能用於命令列標記和檔名,包括修改無效Git分支名稱一類的機密事務。
開始使用zsh
為了快速地開始使用zsh,可利用Robby Russell收集的zsh主題、功能和工具,它們被預先打包成“Oh My Zsh”。
$ wget --no-check-certificate https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh
手動的zsh安裝指令也可用,須要使用者克隆Git repo並複製在.zshrc的草稿模板中。
“Oh My Zsh”知識庫包含一個主題和功能集合用於現有zsh環境的建立和改變。它也能和解除安裝指令碼一塊兒用來簡化移除:
$ uninstall_oh_my_zsh
與zsh shell一塊兒供給的還有一些很好的文件和zsh參考卡。GitHub等網站上的線上資源是.zshrc檔案的例子,它至關於zsh版的.bashrc檔案,這些資源同時也提供如何定製zsh的示例或是示範增強命令列經驗的炫酷技巧。
一些zsh功能可和bash一塊兒用,但在bash上設定、配置更加複雜,這也解釋了為何人們有多頁.bashrc檔案。若是是Shell的高度使用者,zsh會是吸引你用來取代bash的選擇。它的使用快速且簡單,而它的一些重要功能也讓與shell的互動更有趣。
原文
閱讀小記
-
為什麼指令碼開頭有#!/bin/sh
- #! (magic數字)實則告訴系統去後面的路徑找到相關程式來執行當前指令碼
-
echo 和 printf 在bash 和zsh都是內建命令
- 可以用type 命令檢視
- 在bash中預設是不對輸出做轉義,zsh中則需要顯示指定-E才可以
-
bash會對傳遞給命令的字串進行分字
- zsh不會對傳遞給命令的字串分字,多個空格一樣輸出
- Zsh中的變數在傳遞給命令時是不會被自動切分紅words而後以多個引數的形式存在的。它仍然保持為一個量。這是它與傳統的Bourne衍生shell(ksh、bash)的一個重要不相容之處。這是Zsh的特性,而不是一個bug。
-
type 返回這個命令型別,是shell的內建命令
-
type
命令的-t
引數,可以返回一個命令的型別:別名(alias),關鍵詞(keyword),函式(function),內建命令(builtin)和檔案(file)。$ type -t bash file $ type -t if keyword
上面例子中,
bash
是檔案,if
是關鍵詞。
-
-
Zsh中,萬用字元展開失敗是一個語法錯誤。而在Bash中則不是。
- 程式設計的時候需要額外關注了
-
Zsh裡 ==要用“”括起來,不然zsh做過多解釋
-
Bash的陣列下標是從0開始的,訪問陣列元素時記得加上花括號,不然會把$array當作變數處理
-
Zsh的陣列下標是從1開始的,訪問陣列元素時不需要加上花括號
-
AWK關聯陣列支援區別
-
declare -A array array[mort]=foo
-
bash中需要顯式的藉助花括號來訪問一個數組
-
zsh中不需要藉助花括號
-
-
zsh支援方括號進行復雜的globbing
array[mort]
這樣的寫法事實上會形成二義性:到底是取array
這個關聯陣列以mort
為key的元素值呢,仍是以萬用字元展開的方式匹配當前目錄下以"array"
開頭,以"m"
、"o"
、"r"
或"t"
任一字元結尾的檔名呢?- 需要對其加‘’處理
- Zsh這個對方括號展開的特性確實很不方便。若是不想每次都用單引號把引數括起來,能夠徹底禁止Zsh對某條命令後面的引數進行glob擴充套件:(
~/.zshrc
)
-
分號和空語句
- 空字串不是一條命令,‘;’也不是一條命令,所以;在bash裡面會報錯