久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

Mort | Zsh vs. Bash:不完全對比解析(1)

 bdpqlxz 2014-10-07

Zsh和Bash,,究竟有何不同

已經(jīng)有不少人寫過類似“為什么Zsh比Bash好”“為什么Zsh比* shell好”的文章了,講解如何配置Zsh或折騰各種oh-my-zsh主題的教程也是一搜一大籮,,但是卻極少看到Zsh和Bash這兩個Shell作為腳本語言時的具體差異比較,。那么,這里就是一篇,,從語言特性的角度上簡單整理了兩者一些細微的不兼容之處,,供編寫可移植Shell腳本時參考,。(僅僅是從我自己過去的經(jīng)驗教訓(xùn)中總結(jié)出來的,,所以應(yīng)該也是不完全的。)

開始之前:理解Zsh的仿真模式(emulation mode)

一種流行的說法是,,Zsh是與Bash兼容的,。這種說法既對,也不對,,因為Zsh本身作為一種腳本語言,,是與Bash不兼容的。符合Bash規(guī)范的腳本無法保證被Zsh解釋器正確執(zhí)行,。但是,,Zsh實現(xiàn)中包含了一個屌炸天的仿真模式(emulation mode),支持對兩種主流的Bourne衍生版shell(bash、ksh)和C shell的仿真(csh的支持并不完整),。在Bash的仿真模式下,,可以使用與Bash相同的語法和命令集合,從而達到近乎完全兼容的目的,。為了激活對Bash的仿真,,需要顯式執(zhí)行:

$ emulate bash

等效于:

$ emulate sh

Zsh是不會根據(jù)文件開頭的shebang(如#!/bin/sh#!/bin/bash)自動采取兼容模式來解釋腳本的,因此,,要讓Zsh解釋執(zhí)行一個其他Shell的腳本,,你仍然必須手動emulate sh或者emulate ksh,告訴Zsh對何種Shell進行仿真,。

那么,,Zsh究竟在何時能夠自動仿真某種Shell呢?

對于如今的絕大部分GNU/Linux(Debian系除外)和Mac OS X用戶來說,,系統(tǒng)默認的/bin/sh指向的是bash

$ file /bin/sh
/bin/sh: symbolic link to `bash'

不妨試試用zsh來取代bash作為系統(tǒng)的/bin/sh

# ln -sf /bin/zsh /bin/sh

所有的Bash腳本仍然能夠正確執(zhí)行,,因為Zsh在作為/bin/sh存在時,能夠自動采取其相應(yīng)的兼容模式(emulate sh)來執(zhí)行命令,。也許正是因為這個理由,,Grml直接選擇了Zsh作為它的/bin/sh,對現(xiàn)有的Bash腳本能做到近乎完美的兼容,。

無關(guān)主題:關(guān)于/bin/sh和shebang的可移植性

說到/bin/sh,,就不得不提一下,在Zsh的語境下,,sh指的是大多數(shù)GNU/Linux發(fā)行版上/bin/sh默認指向的bash,,或者至少是一個Bash的子集(若并非全部GNU Bash的最新特性都被實現(xiàn)的話),而非指POSIX shell,。因此,,Zsh中的emulate sh可以被用來對Bash腳本進行仿真。

眾所周知,,Debian的默認/bin/shdash(Debian Almquist shell),,這是一個純粹POSIX shell兼容的實現(xiàn),基本上你要的bash和ksh里的那些高級特性它都沒有,。“如果你在一個#!/bin/sh腳本中用到了非POSIX shell的東西,,說明你的腳本寫得是錯的,不關(guān)我們發(fā)行版的事情,?!?/em>Debian開發(fā)者們在把默認的/bin/sh換成dash,導(dǎo)致一些腳本出錯時這樣宣稱道,。當(dāng)然,,我們應(yīng)該繼續(xù)假裝與POSIX shell標(biāo)準(zhǔn)保持兼容是一件重要的事情,,即使現(xiàn)在大家都已經(jīng)用上了更高級的shell。

因為有非GNU的Unix,,和Debian GNU/Linux這類發(fā)行版的存在,,你不能夠假設(shè)系統(tǒng)的/bin/sh總是GNU Bash,也不應(yīng)該把#!/bin/sh用作一個Bash腳本的shebang(——除非你愿意放棄你手頭Shell的高級特性,,寫只與POSIX shell兼容的腳本),。如果想要這個腳本能夠被方便地移植的話,應(yīng)指定其依賴的具體Shell解釋器:

#!/usr/bin/env bash

這樣系統(tǒng)才能夠總是使用正確的Shell來運行腳本,。

(當(dāng)然,,顯式地調(diào)用bash命令來執(zhí)行腳本,shebang怎樣寫就無所謂了)


echo命令 / 字符串轉(zhuǎn)義

Zsh比之于Bash,,可能最容易被注意到的一點不同是,,Zsh中的echoprintf是內(nèi)置的命令。

$ which echo
echo: shell built-in command

$ which printf
printf: shell built-in command

Bash中的echoprintf同樣是內(nèi)置命令:

$ type echo
echo is a shell builtin

$ type printf
echo is a shell builtin

感謝讀者提醒,,在Bash中不能通過which來確定一個命令是否為外部命令,,因為which本身并不是Bash中的內(nèi)置命令which在Zsh中是一個內(nèi)置命令,。

Zsh內(nèi)置的echo命令,,與我們以前在GNU Bash中常見的echo命令,使用方式是不兼容的,。

首先,,請看Bash:

$ echo \
$ echo \\\\

我們知道,因為這里傳遞給echo的只是一個字符串(允許使用反斜杠\轉(zhuǎn)義),,所以不加引號與加上雙引號是等價的,。Bash輸出了我們預(yù)想中的結(jié)果:每兩個連續(xù)的\轉(zhuǎn)義成一個\字符輸出,最終2個變1個,,4個變2個,。沒有任何驚奇之處。

你能猜到Zsh的輸出結(jié)果么,?










$ echo \
$ echo \\\

(゜Д゜*)

解釋稍后,。

我們還知道,要想避免一個字符串被反斜杠轉(zhuǎn)義,,可以把它放進單引號,。正如我們在Bash中所清楚看到的這樣,,所有的反斜杠都照原樣輸出:

$ echo '\\'
\
$ echo '\\\\'
\\\

再一次,,你能猜到Zsh的輸出結(jié)果么?










$ echo '\\'

$ echo '\\\\'
\

((((((゜Д゜*))))))))))))

這個解釋是這樣的:在前一種不加引號(或者加了雙引號)的情形下,,傳遞給echo內(nèi)部命令的字符串將首先被轉(zhuǎn)義,,echo \\中的\\被轉(zhuǎn)義成\echo \\\\中的\\\\被轉(zhuǎn)義成\\。然后,,在echo這個內(nèi)部命令輸出到終端的時候,,它還要把這個東西再轉(zhuǎn)義一遍,一個單獨的\沒法轉(zhuǎn)義,,所以仍然是作為\輸出,;連續(xù)的\\被轉(zhuǎn)義成\,所以輸出就是\,。因此,,echo \\echo \\\\的輸出相同,都是\,。

為了讓Zsh中echo的輸出不被轉(zhuǎn)義,,需要顯式地指明-E選項:

$ echo -E \
$ echo -E \\\\

于是,我們也就知道在后一種加單引號的情形下,,如何得到與原字符串完全相同的輸出了:

$ echo -E '\\'
\
$ echo -E '\\\\'
\\\

而Bash的echo默認就是不對輸出進行轉(zhuǎn)義的,,若要得到轉(zhuǎn)義的效果,需顯式地指定-e選項,。Bash和Zsh中echo命令用法的不兼容,,在這里體現(xiàn)出來了。

變量的自動分字(word splitting)

在Bash中,,你可以通過調(diào)用外部命令echo輸出一個字符串:

echo $text

我們知道,,Bash會對傳遞給命令的字符串進行分字(根據(jù)空格或換行符),然后作為多個參數(shù)傳給echo,。當(dāng)然,,作為分隔符的換行,在最終輸出時就被抹掉了,。于是,,更好的習(xí)慣是把變量名放在雙引號中,把它作為一個字符串傳遞,,這樣就可以保留文本中的換行符,,將其原樣輸出。

echo "$text"

在Zsh中,,你不需要通過雙引號來告訴解釋器“$text是一個字符串”,。解釋器不會把它轉(zhuǎn)換成一個由空格或者\n分隔的參數(shù)列表或者別的什么。所以,,沒有Bash中的trick,,直接echo $text就可以保留換行符。但是,,如前一節(jié)所說,,我們需要一個多余的工作來保證輸出的是未轉(zhuǎn)義的原始文本,,那就是-E選項:

echo -E $text

從這里我們看到,Zsh中的變量在傳遞給命令時是不會被自動切分成words然后以多個參數(shù)的形式存在的,。它仍然保持為一個量,。這是它與傳統(tǒng)的Bourne衍生shell(ksh、bash)的一個重要不兼容之處,。這是Zsh的特性,,而不是一個bug

通配符展開(globbing)

通配符展開(globbing)也許是Unix shell中最為實用化的功能之一,。比起正則表達式,,它的功能相當(dāng)有限,不過它的確能滿足大部分時候的需求:依據(jù)固定的前綴或后綴匹配文件,。需要更復(fù)雜模式的時候其實是很少見的,,至少在文件的命名和查找上。

Bash和Zsh對通配符展開的處理方式有何不同呢,?舉個例子,,假如我們想要列舉出當(dāng)前目錄下所有的.markdown文件,但實際上又不存在這樣的文件,。在Zsh中:(注意到這里使用了內(nèi)置的echo,,因為我們暫時還不想用到外部的系統(tǒng)命令)

$ echo *.markdown
zsh: no matches found: *.markdown

Bash中:

$ echo *.markdown
*.markdown

Zsh因為通配符展開失敗而報錯;而Bash在通配符展開失敗時,,會放棄把它作為通配符展開,、直接把它當(dāng)做字面量返回??雌饋?,Zsh的處理方式更優(yōu)雅,因為這樣你就可以知道這個通配符確實無法展開,;而在Bash中,,你很難知道究竟是不存在這樣的文件,還是存在一個文件名為'*.markdown'的文件,。

接下來就是不那么和諧的方面了,。

在Zsh中,用ls查看當(dāng)然還是報錯:

$ ls *.markdown
zsh: no matches found: *.markdown

Bash,,這時候調(diào)用ls也會報錯,。因為當(dāng)前目錄下沒有.markdown后綴的文件,通配符展開失敗后變成字面的'*.markdown',,這個文件自然也不可能存在,,所以外部命令ls報錯:

$ ls *.markdown
ls: cannot access *.markdown: No such file or directory

同樣是錯誤,差別在哪里,?對于Zsh,,這是一個語言級別的錯誤;對于Bash,,這是一個外部命令執(zhí)行的錯誤,。這件差別很重要,因為它意味著后者可以被輕易地catch,,而前者不能,。

想象一個常見的命令式編程語言,Java或者Python,。你可以用try...catch或類似的語言結(jié)構(gòu)來捕獲運行時的異常,,比較優(yōu)雅地處理無法預(yù)料的錯誤。Shell當(dāng)然沒有通用的異常機制,,但是,,你可以通過檢測某一段命令的返回值來模擬捕獲運行時的錯誤。例如,,在Bash里可以這樣:

$ if ls *.markdown &>/dev/null; then :; else echo $?; fi
2

于是,,在通配符展開失敗的情形下,我們也能輕易地把外部命令的錯誤輸出重定向到/dev/null,,然后根據(jù)返回的錯誤碼執(zhí)行后續(xù)的操作,。

不過在Zsh中,這個來自Zsh解釋器自身的錯誤輸出卻無法被重定向:

$ if ls *.markdown &>/dev/null; then :; else echo $?; fi
zsh: no matches found: *.markdown
1

大部分時候,,我們并不想看到這些丑陋多余的錯誤輸出,,我們期望程序能完全捕獲這些錯誤,然后完成它該完成的工作,。但這也許是一種正常的行為,。理由是,在程序語言里,,syntax error一般是無法簡單地由用戶在運行階段自行catch的,,這個報錯工作將直接由解釋器來完成。除非,,當(dāng)然,,除非我們用了邪惡的eval

$ if eval "ls *.markdown" &>/dev/null; then :; else echo $?; fi
1

Eval is evil. 但在Zsh中捕獲這樣的錯誤,,似乎沒有更好的辦法了,。必須這么做的原因就是:Zsh中,通配符展開失敗是一個語法錯誤,。而在Bash中則不是,。

基于上述理由,依賴于Bash中通配符匹配失敗而直接把"*"當(dāng)作字面量傳遞給命令的寫法,,在Zsh中是無法正常運行的,。例如,,在Bash中你可以:(雖然在大部分情況下能用,但顯然不加引號是不科學(xué)的)

$ find /usr/share/git -name *.el

因為Zsh不會在glob擴展失敗后自動把"*"當(dāng)成字面量,,而是直接報錯終止運行,,所以在Zsh中你必須"*.el"加上引號,來避免這種擴展:

$ find /usr/share/git -name "*.el"

字符串比較

在Bash中判斷兩個字符串是否相等:

[ "$foo" = "$bar" ]

或與之等效的(現(xiàn)代編程語言中更常見的==比較運算符):

[ "$foo" == "$bar" ]

注意等號左右必須加空格,,變量名一定要放在雙引號中,。(寫過Shell的都知道這些規(guī)則的重要性)

在條件判斷的語法上,Zsh基本和Bash相同,,沒有什么改進,。除了它的解釋器想得太多,以至于不小心把==當(dāng)做了一個別的東西:

$ [ foo == bar ]; echo $?
zsh: = not found

要想使用我們最喜歡的==,,只有把它用引號給保護起來,,不讓解釋器做多余的解析:

$ [ foo "==" bar ]; echo $?
1

所以,為了少打幾個字符,,還是老老實實用更省事的=吧,。

數(shù)組

同樣用一個簡單的例子來說明。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的數(shù)組下標(biāo)是從0開始的,。$array取得的實際上是數(shù)組的第一個元素的值,也就是${array[0]}(這些行為和C有點像),。要想取得整個數(shù)組的值,,必須使用${array[*]}${array[@]},因此,,獲取數(shù)組的長度可以使用${#array[*]},。在Bash中,必須記得在訪問數(shù)組元素時給整個數(shù)組名連同下標(biāo)加上花括號,,比如,,${array[*]}不能寫成$array[*],否則解釋器會首先把$array當(dāng)作一個變量來處理,。

再來看這段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[*]一樣,可以用來取得整個數(shù)組的值,。因此獲取數(shù)組的長度可直接用$#array,。

Zsh的默認數(shù)組下標(biāo)是從1而不是0開始的,這點更像C shell,。(雖然一直無法理解一個名字叫C的shell為何會采用1作為數(shù)組下標(biāo)開始這種奇葩設(shè)定)

最后,,Zsh不需要借助花括號來訪問數(shù)組元素,因此Bash中必需的花括號都被略去了。

關(guān)聯(lián)數(shù)組

Bash 4.0+和Zsh中都提供了對類似AWK關(guān)聯(lián)數(shù)組的支持,。

declare -A array
array[mort]=foo

和普通的數(shù)組一樣,,在Bash中,必須顯式地借助花括號來訪問一個數(shù)組元素:

echo ${array[mort]}

而Zsh中則沒有必要:

echo $array[mort]

說到這里,,我們注意到Zsh有一個不同尋常的特性:支持使用方括號進行更復(fù)雜的globbing,,array[mort]這樣的寫法事實上會造成二義性:究竟是取array這個關(guān)聯(lián)數(shù)組以mort為key的元素值呢,還是以通配符展開的方式匹配當(dāng)前目錄下以"array"開頭,,以"m",、"o",、"r""t"任一字符結(jié)尾的文件名呢,?

array[mort]=作為命令開始的情況下,不存在歧義,,這是一個對關(guān)聯(lián)數(shù)組的賦值操作,。在前面帶有$的情況下,Zsh會自動把$array[mort]識別成取關(guān)聯(lián)數(shù)組的值,,這也沒有太大問題,。問題出在它存在于命令中間,卻又不帶$的情況,,比如:

read -r -d '' array[mort] << 'EOF'
hello world
EOF

我們的本意是把這個heredoc賦值給array[mort]數(shù)組元素,。在Bash中,這是完全合法的,。然而,,在Zsh中,解釋器會首先試圖對"array[mort]"這個模式進行g(shù)lob展開,,如果當(dāng)前目錄下沒有符合該模式的文件,,當(dāng)然就會報出一個語法錯誤:

zsh: no matches found: array[mort]

這是一件很傻的事情,為了讓這段腳本能夠被Zsh解釋器正確執(zhí)行,,我們需要把array[mort]放在引號中以防止被展開:

read -r -d '' 'array[mort]' << 'EOF'
hello world
EOF

這是Zsh在擴展了一些強大功能的同時帶來的不便之處(或者說破壞了現(xiàn)有腳本兼容性的安全隱患,,又或者是讓解釋器混亂的pitfalls)。

順便說一句,,用Rake構(gòu)建過項目的Rails程序員都知道,,有些時候需要在命令行下通過方括號給rake傳遞參數(shù)值,如:

$ rake seeder:seed[100]

Zsh這個對方括號展開的特性確實很不方便,。如果不想每次都用單引號把參數(shù)括起來,,可以完全禁止Zsh對某條命令后面的參數(shù)進行g(shù)lob擴展:(~/.zshrc

alias rake="noglob rake"

嗯,對于rake命令來說,,glob擴展基本是沒有用的,。你可以關(guān)掉它。

分號與空語句

雖然有點無聊,但還是想提一下:Bash不允許語句塊中使用空語句,,最小化的語句是一個noop命令(:),;而Zsh允許空語句

剛開始寫B(tài)ash的時候,,總是記不得什么時候該加分號什么時候不該加,。比如

if [ 1 ]
then
  :
fi

如果放在一行里寫,應(yīng)該是

if [ 1 ]; then :; fi

then后面是不能接分號的,,如果寫成

if [ 1 ]; then; :; fi

就會報錯:

bash: syntax error near unexpected token `;'

解釋是:then表示一個代碼段的開始,,fi表示結(jié)束,這中間的內(nèi)容必須是若干行命令,,或者以分號;結(jié)尾的放在同一行內(nèi)的多條命令,。我們知道在傳統(tǒng)的shell中,分號本身并不是一條命令,,空字符串也不是一條命令,,因此,then后面緊接著的分號就會帶來一條語法錯誤,。(有些時候?qū)δ硞€“語言特性”的所謂解釋只是為了掩飾設(shè)計者在一開始犯的錯誤,,所以就此打住)

在Zsh中,,上述兩種寫法都合法,。因為它允許只包含一個分號的空命令。

$ ;

當(dāng)然,,因為分號只是一個語句分隔符,,所以沒有也是可以的。這種寫法在Zsh中合法:(then的語句塊為空)

if [ 1 ]; then fi

第二彈

其實只是先挖個坑而已,。我也不知道有沒有時間寫,,暫且記上。

Zsh vs. Bash:不完全對比解析(2)

  • 別名,,函數(shù)定義和作用域
  • 協(xié)進程(coprocess)
  • 重定向
  • 信號和陷阱(trap)

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,謹防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多