中国开发网: 论坛: 数据库: 贴子 825270
DeepBlue: [好玩]Unix痛恨者手冊--ZZ
轉載自:Simson Garfinkel, Daniel Weise, Steven Strassmann

一本很有趣的書, 是那些用慣UNIX的人對UNIX的各種指責,詆毀,謾罵和嘲笑. 是由愛而生的恨. 即使當成一本高級笑話書,也是很有價值的.

UNIX痛恨者手冊

By Simson Garfinkel, Daniel Weise, Steven Strassmann

第一章 UNIX

世界上第一個電腦病毒

「伯克利的兩項最著名的產品是UNIX和LSD (一種毒品),我想這不是巧合」

病毒依賴於微小的個體和強大的適應性得以生存。它們並不複雜:它們沒有為呼吸,新陳代謝,肌體活動等功能提供什麼,只有足夠的DNA或RNA以供繁衍。比如,肺炎病毒比起它們入侵的細胞要小得多,但它們在每個肺炎流行季節都能夠產生新的變種,造成無數人死亡。

一個好病毒的特點是:

* 個頭小
病毒做的事情不多,所以不需要很大。有人認為病毒不是生物,只是一些有破壞性的酸和蛋白質。
* 可移植性
病毒經常變異,以便以不同的方式攻擊不同的細胞。據說AIDS就是由猴子身上的病毒變異而成的。
* 耗盡寄主的資源
* 快速變異

UNIX具有以上所有優點。在它剛誕生時,很小,功能不多,缺乏真正操作系統所需要的功能(如文件映射,告訴IO,健壯的文件系統,設備鎖,合理的進程間通訊),它的移植性很好。UNIX耗盡主機的資源,沒有系統管理員的時時呵護,UNIX會不斷恐慌(panic),core dump,掛起。UNIX不斷變異:同一個補丁在一個版本上工作,在另一個版本上就不行。

UNIX是有用戶界面的計算機病毒。

標準化那些不一致的

「標準的偉大之處在於它可以有很多」 --- Grace Murray Hopper

自從UNIX 80年代開始流行以來,UNIX廠商一直在努力進行UNIX標準化工作。SUN, IBM,HP和DEC在這個他們自己製造的難題上傾注了數百萬美元。

為什麼UNIX廠商不喜歡UNIX標準化?

許多用戶受夠了複雜繁多的UNIX,最終只好使用Windows,因為他們的這個UNIX無法支持那個UNIX上的應用程序。

如果UNIX標準化了,誰還會買SUN的機器呢

標題: 第二章 歡迎新用戶

歡迎新用戶如同用一把上了六顆子彈的左輪槍玩俄羅斯輪盤賭Ken Thompson 自己設計過一輛汽車。和其他車不同,它沒有速度計、汽油計,也沒有那些愚蠢的指示燈討司機的厭。如果司機犯了什麼錯誤,儀表盤上就會出現一個大大的「?」。「有經驗的司機,」Thompson說,「應該知道哪兒搞錯了。」

計算機系統的新手需要一個友好的系統。至少,一個得體的系統會這樣招待自己的客人:

與功能有邏輯關係的命令名
對危險命令的小心處理
一致的命令行為和命令行參數解析
易得和易讀的在線文檔
當命令失敗時,給出可理解和有用的錯誤反饋

在建造UNIX的過程中,從沒邀請過住戶。來訪的都是些戴著安全帽的建築工人,被安插在這個破木板房子的各個角落。不幸的是,不僅沒有人性因素(human factors)工程師的參與,而且住戶的需要就從來沒有被考慮過。所以抽水馬桶、中央供暖、窗戶等這些方便設施在後期就很難再添加了。但是建築師們仍然為UNIX的設計而驕傲,似乎他們並不介意在一個沒有煙火探測器的屋子裡睡覺。

在其發展的大部分歷史中,UNIX只是大學和工業研究人員的研究工具。隨著大批便宜工作站的出現,UNIX作為平台軟件進入了新時代。這一變化大約發生在 1990年,其標誌就是工作站廠商把C編譯器從UNIX發佈中剔除出去,以降低成本滿足非開發用戶的需求。可見,只是最近幾年中UNIX廠商才開始考慮非程序員用戶的需要,開始為他們提供shell以外的圖形界面。

含糊的命令名

UNIX新手總是對UNIX對命令的命名表示驚訝。在DOS和Mac上受的教育不足以讓他們體會到cp、rm、ls這類兩字母命令的簡潔和優美。

像我們這樣用過70年代早期的IO設備的人都能理解,ASR-33 Teletype這類設備的速度、可靠性,以及它的鍵盤是萬惡之源。和今天這種基於反饋原理、只需要關閉一個微開關的鍵盤不同,你必須用足力氣撳下Teletype的鍵至少半英吋,以發動一個類似自行車上用的小型發電機,在上面操作要冒指骨骨折的危險。

如果當時Dennis和Ken用的是Selectric而不是Teletype,可能今天我們敲的將不是」cp」和」rm」而是」copy」和」remove」了。(Ken Thompson曾被問道如果他能重新設計UNIX他將做什麼修改,他回答說:「我會在creat命令後加上個e。」),科技在拓寬我們的選擇的同時,也能限制我們的選擇,此一例也。

20多年過去了,還有什麼理由延續這一傳統呢?理由就是「歷史的無可替代的力量」,歷史就是那些存在的代碼和教科書。如果一個廠商用remove替代了rm,那麼所有UNIX教科書就不適用於這一系統了,每個使用rm的shell腳本都需要被修改。而且這也不合POSIX標準。

一個世紀前,打字高手由於擊鍵過快,經常把打字鍵柄攪在一起,工程師設計了QWERTY鍵盤,於是問題得到了解決,因為沒人能在這樣的鍵盤上打得快。計算機的鍵盤不再有機械鍵柄,但QWERTY的鍵盤佈局仍然在使用。同理,在未來的一個世紀中,我們仍然會繼續使用rm。

事故會發生

用戶十分關心自己的數據和文件。他們使用計算機來產生、分析和存儲重要信息。他們相信計算機能夠保護他們的重要財產。如果沒有了這種信任,他們和計算機的關係就會蒙上陰影。UNIX辜負了我們的信任,它拒絕對使用危險命令的用戶提供保護。比如rm就是以刪除文件為目的的危險命令。

所有UNIX新手都有不小心無可挽回地刪除重要文件的經歷,即使是專家和系統管理員也遇到過。因此而每年損失的時間、精力可能價值幾百萬美元。這是個值得解決的問題;我們不理解為何UNIX一直拒絕解決這一問題。難道結果還不夠悲慘麼?

UNIX比其他操作系統更需要提供恢復刪除功能,原因是:

UNIX文件系統沒有版本功能

自動的版本維護能保留文件的歷史版本,防止新版本沖掉老版本。
UNIX程序員在錯誤處理方面臭名昭著許多程序不檢查是否所有內容都被寫入了磁盤,或被寫入的文件是否存在。有些程序總是刪除輸入文件。

UNIX shell擴展「*」,而不是其子命令

於是rm這樣的命令就無法檢查「*」這些危險的參數。即使是DOS也對」del *.*」有些提示。但在UNIX下,rm * 和 rm file1 file2…是沒有區別的。

刪除是永久的

UNIX沒有undelete命令。許多其他更安全的系統則只是標記被刪除文件所用的塊為「可被使用」,然後把它移到一個特殊目錄下。如果磁盤滿了,這些文件塊才會被重新使用。這一技術不是什麼火箭科學,Macintosh在1984年就提出了「回收站」的想法,而Tenex早在1974年就採用了這一技術。連DOS也提供了簡單的undelete功能,雖然並不總有效。

這四個問題互相合作,製造了無數無法恢復的重要文件。解決的方法早就存在,但UNIX「標準」版中卻從來沒有提供。

歡迎來到未來世界。

「rm」就是終結

許多實際的恐怖故事說明了以上的這些原則。以下是alt.folklore.computers新聞組上流傳的一系列故事中的一個:

Date: Wed, 10 Jan 90
X-Virus: 6
From: djones@megatest.uucp (Dave Jones)
Subject: rm *
Newsgroups: alt.folklore.computers

是否有人曾想執行以下命令:

% rm *.o

結果卻打成了:

% rm *>;o

現在你得到了一個空文件o,以及大量的空間來存放它!

事實上,你可能連o也得不到,因為shell的文檔並沒有說o是在*被擴展前還是被擴展後被建立的。

上回書說到如何用rm獲得一個空文件和很大的磁盤空間,下面是另一種用法:

Date: Wed, 10 Jan 90
X-Virus: 6
From: ram@attcan.uucp
Subject: Re: rm *
Newsgroups: alt.folklore.computers

我也被rm搞過。有一次我想刪除一些/usr/foo/下的東西,我在/usr/foo下敲了以下命令:

% rm –r ./etc
% rm –r ./adm

當我要刪除./bin目錄時,我忘敲了那個點。我的系統似乎不太喜歡這個。

當受了這一致命一擊後,UNIX就徹底完蛋了。聰明的系統會給用戶一個恢復的機會(或至少提醒用戶這一操作會導致系統崩潰)。

UNIX迷認為偶爾的文件誤刪除是正常的。比如,可以參考以下下面這個comp.unix.questions上的FAQ:

6) 如何反刪除一個文件?

也許有一天,你不小心執行了一下這個命令:

% rm * .foo

然後發現你把「*」刪掉了。你應該把這當作人生的一課。

當然稱職的系統管理員應該定時備份系統。所以你最好問問他們手中是不是有你的文件備份。

「人生的一課」?沒有任何一個其他廠商用這樣的態度對待一個有缺陷的產品。「大人,我知道您的油箱炸了,但這是人生的一課。」「陪審團的先生女士們,我們將證明電鋸保險開關的失效不過是給用戶上的人生的一課。」不錯。

改變rm的行為也不是個辦法

被rm咬了幾次後,往往會想到用」rm -i」替換rm,或整個替換掉rm,把所有被刪除的文件放到~/.deleted目錄中。這些小技巧讓用戶有了錯誤的安全感。

Date: Mon,16 Apr 90 18:46:33 199
X-Virus: 6
From: Phil Agre <agre@gargoyle.uchicago.edu>;
To: UNIX-HATERS
Subject: deletion

在我們的系統上,「rm」並不真正刪除文件,而是給文件換了名,這樣」undelete」(不是unrm)這樣的工具就能恢復被刪的文件。

這個功能讓我不再對刪除文件多加小心,反正刪掉了也能找回來。可是,我錯了。Emacs中的刪除並不支持這個功能,Dired命令也是如此。這當然是因為文件恢復並不是操作系統的一個功能。

所以,現在我腦子裡有兩個概念,一個是」deleting」一個文件,一個是」rm』ing」一個文件。當我的手要我的腦子刪除一個文件時,我總要把這兩個概念區分一遍。

一些UNIX專家由此得出了荒謬的結論,他們認為最好別把rm搞得更友好。他們爭辯說,讓UNIX更友好的努力往往適得其反。不幸的是,他們是對的。

Date: Thu, 11 Jan 90 17:17 CST
X-Virus: 6
From: merlyn@iwarp.intel.com (Randal L. Schwartz)
Subject: Don』t Overload commands! (was Re: rm *)
Newsgroups: alt.folklore.computers

請千萬別讓人用「安全」命令去替換標準命令。

(1) 許多shell程序會對多嘴的rm感到驚訝,而且也不會想到刪除了的文件仍然佔有磁盤空間。

(2) 並不是所有刪除操作都是安全的,有戶會因此產生一切都能恢復的錯覺。

(3) 那些不標準的命令對系統管理員來說尤其可恨。如果你想有個有確認功能的」rm」,用下面的命令:

% alias del rm -i

千萬別替換rm!

最近,comp.unix.questions上有過一次對系統管理員的調查,讓他們說出最恐怖的系統管理故事。72小時內,就有了300多條回應。許多和我們上面描述的文件刪除有關。可笑的是,這些可是UNIX高手。然而正是他們在對「UNIX對用戶不友好」這類指責進行著辯護。

對用戶不友好?UNIX對系統管理員又友好過麼?請看

Date: Wed, 14 Sep 88 01:39 EDT
X-Virus: 6
From: Matthew P Wiener <weemba@garnet.berkeley.edu>;
To: RISKS-LIST@kl.sri.com
Subject: 「Single Keystroke」

在UNIX上,即使是有經驗的用戶也會誤用rm。我從來沒有誤刪除過文件,可是有一天,我用!r重複執行一個歷史命令,我驚訝地發現被運行的是」rm –r *」。

為什麼不能有個沒有history功能的shell?

我還聽到過一個用戶試圖刪除一個名叫」*」的文件,好在他沒有權限。

這個用戶還想修改shell來避免對*進行展開。不幸的是,這個補救如同是在滲水的牆上再刷一層漆,治標不治本。

在線幫助

用戶讀打印文檔的次數比他們參加選舉投票的次數還要少。只有觸手可及的在線文檔才是有用的。下面我們看看UNIX的man是如何讓最需要它的新用戶失望的。

不是每個命令都是平等的,有些是外部命令,有些是內部命令。有些有man page,有些沒有。UNIX要求你能區分這些命令。比如,wc, cp和ls是外部命令,它們都有man page,而fg, jobs, set和alias(這些長文件名是從哪裡來的?)是內部命令,它們沒有man page。

UNIX告訴新手用」man command」命令獲得幫助,他們可不知道並不是所有命令都是如此。另外,如果他們的shell設置得有些不標準,他們就只能請教高手來獲得幫助了。

錯誤信息和錯誤檢查?沒門!

新手很容易犯錯誤,比如用錯命令,或用錯選項。系統應該能識別這些錯誤,並且反饋給用戶。不幸的是,UNIX程序從來不麻煩自己。相反,UNIX往往把各種錯誤混在一起,直到產生致命的結果。

上面一節我們說明了rm如何容易造成誤刪除。但你可能不知道不用rm也能很容易地誤刪除文件。

想刪除你的文件麼?試試編譯器

一些cc版本經常根本不考慮用戶的可能輸入錯誤,而刪除一些源代碼文件。一些本科生常常著了道。

Date: Thu, 26 Nov 1992 16:01:55 GMT
X-Virus: 6
From: tk@dcs.ed.ac.uk (Tommy Kelly)
Subject: HELP!
Newsgroups: cs.questions
Organization: Lab for the Foundations of Computer Science, Edinburgh UK

我剛才想這麼編譯程序:

% cc –o doit doit.c

不小心敲成了:

% cc –o doit.c doit

不用說我的doit.c被沖掉了。有沒有辦法能恢復我的程序?(我干了整整一個上午)

其他一些程序也有同樣的行為:

Date: Thu, 1 July 1993 09:10:50 - 0700
X-Virus: 6
From: Daniel Weise <Daniel@dolores.stanford.edu>;
To: UNIX-HATERS
Subject: tarred and feathered

經過幾次努力,我總算從歐洲的一個脆弱ftp站點上下載了了一個3.2M的文件。該untar它了。我敲了一下命令:

% tar –cf thesis.tar

…沒有回應。

老天!

是不是該用x選項而不是c?

是的。

tar是不是給出了錯誤信息說沒有指定輸入文件?

沒有。

tar是否感覺到有什麼不對勁?

沒有。

tar是不是真的什麼也沒有tar?

是的。

tar是否把thesis.tar用垃圾覆蓋了?

當然,這就是UNIX。

我是不是需要再花 30分鐘從歐洲下載這個文件?

當然,這就是UNIX。

我敢肯定有不少人遇到過這一不幸,有那麼多的解決辦法,比如:錯誤信息,文件版本化,確認用戶是否想覆蓋一個已有文件,等等等等。tar似乎在有意給用戶找麻煩。

對於經常用tar備份的系統管理員來說,這個bug更是危險。不少系統管理員都曾經在備份腳本中錯誤地使用過「tar xf…」。在需要恢復備份的時候,才發現原來什麼也沒做。

欲知是否還有其他這樣的恐怖命令,請聽下回分解。

上回書說到cc、tar等命令是如何幫助你刪除重要文件的。UNIX的強大當然不局限於此。

因為沒有錯誤檢查,在眾多「UNIX 強大編程工具」的支持下,用戶有各種選擇來刪除他們的重要文件。

Date: Sun, 4 Oct 1992 0:21:49 PDT
X-Virus: 6
From: Pavel Curtis <pavel@parc.xerox.com>;
To: UNIX-HATERS
Subject: So many bastards to choose from…

我有一個總在運行的程序foo,用來提供網絡服務,並且每隔24小時檢查系統內部狀態。

一天,我cd到foo所在的目錄,因為這不是開發目錄,我想看看foo的版本是多少。代碼是由RCS來維護的,所以我自然而然地使用了以下命令:

% ident foo

先別管RCS的種種劣跡,也別管ident如何原始瘋狂。我這次的麻煩是,我的手指自行其是地選擇了更像一個詞的indent而不是ident:

% indent foo

indent是UNIX的一個愚蠢的C代碼風格轉換工具。那個寫indent的混蛋是否判斷了一下輸入文件真的為C程序麼 (天哪,至少可以看看文件的後綴是否為.c吧)?我想你知道答案。而且,這個SB(Said Bastard)認為如果你只給了一個參數,那麼你就是想要進行在線風格轉換。不過別著急,這個SB 考慮到了可能帶來的麻煩,他保存了一個備份foo.BAK。然而他是否只是簡單地把foo換了個名字呢?沒有,他選擇了拷貝(毫無疑問,那個寫 indent的程序員在準備備份的時候已經打開了foo,而且rename系統調用是後來才有的)。

現在,你可能知道發生了些什麼了…

我那正在運行中的foo在準備頁面扇出的時候,發現原來的可執行文件已經不在了,這可不是什麼好事,於是我的foo崩潰了,我丟掉了20小時的系統狀態信息。

自然那些設計(咳嗽)UNIX的混蛋們對複雜的文件版本化功能不感興趣,而這一功能就能救我的命。當然,那些混蛋也從未想到過對準備進行頁面扇出的文件加鎖,是不是?

有那麼多混蛋可供選擇,為什麼不把他們都宰了?

Pavel

想像一種散發氯氣的油漆,按照說明,用在戶外是不成問題的,但如果用它刷你臥室的牆壁,你的腦袋就要大了。這樣的油漆能在市場上存活多久呢?當然不會超過20年。

錯誤信息笑話

當你看到飯館跑堂的把一盤菜撒在顧客腦袋上時,你會笑麼?UNIX迷會的。但那些無助的用戶對著錯誤信息百思不得其解的時候,他們是最先發出笑聲的。

有人整理了一些UNIX最為可笑的錯誤信息,把他發佈在Usenet上。他們使用的是C shell.

% rm meese-ethics
rm: messe-ethics nonexistent

% ar m God
ar: God does not exist

% 「How would you rate Dan Quayle』s incompetence?
Unmatched 「.

% ^How did the sex change^ operation go?
Modifier failed.

% If I had a ( for every $ the Congress spent, what would I have?
Too many (『s

% make love
Make: Don』t know how to make love. Stop.

% sleep with me
bad character

% got a light?
No match

% man: why did you get a divorce?
man:: Too many arguments.

% ^What is saccharine?
Bad substitute.

% %blow
%blow: No such job.

下面的這些幽默作品來自Bourne Shell:

$ PATH=pretending! /usr/ucb/which sense
no sense in pretending

$ drink <bottle; opener
bottle: cannot open
opener: not found

$ mkdir matter; cat >;matter
matter: cannot create

UNIX態度

我們展現了一個非常慘淡的圖景: 迷一般的命令名,不一致和無法預計的運行結果,危險命令沒有保護,無法接受的在線文檔以及在錯誤檢查和容錯性方面的稀鬆工作。那些參觀UNIX的人不是為了得到熱情款待,他們不是迪斯尼公園中的遊客,更像是執行任務中的聯合國維和部隊。UNIX怎麼會搞成這個樣子?如我們曾指出的那樣,其中有一些是歷史原因造成的。但是還有其他的原因:那就是多年來形成的UNIX文化,這種文化被稱為 「UNIX哲學」。

UNIX哲學不是來自Bell實驗室或UNIX系統實驗室的手冊。他是自然形成的,其中包含了許多人的貢獻。Don Libes和Sandy Ressler在《UNIX生活》(Life with UNIX)中對UNIX哲學作了很好的總結:

小即是美
用10%的工作解決90%的任務
如果必須作出選擇,選擇最簡單的那個。

根據UNIX程序和工具的實際表現來看,對UNIX哲學更為精確的總結應該是:

小的程序比正確的程序更好
粗製濫造是可以接受的
如果必須作出選擇,選擇責任最小的那個。

UNIX沒有哲學,UNIX只有態度。這個態度指出簡單的做了一半的工作比複雜完整的工作更好。這個態度指出程序員的時間比用戶的時間更為珍貴,即使用戶比程序員要多得多。這個態度指出達到最低要求就足夠了。

Date: Sun, 24 Dec 89 19:01:36 EST
X-Virus: 6
From: David Chapman <zvona@ai.mit.edu>;
To: UNIX-HATERS
Subject: killing jobs; the Unix Design Paradigm

我最近學會了如何在UNIX上殺掉任務。在這個過程中我體會到了不少UNIX的強大和智慧,我想應該和大家分享一下。

你們中的大多數當然不用UNIX,所以知道如何UNIX上殺任務估計沒什麼用。但是,你們中的一些人,包括我,可能會經常運行一些TeX任務,那麼學會殺任務就顯得尤為重要了。「kill」命令繼承了UNIX的設計原則,所以下面的一些體會有更為通用的意義。

在UNIX中你可以用^Z掛起一個任務,或者用^C終止一個任務。但是LaTex自己截獲^C。結果是,我經常搞出一堆LaTex任務。我對此到不在乎,可還是覺得應該想辦法除掉它們。

許多操作系統有「kill」這樣的命令,UNIX也不例外。大多數操作系統上的「kill」僅僅用來殺死進程。但UNIX的設計更為通用:「kill」被用來向進程發送一個信號,這體現了UNIX的一個設計原則:

盡量使操作通用,給予用戶強大力量(power)

「kill」命令功能很是強大;你能用它給進程發送各種各樣的信號。比如,9這個信號用來殺死進程。注意到9是最大的一位數,這體現了UNIX的另一個設計原則:

使用能體現功能的最簡單的名字

在其他我知道的操作系統上,不帶參數的「kill」用於殺死當前任務。單UNIX的「kill」總是需要參數。這體現了UNIX的一個明智的設計原則:

盡量使用長參數或提示來防止用戶不小心把自己**了(screwing himself)

這一設計原則在許多UNIX應用程序中得到了體現,我不想列舉它們,但還是想提一下UNIX上logout和文件刪除的實現,希望你知道我的意思。

在其他我知道的操作系統上,「kill」接受的參數是任務名。這不是好的選擇,因為你可能有許多LaTex任務同時運行,它們都有同樣的任務名「latex」。所以「kill –9 latex」可能會產生歧義。

和其他操作系統一樣,UNIX提供一個列出任務的命令「jobs」,下面是一個例子:

zvona@rice-chex>; jobs
[1] – Stopped latex
[1] – Stopped latex
[1] + Stopped latex

這樣你可以用job號(表示在[]中)標識一個任務。

如果你受到那些未經精雕細刻的操作系統的影響,你會想用「kill –9 1」來殺掉第一個latex任務。但你會發現下面的錯誤信息:

zvona@rice-chex>; kill -9 1
1: not owner

正確的做法是使用進程號,比如18517。你能用「ps」命令獲得它。當找到了相應進程號後,你只要:

zvona@rice-chex>; kill -9 18517
zvona@rice-chex>;
[1] Killed latex

注意到UNIX在你的任務被真正殺掉之前就給了你提示符。這又體現了一個UNIX設計原則:

對於給用戶的反饋,能少說絕不多說,能晚說絕不早說。以免過多信息所可能導致的用戶腦損傷。

我希望這些體會能對大家有用。在這一學習過程中,我自己當然被UNIX設計哲學所深深吸引了。我們都應該從UNIX kill命令的雅致、強大和簡潔中學到些東西。

第二章就這麼完了,經歷了這麼多艱難困苦的你已經不是新手了,下回書將介紹UNIX之文檔,或者說UNIX之沒有文檔.

標題: 第三章 文檔

OK,不是新手的你可能想進一步學習瞭解UNIX。不錯,UNIX文檔正是你需要的。

文檔
什麼文檔

「使用UNIX進行操作系統教學的一個好處是,學生的書包能裝下所有的UNIX源代碼和文檔。」

—— John Lions, 新南威爾士大學,1976年在談論UNIX版本6時說的一段話。

多年以來,有三個獲得UNIX有關知識的簡單途徑:

閱讀源代碼
寫一個自己的UNIX
給寫UNIX的程序員打電話(或是發email)

和荷馬史詩一樣,UNIX被口頭傳誦著。如果不成為內核黑客,你就不可能是一個嚴肅的UNIX用戶——或者至少應該在身邊有個觸手可及的內核黑客。那個確實存在的文檔——man手冊——不過是一些已經知道自己在做什麼了的人所收集的一些備忘錄。UNIX的文檔是這麼簡潔,你能在一下午讀完它。

在線文檔

man工具是UNIX文檔系統的基礎。man接受你輸入的參數,找到相應的文檔文件,把它輸出到nroff(還包括一些地球上沒有其他地方使用的一些文本格式宏),最後結果被發送到pg或more。

起先,這些零碎文檔被叫做」man頁」(man pages),因為這些文檔多為一頁左右(多數情況是少於一頁)。

man對於那個時代是個不錯的玩意,但那個時代早已一去不復返了。

多年來,man系統不斷發展成熟。值得稱讚的是,它並沒有像UNIX的其他部分一樣搞得代碼混亂程序難懂,可是它也沒變得更有用。事實上,在過去的15年中,UNIX的文檔系統只有了兩處改進:

catman. 程序員曾「驚喜地」發現除了nroff格式以外,他們還能存儲處理過的文檔文件,這樣文檔調出的速度就更快了。 對於今天的快速處理器,catman似乎不那麼需要了。

但是許多nroff處理過的文檔文件仍然佔據著用戶的幾兆磁盤空間。makewhatis, apropos和key (最終構成了man –k功能)是一個對man手冊進行索引的系統,這樣即使不知道程序的確切名字也能進行查詢。

與此同時,電子出版的勢頭早已超過了man手冊。使用今天的超文本系統你能用鼠標從一篇文章跳到另一篇文章;與之相比,man手冊僅僅在末尾提供」 SEE ALSO」一節,讓用戶自己再man下去。在線文檔的索引功能又是如何呢?今天你可以買到CD-ROM上的牛津英語詞典,它對其中的每一個詞都加了索引;可是man手冊還是僅僅對命令名和描述行進行索引。今天甚至連DOS都提供了有索引的超文本文檔。可是man手冊還是採用適合DEC打印終端的80列66 行的格式。

公平點說,有些廠商是在看不下去,提供了自己的超文本在線文檔系統。在這些系統上,man手冊走到了進化的盡頭,常常不是過時了,就是根本不存在。

「我知道它就在這裡,可就是找不到」

對於今天還在使用man手冊的人來說,最大的問題是告訴man你要的man手冊就在那裡。在以前,查找man手冊是很容易的:全都在/usr/man下頭。後來man手冊按章節分成了不同的目錄:/usr/man/man1, /usr/man/man2,/usr/man/man3等等。有的系統甚至把「本地」man手冊也放在/usr/man/man1下。

當AT&T發佈系統V的時候,情況變得費解了。/usr/man/man1目錄變成了/usr/man/c_man,似乎字母比數字更好記。在有些系統上,/usr/man/man1變成了/usr/local/man。那些銷售UNIX應用程序的公司開始建立自己的man目錄。

最終,伯克利修改了man程序使得它能對環境變量$MANPATH中指定的一系列目錄進行查找。這是個偉大的想法,只有一個小毛病:它不工作。(以下省略100字, 因為我太懶了,內容也有些太過時了,Linux上的man還是不錯的,除了無法獲得shell內部命令的man手冊,當然,man bash是一個選擇 -- me)。

這個就是內部文檔?

一些大的UNIX工具也提供自己的文檔。許多程序的在線文檔是一行叫人費解的 「使用」(usage)說明。下面是awk的「使用」說明:

% awk
awk: Usage: awk [-f source | 『cmds』] [files]

是不是挺有用的?複雜一些的程序有著更深入的在線文檔。不幸的是,它們有時候描述的似乎不是你正在運行的程序。

Date: 3 Jan 89 16:26:25 EST (Tuesday)
X-Virus: 6
From: Reverend Heiny <Heiny.henr@Xerox.COM>;
To: UNIX-HATERS
Subject: A conspiracy uncovered (陰謀被揭露了)

經過幾個小時的專心研究,我得出了一個重要的結論:

UNIX是狗屎 (UNIX sucks)

現在,你可能覺得很驚訝,但這是事實。這項研究已經被遍佈全球的研究人員所證實了。

更為重要的是,這不僅僅是攤狗屎,而是又稀又粘的臭狗屎,是大寫的臭狗屎。看看下面這個例子,你就知道了:

toolsun% mail
Mail version SMI 4.0 Sat Apr 9 01:54:23 PDT 1988 Type ? for help
「/usr/spool/mail/chris」: 3 messages 3 new
>;N 1 chris Thu Dec 22 15:49 19/643 editor saved 「trash1」
N 2 chris Tue Jan 3 10:35 19/636 editor saved 「trash1」
N 3 chris Tue Jan 3 10:35 19/656 editor saved 「/tmp/ma9」
& ?
Unknown command: 「?」
&

什麼樣的系統環境(特別是這個到了能開車、投票、喝啤酒年齡的傢伙)會拒絕一個它讓使用的命令?

為什麼用戶手冊是如此脫離現實?

為什麼這些神秘的命令是這麼和功能不符?

我們不知道Heiny的問題是什麼;和我們上面提到的一些問題一樣,這個bug似乎已經被修正了。或者說,它被轉移到了其他程序中。

Date: Tuesday, September 29, 1992 7:47PM
X-Virus: 6
From: Mark Lottor <mkl@nw.com>;
To: UNIX-HATERS
Subject: no comments needed (無需多說)

fs2# add_client
usage: add_client [options] clients
add_client -i | -p [options] clients
-i interactive mode – invoke full-screen mode

[還有一些選項,這裡省略了]

fs2# add_client -i

Interactive mode uses no command line arguments

如何得到真正的文檔

實際上,UNIX最好的文檔是經常用strings處理程序二進制代碼。使用strings你能得到所有程序中定死了的文件名,環境變量,未公開的選項,怪異的錯誤信息等等。比如,如果你想知道cpp是如何去查找頭文件的,你最好使用strings而不是man:

next% man cpp

No manual entry for cpp.

next% strings /lib/cpp | grep /
/lib/cpp
/lib/
/usr/local/lib/
/cpp
next%

嗯…別著急

next% ls /lib
cpp* gcrt0.o libssy_s.a
cpp-precomp* i386/ m68k/
crt0.o libsys_p.a posixcrt0.o
next% strings /lib/cpp-precomp | grep /
/*%s*/
//%s
/usr/local/include
/NextDeveloper/Headers
/NextDeveloper/Headers/ansi
/NextDeveloper/Headers/bsd
/LocalDeveloper/Headers
/LocalDeveloper/Headers/ansi
/LocalDeveloper/Headers/bsd
/NextDeveloper/2.0CompatibleHeaders
%s/%s
/lib/%s/specs
next%

我真笨。NEXTSTEP的cpp使用了/lib/cpp-precomp。你不可能在man手冊中發現這些。

next% man cpp-precomp

No manual entry for cpp-precomp.

OK. 這一切究竟是因為什麼?這一切究竟是從何而來?下回分解。

上回書說到源代碼是最好和唯一的文檔,根本原因是因為UNIX是...

給程序員用的,不是用戶

別因為UNIX蹩腳的文檔而責怪Ken和Dennis。 UNIX剛開始建立文檔時並沒有遵守業界流行的文檔標準,一些bug和潛在的陷阱,而不是程序的功能,被記錄了下來,這是因為讀這些文檔的人往往就是 UNIX系統開發者。對於許多開發者來說,man手冊不過是收集bug報告的地方。那些針對初級用戶、程序員和系統管理員提供文檔的觀念是新玩意。可悲的是,由於70年代建立的UNIX文檔系統,這一觀念實現的並不是很成功。

UNIX世界認識到了這些文檔方面的現狀,但並不覺得有什麼大不了的。《UNIX生活》很客觀地說明了UNIX對於文檔的態度:

UNIX源代碼是最好的文檔。畢竟,這是系統用以決定該如何運行時所參照的文檔。文檔用來解釋代碼,經常是一些不同的人在不同的時間寫成的,而這些人往往不是寫代碼的人。你應該把這些文檔看作是指南。有時候這些文檔不過是些期望而已。

但是,更一般的做法是去源代碼中尋找未被文檔化使用方法和功能說明。有時候你會發現一些文檔中記錄的功能其實並沒有被實現。

這還只是針對用戶程序。對於內核,情況就更為糟糕了。直到最近,還沒有廠商提供的設備驅動編寫和內核級調用函數的文檔資料。有人開玩笑說:「如果你覺得需要閱讀關於內核函數的文檔,那麼很可能你本來就不配使用這些函數。」

真相恐怕更為邪惡。之所以沒有內核文檔是因為AT&T把它的代碼看成是商業機密。如果你想寫一本說明UNIX內核的書,那麼你就等著入被告席吧。

源代碼就是文檔

命裡注定,AT&T的計劃弄巧成拙了。由於沒有文檔,瞭解內核和應用程序的唯一途徑就是閱讀源代碼。結果是,UNIX源代碼在在最初的20年中被瘋狂的盜版。咨詢人員,程序員和系統管理員去搞UNIX源代碼並不是為了重新編譯或製作出售自己的UNIX版本,他們需要文檔,而源代碼是唯一的選擇。 UNIX源代碼從大學流向周邊的高科技公司。這當然是非法的,但是情有可原:UNIX廠商提供的文檔不夠用。

這並不是說源代碼中有什麼值錢的秘密。所有讀過UNIX代碼的人都被下面的一行粗暴註釋驚呆過:

/* you are not expected to understand this */ (/* 沒指望你能明白 */)

儘管這行註釋最開始出現在UNIX V6內核中,但是幾乎所有的原始AT&T代碼都差不多,其中充滿了內聯手動優化和怪異的宏。寄存器變量被冠以p, pp和ppp這類的名字。「這個函數是遞歸的」這樣的註釋似乎表明遞歸調用是什麼難理解的概念。事實上,AT&T在文檔方面好為人師的態度只不過是其寫代碼的馬虎態度的一個反映。

要識別一個蹩腳手藝人其實很簡單:你會看到裂縫上的油漆,一個接一個的補丁,所有東西被膠帶和口香糖勉強湊合在一塊兒。必須承認:如果想從頭建立和重新設計什麼,必須要多思考,多下功夫。

Date: Thu,17 May 90 14:43:28 -0700
X-Virus: 6
From: David Chapman <zvona@gang-of-four.stanford.edu>;
To: UNIX-HATERS

這是man man中的一段,挺有意思:

DIAGNOSITICS

如果你使用-M選項而且給出的路徑並不存在,那麼輸出的錯誤信息可能有點兒不對。比如/usr/foo/目錄不存在,如果你運行:

man –M /usr/foo ls

那麼你得到的錯誤信息是「No manual entry for ls」(「沒有ls的手冊記錄」)。正確的錯誤信息時告訴你目錄/usr/foo不存在。

有寫這段說明的功夫,恐怕足夠修改這個bug了。

無言UNIX:課程設置建議

Date: Fri, 24 Apr 92 12:58:28 PT
X-Virus: 6
From: cj@eno.corp.sgi.com (C J Silverio)
Organization: SGI TechPubs
Newsgroups: talk.bizarre
Subject: UNIX Without Words (無言UNIX)

[在一場關於文檔無用論的激烈辯論中,我提出了下面這個建議。我膽子小,所以現在才敢公開,供大家參考。]

UNIX Ohne Worter (不會翻 – me)

我被這裡散步的文檔無用論觀點深深折服了。事實上,我進一步認為文檔就是毒品,我對於它的依賴性是人為造成的。在專業人士的幫助下,我想我能夠戒掉它。

而且,我的良心告訴我不能再靠販賣這種毒品為生了。我決定回到數學研究院脫胎換骨,徹底從這個寄生蟲一樣的職業中脫身。

雖然下面這份文檔似乎表明了我中毒有多麼深,可我還是覺得下一版SGI中應該把它提供給用戶。這不過是暫時之舉,以後會把它搞掉的。

這是我的建議:

標題:「無言UNIX」

對像:UNIX新手

簡介:提供在沒有文檔條件下使用UNIX的通用策略。展示在沒有文檔條件下摸清任何操作系統的通用原則。

內容:

介紹:「無文檔」哲學簡介
為什麼手冊是惡魔
為什麼man手冊是惡魔
為什麼你還是應該讀這份文檔
「這將是你讀的最後一份文檔!」

第一章:如何猜測可能存在哪些命令

第二章:如何猜測命令名

UNIX的怪異縮略命名法
案例:grep

第三章:如何猜測命令選項

如何破解怪異的使用說明
案例:tar
如何知道什麼時候順序是重要的
案例:fine

第四章:如何知道運行正確:沒有消息就是好消息

從錯誤中恢復

第五章:口頭傳統:你的朋友

第六章:如何獲得和維持一個活生生的UNIX高手

如何餵飽你的高手
如何讓高手高興
提供全部新聞組連接的重要性
為什麼你的高手需要最快的計算機
免費可樂:高手的長生不老藥
如何保持高手身體健康
高手什麼時候睡覺?

第七章:常見疑難:你的高手不理你了

識別愚蠢的問題
如何安全地提出愚蠢問題

第八章:如何承受壓力

如何對待失敗

註:可能只有6、7章才是真正需要的。是的,這才是正路:我把它稱為「UNIX高手馴養指南」。

OK, 再也沒有文檔了。下回書將帶你進入sendmail的美好世界,為什麼「使用sendmail的感覺和得了花柳病一樣。」?下回分解。

標題: 第八章 csh, pipes和find (part 1)

UNIX演義又開始了,本來這回書要表一表sendmail和花柳病的關係,不過sendmail似乎已經從良了,從良妓女比貞節烈女對我們民族的貢獻要大得多,所以不想再找她麻煩了,對妓女發展史和性病鬥爭史感興趣的,我們可以私下交流。

作為程序員而不是妓女的你,可能對UNIX的編程環境更感興趣,所以這一節中介紹一下UNIX Shell的歷史。我GPL,你沒花錢,所以只能任我擺佈,我上什麼你就吃什麼,不要廢話。

GPL的好處在於你不必為自己的工作負責,也不必對用戶負責,所以sourseforge上充斥著良莠不齊的自由項目。我希望我的心上人也能理解這一點,這一切的開始並不是為了什麼價值、責任、過去或是未來,這一切甚至不是為了現在,這一切只是源於passion。

在大海吐出的每個泡沫中
在上班路上吸入的每一粒塵埃中
在過去歲月的每一次陣痛中
在一次一次睡去和醒來中
在天氣預報和新聞聯播中
在七月流火和九月授衣中
在七月長生殿七日中
在矢車菊和芙蓉中
在長繩紀日中
在天長地久中
在你身邊
在我心裡
無須尋求意義

第八章 csh, pipes和find

強有力的工具給強有力的傻瓜

「有些操作系統從沒有被好好計劃,以至於只好用反芻的噪音來命名它的命令(awk, grep, fsck, norff),我想到這個就反胃。」

—— 無名氏

UNIX所謂的「強大工具」是個騙局。這不過是UNIX為了那些東拼西湊的命令和工具所打的幌子。真正的強大工具不需要用戶付出太多努力就能提供強大的功能。任何會使改錐和鑽頭的人都會用電動改錐和電鑽。他們不需要搞懂電學、電機、轉矩、電磁學、散熱或維護。他們只需要把它通上電,帶上安全眼鏡,然後打開開關。許多人連安全眼鏡也不省了。你在五金商店裡找不到有致命缺陷的工具:它們不是根本沒能投放市場,就是被訴訟搞得焦頭爛額。

UNIX設計者的最初目標是提供簡單的工具,然而現在的工具則充滿了過分的設計和臃腫的功能。比如ls這個列文件的命令竟然有18個選項,提供從排序到指定顯示列數的種種功能,而這些功能如果用其他程序實現會更好些(以前正是這樣的)。find命令除了查找文件以外還輸出cpio格式的文件(而這個功能其實用UNIX名聲狼藉的管道很好地實現)。今天,和UNIX類似的電鑽將有20個旋鈕,連著不標準的電源線,不匹配3/8英吋和7/8英吋的鑽頭(而這一點會在手冊的BUG一章中說明)。

和五金店裡的工具不同,許多UNIX強大工具是有缺陷的(有時對文件是致命的):比如tar的不接受超過100個字符的文件名;又比如UNIX調試器總是垮掉,這還不夠,它的core文件將覆蓋你自己的core,讓你下次可以用調試器去調試調試器在調試調試器中生成的core。

Shell遊戲

UNIX的發明人有個偉大的想法: 把命令解析器作為一個用戶程序實現。如果用戶不喜歡缺省的命令解析器,他可以自己寫一個。更重要的是,shell能夠進化,這樣shell將不斷進步,變得越來越強大,靈活和易用,至少理論上是這樣。

這真是個偉大的想法,不過弄巧成拙了。功能的逐漸增加帶來的是一團糟。因為這些功能沒有經過設計,只是在不斷演化。和所有編程語言所遭到過的詛咒一樣,那些利用這些功能的既存shell腳本成為了shell的最大敵人。只要有新的功能加入shell,就會有人在自己的腳本中使用它,這樣這個功能就從此長生不老了。壞主意和臭功能往往怎麼也死不掉。

於是,你得到了一個不完整、不兼容的shell的大雜燴 (以下每個shell的描述都來自於他們各自的man pages):

sh 是個命令編程語言,用於執行來自終端或文件的命令。
Jsh 和sh一樣,但具有csh風味的工作控制 (job control)
Csh C類型語法的shell
Tcsh emacs編輯口味的csh
Ksh KornShell,你的另一個命令和編程語言
Zsh Z Shell
Bash GUN Bourne-Again Shell (GNU Bourne復出Shell)

五金商店裡的螺絲刀和鋸子,儘管可能來自3、4個不同的廠商,但操作方法都差不多。典型的UNIX在/bin或/usr/bin下存了成百個程序,它們來自眾多自以為是的程序員,有著自己的語法、操作範例、使用規則(這一個可以當成管道,而那一個則操作臨時文件),不同的命令行參數習慣,以及不同的限制。拿grep和它的變種fgrep, egrep來說,哪一個是最快的?為什麼它們接受的參數都不一樣,甚至對正則表達式的理解也不盡相同?為什麼不能有一個程序提供所有功能?負責的傢伙在哪兒啊?

當把命令之間的種種不同都深深烙在腦海中後,你還不能避免被驚著。

Shell Crash

下面這條消息來自哥倫比亞大學編譯原理課程的BBS。

Subject: Relevant UNIX bug
October 11, 1991

W4115x課程的同學們:

我們剛學習了活動記錄(activation record),參數傳遞(argument passing)和函數調用規則(calling conventions),你們是否知道下面的輸入將讓任何一個cshell立刻崩潰?

:!xxx%s%s%s%s%s%s%s%s

你們知道為什麼麼?

以下的問題供你們思考:

Shell遇到 「!xxx」會做什麼?

Shell遇到 「!xxx%s%s%s%s%s%s%s%s」會做什麼?

為什麼cshell會崩潰?

你將如何修改有關代碼來解決這個問題?

最重要的一點:

當你(是的,就是你)將這個前途遠大的操作系統用21個字符治服的時候,你覺得天理能容麼?

你可以自己試一試。根據UNIX的設計,如果shell垮掉了,你的所有進程將被殺死,你也會被踢出系統。其他操作系統在遇到非法內存訪問錯誤時會彈出調試器,但不是UNIX。

可能這就是為什麼UNIX shells不讓你在shell的地址空間裡動態加載自己的模塊,或者直接調用其他程序中的函數。如果這樣就太危險了。一步走錯,唉喲,你已經被踢出門外了。愚蠢的用戶是應該被懲罰的,程序員的錯誤更是不可容忍。

下回書裡我們將進入色彩斑斕的UNIX語法世界。

[好玩]Unix痛恨者手冊--ZZ

半年前的上回書說到你如何去玩shell遊戲,估計現在你已經玩膩了,不過不要著急,下面,該輪到shell玩你了。

歡迎來到元語法(metasyntacitic)動物園

C Shell的元語法操作符帶來了大量和引用有關的問題和混亂。元操作符在一個命令被執行前對其進行轉換。我們把這些操作符叫做元操作符是因為它們不屬於命令的語法成分,它們作用於命令本身。大多數程序員對元操作符(有時也叫做escape operators)並不陌生。比如,C字符串中的反斜槓(\)就是一個元語法操作符;它不代表自己,而是對其後的字符的說明。如果你不希望如此,你必須使用引用機制來告訴系統把元操作符當做一般字符來處理。回到剛才C字符串的例子,如果想得到反斜槓字符,你必須寫成\\。

簡單的引用機制在C Shell中很難工作,這是因為Shell和它執行的程序之間各執一詞,無法統一。例如,下面是個簡單的命令:

grep string filename;

參數string包含了grep定義的字符,比如?, [, 和]等等,但這些字符是shell的元操作符。這意味著必須對它們進行引用。然而,有些時候可能不需要如此,這和你使用什麼樣的shell以及環境變量有關。

如果想在字符串中尋找包含點(.)或其他以橫槓(-)開頭的模式,這就更為複雜了。

一定要記住對元字符進行正確引用。不幸的是,和模式識別一樣,操作系統的各個部分都充斥著互不兼容的引用標準。

C Shell的元語法動物園裡飼養著七種不同的元操作符家族。斗轉星移,轉眼間動物園裡已經人滿為患了,籠子使用的不再是鋼鐵,而是用錫了。動物間的小磨擦不斷。這七種針對shell命令行的轉換方式是:

別名 alias, unalias

命令輸出替代 `

文件名替代 *, ?, []

歷史替代 !, ^

變量替代 $, set, unset

進程替代 %

引用 ',"

這一「設計」的結果是,問號(?)永遠被shell當成單字符匹配符,它永遠無法被作為命令行參數傳給用戶程序,所以別想使用問號來作為幫助選項。

如果這七種元字符有著清晰的邏輯順序和定義,那麼情況也不會太糟糕。可事實並非如此:

日期: Mon, 7 May 90 18:00:27 - 0700
發信人: Andy Beals <bandy@lll-crg.llnl.gov>;
主題: Re: today's gripe: fg %3 (今天之不爽事:fg %3)
收信人: UNIX-HATERS

你可以使用%emacs或者%e來恢復一個任務(如果唯一的話),也可以使用%?foo,如果"foo"出現在命令行中。

當然,!ema和!?foo也可以用於歷史命令替換上。

但是,UCB的豬頭(pinheads)們沒有想到!?foo後面可能伴隨的編輯命令:

!?foo:s/foo/bar&/:p

多向前掃瞄一個編輯字符真的這麼困難麼?

哪怕是Unix「專家」,也要暈菜了。下面再來看看Milt Epstein的這個例子,他想寫個shell腳本獲得實際被敲入的命令行,而不是那個經shell處理後的結果。他最後發現這並不容易,因為shell 為命令做了太多的「好事」。要做到這一點,需要各種稀奇古怪的變形技術,連Unix專家也會望而卻步。這就是Unix的典型做法:把簡單的東西搞得異常複雜,這只是因為這些東西在Unix誕生之時從沒被仔細考慮過:

日期: 19 Aug 91 15:26:00 GMT
發信人: Dan_Jacobson@att.com
主題: ${1+"$@"} in /bin/sh family of shells shell scripts
收信人: comp.emacs.gnu.emacs.help, comp.unix.shell

>;>;>;>;>; On Sun, 19 Arg 91 18:21:58 - 0500
>;>;>;>;>; Milt Epstein <esptein@suna0.cs.uiuc.edu>;
寫到:

Milt>; "${1+"$@"}"究竟是什麼意思?我估計這是用來
Milt>; 讀取其餘的命令行參數,但我不敢肯定。

這是/bin/sh裡用來完整複製命令行參數的一種方法。

它的意思是:如果有至少一個參數(${1+),那麼用所有參數("$@")來替代以保留所有的空白字符。

如果我們只使用"$@",那麼在沒有參數的情形下會得到""而不是我們想要的空參數。

那麼,為什麼不使用"$*"呢?sh(1)的man手冊是這樣說的:

雙引號之間的參數和命令會被替換,shell會對結果加上引用,以避免解析空格或生成文件名。如果$*出現在雙引號之中,各個參數之間的空格會被加上引用("$1 $2 ..."),而如果$@出現在雙引號之中,各個參數之間的空格不會被加上引用("$1""$2" ...)。

我認為${1+"$@"}一直可以兼容到「版本7」的shell。

老天!一直兼容到「版本7」。

聽"chdir"的還是聽"cd"的?

Unix在漫長的演化過程中幾經易手,這些Unix系統開發者把Unix引向了不同方向,他們之中沒有一個人停下來考慮一下自己的做法會不會對和其他人發生衝突。

日期: Mon, 7 May 90 22:58:58 EDT
發信人: Alan Bawden <alan@ai.mit.edu>;
主題: cd ..: I am not making this up (cd ..: 這不是我編造出來的)
收信人: UNIX-HATERS

有什麼命令能比"cd"更直接了當的呢?讓我們看這個簡單的例子:"cd ftp"。如果我的當前目錄/home/ar/alan中有個子目錄叫做"ftp",那麼它就變成了我新的當前目錄,現在我在/home/ar/alan/ftp下了。簡單吧?

現在,你們知不知道"."和".."?每個目錄都會有兩個記錄:"."是指該目錄自己,".."是指父目錄。在上面的例子中,如果我想回到/home/ar/alan,只要敲"cd .."就可以了。

現在假設"ftp"是一個符號鏈接。假設它指向的是目錄/com/ftp/pub/alan。如果執行"cd ftp",我的當前目錄將是/com/ftp/pub/alan。

和其他所有目錄一樣,/com/ftp/pub/alan也有一個叫".."的記錄,它指的是父目錄:/com/ftp/pub。如果我想進入那個目錄,我敲入命令:

% cd ..

猜一下我現在在哪兒呢?我又回到了/home/ar/alan!shell(準確的說是人工智能實驗室裡用的tcsh)認為我其實是想回到那個裝有符號鏈接的目錄。現在我必須使用"cd ./.."才能進入/com/ftp/pub。

Shell編程

Shell程序員和《侏羅紀公園》裡的恐龍製造者有些類似。他們手上沒有所需的完整材料,所以不得不用一些亂七八糟的材料填充。儘管有著無窮的自信和能力,他們似乎並不是總能控制住造出來的那些玩意。

理論上說,使用Shell編程比用C語言要有很多好處:Shell程序移植容易。這指的是使用shell「編程語言」寫的程序能夠在不同的體系結構和不同的Unix變種上運行,因為shell會解析這些程序,而不是把這些程序編譯成機器碼運行。而且,標準Unix Shell sh 自從1977年以來就成為Unix中不可或缺的一部分,所以你可以在許多機器上找到它。

讓我們來驗證一下這個理論,寫個腳本列舉目錄下的所有文件,並使用file命令來顯示文件的類型:

日期: Fri, 24 Apr 92 14:45:48 EDT
發信人: Stephen Gildea <gildea@expo.lcs.mit.edu>;
主題: Simple Shell Programming (簡單Shell編程)
收信人: UNIX-HATERS

同學們好。今天我們將學習"sh"編程。"sh" 是個簡單,用途廣泛的程序,讓我們先看個基本的例子:

打印一個目錄下所有文件的類型

(我聽到你們在後面說什麼了!那些已經會了的同學可以寫個腳本在遠程啟動一個X11客戶端,不要吵吵!)

我們學習sh編程的同時,當然也希望自己的程序是健壯,可移植和優雅的。我假設你們都讀過了相應的man手冊,所以下面這個實現應該很簡單:

file *

很不錯,是不是?簡單的答案給簡單的問題;符號 * 用來匹配目錄下的所有文件。嗯,不一定。以點(.)開頭的文件會被忽略,*不會匹配它們。也許這種情況很少發生,但既然要寫健壯的程序,我們將使用"ls"的一個特殊選項:

for file in `ls -A`

do

flie $file

done

多麼優雅,多麼健壯!不過,唉,一些系統上的"ls"不接受"-A"選項。沒問題,我們使用"-a"選項,然後再去掉"."和"..":

for file in `ls -a`

do

if [ $file != . -a $file != ..] then

file $file

fi

done

不是那麼優雅,但至少是健壯的和可移植的。你說什麼?"ls -a"也不是哪裡都能用的?沒問題,我們用"ls -f"好啦。它還快一點呢。我希望你們能從man手冊中看到所有這些東西。

唔,可能不是那麼健壯。Unix文件名中除了斜槓(/)以外可以使用任何字符。如果文件名中有個空格的話,這個腳本就完蛋了,這是因為shell會把它當成兩個文件名傳給"file"命令。不過這也不是太難對付。我們只要把它放到引用中就可以了:

for file in `ls -f`

do

if [ "$file" != . -a "$file" != ..]

then

file "$file"

fi

done

你們中可能已經有人看出來了,我們只是減少了問題,但還是沒有完全解決。因為換行符也能用在文件名中。

我們的腳本不是那麼簡單了,看來得重新評估一下我們用的方法了。如果我們不使用"ls"就不用費勁去處理它的輸出了。這個怎麼樣:

for file in * .*

do

if [ "$file" != . -a "$file" != ..]

then

file "$file"

fi

done

看起來不錯。能夠處理點(.)文件和有非打印字符的文件名。我們不斷把一些稀奇古怪的文件名加入到測試目錄下,這個腳本始終工作得很好。然而有個傢伙用它測一個空目錄,這時候 * 產生了"No such file"(沒有這個文件)的輸出。不過,我們當然可以繼續處理這種情況...

....到了這一步uucp可能會嫌我的這封郵件可能,看來我只能到此為止了,請讀者去自己解決剩下的bug吧。

Stephen

還有一個更大的問題Stephen沒有想到,我們從一開始就有意隱藏著:Unix file 命令不工作。

日期: Sat, 25 Apr 92 17:33:12 EDT
發信人: Alan Bawden <Alan@lcs.mit.edu>;
主題: Simple Shell Programming (簡單Shell編程)
收信人: UNIX-HATERS

喔!別忙。再仔細看看。你真的想用'file'命令?如果誰想開心大笑一場可以馬上去找一台Unix機器,在有各種各樣文件的目錄下敲命令"file *"。

例如,我在有各種C源代碼文件的目錄下運行"file"——這是一些結果:

arith.c: c program text

binshow.c: c program text

bintxt.c: c program text

看起來還不錯。不過這個就不太對了:

crc.c: ascii text

看到了麼?'file'並不是根據後綴".c"去判斷的,它對文件的內容採用了一些啟髮式(heuristics)算法。很明顯crc.c看起來不那麼象C代碼——儘管對我來說它就是。

gencrc.c.~4~: ascii text

gencrc.c: c program text

估計我在第4版本以後做了一些修改,使得gencrc.c更像是C代碼了...

tcfs.h.~1~: c program text

tcfs.h: ascii text

很明顯第1版本以後的tcfs.h不太像是C代碼了。

time.h: English text

沒錯,time.h看起來更像英語,而不是一般的ascii碼。我不知道'file'是不是還能判斷出西班牙語或法語。(順便說一下,你的TeX文檔會被當成"ascii text"而不是"English text",不過這有點兒跑題了)

words.h.~1~: ascii text

words.h: English text

這可能是因為我在第1版本以後在words.h中加入了一些註釋。

我把最精采的留在最後:

arc.h: shell commands

Makefile: [nt]roff, tbl, or eqn input text

都錯得一塌糊塗。我不知道如果根據'file'的判斷結果去使用這些文件會造成什麼結果。

—Alan

Shell變量

當然Alan還不算最倒霉的,至少他沒試過shell變量。

我們前面說過,sh和csh對shell變量的實現不太一樣。這本來沒有什麼,可是一些shell變量的語義(比如定義的時刻,改變的原子性等)沒有被好好說明或定義。總會遇到一些奇怪反常規的shell變量,只有反覆試驗以後才能明白。

日期: Thu, 14 Nov 1991 11:46:21 PST
發信人: Stanley's Tool Works <lanning@parc.xerox.com)
主題: You learn something new every day (每天都有新發現)
收信人: UNIX-HATERS

運行一下這個腳本:

#!/bin/csh
unset foo
if ( ! $?foo ) hen
echo foo was unset
else if ("$foo" = "You lose") then
echo $foo
endif

會產生如下錯誤:

foo: Undefined variable.

如果要讓這個腳本"正確工作",你必需求助於以下這個腳本:

#!/bin/csh
unset foo
if ( ! $?foo ) hen
echo foo was unset
set foo
else if ("$foo" = "You lose") then
echo $foo
endif

[注意,我們必須在發現foo沒有被定義的時候'set foo'.] 清楚了麼?

錯誤碼和錯誤檢查

我們上面的例子沒有指出file命令如何將錯誤返回給腳本。事實上,它根本就沒有返回錯誤。錯誤被忽略了。這不是因為我們粗心大意:許多Unix shell腳本(以及其他程序)忽略所調用程序所返回的錯誤碼。這個做法是可取的,這是因為沒有標準的錯誤碼。

也許之所以錯誤碼被廣泛地忽略,是因為當用戶敲命令的時候這些錯誤碼很少被顯示出來。錯誤碼和錯誤檢查在Unix陣營中是如此少見,以至於有些程序甚至根本就不費勁去報告錯誤。

日期: The, 6 Oct 92 08:44:17 PDT
發信人: Bjorn Freeman-Benson <bnfb@ursamajor.uvic.ca>;
主題: It's always good news in Unix land (Unix世界裡都是好消息)
收信人: UNIX-HATERS

看看這個tar程序。和所有的Unix"工具"(似乎不太準確)一樣,tar的工作方式非常奇怪和特別。例如,tar是一個極為樂觀向上的程序,它認為從不會有什麼壞事,所以太從來不返回錯誤狀態。事實上,哪怕是在屏幕上打出了錯誤信息,它返回的仍然是"好消息" (狀態0)。運行一下這個腳本:

tar cf temp.tar no.such.file

if ( $status == 0 ) echo "good news! No error."

你將得到如下結果:

tar: no.such.file: No such file or directory
Good news! No error.

我明白了——我從一開始就不應該奢望什麼一致,有用,幫助良好,快速,甚至是正確的結果...

Bjorn

OK, 被shell折騰得很爽吧?還沒過足癮?不要緊,下回書我們換個地方,鑽進Unix下水道(pipe)裡體驗無窮的痛苦和快樂

管道

Unix受虐狂們,歡迎來到Unix下水道。

「在我們這個世紀,巴黎下水道仍是一個神秘的場所。如果知道自己的下面是個可怕的大窖,巴黎會感到不安。」 —— 雨果 《悲慘世界》

下面只是我自己對Unix的看法。大約六年前(當我有了第一台工作站的時候),我用了很多時間學習Unix。應該學得算是不錯的。幸運的是,腦子裡的這些垃圾正隨著時間的推移慢慢降解。可是,自從這個討論開始以來,不少Unix支持者發給我例子來「證明」Unix的強大。這些例子當然喚起了我許多美好的回憶:他們都是用一種最怪異的方式去實現一些簡單而無用的功能。

有個傢伙發了篇貼子講述一個shell腳本是如何讓他獲得「圓滿」的(這個腳本使用了四個噪聲一樣的命令把他所有的'.pas'後綴的文件改名為'.p' 文件)。可我還是想把自己的宗教熱情留給比改幾個文件名更重要的事情上。是的,這就是Unix工具留給我的記憶:你用大量的時間去學那些複雜奇特的花架子,可到頭來卻是一場空。我還是去學些有用的真功夫吧。

——Jim Giles
Los Alamos國家實驗室

Unix迷們拜倒在管道(pipe)的真善美之下。他們他們歌唱管道:沒有管道就沒有Unix。他們異口同聲地頌揚管道:「管道能夠讓你用簡單的程序去構造更複雜的程序。管道能夠以意想不到的方式去使用命令,管道使得實現更為簡單。」不幸的是,頌歌對Unix的作用並不比對偉大旗手的要好多少。

管道並不是一無是處。模塊化和抽像化是建立複雜系統中所必需的,這是計算機科學的基本原則。基本工具越是優秀,用其建立的複雜系統就會更為成功,可維護性也越高。管道作為構造工具還是有價值的。

以下是個管道的例子:

egrep '^To:|^Cc:' /var/spool/mail/$USER | \

cut -c5- | \

awk '{ for (i = 1; i <= NF; i++) print $i}' | \

sed 's/,//g' | grep -v $USER | sort | uniq

看明白了麼?這個程序通過讀取用戶的郵箱,得到用戶所在的郵件列表(差不多是這個意思)。和你家裡的水管一樣,這個Unix管道也會在特定情況下神秘地破裂。

管道有時的確很有用,但它通過連接標準輸入輸出的方式進行進程間通訊,這個機制限制了它的應用。首先,信息只能單向流動。進程無法通過管道進行雙向通訊。其次,管道不支持任何形式的抽像。發送方和接收方只能使用字符流傳輸信息。比字符稍微複雜一點的對象是不能通過管道直接傳輸的,必須串行化為字符流以後才成,當然接收方必須對得到的字符流進行重新組裝。這意味著你無法傳輸一個對像以及用於建立這個對象的定義代碼。你無法傳輸指針到另一個進程的地址空間。你無法傳輸文件句柄或socket句柄或文件權限屬性。

冒著被罵做自以為是的風險,我們認為正確的模型應該是過程調用(本地的或是遠程的),用以傳遞第一類結構(first-class structures)(這是C語言從一開始就支持的)和函數組合(functional composition)。

管道對簡單任務是不錯的,比如文本流處理,但用它來建立健壯軟件就顯得有些捉襟見肘了。例如,早期關於管道的一篇論文中說明了如何使用管道把一些小程序組合在一起來構成一個拼寫檢查程序。這是體現簡單性的經典之作,但如果真的用來檢查拼寫錯誤就再糟糕沒有了。

管道在shell腳本中有經常能露一小手。程序員用它實現一些簡單而脆弱的解決方案。這是因為管道使得兩個程序之間產生了倚賴關係,如果你修改了一個程序的輸出格式,就必須同時修改另一個程序的輸入處理。

大多數程序是一步步建立起來的:首先制定程序的需求規範,然後程序的內部逐漸成型,最後寫一個輸出處理函數。管道則不把這一套放在眼裡:只要有人把一個半生不熟的程序放到了管道中,其輸出格式就定死了,不管是多麼不一致,不標準和低效,你都只能認命了。

管道不是程序間通訊的唯一選擇。Macintosh就沒有管道,我們最喜歡的Unix宣傳手冊是這樣寫的:

但是,Macintosh採用的則是截然相反的一種模型。系統不和字符流打交道。數據文件具有更高的層次,總是和特定的程序相關的。你什麼時候把一個 Mac程序的輸出傳給另一個過?(如果能找到管道符都算你運氣)程序自成一體,你必須徹底明白自己在幹嘛呢。你無法把MacFoo和MacBar搞到一起。-— 摘自 《Unix生活》 Libes和ressler著

是呀,這些可憐的Mac用戶。如果無法把字符流通過管道四處亂傳,他們怎麼能在文檔中插入繪畫程序製作的圖片?怎麼能插入一個表格文檔?怎麼能把這個東拼西湊成的用電子郵件發出去?接到以後又怎麼能無縫地對它進行瀏覽和編輯,再回復回去?沒有管道,我們不能想像這一切在過去的十年中是如何被 Macintosh做到的。

上次你的Unix工作站和Macintosh一樣有用是什麼時候?上次你能在它上面跑不同公司(甚至是同一公司的不同部門)的軟件是什麼時候?更不用說這些軟件能互相通信。如果Unix真做到了這一點,那是因為Mac軟件開發商拼了老命把他們的軟件移植到了Unix上,寄希望於讓Unix看起來像Mac一些。

Unix和Macintosh操作系統的根本區別是,Unix是為取悅程序員而設計的,而Mac是為了取悅用戶。(Windows一門心思想取悅的則是會計,不過這有些跑題了)。

研究表明管道和重定向是難於使用的,不是因為想法本身,而是由於其隨意和不直觀的限制。Unix自己的文檔早就指明了只有Unix死黨才能體會管道的妙處。

日期: thu, 31 Jan 91 14:29:42 EST
發信人: Jim Davis <jrd@media-lib.media.mit.edu>;
收信人: UNIX-HATERS
主題: Expertise (專業知識)

今天早上我讀到《人機接口雜誌》上的一篇文章《計算機操作系統專業知識》,是Stephanie M. Doane和其他兩位作者寫的。猜猜他們研究的是什麼操作系統?Doane對Unix新手、中手和專家的知識和表現進行了研究,下面是一些摘要:

「只有專家能夠使用Unix特有的一些功能(例如管道和重定向)來構造命令組合」

換句話說,Unix的每個新功能(除了那些從其他系統上生搬硬套過來的)都是如此怪異,以至於必須經過多年同樣怪異的學習和實踐才能掌握。

「這個發現有些出乎意料,因為這些正是Unix的基礎功能,而且這些功能是所有初級課程都會涉及的」

她還引用了S. W. Draper的一些文章,Draper相信:

「世上根本沒有什麼Unix專家,如果專家指的是這樣一些人,他們窮盡了某專業的所有知識,無需再學習什麼了。」

這一點我不能苟同。在學習Unix各種荒謬技術的征途上,已經有無數人被「窮盡」了。

有些程序甚至吃飽了撐的,把管道和文件重定向區別對待了:

發信人: Leigh L. Klotz <klotz@adoc.xerox.com>;
收信人: UNIX-HATERS
主題: | vs. < (|對<)
日期: Thu, 8 Oct 1992 11:37:14 PDT

collard% xtpanel -file xtpanel.out < .login
unmatched braces
unmatched braces
unmatched braces
3 unmatched right braces present

collard% cat .login | xtpanel -file xtpanel.out
collard%

你自己琢磨琢磨吧。

Find

Unix最為恐怖的是,不管你被它開過多少次瓢,你總是沒法失去知覺。它就這麼開來開去,沒完沒了。

——Patrick Sobalvarro

在一個龐大的文件系統中遺失個把文件是常有的事(想像一下大海撈針)。現在由於更大更便宜的磁盤的出現,PC和Apple用戶也遇到了這樣的問題。為了解決這個問題,系統往往提供一個搜索程序,根據各種條件(比如文件名稱,類型,創建時間等等)進行文件搜索。Apple Macintosh和微軟Windows都提供強大、方便、穩定的文件搜索程序。這些搜索程序的設計中考慮到了用戶習慣和現代網絡。Unix的搜索程序 find考慮的則不是用戶,而是cpio,一個Unix備份工具。Find沒能預見到網絡的存在和文件系統的新功能(如符號鏈接),即使是經歷了反覆修改,它還是無法很好工作。於是,儘管它對於遺失文件的用戶意義重大,find還是不能穩定、正常的工作。

Unix的作者們努力是find跟上系統其他部分的發展,但這並不容易。今天的find有各種特殊的選項用於處理NFS文件系統,符號鏈接,執行程序,交互式地執行程序,甚至直接使用cpio或cpio-c格式對找到的文件進行歸檔。Sun公司修改了find,添加了一個後台程序建立系統上每個文件的索引數據庫,由於一些奇怪的理由,當你不加任何參數執行"find filename"時,這個數據庫被用於進行搜索,(夠安全的,是吧?) 即使有個這麼多修修補補,find還是不能正常工作。

例如,csh見到符號鏈接會順著走下去,但find不會:csh是伯克利(符號鏈接的發源地)的傢伙們寫的,可是find是從AT&T的原始時代開始就有了。就這樣,東西方的文化差異激烈地碰撞了,造成了巨大的混亂:

日期: Thu, 28 Jun 1990 18:14 EDT
發信人: pgs@crl.dec.com
主題: more things to hate about Unix (更多恨的理由,就在Unix)
收信人: UNIX-HATERS

這個是我的最愛。我在一個目錄下工作,想用find去找另一個目錄裡的文件,我是這麼做的:

po>; pwd
/ath/u1/pgs
po>; find ~halstead -name "*.trace" -print
po>;

看來沒有找到。不過別忙,看看這個:

po>; cd ~halsead
po>; find . -name "*.trace" -print
../learnX/fib-3.trace
../learnX/p20xp20.trace
../learnX/fib-3i.trace
../learnX/fib-5.trace
../learnX/p10xp10.trace
po>;

嘿!文件就在那裡呀!下次如果你想找一個文件,記住隨機到各個目錄下轉轉,說不定你要的文件就藏在那裡呢。Unix這個廢物。

可憐的Halstead同志的/etc/passwd記錄一定是使用了符號鏈接去指向了真正的目錄,所以有的命令工作,有的不工作。

為什麼不改改find,也讓它順著符號鏈接呢?這是因為任何一個指向高一級目錄的符號鏈接都會把find引入死循環。要處理這種情況需要精心的設計和小心的實現,以保證系統不會重複搜索同一個目錄。Unix採用了最簡單的做法:索性不處理符號鏈接,讓用戶自己去看著辦吧。

聯網系統變得越來越複雜,問題也越來越難以解決了:

日期: Wed, 2 Jan 1991 16:14:27 PST
發信人: Ken Harrenstien <klh@nisc.sri.com>;
主題: Why find doesn't find anything (為什麼find什麼也找不到?)
收信人: UNIX-HATERS

我剛剛發現為什麼"find"不再工作了。

儘管"find"的語法非常噁心怪異,我還在勉強用它,以免幾小時泡在在迷宮似的文件目錄中去尋找文件。

在這個有NFS和符號鏈接存在的勇敢新世界裡,"find"沒用了。我們這裡的所謂文件系統是由眾多文件服務器和符號鏈接組成的一團亂麻,"find"哪個也不想去處理,甚至連選項也不提供... 結果是大量的搜索路徑被無聲無息地忽略了。我注意到了這個,是在一個很大的目錄下搜索時結果一無所獲,最後發現是因為那個目錄是個符號鏈接。

我不想自己去檢查每一個交給find的搜索目錄——這他媽應該是find的工作。我不想去每次這類情況發生時都要去調查一下系統軟件。我不想浪費時間來和SUN或者整個Unix黨徒們做鬥爭。我不想用Unix。恨,恨,恨,恨,恨,恨,恨,恨。

——Ken (感覺好些了,可還是有點惱)

如果想寫個複雜一點的shell腳本對找到的文件進行處理,結果往往會很奇怪。這是shell傳遞參數方式所產生的悲慘後果。

日期: Sat, 12 Dec 92 01:15:52 PST
發信人: Jamie Zawinski <jwz@lucid.com>;
主題: Q: what's the opposite of 'find?' A: 'lose'
(問題:'find'的反義詞是什麼? 答案:丟失)
收信人: UNIX-HATERS

我想找出一個目錄下的所有的沒有對應.elc文件存在的.el文件。這應該不太難,我用的是find.

不過我錯了。

我先是這麼幹的:

% find . -name '*.el' -exec 'test -f {}c'
find: incomplete statement

噢,我記起來了,它需要個分號。

% find . -name '*.el' -exec 'test -f {}c'\;
find: Can't execute test -f {}c:
No such file or directory

真有你的,竟然沒去解析這個命令。

% find . -name '*.el' -exec test -f {}c \;

咦,似乎什麼也沒做...

% find . -name '*.el' -exec echo test -f {}c \;
test -f c
test -f c
test -f c
test -f c
....

明白了。是shell把大括號給展開了。

% find . -name '*.el' -exec test -f '{}'c \;
test -f {}c
test -f {}c
test -f {}c
test -f {}c

嗯?也許我記錯了,{}並不是find使用的那個「替換成這個文件名」的符號。真的麼?...

% find . -name '*.el' \
-exec test -f '{}' c \;
test -f ./bytecomp/bytecomp-runtime.el c
test -f ./bytecomp/disass.el c
test -f ./bytecomp/bytecomp.el c
test -f ./bytecomp/byte-optimize.el c
....

喔,原來如此。下面該怎麼辦呢?我想,我似乎可以試試"sed..."

可我忘記了一個深刻的哲理:「當遇到一個Unix問題的時候,有的人會想『我懂,我可以試試sed.』這下他們有兩個問題去對付了。」

試驗了五次,閱讀了sed手冊兩遍,我得到了這個:

% echo foo.el | sed 's/$/c/'

於是:

% find . -name '*.el' \
-exec echo test -f `echo '{}' \
| sed 's/$/c'` \;
test -f c
test -f c
test -f c
....

OK, 看來只能去試試所有shell引用的排列組合了,總會有一款和我意吧?

% find . -name '*.el' \
-exec echo test -f "`echo '{}' \
| sed 's/$/c'`" \;
Variable syntax.
% find . -name '*.el' \
-exec echo test -f '`echo "{}" \
| sed "s/$/c"`' \;
test -f `echo "{}" | sed "s/$/c"`
test -f `echo "{}" | sed "s/$/c"`
test -f `echo "{}" | sed "s/$/c"`
....

嗨,最後一個似乎有戲。我只需要這麼幹一下:

% find . -name '*.el' \
-exec echo test -f '`echo {} \
| sed "s/$/c"`' \;
test -f `echo {} | sed "s/$/c"`
test -f `echo {} | sed "s/$/c"`
test -f `echo {} | sed "s/$/c"`
....

別急,這是我想要的,可是你為什麼不把{}替換成文件名呢?你再仔細瞅瞅,{}兩邊不是有空格麼?你究竟想要什麼?

哦,等等。那個反單引號間的引用被當成了一個元素。

或許我能用sed把這個反單引號過濾掉。嗯,沒戲。

於是我用了半分鐘去想如何能運行"-exec sh -c..."之類的東西,終於出現了曙光,寫了一段emcas-lisp代碼去做這件事。這不困難,挺快的,而且工作了。

我真高興。我以為一切都過去了。

今天早上我洗澡的時候突然想到了另一種做法。我試了一次又一次,深深墜入了她的情網,意亂情迷,無法自拔。醉了。只有罕諾塔的Scribe實現曾給過我這樣的快感。我僅試了12次就找到了解法。對於每個遍歷到的文件它只產生兩個進程。這才是Unix之道!

% find . -name '*.el' -print \
| sed 's/^/FOO-/'|\
sed 's/$/; if [ ! -f ${FOO}c]; then \
echo \ $FOO; fi/' | sh

BWAAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH!!!!

—Jamie

OK, 在下水道裡玩捉迷藏挺有意思的吧?第8章就在歡聲笑語中這麼結束了。下回書我們就要開始編程了,還記得小時候那個可愛迷人的護士阿姨是怎麼對你說的麼?

「牛牛別怕,不疼的。」

[好玩]Unix痛恨者手冊--ZZ

第九章 編程

「牛牛別怕,不疼的。」

別惹Unix,它弱不禁風,動不動就吐核(core dump)

——無名氏

如果你是通過在Unix上寫C代碼而學會的編程,那麼可能會覺得這一章有些彆扭。不幸的是,Unix如此廣泛地被應用到了科研教育領域,很少有學生能意識到Unix的許多設計並不是嚴瑾合理的。

例如,聽了我們關於有許多語言和環境比C/Unix要好的說法後,一個Unix愛好者是這麼為Unix和C辯護的:

日期: 1991 Nov 9
發信人: tmb@ai.mit.edu (Thomas M. Breuel)

Scheme, Smalltalk和Common Lisp這些語言確實提供了強大的編程環境。但是Unix內核,shell和C語言則針對的是更為廣泛的問題空間,而這些問題不是上面那些語言所擅長的(有的根本就無法處理)。

這些問題空間包括內存管理和局部性(locality)(在進程的產生和終止中實現),、持續性(persistency)(使用文件存儲數據結構),並行性(parallelism)(通過管道,進程和進程通訊機制來實現),保護和恢復(通過獨立的地址空間實現),以及可直觀讀取的數據表現方式(使用文本文件實現)。從實用的角度來看,Unix能很好地處理這些問題。

Thomas Breuel誇獎Unix能夠解決複雜的計算機科學問題。幸運的是,這不是其他科學領域用來解決問題的方法。

日期: Tue, 12 Nov 91 11:36:04 -0500
發信人: markf@altdorf.ai.mit.edu
收信人: UNIX-HATERS
主題: Random Unix similes (隨機的Unix笑臉)

通過控制進程的產生與終止來進行內存管理,這就如同通過控制人的生死來對付疾病——這忽視了真正問題。

通過Unix文件獲得持續性就如同把你所有的衣服仍進衣櫃,幻想著能從裡面找到需要的衣服(不幸的是,我正是這麼去做的)。

通過管道,進程和進程通訊機制來實現並行化?Unix進程的代價是如此之高,以至於並行化得不償失。就像是鼓勵員工多生孩子,以解決公司人力資源短缺問題。

不錯,Unix當然可以處理文本。他還能處理文本。嗯,還有,我有沒有提到過Unix能夠很好地處理文本?

——Mark

蔚為壯觀的Unix編程環境

Unix狂熱分子們總在宣揚Unix的所謂「編程環境」。他們說Unix提供了豐富的工具,能夠使得編程工作更為容易。這是Kernighan和Mashey在《Unix編程環境》一文中的說法:

Unix環境最能提高編程效率,這歸功於眾多的又小又有用的程序——工具,這些工具為日常的編程工作提供幫助。下面列舉的這些程序被認為是其中最為有用的。我們在下文中將以他們為例說明其他觀點。

wc files —— 統計文件中的行數,字數和字符數。
pr files —— 打印文件,支持標題和多欄打印。
lpr files —— 打印文件
grep pattern files —— 找到符合某種模式的文件行。

許多程序員的工作就是用它們和一些其他相關程序完成的。例如:

wc *.c

用於對所有C源代碼文件進行代碼量統計;

grep goto *.c

用於找到所有的goto語句。

這些就是「最為有用的」?!?!

有道理。這就是程序員的日常工作。事實上,今天我就用了不少時間來統計我的C代碼量,以至於沒有多少時間去做其他事情。等一下,我想我還得再數一遍。

同一期《IEEE計算機》上還有一篇文章,是Warren Teitelman和Larry Masinter寫的《Interlisp編程環境》.Interlisp是個極為複雜的編程環境。1981年Interlisp就有了Unix程序員到了1984還在夢想的工具。

Interlisp環境的設計者們使用的是完全不同的方法。他們決定開發一個複雜的工具,需要花不少時間來掌握,好處是一旦學會了,極大地提高編程效率。聽上去有些道理。

悲哀的是,今天很少有程序員能體會使用這類環境的感覺了。

在柏拉圖的洞穴裡編程

我總有一種感覺,計算機語言設計和工具開發的目標應該是提高編程效率而不是降低。

——comp.lang.c++上的一個貼子

計算機以外的其他產業早就體會到了自動化的意義。當人們走進快餐點,他們需要的是一致標準的東西,而不是什麼法國大菜。大規模地提供一致的一般食物,這比小批量的精耕細作要賺錢得多。

——netnews上一個技術人員的回復

Unix不是世界上最好的軟件環境——它甚至不是一個好的環境。Unix編程工具又簡陋又難用;Unix調試器和PC上的沒法比;解析器(interpreters)仍然是富人的玩具;修改日誌(change log)和審記(audit trail)總是想起來才去做。可Unix仍然被當成程序員的夢。也許它只能讓程序員夢到了效率的提高,而不是真的提高效率。

Unix程序員有點像數學家。你能從他們身上觀察到一個神秘現象,我們稱之為「空頭編程」(Programming by Implication)。一次我們和一個Unix程序員聊天,談到需要這樣一個工具,能夠回答諸如「函數foo被誰調用過?」或者「那個函數改變過全局變量bar」之類的問題。他也認為這個工具會很有用,提議到,「你們可以自己寫一個。」

公平地說,他之所以只是說「你們可以自己寫一個」而不是真正寫一個,這是因為C語言的一些特性和Unix「編程環境」的強強聯手,使得寫這樣的程序難於上青天。

使用yacc進行解析(parsing with yacc)

"Yacc"就是我用過yacc(1)之後想喊的。

——匿名

"YACC"是再一個編譯編譯器的編譯器(Yet Another Compiler Compiler)的意思。它接受與上下文無關(context-free)的語法,構造用於解析的下推自動機(pushdown automaton)。運行這個自動機,就得到了一個特定語言的解析器。這一理論是很成熟的,因為以前計算機科學的一個重要課題就是如何減少編寫編譯器的時間。

這個方法有個小問題:許多語言的語法不是與上下文無關的。這樣yacc的使用者不得不在每一個狀態轉換點上加上相關代碼,以處理和上下文有關的部分(類型檢查一般就是這麼處理的)。許多C編譯器使用的都是yacc生成的解析器;GCC 2.1的yacc語法有1650行之多 (如果不用yacc,GCC應該能成為自由軟件基金會不錯的作品)。由yacc生成的代碼就更多了。

有些編程語言的語法比較容易解析。比如,Lisp能夠用一個遞歸下降解析器進行解析。「遞歸下降」是一個計算機術語,含義是「喝杯可樂的功夫就能實現」。作為試驗,我們寫了一個Lisp遞歸下降解析器,只用了250行C代碼。如果是用Lisp寫的,那麼一頁紙也用不了。

在上面提到的那個計算機科學原始時代,這本書的編輯還沒有生出來呢。計算機房是恐龍的天下,「真正的人」都在用儀表盤上的開關來編程。今天,社會學家和歷史工作者想破腦袋也無法理解為什麼理智的程序員卻設計、實現和傳播了如此難解析的語言。也許他們那時候極需一個困難的研究項目,設計一個難於解析的語言似乎是個不錯的課題。

一直想知道他們在那個時代吃的是什麼藥。

上面提到的那個工具類似於一個C編譯器的前端。C編譯器前端是個極其複雜的東西,這是C的複雜語法和yacc的使用造成的。沒有人真正動手去寫一個這樣的工具,這還有什麼奇怪的麼?

死硬的Unix分子會說你不需要這麼一個程序,因為有grep就足夠了。而且,你還能在shell管道中使用grep。有一天,我們想找出BSD內核源碼中所有使用min函數的地方。這是其中一個結果:

% grep min netinet/ip_icmp.c
icmplen = oiplen + min(8, oip->;ip_len);
* that not corrupted and of at least minimum length.
* If the incoming packet was addressed directly to us,
* to the incoming interface.
* Retrieve any source routing from the incoming packet;
%

挺不錯的吧,grep找到了所有的min函數調用,而且還不止這些。

「不知道怎麼做愛。我撤。」("Don't know how to make love. Stop.")

理想的編程工具應該是這樣的,它能讓簡單的問題保持簡單,讓複雜的問題有解決的可能。不幸的是,許多Unix工具過分追求通用性,而忽視了簡潔。

Make就是這樣一個典型。從抽像意義而言,make的輸入是一個倚賴關係的描述。倚賴圖上的每個節點都對應這一組命令,當節點過期時(由它所倚賴的節點來決定),這些命令會被執行。節點和文件相關,文件的修改時間決定了節點是否過期。下面是一個簡單的倚賴關係圖,也就是Makefile:

program: source1.o source2.o
cc -o program source1.o source2.o

source1.o: source1.c
cc -c source1.c

source2.o: source2.c
cc -c source2.c

這裡program, source1.o, source2.o, source1.c,source2.c就是關係圖上的節點。節點program倚賴於source1.o和source2.o。

如果source1.o或source2.o比program要新,make便會運行命令cc -o program source1.o source2.o重新生成program。當然,如果修改了source1.c,那麼source1.o和program都會過時,所以make會重新進行編譯和鏈接。

儘管make的模型很通用,可惜設計者從沒有考慮過簡單性。不過,許多Unix新手都能體會到make能多麼簡單地「鑽」(screw)了他們。

繼續我們上面的那個例子,假定有個程序員Dennis想調試source1.c,於是要編譯使用調試選項。他修改了一下Makefile:

program: source1.o source2.o
cc -o program source1.o source2.o

# I'm debugging source1.c
source1.o: source1.c
cc -c source1.c
source2.o: source2.c
cc -c source2.c

"#"打頭的那行是註釋,會被make忽略。可憐的Dennis運行了一下make,這是它得到的:

Make: Makefile: Must be a speparator on line 4.
Stop

make歇菜了。Dennis盯著Makefile看了有好幾分鐘,又看了幾小時,還是不明白哪兒出錯了。他覺得是註釋行的問題,可不是很肯定。

毛病出在當他加入註釋行時,他不小心在第二行開始的製表符(tab)前敲入了一個空格。製表符是Makefile語法的一個重要部分。所有的命令行(例子中cc開始的行)必須以製表符打頭。這就是Dennis的Makefile不工作的原因。

「那又怎樣?」你可能會說,「這有什麼不對的?」

它本身沒什麼不對。不過如果你想一下其他Unix編程工具的工作方式,就會覺得製表符語法就好像《地雷戰》裡的頭髮絲雷,看上去一馬平川,踩上去嗚呼哀哉。

你知道,製表符、空格符和換行符一般被統稱為「白字符」(whitespacecharacters)。「白字符」意味著「你可以放心大膽地忽略它」許多程序正是這麼做的,對空格和製表符一視同仁。就make孤芳自賞桀驁不馴鶴立雞群冰清玉潔眾人皆醉唯我獨醒。於是我們這位Dennis兄弟恐怕只能給自己腦袋來一槍,告別這悲慘的Unix世界。

可憐的Dennis最終也沒有找到自己那個Makefile的毛病,他現在落魄到只好去給一個中西部州立大學維護sendmail配置文件。默哀三分鐘。

頭文件

C語言有個東西叫頭文件,裡面是一些說明信息,在編譯時被源文件使用。和Unix上的其他玩意一樣,如果只有一個兩個,可以工作得很好,多了就沒戲了。

要知道你的源文件該使用那個頭文件,這可不是件容易事。頭文件是C預處理器(preprocessor)根據#include指令(directive)加載的。這個指令有兩個用法:

#include <header1.h>;



#include "header2.h"

這兩種用法的區別和各個C預處理器的實現有關,也就是說,任何實現都可以大著膽子撒著歡兒由著性子亂來。

讓我們來看看Dennis的朋友Joey,Joey也是個Unix新手。Joey有個C程序foo.c,使用了foo.h中定義的一些數據結構, foo.c和foo.h放在了同一個目錄下。你可能已經知道"foo"是程序員常用的名字。Joey機器上的系統程序員也做了一個foo.h文件,並把它放到了缺省系統頭文件目錄/usr/include

倒霉蛋Joey編譯了foo.c,得到一堆語法錯誤。他迷惑不解,編譯器總在他定義的一些數據結構處報錯,可是這些數據結構在foo.h裡被定義的好好的呀。

你我估計能猜到Joey的問題在哪兒,他一定是這麼加載頭文件的:

#include <foo.h>;

而不是寫成:

#include "foo.h"

可Joey不知道這個。也可能他確實是用的引號方式,只是他的編譯器的查找方式有些特別。不管怎樣,Joey是被幹掉了,很無辜地被干了。

維護很多頭文件是件挺頭疼的事,不幸的是,如果你寫個有用點兒的C程序,這是不可避免的。頭文件一般 於定義數據結構,一個頭文件往往倚賴於其他一?頭文件。去把那些頭文件的倚賴關係整理一下,你這回可不愁沒事兒做了。

當然,編譯器會幫你的。如果你把倚賴關係搞錯了,編譯器會毫不留情地指出語法錯誤。記住,編譯器是個很忙很有身份的程序,它沒時間去區分未定義的數據結構和輸入錯誤的區別。事實上,即使你只是忘了敲個分號,C編譯器也會惱羞成怒,立馬撂挑子不幹了。

在編譯器社區,這一現象被稱為「錯誤雪崩」,或者按照編譯器自己的說法:「我完蛋了,起不來了。」 缺個分號會把解析器徹底搞暈,狂吐不止。這個解析器很可能是用yacc寫成的,yacc對語法正確的程序(很少見的一種情況)處理得很好,但要讓它生成健壯容錯自動恢復的解析器,這就有點兒勉為其難了。有經驗的C程序員都知道只有第一條解析錯誤才是有意義的。

工具程序和Man手冊

Unix工具是自成一體的;可以任意解釋命令行參數。這樣的自由有些煩人;別以為學會了一套命令行規則就一勞永逸了,你必須去讀每個命令的Man手冊,才能知道如何去使用。

知道有那麼多清楚明白的Man手冊供你參考,你一定很開心吧。

看一下下面這個例子。「摘要」一欄總結得挺不錯的,是不是?

LS(1) Unix程序員手冊 LS(1)

名稱
ls - 列出目錄內容

摘要
ls [ -acdfgilqrstu1ACLFR ] 名稱 ...

描述
對於每個目錄參數,ls列舉那個目錄的內容;對於每個文件參數,
ls 給出文件名以及要求的其他信息。缺省情況下,輸出將按照字
母順序排列。如果沒有參數,則列舉當前目錄的內容。如果有不只
一個參數,這些參數首先會被適當排序,但是文件參數總是會被排
在目錄參數前面。

ls有很多選項:

[ ... ]

BUGS
文件名中的換行符和製表符會被可打印字符

輸出設備會被假設有80列寬

輸出會根據輸出設備的不同而不同,比如"ls -s"的結果和"ls -s| lpr"的結果不一樣。這是不正確的,然而如果不這麼做,一些倚賴這個功能的舊有shell腳本就會完蛋。

如果你想玩個遊戲,不妨讀一下每個Man手冊的BUGS部分,然後想像一下每個bug是如何造成的。看一下這個shell的man手冊:

SH(1) Unix程序員手冊 SH(1)

名稱
sh, for, case, if, while, :, ., break, continue, cd,
eval, exec, exit, export, login, read, readonly, set,
shift, times, trap, umask, wait - 命令語言

摘要
ls [ -ceiknrstuvx ] [參數] ...

描述
Sh是一個命令程序語言,它執行來自終端或文件的命令。下面是各
個選項的說明。

[ ... ]

BUGS

如果把使用<<提供的標準的輸入提供給使用&運行起來的非同步的進程,shell會搞不清楚輸入文檔的名字。會生成一個垃圾文件/tmp/sh*,shell會抱怨找不到使用另外一個名字的文檔。

我們用了好幾分鐘也沒搞明白這個bug究竟是他媽什麼意思。一個Unix專家看過之後說:「我邊看邊撓腦袋,有寫這段BUGS的功夫,估計足夠這傢伙改掉這個吊玩意了。」

不幸的是,修改bug幾乎是不可能的,因為它會隨著每個新發佈的操作系統而捲土重來。在80年代早期,在這些bug還沒有被Unix信徒奉為神聖以前,一個BBN的程序員真的修改了伯克利make的這個製表符bug。這不是很難,也就是幾行代碼的事兒。

和所有責任感的公民一樣,BBN的駭客們把補丁發給了伯克利,希望能把它加入主Unix代碼中。一年過後,伯克利發佈了新版本的Unix,make的這個bug還是存在。BBN的駭客第二次做了修改,又把補丁交給了伯克利。

....然而伯克利的第三次發佈還是老樣子,BBN的程序員徹底失望了。他們沒有再提交補丁,而是把他們所有的Makefile中空格打頭的行替換成了製表符。畢竟BBN僱傭他們是來寫新程序的,而不是反覆修改同一個bug。

(據說,Stu Felman(make的作者)一開始就查覺到了這個問題,他沒有修改,因為那時已經有10個用戶開始用了。)

源碼就是文檔。哇~~ 牛逼!

如果我寫著不容易,那麼你理解起來就不應該容易。

—— 一個Unix程序員

我們在《文檔》一章裡提到Unix程序員認為操作系統的源代碼是最好的文檔。一個著名的Unix歷史學家曾經指出:「畢竟,操作系統自己也是靠讀源代碼來知道下一步該幹嘛的。」

可是通過閱讀源代碼來理解Unix,這就如同開著Ken Thompson的老爺車(對,就是閃著大紅問號的那輛)周遊世界。

Unix內核源碼(更準確的說,是ftp.uu.net上發佈的伯克利網絡磁帶2版的代碼)幾乎沒有註釋,充斥這大"段"沒有空行的代碼,goto 隨處可見,絞盡腦汁給妄圖讀懂它的人製造麻煩。有個駭客感歎到:「閱讀Unix代碼就好像走在伸手不見五指的巷子裡。我總是停下來摸摸口袋,腦子裡迴響著一個聲音『老天,我就要遭劫了。』」

當然,內核代碼有它自己的警報系統。四處散佈著這樣的小小註釋:

/* XXX */

意思是有什麼東西不太對勁兒。你應該知道哪兒出事兒了。

這絕不可能是bug,我的Makefile需要它!

BBN的程序員應該算是另類。大部分Unix程序員是不去修改bug的:他們沒有源代碼。即使修改了也於事無補。這就是為什麼Unix程序員遇到bug的第一個反應不是修了它,而是繞過它。

於是我們看到了悲慘的一幕:為什麼不一勞永逸地解決問題,而是一錯再錯?也許早期的Unix程序員是尼采「永恆輪迴」思想的信徒。

對於調試方法,存在著兩個截然不同的派別:一個是「外科手術派」,包括流行於早期ITS和Lisp系統,程序運行過程中始終有調試器參與,如果程序崩潰了,調試器(也就是所謂外科大夫)會對問題進行診斷醫治。

Unix是屬於更古老的「屍體解剖派」。Unix下如果一個程序崩潰了,會遺留下一個core文件,從各個方面看這都和屍體沒什麼兩樣。Unix調試器然後會找出死因。有趣的是,Unix程序常常和人一樣,死於本可治療的疾病、事故以及疏忽。

對付Core

如果你的程序吐核(core)了,你首先要做的是找到它。這不該太困難,因為core文件總是很大——4, 8, 甚至12兆。

core文件之所以這麼大,是因為它包括了所有用來調試的信息:堆棧,數據,代碼指針等等,無所不包,除了程序的動態狀態。如果你在調試一個網絡程序,在你的程序吐核的時候,已經為時太晚了;程序的網絡連接已經沒有了,更致命的一擊是,所有打開的文件現在都被關上了。

不幸的是,在Unix上只能如此。

例如,不能把調試器作為命令解析器,或者在內核發生異常時把控制交給調試器。如果想讓調試器在程序崩潰時進行接管,那你只能在調試器裡面運行所有程序(是的,有的Unix版本讓你用調試器接管一個運行中的進程,但是你手邊必須有一個還有符號的程序文件)。如果你想調試中斷代碼,你的調試器必須截獲每個中斷,然後把合適的中斷返回給程序。你能想像emacs裡每敲一鍵都發生3個進程切換(context switch)的感覺麼?顯然,例程調試(routine debugging)思想和Unix哲學是格格不入的。

日期: Wed, 2 Jan 91 07:42:04 PST
發信人: Michael Tiemann <cygint!tiemann@labrea.stanford.edu>;
收信人: UNIX-HATERS
主題: Debuggers (調試器)

想過Unix調試器為什麼這麼蹩腳麼?這是因為如果它想提供什麼功能,那一定會跟來一堆bug,如果有bug,它一定會吐核(dump core),如果它吐核,靠,你用來調試的那個core文件就會被覆蓋。如果能讓程序來控制如何吐核,何時吐核,以及吐在哪裡,那就太好了。

bug骨灰盒

和其他操作系統不同,Unix把bug供奉為標準操作。之所以那麼多Unix bugs得不到修正,這裡有個不可告人的原因——如果修正了,那麼已有的一些程序就會死逼了。然而,荒唐的是,Unix程序員在增加新功能時卻從來不去考慮向下兼容。

考慮到這些,Michael Tiemann給出了Unix調試器覆蓋core文件的10個理由:

日期: Thu, 17 Jan 91 10:28:11 PST
發信人: Michael Tiemann <tiemann@cygnus.com>;
收信人: UNIX-HATERS
主題: Unix Debuggers (Unix調試器)

David Letterman (美國著名晚間脫口秀主持人)的10個最佳理由是:

10. 這會破壞已有代碼。
9. 這需要修改文檔。
8. 太難實現了。
7. 這怎麼是調試器的活兒?為什麼不寫個「工具」做它?
6. 如果調試器吐了核,你應該丟開你自己的程序,開始調試調試器。
5. 太難理解了。
4. 哪兒有餅乾?
3. 為什麼非得現在做?
2. Unix也不是神仙。
1. 哪兒有問題?

Unix程序員總是打著「這會破壞已有代碼」的幌子,不願意修正bug。可這裡面還有內幕,修正bug不但會破壞已有代碼,還必須修改簡單完美的Unix 接口,而這正是Unix教眾們的命根子。至於這個接口是否工作,這並不重要。Unix教眾們不去提出更好的接口,也不去修正bug,而是齊聲高唱 「Unix接口好簡潔,好簡潔。Unix接口就是美,就是美!Unix無罪!Unix有理!」。

不幸的是,繞過bug是個很惡劣的行為,它使得錯誤成為了操作系統規範的一部分。你越是等,就越難以修正,因為越來越多的程序會盡力繞過bug,以至於沒有了bug反而活不了了。同理,修改操作系統接口帶來的影響更大,因為更多的程序必須根據這個正確的新接口進行修改。(這解釋了為什麼ls有那麼多的選項來完成幾乎一樣的工作)。

如果你把一隻青蛙仍到開水裡,它會馬上跳出來。它知道開水很燙。可是,如果你把青蛙放到冷水裡,再慢慢地加熱,青蛙感覺不到什麼,直到最後被燙死。

Unix接口已經開鍋了。以前,輸入/輸出的全部接口只包括open, close, read和write。網絡支持給Unix添了一大把柴禾。現在,至少有五種方法向一個文件句柄輸入數據:write, writev, send, sendto和sendmsg。每個都在內核中有不同的實現,這意味著有五倍的可能出現bug,有五種不同的性能結果需要考慮。讀文件也一樣(read, recv, recvfrom和recvmsg)。等死吧,青蛙們。

文件名擴展

Unix「所有程序自成一體」的規定有一個例外。Unix程序經常要處理一個或多個文件。Unix shells提供了命名一組文件的方法,shell會把這組文件展開,做為一個文件列表傳遞給各個命令。

例如,假設你的目錄下有文件A, B和C。如果象刪除所有這些文件,你可以運行rm *。shell會把"*"擴展成為"A B C",並把他們做為rm的參數傳遞給它。這個方法有不少問題,這在上一章已經提到過了。不過,你應該知道讓shell來擴展文件名不是偶然的:而是精心設計的結果。在Kernighan和Mashey發表的《Unix編程環境》一文中(IEEE計算機雜誌,1981年四月),他們指出:「把這個作為 shell的一個機制,這避免了各個程序的重複勞動,而且保證了為所有程序提供一致的輸入。」 (Unix的一個理想是讓任何人能夠運行任何shell。現在你沒法運行任何shell;你的shell必須提供文件名擴展)。

別忙。標準輸入/輸出庫(Unix所謂的stdio)不就能「為所有程序提供一致的輸入」麼?提供一個用於擴展文件名的庫函數不就成了?這些傢伙沒有聽說過鏈接庫麼?那些關於性能的說法也是無稽之談,因為他們無法提供任何的性能數據,他們甚至沒有說明「性能指標」是什麼。指的是開發一個小程序會快一些?還是指能高性能地把一個新手的所有文件一掃而光?

大多數情況下,讓shell進行文件名擴展也無所謂,因為這和程序自己擴展的結果沒什麼不同。可是,和Unix上的許多玩意一樣,它早晚會咬你一口,而且不輕。

假設你是個Unix新手,目錄下有兩個文件A.m和B.m。你習慣了MS-DOS,想把它們的名字換成A.c和B.c。嗯~~ 沒找到rename命令,不過mv命令似乎差不多。於是你執行mv *.m *.c。shell將這個命令擴展為 mv A.m B.m,你辛辛苦苦寫了幾小時的B.m就這麼被幹掉了。

再好好思考一下上面這個問題,你就會發現理論上你完全不可能提供一個和MS-DOS "rename"一樣的功能。對於軟件工具,就扯這麼多吧。

健壯性,或者說「所有輸入行必須小於80個字符」

1990年11月份的《ACM通訊》上登了Miller Fredriksen等人寫的一篇精采文章,題目是《Unix工具的穩定性的經驗性研究》。他們使用一些隨機數據作為Unix工具的輸入,發現有24- 33%(不同的Unix發佈結果有所不同)的工具崩潰了。有時候甚至整個系統都完蛋了。

文章是以一個笑話開頭的。其中一位作者曾使用一個極差的電話連接工作,發現許多工具都垮掉了。於是他決定針對這一現象進行更系統的調查研究。

許多bug都可以歸因於C語言的陳規陋習。事實上,Unix的許多內在腦損傷都是C語言造成的。Unix的核心以及所有的工具程序都是用C語言寫的。著名語言學家Benjamin Whorf說過:語言決定思想。Unix有深深的C烙印。C語言使得程序員根本無法想像能寫出健壯的程序。

C語言是極小的。它被設計成能在各種硬件上快速地進行編譯,所以它有著和硬件類似的結構。

Unix誕生之初,使用高級語言編寫操作系統是個革命性的想法。現在則應該考慮使用一種有錯誤檢查的語言了。

C是個最為底層的語言,誕生於硬件更為底層的時代。PDP-11沒有的,C語言也不會有。過去幾十年的編程語言研究表明,語言中加入錯誤處理,自動內存管理和抽像數據類型等功能,會使得開發出的程序更為健壯可靠。你在C裡面找不到這些東西。C語言太流行了,沒人去考慮給它增加諸如數據標記或硬件垃圾回收支持等功能。即使硬件提供了垃圾回收功能,也只是多費了一些硅片罷了,因為許多C語言編寫的程序根本無法使用它。

回想一下,C是無法處理整數溢出的。解決方法是使用超過問題需要的整數大小,希望這個大小在你有生之年足夠用。

C也沒有真正意義上的數組,它有個像是數組的東西,實際不過是一個指向一塊內存的指針。數組定位表達式(array[index])不過是表達式 (*(array+index))的簡寫版。所以你甚至可以說index[array],這和表達式(*(array+index))是一個意思。聰明吧?在字符處理時經常能見到這個用法。數組變量和指針變量經常可以互換。

舉個例子,假設你有:

char *str = "bugy";

於是下面的這些語句都是一樣的:

0[str] == 'b'
*(str+1) == 'u'
*(2+str) == 'g'
str[3] == 'y'

C語言夠偉大的吧?

這個做法的問題是C根本不做任何自動數組邊界檢查。為什麼該C去做呢?數組在C裡只是個指針而已,你可以把指針指向內存的任何地方,是不是?不過,一般你不想在內存裡亂寫亂畫,特別在是一些關鍵的地方,比如程序的堆棧。

這把我們引到了Miller的文章裡提到的一類bug。許多程序是在讀取輸入到堆棧上的一塊字符緩衝區時崩潰的。許多C程序是這麼做的;下面的C程序把一行輸入讀到堆棧上的一個數組裡,然後調用do_it函數進行處理。

a_function()
{
char c, buff[80];
int i = 0;

while ((c = getchar()) != '\n')
buff[i++] = c;
buff[i] = '\000';
do_it(buff);
}

這類代碼把Unix搞得臭不可聞。知道為什麼緩衝區被定為80個字符麼?這是因為許多Unix文件每行最多有80個字符。知道為什麼沒有邊界檢查,也沒有文件尾檢查麼?這是因為這個程序員喜歡把c = getchar()這樣的賦值語句嵌入到while循環中。信不信,有些人還推崇C的這種縮簡寫法,管他媽什麼可讀性可維護性。最後,調用do_it(),數組搖身一變成了指針,作為第一個參數傳了進去。

作為練習:如果在一行當中到達了文件尾,這個程序的結果是什麼?

當Unix用戶查覺到這個內置的限制後,他們想到的不是去修正這個bug,而是想方設法躲過它。比如,Unix的磁帶備份工具(tape archiver)tar不能處理超過100個字符的路徑名(包括目錄)。解決方法是:不要備份目錄到磁帶,或者使用dump。更好的辦法是:不要建立太深的目錄,這樣文件的絕對路徑就不會超過100個字符。

2038年1月18日上午10點14分07秒,Unix馬虎編程將在這一刻上演精采的一幕,那時Unix的32位timeval將耗盡...

再回到我們前面那個例子,假設輸入行有85個字符。這個函數毫無問題地接受了這個輸入,可問題是最後那5個字符會被放到哪裡呢?答案是它們會佔據任何排放在數組後面的5個字節。之前那裡放著的是什麼呢?

c和i這兩個變量可能會被分配在字符數組之後,所以有可能會被85字符長的輸入衝垮。如果輸入了850個字符呢?則可能會毀掉堆棧上的重要的C運行環境系統信息,比如返回地址等。毀掉這些信息的最好結果是程序可能崩潰。

我們說「可能崩潰」是因為程序的編寫者從沒想到過你竟能毀掉堆棧。想像一下我們的這個程序讀入了很長的一行,約有2,000個字符,這行字符被用來覆蓋堆棧上的返回地址以及其他環境信息,它將調用2,000個字符裡埋藏的一段代碼。這段代碼可能會做一些很有用的事情,比如執行(exec)出一個 shell,運行一些命令。

Robert T. Morris的著名Unix蠕蟲病就是使用了這個機制(以及其他一些技巧)黑進Unix主機的。我不知道其他人為什麼還會這麼做,真的不知道,嘻嘻。

日期: Thu, 2 May 91 18:16:44 PDT
發信人: Jim McDonald <jlm%missoula@lucid.com>;
收信人: UNIX-HATERS
主題: how many fingers on your hands? (你共有幾根手指?)

:( 下面是給我的上司的一個報告:

一個用來更新Make文件的程序使用了一個指針,對它的訪問毀掉了一個存放倚賴關係的數組,這個倚賴關係被用來生成Makefile。直接後果是生成的錯誤Makefile不能用於編譯任何東西,沒有生成所需的對象文件(.o),所以編譯最終失敗了。一天的工作就這麼付之東流了,只是因為一個傻瓜認為10個頭文件足夠所有人使用了,然後對它進行了極其危險的優化以在1毫秒內生成所有的Make文件!

網絡化的壞處是,你沒法再闖進某人的辦公室裡把他的心給挖出來。

(關於堆棧溢出攻擊,可參考經典論文href=http://www.phrack.org/phrack/49/P49-14>;

Smashing The Stack For Fun And Profit --me)

異常處理

編寫健壯程序的最大挑戰是如何正確處理錯誤和其他異常。不幸的是,C幾乎沒有為此提供什麼幫助。今天在學校裡學會編程的人裡很少有誰知道異常是什麼。

異常是函數無法正常運行時所產生的一個狀態。異常經常發生在請求系統服務時,比如分配內存,打開文件等。由於C沒有提供異常處理支持,程序員必須自己在服務請求時加入異常處理代碼。

例如,下面是所有C語言課本中推薦的使用malloc()分配內存的方法:

struct bpt *another_function()
{
struct bpt *result;

result = malloc(sizeof(struct bpt));
if (result == 0) {
fprintf(stderr, "error: malloc: ???\n");

/* recover gracefully from the error */
[...]
return 0;
}
/* Do something interesting */
[...]
return result;
}

another_function函數分配了一個類型為bpt的結構,返回了一個指向這一結構的指針。這段代碼說明了如何分配內存給這個結構。因為C沒有顯式的異常處理支持,C程序員必須自己去做這件事(就是粗體的那些代碼)。

當然你可以不這麼幹。許多C程序員認為這是小事一樁,從來不做異常處理。他們的程序往往是這樣的:

struct bpt *another_function()
{
struct bpt *result = malloc(sizeof(struct bpt));

/* Do something interesting */
[...]
return result;
}

多麼簡單,多麼乾淨,大多數系統服務請求都會成功的,是不是?這樣的程序在大多數場合運行良好,直到它們被應用到複雜特殊的地方,往往就會神秘地失效。

Lisp的實現總是包括一個異常處理系統。異常條件包括OUT-OF-MEMORY這樣的名稱,程序員可以為特定的異常提供異常處理函數。這些處理函數在異常發生時被自動調用——程序員不需要介入,也不需要做特殊的檢查。適當地使用,可以讓程序更為健壯。

CLU這樣的編程語言也有內置的異常處理。每個函數定義都有一系列可以發出的異常條件。對異常的顯式支持可以幫助編譯器檢查那些未被處理的異常。CLU程序總是十分健壯,因為編譯器逼著CLU程序員去考慮異常處理問題。C程序是個什麼樣子呢:

日期: 16 dec 88 16:12:13 GMT
主題: Re: GNU Emacs
發信人: debra@alice.UUCP

<448@myab.se>; lars@myab.se (Lars Pensy)>;寫到:
... 所有的程序都應該檢查系統調用(如write)的返回結果,這非常重要。

同意,可不幸的是很少有程序在進行讀(read)寫(write)時這麼做。

Unix工具程序一般會檢查open系統調用的返回值,假設所有隨後的read,write和close總會成功。

原因很明顯:程序員很懶,不做錯誤處理程序會顯得更小更快。(這樣你的系統會有更優異的性能表現)。

這封信的作者繼續指出,由於大部分系統工具不對write()等系統調用的返回值進行檢查,系統管理員就必須保證文件系統時時刻刻都有足夠的空間。正是如此:許多Unix程序假設它們可以寫任何成功打開的文件,想寫多少就寫多少。

讀到這裡你可能會皺眉頭,"嗯~~」一下。最為可怕的是,就在《Unix工具的穩定性的經驗性研究》這篇文章的前幾頁,登載了一份報告,說明休斯頓外層空間中心的飛船控制實時數據採集系統是如何轉型為Unix系統的。"嗯~~」

捕捉bug是社會所不能接收的

不去檢查和報告bug,這會使製造商生產的系統顯得似乎更為健壯和強大。更重要的是,如果Unix系統報告每一個錯誤,那麼就根本不會有人去用它!這是活生生的現實。

日期: Thu, 11 Jan 90 09:07:05 PST
發信人: Daniel Weise <daniel@mojave.stanford.edu>;
收信人: UNIX-HATERS
主題: Now, isn't that clear? (現在明白了麼?)

惠普做了一些工作,這樣我們的惠普Unix系統能夠報告一些可能會影響它的網絡錯誤。這些惠普系統和SUN, MIPS, DEC工作站共享一個網絡。我們經常會發現其他機器所引發的問題,可是當我們通知給那些機器的主人時(因為這些系統不報告錯誤,他們不知道自己的機器有一半時間是用在重發數據包上了),他們往往反稱是我們這裡的問題,因為只有我們這裡報出了錯誤。

「兩國相爭,不斬來使」,不過在Unix世界裡,你最好別當信使。

修不了?重啟!

如果某個關鍵軟件不能適當處理錯誤的數據和操作條件,那麼系統管理員該如何是好呢?嗯~~,如果它能在一段時間裡正常工作,你就能通過不斷重啟它來湊合著運行。這個法子不是很可靠,也不具有擴展性,不過足夠讓Unix苟 硬寫 一陣子了。

下面就是這麼一個例子,說明如何在named程序不穩定的情況下提供郵件服務:

日期: 14 May 91 05:43:35 GMT
發信人: tytso@athena.mit.edu (Theodore Ts'o) (著名的Ted Ts'o? --me)
主題: Re: DNS performance metering: a wish list for bind 4.8.4
(DNS性能測試:bind 4.8.4的期待功能表)
收信人: comp.protocols.tcp-ip.domains

我們現在是這麼解決這個問題的:我寫了一個叫"ninit"的程序以非精靈(deamon)模式(nofork)運行named,然後等待它退出。當named退出時,ninit重新啟動一個新的named。另外,每隔五分鐘,ninit會醒來一次發給named一個SIGIOT信號,named接到這個信號後會包一些狀態信息寫入/usr/tmp/named.stats文件中。每隔60秒鐘,ninit會用本地named進行一次域名解析。如果短時間內沒有得到結果,它會殺掉named,重新啟動一個新的。

我們在MIT的名稱服務器上和我們的郵件網關(mailhub)上運行了這個程序。我們發現它很有用,能夠捕捉named的神秘死亡或僵死。這在我們的郵件網關上更是不可缺少,因為即使域名解析中斷一小會兒,我們的郵件隊列也會給撐炸了。

當然,這類辦法會引發這樣的問題:如果ninit有bug,那麼該怎麼辦呢?難道也要寫一個程序不斷重啟ninit麼?如果寫了,你又如何保證那個正常工作呢?

對於軟件錯誤的這種態度並不少見。下面這個man手冊最近出現在我桌上。我們還不能肯定這是不是個玩笑。BUGS部分很是發人深省,因為那裡列舉的bug是Unix程序員總也無法從代碼中剔除的:

NANNY(8) Unix程序員手冊 NANNY(8)

名稱
nanny - 奶媽,運行所有服務的服務

摘要
/etc/nanny [switch [argument]] [...switch [argument]]

描述
許多系統都為用戶提供各種服務(server)功能。不幸的是,這些服務經常不明不白地罷工,造成用戶無法獲得所需要的服務。Nanny(奶媽)的作用就是照看(babysit)好這些服務,避免關鍵服務的失效,而不需要系統管理員的隨時監視。

另外,許多服務使用日誌文件作為輸出。這些數據常會很討厭地充滿磁盤。可是,這些數據又是重要的跟蹤記錄,應該盡量保存。Nanny會定期把日誌數據重定向到新文件。這樣,日誌數據被化整為零,舊的日誌文件就能被任意轉移走,而不對服務構成影響。(現在這成了logrotate的任務 --me)

最後,nanny還提供了一些控制功能,使得系統管理員能夠對nanny以及它所照看的服務進行運行時操作。

選項
...

BUGS
有個服務在nanny中做分離fork(detaching fork)。nanny會錯誤地認為這個服務死掉了,不斷重啟它。

到目前為止,nanny還不能容忍配置文件的錯誤,如果配置文件的路徑不對或者內容有錯誤,nanny必死無疑。

不是所有的選項都被實現了。

Nanny倚賴系統提供的網絡功能進行進程間通訊。如果網絡代碼有錯誤,nanny將無法處理這些錯誤,可能僵死或是死循環。

對不穩定軟件經常重啟,這已經成了MIT雅典娜計劃(Project Athena)的日常工作,現在他們每星期天的凌晨4點都會重啟AFS(Andrew File System, 一種網絡文件系統)服務器。但願沒有人週末熬夜趕寫下週一要交的作業...

怎麼樣,Unix編程很有趣吧?驚險,刺激,痛並快樂!該回家了,休息一下,大麻沒勁了,有海洛英;C用膩了,我們還有C++!放心,離死不遠了。

第十章 C++
90年代的COBOL

問:"C"和"C++"的名字是怎麼來的?
答:這是他們的成績

——Jerry Leichter

再沒有比C++更能體現Unix「絕不給用戶好臉」的哲學思想的了。

面向對像編程可以追溯到60年代的Simula語言,在70年代的Smalltalk語言上得到極大發展。許多書會告訴你面向對像語言如何能提高編程效率,使代碼更健壯,和減少維護費用。不過你甭想在C++裡得到這些。

這是因為C++根本就沒理解面向對象的實質。非但沒有簡化什麼,反而增加了更多的複雜性。和Unix一樣,C++從沒被好好設計過,它從一個錯誤走向另一個錯誤,是件打滿補丁的破衣服。連自己的語法都沒有嚴格定義(沒一個語言敢這樣),所以你甚至無法知道一行代碼是不是合法。

把C++比做COBOL,其實是對COBOL的污辱。COBOL在那個時代的技術條件下,是做出了很不同凡響的貢獻的。如果有誰用C++做成過什麼事,那就算是很不同凡響了。幸運的是,很多不錯的程序員知道必須盡量避免C++的傷害,他們只用C,對那些荒唐費解的功能敬而遠之。通常,這意味著他們必須自己寫個非面向對象的工具,以獲得自己想要的功能。當然,他們的代碼會顯得極為怪異,失去兼容性,難於理解和重用。不過只要有一點兒C++的味道,就足夠說服頭頭批准他們的項目。

許多公司已經被多年遺留下來的混亂難懂的COBOL代碼搞得焦頭爛額了。那些轉而使用C++的公司剛剛意識到自己上了當。當然,這已經太晚了。軟件災難的種子已經播下了,澆水施肥,得到悉心照料,就等著十幾年後長成參天大樹了。等著瞧吧!

面向對象的彙編語言

C++沒有一絲一毫高層次語言的特性。為什麼這麼說?讓我們看看高層次語言應該具備那些特性:

優雅:在表示方式及其所表達的概念之間有著簡單易懂的關係
抽像:高層次語言的每個表達式只表示一個概念。概念能夠被獨立表達並能自由使用
強大:高層次語言的能夠對任何精確完整的程序行為進行提供直接了當的表述方式
高層次語言使程序員能夠採用問題空間的方式描述解決方案。高層次的程序很容易維護,因為它們的目的性(intent)十分明確。根據一行高層次程序代碼,現代編譯器能夠為各種平台生成高效的代碼,所以高層次程序的可移植性和可重用性自然會很強。

使用低層次語言則需要對考慮無數細節,其中大部分是和機器內部操作有關的東西,而不是要解決的問題本身。這不但造成代碼難於理解,而且很容易過時。現在幾乎每隔今年就要更新系統,過時的必須花費很高代價修改低層代碼或者徹底重寫。

對不起,你的內存洩漏了

高層次語言對於常見問題有內置解決方案。例如,眾所周知內存管理是產生錯誤最多的地方。在使用一個對像之前,你必須為它分配內存,適當進行初始化,小心跟蹤使用,並正確釋放。當然,每件事兒都異常乏味而且很容易出錯,極小的一個錯誤可能會導致災難性後果。定位和修改這類錯誤是臭名昭著的困難,因為它們對於配置或使用方式的變化極其敏感。

使用未分配內存的結構指針會造成程序崩潰。使用未正確初始化的結構也會使你的程序崩潰,不過不一定立刻完蛋。如果未能好好跟蹤結構的使用情況,則很可能釋放一塊還在使用中的內存。還是崩潰。最好再分配一些結構用來跟蹤那些結構對象。不過如果你太保守,不去釋放那些不很肯定未在使用的內存,那麼你可要小心了。內存中很快就會充斥著無用的對象,直到內存耗盡,程序崩潰。這就是恐怖的「內存洩漏」。

如果你的內存空間碎片太多,那該怎麼辦呢?解決辦法是通過移動對像對內存重新歸整,不過在C++裡沒戲——如果你忘了更新對象的所有引用(reference),那麼你就會搞亂程序,結果還是崩潰。

很多真正的高層次語言提供了解決辦法——那就是垃圾回收(garbage collector)。它記錄跟蹤所有的對象,如果對像用完了會加以回收,永遠不會出錯。如果你使用有垃圾回收功能的語言,會得到不少好處:

大量的bug立馬無影無蹤。是不是很爽呀?

代碼會變得更短小更易讀,因為它不必為內存管理的細節操心。

代碼更有可能在不同平台和不同配置下高效運行。

唉,C++用戶必須自己動手去揀垃圾。他們中的許多人被洗了腦子,認為這樣會比那些專家提供的垃圾回收工具更為高效,如果要建立磁盤文件,他們估計不會使用文件名,而更願意和磁道扇區打交道。手動回收垃圾可能會對一兩中配置顯得更高效些,不過你當然不會這麼去使用字處理軟件。

你不必相信我們這裡說的。可以去讀一下B. Zorn的《保守垃圾回收的代價測量》(科羅拉多大學Boulder分校,技術報告CU-CS-573-92),文中對程序員用C手動優化的垃圾回收技術和標準垃圾回收器進行了性能比較,結果表明C程序員自己寫的垃圾回收器性能要差一些。

OK,假設你是個幡然醒悟的C++程序員,想使用垃圾回收。你並不孤單,很多人認為這是個好主意,決定寫一個。老天爺,猜猜怎麼著?你會發現根本沒法在C ++中提供其他語言內置的那樣好的垃圾回收。其中一個原因是,(驚訝!)C++裡的對象在編譯後和運行時就不再是對象了。它們只是一塊十六進制的爛泥巴。沒有動態類型信息——垃圾回收器(還有調試器)沒法知道任何一塊內存裡的對象究竟是什麼,類型是什麼,以及是否有人此時正在使用它。

另一個原因是,即使你能寫個垃圾回收器,如果你用了其他未使用垃圾回收功能的代碼,你還是會被幹掉。因為C++沒有標準的垃圾回收器,而且很有可能永遠也不會有。假設我寫了一個使用了我的垃圾回收功能的數據庫程序,你寫了一個使用你自己的垃圾回收功能的窗口系統。但你關閉一個裝有我的數據記錄的窗口,你的窗口不會去通知我的數據記錄,告訴它已經沒有人引用它了。這個對象將不會被釋放,直到內存耗盡——內存洩漏,老朋友又見面了。

學起來困難?這就對了

C++和彙編語言很相像——難學難用,要想學好用好就更難了。

日期: Mon, 8 Apr 91 11:29:56 PDT
發信人: Daniel Weise <daniel@mojave.stanford.edu>;
收信人: UNIX-HATERS
主題: From their cradle to our grave (從他們的搖籃到我們的墳墓)

造成Unix程序如此脆弱的一個原因是C程序員從啟蒙時期就是這麼被教育的。例如,Stroustrup(C++之父)的《C++編程語言》第一個完整程序(就是那個300K大小的"hello world"程序之後的那個)是一個英制/公制轉換程序。用戶用結尾"i"表示英制輸入,用結尾"c"表示公制輸入。下面是這個程序的概要,是用真正的Unix/C風格寫的:

#include <stream.h>;

main() {
[變量聲明]
cin >;>; x >;>; ch;
;; A design abortion.
;; 讀入x,然後讀入ch。

if (ch == 'i') [handle "i" case]
else if (ch == 'c') [handle "c" case]
else in = cm = 0;
;; 好樣的,決不報告錯誤。
;; 隨便做點兒什麼就成了。

[進行轉換]

往後翻13頁(第31頁),給了一個索引範圍從n到m的數組(而不是從0到m)的實現例子。如果程序員使用了超出範圍的索引,這個程序只是笑嬉嬉地返回數組的第一個元素。Unix的終極腦死亡。

語法的吐根糖漿(Syrup of Ipecac,一種毒藥)

語法糖蜜(Syntactic sugar)是分號癌症的罪魁禍首。

——Alan Perlis

在使用C編程語言中所能遇到的所有語法錯誤幾乎都能被C++接受,成功編譯。不幸的是,這些語法錯誤並不總能生成正確的代碼,這是因為人不是完美的,他們總是敲錯鍵盤。C一般總能在編譯是發現這些錯誤。C++則不然,它讓你順利通過編譯,不過如果真的運行起來,就等著頭痛吧。

C++的語法形成也和它自身的發展密不可分。C++從來沒有被好好設計過:它只是逐步進化。在進化過程中,一些結構的加入造成了語言的二義性。特別的規則被用於解決這些二義性,這些難懂的規則使得C++複雜難學。於是不少程序員把它們抄在卡片上以供不時之需,或者根本就不去使用這些功能。

例如,C++有個規則說如果一個字符串既可以被解析為聲明也可以被解析為語句,那麼它將被當做聲明。解析器專家看到這個規則往往會渾身發冷,他們知道很難能正確實現它。AT&T自己甚至都搞不對。比如,當Jim Roskind想理解一個結構的意思時(他覺得正常人會對它有不同的理解),他寫了段測試代碼,把它交給AT&T的"cfront"編譯器。Cfront崩潰了。

事實上,如果你從ics.uci.edu上下載Jim Roskind的開放C++語法,你會發現ftp/pub目錄裡的c++grammar2.0.tar.Z有這樣的說明:「注意我的語法和cfront不一定保持一致,因為 a) 我的語法內部是一致的(這源於它的規範性以及yacc的確證。b) yacc生成的解析器不會吐核(core dump)。(這條可能會招來不少臭雞蛋,不過...每次當我想知道某種結構的語法含義是,如果ARM(Annotated C++ Reference Manual, 帶註釋的C++參考手冊)對它的表述不清楚,我就會拿cfront來編譯它,cfront這時總是吐核(core dump))」

日期: Sun, 21 May 89 18:02:14 PDT
發信人: tiemann (Michael Tiemann)
收信人: sdm@cs.brown.edu
抄送: UNIX-HATERS
主題: C++ Comments (C++註釋)

日期: 21 May 89 23:59:37 GMT
發信人: sdm@cs.brown.edu (Scott Meyers)
新聞組: comp.lang.c++
組織: 布朗大學計算機系

看看下面這行C++代碼:

//**********************

C++編譯器該如何處理它呢?GNU g++編譯器認為這是一行由一堆星星(*)組成的註釋,然而AT&T編譯器認為這是一個斜槓加上一個註釋開始符(/*)。我想知道哪個是正確解析方式,可是Stroustrup的書(《C++編程語言》)裡面卻找不到答案。

實際上如果使用-E選項進行編譯,就會發現是預處理器(preprocessor)搞的鬼,我的問題是:

這是否AT&T預處理器的bug?如果不是,為什麼?如果是bug,2.0版是否會得到修正?還是只能這麼下去了?

這是否GNU預處理器的bug?如果是,為什麼?

Scott Meyers

sdm@cs.brown.edu

UNIX解析中有個古老的規則,盡量接受最長的語法單元(token)。這樣'foo'就不會被看成三個變量名('f', 'o'和'o'),而只被當成一個變量'foo'。看看這個規則在下面這個程序中是多麼的有用(還有選擇'/*'作為註釋開始符是多麼的明智):

double qdiv (p, q)
double *p, *q;
{
return *p/*q;
}

為什麼這個規則沒有被應用到C++中呢?很簡單,這是個bug。

Michael

糟糕的還在後頭,C++最大的問題是它的代碼難讀難理解,即使對於每天都用它的人也是如此。把另一個程序員的C++的代碼拿來看看,不暈才怪。C++沒有一絲品位,是個亂七八糟的醜八怪。C++自稱為面向對像語言,卻不願意承擔任何面向對象的責任。C++認為如果有誰的程序複雜到需要垃圾回收,動態加載或其他功能,那麼說明他有足夠的能力自己寫一個,並且有足夠的時間進行調試。

C++操作符重載(operator overloading)的強大功能在於,你可以把一段明顯直白的代碼變成能和最糟糕的APL, ADA或FORTH代碼相媲美的東西。每個C++程序員都能創建自己的方言(dialect),把別的C++程序員徹底搞暈。

不過——嘿——在C++裡甚至標準的方言也是私有的(private)。

抽像些什麼?

你可能會覺得C++語法是它最糟糕的部分,不過當你開始學習C++時,就會知道你錯了。一旦你開始用C++編寫一個正式的大型軟件,你會發現C++的抽像機制從根兒上就爛了。每本計算機科學教材都會這樣告訴你,抽像是良好設計之源。

系統各個部分的關聯會產生複雜性。如果你有一個100,000行的程序,其中每一行都和其他行代碼的細節相關,那你就必須照應著 10,000,000,000種不同的關聯。抽像能夠通過建立清晰的接口來減少這種關聯。一段實現某種功能的代碼被隱藏在模塊化牆壁之後發揮作用。

類(class)是C++的核心,然而類的實現卻反而阻礙著程序的模塊化。類暴露了如此多的內部實現,以至於類的用戶強烈倚賴著類的具體實現。許多情況下,對類做一點兒改變,就不得不重新編譯所有使用它的代碼,這常常造成開發的停滯。你的軟件將不再「柔軟」和「可塑」了,而成了一大塊混凝土。

你將不得不把一半代碼放到頭文件裡面,以對類進行聲明。當然,類聲明所提供的public/private的區分是沒有什麼用的,因為「私有」 (private)信息就放在了頭文件裡,所以成了公開(public)信息。一旦放到頭文件裡,你就不大願意去修改它,因為這會導致煩人的重編譯。程序員於是通過修補實現機制,以避免修改頭文件。當然還有其他一些保護機制,不過它們就像是減速障礙一樣,可以被心急的傢伙任意繞過。只要把所有對象都轉換 (cast)成void*,再也沒有了討厭的類型檢查,這下世界清淨了。

其他許多語言都各自提供了設計良好的抽像機制。C++丟掉了其中一些最為重要的部分,對於那些提供的部分也叫人迷惑不解。你是否遇到過真正喜歡模板 (template)的人?模板使得類的實現根據上下文不同而不同。許多重要的概念無法通過這種簡單的方式加以表達;即使表達出來了,也沒法給它一個直接的名字供以後調用。

例如,名空間(namespace)能夠避免你一部分代碼的名字和其他部分發生衝突。一個服裝製造軟件可能有個對象叫做"Button"(鈕扣),它可能會和一個用戶界面庫進行鏈接,那裡面也有個類叫做"Button"(按鈕)。如果使用了名空間,就不會有問題了,因為用法和每個概念的意思都很明確,沒有歧義。

C++裡則並非如此。你無法保證不會去使用那些已經在其他地方被定義了的名字,這往往會導致災難性後果。你唯一的希望是給名稱都加上前綴,比如ZjxButton,並但願其他人不會用同一個名字。

日期: Fri, 18 Mar 94 10:52:58 PST
發信人: Scott L. Burson <gyro@zeta-soft.com>;
主題: preprocessor (預處理器)

C語言迷們會告訴你C的一個最好的功能是預處理器。可事實上,它可能一個最蹩腳的功能。許多C程序由一堆蜘蛛網似的#ifdef組成 (如果各個Unix之間能夠互相兼容,就幾乎不會弄成這樣)。不過這僅僅是開始。

C預處理器的最大問題是它把Unix鎖在了文本文件的監牢裡,然後扔掉了牢 旁砍 。這樣除了文本文件以外,C源代碼不可能以任何其他方式存儲。為什麼?因為未被預處理的C代碼不可能被解析。例如:

#ifdef BSD
int foo() {
#else
void foo() {
#endif
/* ... */
}

這裡函數foo有兩種不同的開頭,根據宏'BSD'是否被定義而不同。直接對它進行解析幾乎是不可能的 (就我們所知,從來沒實現過)。

這為什麼如此可惡?因為這阻礙了我們為編程環境加入更多智能。許多Unix程序員從沒見過這樣的環境,不知道自己被剝奪了什麼。可是如果能夠對代碼進行自動分析,那麼就能提供很多非常有用的功能。

讓我們再看一個例子。在C語言當道的時代,預處理器被認為是唯一能提供開碼(open-coded,是指直接把代碼嵌入到指令流中,而不是通過函數調用)的方式。對於每個簡單常用的表達式,開碼是一個很高效的技術。比如,取小函數min可以使用宏實現:

#define min(x,y) ((x) < (y) ? (x) : (y))

假設你想寫個工具打印一個程序中所有調用了min的函數。聽上去不是很難,是不是?但是你如果不解析這個程序就無法知道函數的邊界,你如果不做經過預處理器就無法進行解析,可是,一旦經過了預處理,所有的min就不復存在了!所以,你的只能去用grep了。

使用預處理器實現開碼還有其他問題。例如,在上面的min宏裡你一定注意到了那些多餘的括號。事實上,這些括號是必不可少的,否則當min在另一個表達式中被展開時,結果可能不是你想要的。(老實說,這些括號不都是必需的——至於那些括號是可以省略的,這留做給讀者的練習吧)。

min宏最險惡的問題是,雖然它用起來像是個函數調用,它並不真是函數。看這個例子:

a = min(b++, c);

預處理器做了替換之後,變成了:

a = ((b++) < (c) ? (b++) : (c))

如果'b'小於'c','b'會被增加兩次而不是一次,返回的將是'b'的原始值加一。

如果min真是函數,那麼'b'將只會被增加一次,返回值將是'b'的原始值。

C++對於C來說,就如同是肺癌對於肺

「如果說C語言給了你足夠的繩子吊死自己,那麼C++給的繩子除了夠你上吊之外,還夠綁上你所有的鄰居,並提供一艘帆船所需的繩索。」

——匿名

悲哀的是,學習C++成了每個計算機科學家和嚴肅程序最為有利可圖的投資。它迅速成為簡歷中必不可少的一行。在過去的今年中,我們見過不少C++程序員,他們能夠用C++寫出不錯的代碼,不過...

...他們憎惡它。

程序員進化史

初中/高中

10 PRINT "HELLO WORLD"
20 END

大學一年級

program Hello(input, output);
begin
writeln('Hello world');
end.

大學四年級

(defun hello ()
(print (list 'HELLO 'WORLD)))

剛參加工作

#include <stdio.h>;

main (argc, argv)
int argc;
char **argv; {
printf ("Hello World!\n");
}

老手

#include <stream.h>;

const int MAXLEN = 80;

class outstring;
class outstring {
private:
int size;
char str[MAXLEN];

public:
outstring() { size=0; }
~outstring() { size=0; }
void print();
void assign(char *chrs);
};

void outstring::print() {
int in;
for (i=0; i<size; i++)
cout << str[i];
cout << "\n";
}

void outstring::assign(char* chrs) {
int i;
for (i=0; chars[i]!='\0'; i++)
str[i] = chrs[i];
size=i;
}

main (int argc, char **argv) {
outstring string;
string.assign("Hello World!");
string.print();
}

老闆

「喬治,我需要一個能打印'Hello World!'的程序」

好了,換個角度想想,C++可能是你最好的朋友,C++之父Stroustrup之所以設計C++,其實http: //www.chunder.com/text/ididit.html正是為了我們這些程序員啊,當然如果你真的發誓不當C++程序員了,而且一時半會兒也當不了老闆,你還可以考慮做系統管理員,叫人羨慕的sysadmin。


执行力=流程+计划+组织

把理想变成计划,
把计划变成步骤,
把步骤变成行动,
把行动变成成果。

好語說盡人必易之。規矩行盡人必繁之。福若受盡緣必孤。勢若使盡禍必至。

相关信息:


欢迎光临本社区,您还没有登录,不能发贴子。请在 这里登录