Unix有缺陷嗎?Unix的缺陷是什么?
我想通過(guò)這篇文章解釋一下我對(duì) Unix 哲學(xué)本質(zhì)的理解。我雖然指出 Unix 的一個(gè)設(shè)計(jì)問(wèn)題,但目的并不是打擊人們對(duì) Unix 的興趣。雖然 Unix 在基礎(chǔ)概念上有一個(gè)挺嚴(yán)重的問(wèn)題,但是經(jīng)過(guò)多年的發(fā)展之后,這個(gè)問(wèn)題恐怕已經(jīng)被各種別的因素所彌補(bǔ)(比如大量的人力)。但是如果開(kāi)始正視這個(gè)問(wèn)題,我們也許就可以緩慢的改善系統(tǒng)的結(jié)構(gòu),從而使得它用起來(lái)更加高效,方便和安全,那又未嘗不可。同時(shí)也希望這里對(duì) Unix 命令本質(zhì)的闡述能幫助人迅速的掌握 Unix,靈活的應(yīng)用它的潛力,避免它的缺點(diǎn)。
通常所說(shuō)的“Unix哲學(xué)”包括以下三條原則[Mcllroy]:
一個(gè)程序只做一件事情,并且把它做好。程序之間能夠協(xié)同工作。程序處理文本流,因?yàn)樗且粋€(gè)通用的接口。
這三條原則當(dāng)中,前兩條其實(shí)早于 Unix 就已經(jīng)存在,它們描述的其實(shí)是程序設(shè)計(jì)最基本的原則 —— 模塊化原則。任何一個(gè)具有函數(shù)和調(diào)用的程序語(yǔ)言都具有這兩條原則。簡(jiǎn)言之,第一條針對(duì)函數(shù),第二條針對(duì)調(diào)用。所謂“程序”,其實(shí)是一個(gè)叫 "main" 的函數(shù)(詳見(jiàn)下文)。
所以只有第三條(用文本流做接口)是 Unix 所特有的。下文的“Unix哲學(xué)”如果不加修飾,就特指這第三條原則。但是許多的事實(shí)已經(jīng)顯示出,這第三條原則其實(shí)包含了實(shí)質(zhì)性的錯(cuò)誤。它不但一直在給我們制造無(wú)需有的問(wèn)題,并且在很大程度上破壞前兩條原則的實(shí)施。然而,這條原則卻被很多人奉為神圣。許多程序員在他們自己的程序和協(xié)議里大量的使用文本流來(lái)表示數(shù)據(jù),引發(fā)了各種頭痛的問(wèn)題,卻對(duì)此視而不見(jiàn)。
Linux 有它優(yōu)于 Unix 的革新之處,但是我們必須看到,它其實(shí)還是繼承了 Unix 的這條哲學(xué)。Linux 系統(tǒng)的命令行,配置文件,各種工具之間都通過(guò)非標(biāo)準(zhǔn)化的文本流傳遞數(shù)據(jù)。這造成了信息格式的不一致和程序間協(xié)作的困難。然而,我這樣說(shuō)并不等于 Windows 或者 Mac 就做得好很多,雖然它們對(duì)此有所改進(jìn)。實(shí)際上,幾乎所有常見(jiàn)的操作系統(tǒng)都受到 Unix 哲學(xué)潛移默化的影響,以至于它們身上或多或少都存在它的陰影。
Unix 哲學(xué)的影響是多方面的。從命令行到程序語(yǔ)言,到數(shù)據(jù)庫(kù),Web…… 計(jì)算機(jī)和網(wǎng)絡(luò)系統(tǒng)的方方面面無(wú)不顯示出它的影子。在這里,我會(huì)把眾多的問(wèn)題與它們的根源 —— Unix哲學(xué)相關(guān)聯(lián)。現(xiàn)在我就從最簡(jiǎn)單的命令行開(kāi)始吧,希望你能從這些最簡(jiǎn)單例子里看到 Unix 執(zhí)行命令的過(guò)程,以及其中存在的問(wèn)題。(文本流的實(shí)質(zhì)就是字符串,所以在下文里這兩個(gè)名詞通用)
一個(gè) Linux 命令運(yùn)行的基本過(guò)程
幾乎每個(gè) Linux 用戶都為它的命令行困惑過(guò)。很多人(包括我在內(nèi))用了好幾年 Linux 也沒(méi)有完全的掌握命令行的用法。雖然看文檔看書(shū)以為都看透了,到時(shí)候還是會(huì)出現(xiàn)莫名其妙的問(wèn)題,有時(shí)甚至?xí)馁M(fèi)大半天的時(shí)間在上面。其實(shí)如果看透了命令行的本質(zhì),你就會(huì)發(fā)現(xiàn)很多問(wèn)題其實(shí)不是用戶的錯(cuò)。Linux 遺傳了 Unix 的“哲學(xué)”,用文本流來(lái)表示數(shù)據(jù)和參數(shù),才導(dǎo)致了命令行難學(xué)難用。
我們首先來(lái)分析一下 Linux 命令行的工作原理吧。下圖是一個(gè)很簡(jiǎn)單的 Linux 命令運(yùn)行的過(guò)程。當(dāng)然這不是全過(guò)程,但是更具體的細(xì)節(jié)跟我現(xiàn)在要說(shuō)的主題無(wú)關(guān)。
從上圖我們可以看到,在 ls 命令運(yùn)行的整個(gè)過(guò)程中,發(fā)生了如下的事情:
shell(在這個(gè)例子里是bash)從終端得到輸入的字符串 "ls -l *.c"。然后 shell 以空白字符為界,切分這個(gè)字符串,得到 "ls", "-l" 和 "*.c" 三個(gè)字符串。shell 發(fā)現(xiàn)第二個(gè)字符串是通配符 "*.c",于是在當(dāng)前目錄下尋找與這個(gè)通配符匹配的文件。它找到兩個(gè)文件: foo.c 和 bar.c。shell 把這兩個(gè)文件的名字和其余的字符串一起做成一個(gè)字符串?dāng)?shù)組 {"ls", "-l", "bar.c", "foo.c"}. 它的長(zhǎng)度是 4.shell 生成一個(gè)新的進(jìn)程,在里面執(zhí)行一個(gè)名叫 "ls" 的程序,并且把字符串?dāng)?shù)組 {"ls", "-l", "bar.c", "foo.c"}和它的長(zhǎng)度4,作為ls的main函數(shù)的參數(shù)。main函數(shù)是C語(yǔ)言程序的“入口”,這個(gè)你可能已經(jīng)知道。ls 程序啟動(dòng)并且得到的這兩個(gè)參數(shù)(argv,argc)后,對(duì)它們做一些分析,提取其中的有用信息。比如 ls 發(fā)現(xiàn)字符串?dāng)?shù)組 argv 的第二個(gè)元素 "-l" 以 "-" 開(kāi)頭,就知道那是一個(gè)選項(xiàng) —— 用戶想列出文件詳細(xì)的信息,于是它設(shè)置一個(gè)布爾變量表示這個(gè)信息,以便以后決定輸出文件信息的格式。ls 列出 foo.c 和 bar.c 兩個(gè)文件的“長(zhǎng)格式”信息之后退出。以整數(shù)0作為返回值。shell 得知 ls 已經(jīng)退出,返回值是 0。在 shell 看來(lái),0 表示成功,而其它值(不管正數(shù)負(fù)數(shù))都表示失敗。于是 shell 知道 ls 運(yùn)行成功了。由于沒(méi)有別的命令需要運(yùn)行,shell 向屏幕打印出提示符,開(kāi)始等待新的終端輸入……
從上面的命令運(yùn)行的過(guò)程中,我們可以看到文本流(字符串)在命令行中的普遍存在:
用戶在終端輸入是字符串。shell 從終端得到的是字符串,分解之后得到 3 個(gè)字符串,展開(kāi)通配符后得到 4 個(gè)字符串。ls 程序從參數(shù)得到那 4 個(gè)字符串,看到字符串 "-l" 的時(shí)候,就決定使用長(zhǎng)格式進(jìn)行輸出。
接下來(lái)你會(huì)看到這樣的做法引起的問(wèn)題。
冰山一角
在《Unix 痛恨者手冊(cè)》(The Unix-Hater's Handbook,?以下簡(jiǎn)稱 UHH)這本書(shū)開(kāi)頭,作者列舉了 Unix 命令行用戶界面的一系列罪狀,咋一看還以為是脾氣不好的初學(xué)者在謾罵。可是仔細(xì)看看,你會(huì)發(fā)現(xiàn)雖然態(tài)度不好,他們某些人的話里面有非常深刻的道理。我們總是可以從罵我們的人身上學(xué)到一些東西,所以仔細(xì)看了一下,發(fā)現(xiàn)其實(shí)這些命令行問(wèn)題的根源就是“Unix 哲學(xué)” —— 用文本流(字符串)來(lái)表示參數(shù)和數(shù)據(jù)。很多人都沒(méi)有意識(shí)到,文本流的過(guò)度使用,引發(fā)了太多問(wèn)題。我會(huì)在后面列出這些問(wèn)題,不過(guò)我現(xiàn)在先舉一些最簡(jiǎn)單的例子來(lái)解釋一下這個(gè)問(wèn)題的本質(zhì),你現(xiàn)在就可以自己動(dòng)手試一下。
在你的 Linux 終端里執(zhí)行如下命令(依次輸入:大于號(hào),減號(hào),小寫(xiě)字母l)。這會(huì)在目錄下建立一個(gè)叫 "-l" 的文件。
$ >-l
執(zhí)行命令 ls * (你的意圖是以短格式列出目錄下的所有文件)。
你看到什么了呢?你沒(méi)有給 ls 任何選項(xiàng),文件卻出人意料的以“長(zhǎng)格式”列了出來(lái),而這個(gè)列表里面卻沒(méi)有你剛剛建立的那個(gè)名叫 "-l" 的文件。比如我得到如下輸出:
-rw-r--r-- 1 wy wy 0 2011-05-22 23:03 bar.c-rw-r--r-- 1 wy wy 0 2011-05-22 23:03 foo.c
到底發(fā)生了什么呢?重溫一下上面的示意圖吧,特別注意第二步。原來(lái) shell 在調(diào)用 ls 之前,把通配符 * 展開(kāi)成了目錄下的所有文件,那就是 "foo.c", "bar.c", 和一個(gè)名叫 "-l" 的文件。它把這 3 個(gè)字符串加上 ls 自己的名字,放進(jìn)一個(gè)字符串?dāng)?shù)組 {"ls", "bar.c", "foo.c", "-l"},交給 ls。接下來(lái)發(fā)生的是,ls 拿到這個(gè)字符串?dāng)?shù)組,發(fā)現(xiàn)里面有個(gè)字符串是 "-l",就以為那是一個(gè)選項(xiàng):用戶想用“長(zhǎng)格式”輸出文件信息。因?yàn)?"-l" 被認(rèn)為是選項(xiàng),就沒(méi)有被列出來(lái)。于是我就得到上面的結(jié)果:長(zhǎng)格式,還少了一個(gè)文件!
這說(shuō)明了什么問(wèn)題呢?是用戶的錯(cuò)嗎?高手們也許會(huì)笑,怎么有人會(huì)這么傻,在目錄里建立一個(gè)叫 "-l" 的文件。但是就是這樣的態(tài)度,導(dǎo)致了我們對(duì)錯(cuò)誤視而不見(jiàn),甚至讓它發(fā)揚(yáng)光大。其實(shí)撇除心里的優(yōu)越感,從理性的觀點(diǎn)看一看,我們就發(fā)現(xiàn)這一切都是系統(tǒng)設(shè)計(jì)的問(wèn)題,而不是用戶的錯(cuò)誤。如果用戶要上法庭狀告 Linux,他可以這樣寫(xiě):
起訴狀
原告:用戶 luser
被告:Linux 操作系統(tǒng)
事由:合同糾紛
被告的文件系統(tǒng)給用戶提供了機(jī)制建立這樣一個(gè)叫 "-l" 的文件,這表示原告有權(quán)使用這個(gè)文件名。既然 "-l" 是一個(gè)合法的文件名,而 "*" 通配符表示匹配“任何文件”,那么在原告使用 "ls *" 命令的時(shí)候,被告就應(yīng)該像原告所期望的那樣,以正常的方式列出目錄下所有的文件,包括 "-l" 在內(nèi)。但是實(shí)際上原告沒(méi)有達(dá)到他認(rèn)為理所當(dāng)然的結(jié)果。"-l" 被 ls 命令認(rèn)為是一個(gè)命令行選項(xiàng),而不是一個(gè)文件。原告認(rèn)為自己的合法權(quán)益受到侵犯。
我覺(jué)得為了免去責(zé)任,一個(gè)系統(tǒng)必須提供切實(shí)的保障措施,而不只是口頭上的約定來(lái)要求用戶“小心”。就像如果你在街上挖個(gè)大洞施工,必須放上路障和警示燈。你不能只插一面小旗子在那里,用一行小字寫(xiě)著: “前方施工,后果自負(fù)?!蔽蚁朊恳粋€(gè)正常人都會(huì)判定是施工者的錯(cuò)誤。
可是 Unix 對(duì)于它的用戶卻一直是像這樣的施工者,它要求用戶:“仔細(xì)看 man page,否則后果自負(fù)?!逼鋵?shí)不是用戶想偷懶,而是這些條款太多,根本沒(méi)有人能記得住。而且沒(méi)被咬過(guò)之前,誰(shuí)會(huì)去看那些偏僻的內(nèi)容啊。但是一被咬,就后悔都來(lái)不及。完成一個(gè)簡(jiǎn)單的任務(wù)都需要知道這么多可能的陷阱,那更加復(fù)雜的任務(wù)可怎么辦。其實(shí) Unix 的這些小問(wèn)題累加起來(lái),不知道讓人耗費(fèi)了多少寶貴的時(shí)間。
如果你想更加確信這個(gè)問(wèn)題的危險(xiǎn)性,可以試試如下的做法。在這之前,請(qǐng)新建一個(gè)測(cè)試用的目錄,以免丟失你的文件!?
在新目錄里,我們首先建立兩個(gè)文件夾 dir-a, dir-b 和三個(gè)普通文件 file1,file2 和 "-rf"。然后我們運(yùn)行 "rm *",意圖是刪除所有普通文件,而不刪掉目錄。
$ mkdir dir-a dir-b
$ touch file1 file2
$ > -rf
$ rm *
然后用 ls 查看目錄。
你會(huì)發(fā)現(xiàn)最后只剩下一個(gè)文件: "-rf"。本來(lái) "rm *" 只能刪除普通文件,現(xiàn)在由于目錄里存在一個(gè)叫 "-rf" 的文件。rm 以為那是叫它進(jìn)行強(qiáng)制遞歸刪除的選項(xiàng),所以它把目錄里所有的文件連同目錄全都刪掉了(除了 "-rf")。
表面解決方案
難道這說(shuō)明我們應(yīng)該禁止任何以 "-" 開(kāi)頭的文件名的存在,因?yàn)檫@樣會(huì)讓程序分不清選項(xiàng)和文件名?可是不幸的是,由于 Unix 給程序員的“靈活性”,并不是每個(gè)程序都認(rèn)為以 "-" 開(kāi)頭的參數(shù)是選項(xiàng)。比如,Linux 下的 tar,ps 等命令就是例外。所以這個(gè)方案不大可行。
從上面的例子我們可以看出,問(wèn)題的來(lái)源似乎是因?yàn)?ls 根本不知道通配符 * 的存在。是 shell 把通配符展開(kāi)以后給 ls。其實(shí) ls 得到的是文件名和選項(xiàng)混合在一起的字符串?dāng)?shù)組。所以 UHH 的作者提出的一個(gè)看法:“shell 根本不應(yīng)該展開(kāi)通配符。通配符應(yīng)該直接被送給程序,由程序自己調(diào)用一個(gè)庫(kù)函數(shù)來(lái)展開(kāi)。”
這個(gè)方案確實(shí)可行:如果 shell 把通配符直接給 ls,那么 ls 會(huì)只看到 "*" 一個(gè)參數(shù)。它會(huì)調(diào)用庫(kù)函數(shù)在文件系統(tǒng)里去尋找當(dāng)前目錄下的所有文件,它會(huì)很清楚的知道 "-l" 是一個(gè)文件,而不是一個(gè)選項(xiàng),因?yàn)樗緵](méi)有從 shell 那里得到任何選項(xiàng)(它只得到一個(gè)參數(shù):"*")。所以問(wèn)題貌似就解決了。
但是這樣每一個(gè)命令都自己檢查通配符的存在,然后去調(diào)用庫(kù)函數(shù)來(lái)解釋它,大大增加了程序員的工作量和出錯(cuò)的概率。況且 shell 不但展開(kāi)通配符,還有環(huán)境變量,花括號(hào)展開(kāi),~展開(kāi),命令替換,算術(shù)運(yùn)算展開(kāi)…… 這些讓每個(gè)程序都自己去做?這恰恰違反了第一條 Unix 哲學(xué) —— 模塊化原則。而且這個(gè)方法并不是一勞永逸的,它只能解決這一個(gè)問(wèn)題。我們還將遇到文本流引起的更多的問(wèn)題,它們沒(méi)法用這個(gè)方法解決。下面就是一個(gè)這樣的例子。
冰山又一角
這些看似微不足道的問(wèn)題里面其實(shí)包含了 Unix 本質(zhì)的問(wèn)題。如果不能正確認(rèn)識(shí)到它,我們跳出了一個(gè)問(wèn)題,還會(huì)進(jìn)入另一個(gè)。我講一個(gè)自己的親身經(jīng)歷吧。我前年夏天在 Google 實(shí)習(xí)快結(jié)束的時(shí)候發(fā)生了這樣一件事情……
由于我的項(xiàng)目對(duì)一個(gè)開(kāi)源項(xiàng)目的依賴關(guān)系,我必須在 Google 的 Perforce 代碼庫(kù)中提交這個(gè)開(kāi)源項(xiàng)目的所有文件。這個(gè)開(kāi)源項(xiàng)目里面有 9000 多個(gè)文件,而 Perforce 是如此之慢,在提交進(jìn)行到一個(gè)小時(shí)的時(shí)候,突然報(bào)錯(cuò)退出了,說(shuō)有兩個(gè)文件找不到。又試了兩次(順便出去喝了咖啡,打了臺(tái)球),還是失敗,這樣一天就快過(guò)去了。于是我搜索了一下這兩個(gè)文件,確實(shí)不存在。怎么會(huì)呢?我是用公司手冊(cè)上的命令行把項(xiàng)目的文件導(dǎo)入到 Perforce 的呀,怎么會(huì)無(wú)中生有?這條命令是這樣:
find -name *.java -print | xargs p4 add
它的工作原理是,find 命令在目錄樹(shù)下找到所有的以 ".java" 結(jié)尾的文件,把它們用空格符隔開(kāi)做成一個(gè)字符串,然后交給 xargs。之后 xargs 以空格符把這個(gè)字符串拆開(kāi)成多個(gè)字符串,放在 "p4 add" 后面,組合成一條命令,然后執(zhí)行它?;旧夏憧梢园?find 想象成 Lisp 里的 "filter",而 xargs 就是 "map"。所以這條命令轉(zhuǎn)換成 Lisp 樣式的偽碼就是:
(map (lambda (x) (p4 add x))
?????(filter (lambda (x) (regexp-match? "*.java" x))
?????????????(files-in-current-dir)))
問(wèn)題出在哪里呢?經(jīng)過(guò)一下午的困惑之后我終于發(fā)現(xiàn),原來(lái)這個(gè)開(kāi)源項(xiàng)目里某個(gè)目錄下,有一個(gè)叫做 "App Launcher.java" 的文件。由于它的名字里面含有一個(gè)空格,被 xargs 拆開(kāi)成了兩個(gè)字符串: "App" 和 "Launcher.java"。當(dāng)然這兩個(gè)文件都不存在了!所以 Perforce 在提交的時(shí)候抱怨找不到它們。我告訴組里的負(fù)責(zé)人這個(gè)發(fā)現(xiàn)后,他說(shuō):“這些家伙,怎么能給 Java 程序起這樣一個(gè)名字?也太菜了吧!”
但是我卻不認(rèn)為是這個(gè)開(kāi)源項(xiàng)目的程序員的錯(cuò)誤,這其實(shí)顯示了 Unix 的問(wèn)題。這個(gè)問(wèn)題的根源是因?yàn)?Unix 的命令 (find, xargs) 把文件名以字符串的形式傳遞,它們默認(rèn)的“協(xié)議”是“以空格符隔開(kāi)文件名”。而這個(gè)項(xiàng)目里恰恰有一個(gè)文件的名字里面有空格符,所以導(dǎo)致了歧義的產(chǎn)生。該怪誰(shuí)呢?既然 Linux 允許文件名里面有空格,那么用戶就有權(quán)使用這個(gè)功能。到頭來(lái)因此出了問(wèn)題,用戶卻被叫做菜鳥(niǎo),為什么自己不小心,不看 man page。
后來(lái)我仔細(xì)看了一下 find 和 xargs 的 man page,發(fā)現(xiàn)其實(shí)它們的設(shè)計(jì)者其實(shí)已經(jīng)意識(shí)到這個(gè)問(wèn)題。所以 find 和 xargs 各有一個(gè)選項(xiàng):"-print0" 和 "-0"。它們可以讓 find 和 xargs 不用空格符,而用 "NULL"(ASCII字符 0)作為文件名的分隔符,這樣就可以避免文件名里有空格導(dǎo)致的問(wèn)題??墒牵坪趺看斡龅竭@樣的問(wèn)題總是過(guò)后方知。難道用戶真的需要知道這么多,小心翼翼,才能有效的使用 Unix 嗎?
文本流不是可靠的接口
這些例子其實(shí)從不同的側(cè)面顯示了同一個(gè)本質(zhì)的問(wèn)題:用文本流來(lái)傳遞數(shù)據(jù)有嚴(yán)重的問(wèn)題。是的,文本流是一個(gè)“通用”的接口,但是它卻不是一個(gè)“可靠”或者“方便”的接口。Unix 命令的工作原理基本是這樣:??
從標(biāo)準(zhǔn)輸入得到文本流,處理,向標(biāo)準(zhǔn)輸出打印文本流。程序之間用管道進(jìn)行通信,讓文本流可以在程序間傳遞。
這其中主要有兩個(gè)過(guò)程:
程序向標(biāo)準(zhǔn)輸出“打印”的時(shí)候,數(shù)據(jù)被轉(zhuǎn)換成文本。這是一個(gè)編碼過(guò)程。文本通過(guò)管道(或者文件)進(jìn)入另一個(gè)程序,這個(gè)程序需要從文本里面提取它需要的信息。這是一個(gè)解碼過(guò)程。
編碼的貌似很簡(jiǎn)單,你只需要隨便設(shè)計(jì)一個(gè)“語(yǔ)法”,比如“用空格隔開(kāi)”,就能輸出了??墒蔷幋a的設(shè)計(jì)遠(yuǎn)遠(yuǎn)不是想象的那么容易。要是編碼格式?jīng)]有設(shè)計(jì)好,解碼的人就麻煩了,輕則需要正則表達(dá)式才能提取出文本里的信息,遇到復(fù)雜一點(diǎn)的編碼(比如程序文本),就得用 parser。最嚴(yán)重的問(wèn)題是,由于鼓勵(lì)使用文本流,很多程序員很隨意的設(shè)計(jì)他們的編碼方式而不經(jīng)過(guò)嚴(yán)密思考。這就造成了 Unix 的幾乎每個(gè)程序都有各自不同的輸出格式,使得解碼成為非常頭痛的問(wèn)題,經(jīng)常出現(xiàn)歧義和混淆。
上面 find/xargs 的問(wèn)題就是因?yàn)?find 編碼的分隔符(空格)和文件名里可能存在的空格相混淆 —— 此空格非彼空格也。而之前的 ls 和 rm 的問(wèn)題就是因?yàn)?shell 把文件名和選項(xiàng)都“編碼”為“字符串”,所以 ls 程序無(wú)法通過(guò)解碼來(lái)辨別它們的到底是文件名還是選項(xiàng) —— 此字符串非彼字符串也!
如果你使用過(guò) Java 或者函數(shù)式語(yǔ)言(Haskell 或者 ML),你可能會(huì)了解一些類型理論(type theory)。在類型理論里,數(shù)據(jù)的類型是多樣的,Integer, String, Boolean, List, record…… 程序之間傳遞的所謂“數(shù)據(jù)”,只不過(guò)就是這些類型的數(shù)據(jù)結(jié)構(gòu)。然而按照 Unix 的設(shè)計(jì),所有的類型都得被轉(zhuǎn)化成 String 之后在程序間傳遞。這樣帶來(lái)一個(gè)問(wèn)題:由于無(wú)結(jié)構(gòu)的 String 沒(méi)有足夠的表達(dá)力來(lái)區(qū)分其它的數(shù)據(jù)類型,所以經(jīng)常會(huì)出現(xiàn)歧義。相比之下,如果用 Haskell 來(lái)表示命令行參數(shù),它應(yīng)該是這樣:
data Parameter = Option String | File String | ...
雖然兩種東西的實(shí)質(zhì)都是 String,但是 Haskell 會(huì)給它們加上“標(biāo)簽”以區(qū)分 Option 還是 File。這樣當(dāng) ls 接收到參數(shù)列表的時(shí)候,它就從標(biāo)簽判斷哪個(gè)是選項(xiàng),哪個(gè)是參數(shù),而不是通過(guò)字符串的內(nèi)容來(lái)瞎猜。
文本流帶來(lái)太多的問(wèn)題
綜上所述,文本流的問(wèn)題在于,本來(lái)簡(jiǎn)單明了的信息,被編碼成為文本流之后,就變得難以提取,甚至丟失。前面說(shuō)的都是小問(wèn)題,其實(shí)文本流的帶來(lái)的嚴(yán)重問(wèn)題很多,它甚至創(chuàng)造了整個(gè)的研究領(lǐng)域。文本流的思想影響了太多的設(shè)計(jì)。比如:
配置文件:幾乎每一個(gè)都用不同的文本格式保存數(shù)據(jù)。想想吧:.bashrc, .Xdefaults, .screenrc, .fvwm, .emacs, .vimrc, /etc目錄下那系列!這樣用戶需要了解太多的格式,然而它們并沒(méi)有什么本質(zhì)區(qū)別。為了整理好這些文件,花費(fèi)了大量的人力物力。程序文本:這個(gè)以后我會(huì)專門講。程序被作為文本文件,所以我們才需要 parser。這導(dǎo)致了整個(gè)編譯器領(lǐng)域花費(fèi)大量人力物力研究 parsing。其實(shí)程序完全可以被作為 parse tree 直接存儲(chǔ),這樣編譯器可以直接讀取 parse tree,不但節(jié)省編譯時(shí)間,連 parser 都不用寫(xiě)。數(shù)據(jù)庫(kù)接口:程序與關(guān)系式數(shù)據(jù)庫(kù)之間的交互使用含有 SQL 語(yǔ)句的字符串,由于字符串里的內(nèi)容跟程序的類型之間并無(wú)關(guān)聯(lián),導(dǎo)致了這種程序非常難以調(diào)試。XML:?設(shè)計(jì)的初衷就是解決數(shù)據(jù)編碼的問(wèn)題,然而不幸的是,它自己都難 parse。它跟 SQL 類似,與程序里的類型關(guān)聯(lián)性很差。程序里的類型名字即使跟 XML 里面的定義有所偏差,編譯器也不會(huì)報(bào)錯(cuò)。Android 程序經(jīng)常出現(xiàn)的 "force close",大部分時(shí)候是這個(gè)原因。與 XML 相關(guān)的一些東西,比如 XSLT, XQuery, XPath 等等,設(shè)計(jì)也非常糟糕。Web:JavaScript 經(jīng)常被作為字符串插入到網(wǎng)頁(yè)中。由于字符串可以被任意組合,這引起很多安全性問(wèn)題。Web安全研究,有些就是解決這類問(wèn)題的。IDE接口:很多編譯器給編輯器和 IDE 提供的接口是基于文本的。編譯器打印出出錯(cuò)的行號(hào)和信息,比如 "102:32 variable x undefined",然后由編輯器和 IDE 從文本里面去提取這些信息,跳轉(zhuǎn)到相應(yīng)的位置。一旦編譯器改變打印格式,這些編輯器和 IDE 就得修改。log分析: 有些公司調(diào)試程序的時(shí)候打印出文本 log 信息,然后專門請(qǐng)人寫(xiě)程序分析這種 log,從里面提取有用的信息,非常費(fèi)時(shí)費(fèi)力。測(cè)試:很多人寫(xiě) unit test 的時(shí)候,喜歡把數(shù)據(jù)結(jié)構(gòu)通過(guò) toString 等函數(shù)轉(zhuǎn)化成字符串之后,與一個(gè)標(biāo)準(zhǔn)的字符串進(jìn)行比較,導(dǎo)致這些測(cè)試在字符串格式改變之后失效而必須修改。
還有很多的例子,你只需要在你的身邊去發(fā)現(xiàn)。?
什么是“人類可讀”和“通用”接口?
當(dāng)我提到文本流做接口的各種弊端時(shí),經(jīng)常有人會(huì)指出,雖然文本流不可靠又麻煩,但是它比其它接口更通用,因?yàn)樗俏ㄒ蝗祟惪勺x?(human-readable) 的格式,任何編輯器都可以直接看到文本流的內(nèi)容,而其它格式都不是這樣的。對(duì)于這一點(diǎn)我想說(shuō)的是:??
什么叫做“人類可讀”?文本流真的就是那么的可讀嗎?幾年前,普通的文本編輯器遇到中文的時(shí)候經(jīng)常亂碼,要折騰好一陣子才能讓它們支持中文。幸好經(jīng)過(guò)全世界的合作,我們現(xiàn)在有了 Unicode?,F(xiàn)在要閱讀 Unicode 的文件,你不但要有支持 Unicode 的編輯器/瀏覽器,你還得有能顯示相應(yīng)碼段的字體。文本流達(dá)到“人類可讀”真的不費(fèi)力氣?除了文本流,其實(shí)還有很多人類可讀的格式,比如 JPEG。它可比文本流“可讀”和“通用”多了,連字體都用不著。
所以,文本流的根本就不是“人類可讀”和“通用”的關(guān)鍵。真正的關(guān)鍵在于“標(biāo)準(zhǔn)化”。如果其它的數(shù)據(jù)類型被標(biāo)準(zhǔn)化,那么我們可以在任何編輯器,瀏覽器,終端里加入對(duì)它們的支持,完全達(dá)到人類和機(jī)器都可輕松讀取,就像我們今天讀取文本和 JPEG 一樣。
解決方案
其實(shí)有一個(gè)簡(jiǎn)單的方式可以一勞永逸的解決所有這些問(wèn)題:?
保留數(shù)據(jù)類型本來(lái)的結(jié)構(gòu)。不用文本流來(lái)表示除文本以外的數(shù)據(jù)。用一個(gè)開(kāi)放的,標(biāo)準(zhǔn)化的,可擴(kuò)展的方式來(lái)表示所有數(shù)據(jù)類型。程序之間的數(shù)據(jù)傳遞和存儲(chǔ),就像程序內(nèi)部的數(shù)據(jù)結(jié)構(gòu)一樣。
Unix 命令行的本質(zhì)
雖然文本流引起了這么多問(wèn)題,但是 Unix 還是不會(huì)消亡,因?yàn)楫吘褂羞@么多的上層應(yīng)用已經(jīng)依賴于它,它幾乎是整個(gè) Internet 的頂梁柱。所以這篇文章對(duì)于當(dāng)前狀況的一個(gè)實(shí)際意義,也許是可以幫助人們迅速的理解 Unix 的命令行機(jī)制,并且鼓勵(lì)程序員在新的應(yīng)用中使用結(jié)構(gòu)化的數(shù)據(jù)。
Unix 命令雖然過(guò)于復(fù)雜而且功能冗余,但是如果你看透了它們的本質(zhì),就能輕而易舉的學(xué)會(huì)它們的使用方法。簡(jiǎn)而言之,你可以用普通的編程思想來(lái)解釋所有的 Unix 命令:
函數(shù):每一個(gè) Unix 程序本質(zhì)上是一個(gè)函數(shù) (main)。參數(shù):命令行參數(shù)就是這個(gè)函數(shù)的參數(shù)。 所有的參數(shù)對(duì)于 C 語(yǔ)言來(lái)說(shuō)都是字符串,但是經(jīng)過(guò) parse,它們可能有幾種不同的類型: 變量名:實(shí)際上文件名就是程序中的變量名,就像 x, y。而文件的本質(zhì)就是程序里的一個(gè)對(duì)象。字符串:這是真正的程序中的字符串,就像 "hello world"。keyword argument: 選項(xiàng)本質(zhì)上就是“keyword argument”(kwarg),類似 Python 或者 Common Lisp 里面那個(gè)對(duì)應(yīng)的東西,短選項(xiàng)(看起來(lái)像 "-l", "-c" 等等),本質(zhì)上就是 bool 類型的 kwarg。比如 "ls -l" 以 Python 的語(yǔ)法就是 ls(l=true)。長(zhǎng)選項(xiàng)本質(zhì)就是 string 類型的 kwarg。比如 "ls --color=auto" 以 Python 的語(yǔ)法就是 ls(color=auto)。返回值:由于 main 函數(shù)只能返回整數(shù)類型(int),我們只好把其它類型 (string, list, record, ...) 的返回值序列化為文本流,然后通過(guò)文件送給另一個(gè)程序。這里“文件”通指磁盤(pán)文件,管道等等。它們是文本流通過(guò)的信道。我已經(jīng)提到過(guò),文件的本質(zhì)是程序里的一個(gè)對(duì)象。組合:所謂“管道”,不過(guò)是一種簡(jiǎn)單的函數(shù)組合(composition)。比如 "A x | B",用函數(shù)來(lái)表示就是 "B(A(x))"。 但是注意,這里的計(jì)算過(guò)程,本質(zhì)上是 lazy evaluation (類似 Haskell)。當(dāng) B “需要”數(shù)據(jù)的時(shí)候,A 才會(huì)讀取更大部分的 x,并且計(jì)算出結(jié)果送給 B。并不是所有函數(shù)組合都可以用管道表示,比如,如何用管道表示 "C(B(x), A(y))"?所以函數(shù)組合是更加通用的機(jī)制。分支:如果需要把返回值送到兩個(gè)不同的程序,你需要使用?tee。這相當(dāng)于在程序里把結(jié)果存到一個(gè)臨時(shí)變量,然后使用它兩次??刂屏鳎簃ain 函數(shù)的返回值(int型)被 shell 用來(lái)作為控制流。shell 可以根據(jù) main 函數(shù)返回值來(lái)中斷或者繼續(xù)運(yùn)行一個(gè)腳本。這就像 Java 的 exception。shell: 各種 shell 語(yǔ)言的本質(zhì)都是用來(lái)連接這些 main 函數(shù)的語(yǔ)言,而 shell 的本質(zhì)其實(shí)是一個(gè) REPL (read-eval-print-loop,類似 Lisp)。用程序語(yǔ)言的觀點(diǎn),shell 語(yǔ)言完全是多余的東西,我們其實(shí)可以在 REPL 里用跟應(yīng)用程序一樣的程序語(yǔ)言。Lisp 系統(tǒng)就是這樣做的。
數(shù)據(jù)直接存儲(chǔ)帶來(lái)的可能性
由于存儲(chǔ)的是結(jié)構(gòu)化的數(shù)據(jù),任何支持這種格式的工具都可以讓用戶直接操作這個(gè)數(shù)據(jù)結(jié)構(gòu)。這會(huì)帶來(lái)意想不到的好處。
因?yàn)槊钚胁僮鞯氖墙Y(jié)構(gòu)化的參數(shù),系統(tǒng)可以非常智能的按類型補(bǔ)全命令,讓你完全不可能輸入語(yǔ)法錯(cuò)誤的命令??梢灾苯釉诿钚欣锊迦腼@示圖片之類的 "meta data"。Drag&Drop 桌面上的對(duì)象到命令行里,然后執(zhí)行。因?yàn)榇a是以 parse tree 結(jié)構(gòu)存儲(chǔ)的,IDE 會(huì)很容易的擴(kuò)展到支持所有的程序語(yǔ)言。你可以在看 email 的時(shí)候?qū)ζ渲械拇a段進(jìn)行 IDE 似的結(jié)構(gòu)化編輯,甚至編譯和執(zhí)行。結(jié)構(gòu)化的版本控制和程序比較(diff)。(參考我的talk)
還有很多很多,僅限于我們的想象力。
程序語(yǔ)言,操作系統(tǒng),數(shù)據(jù)庫(kù)三位一體
如果 main 函數(shù)可以接受多種類型的參數(shù),并且可以有?keyword argument,它能返回一個(gè)或多個(gè)不同類型的對(duì)象作為返回值,而且如果這些對(duì)象可以被自動(dòng)存儲(chǔ)到一種特殊的“數(shù)據(jù)庫(kù)”里,那么 shell,管道,命令行選項(xiàng),甚至連文件系統(tǒng)都沒(méi)有必要存在。我們甚至可以說(shuō),“操作系統(tǒng)”這個(gè)概念變得“透明”。因?yàn)檫@樣一來(lái),操作系統(tǒng)的本質(zhì)不過(guò)是某種程序語(yǔ)言的“運(yùn)行時(shí)系統(tǒng)”(runtime system)。這有點(diǎn)像 JVM 之于 Java。其實(shí)從本質(zhì)上講,Unix 就是 C 語(yǔ)言的運(yùn)行時(shí)系統(tǒng)。
如果我們?cè)龠M(jìn)一步,把與數(shù)據(jù)庫(kù)的連接做成透明的,即用同一種程序語(yǔ)言來(lái)“隱性”(implicit)的訪問(wèn)數(shù)據(jù)庫(kù),而不是像 SQL 之類的專用數(shù)據(jù)庫(kù)語(yǔ)言,那么“數(shù)據(jù)庫(kù)”這個(gè)概念也變得透明了。我們得到的會(huì)是一個(gè)非常簡(jiǎn)單,統(tǒng)一,方便,而且強(qiáng)大的系統(tǒng)。這個(gè)系統(tǒng)里面只有一種程序語(yǔ)言,程序員直接編寫(xiě)高級(jí)語(yǔ)言程序,用同樣的語(yǔ)言從命令行執(zhí)行它們,而且不用擔(dān)心數(shù)據(jù)放在什么地方。這樣可以大大的減小程序員工作的復(fù)雜度,讓他們專注于問(wèn)題本身,而不是系統(tǒng)的內(nèi)部結(jié)構(gòu)。
實(shí)際上,類似這樣的系統(tǒng)在歷史上早已存在過(guò) (Lisp Machine,?System/38,?Oberon),而且收到了不錯(cuò)的效果。但是由于某些原因(歷史的,經(jīng)濟(jì)的,政治的,技術(shù)的),它們都消亡了。但是不得不說(shuō)它們的這種方式比 Unix 現(xiàn)有的方式優(yōu)秀,所以何不學(xué)過(guò)來(lái)?我相信,隨著程序語(yǔ)言和編譯器技術(shù)發(fā)展,它們的這種簡(jiǎn)單而統(tǒng)一的設(shè)計(jì)理念,有一天會(huì)改變這個(gè)世界。