所有類(lèi) Unix 的操作系統(tǒng)都非常依賴于被用于幾種數(shù)據(jù)類(lèi)型存儲(chǔ)的文本文件。所以這很有道理, 有許多用于處理文本的工具。在這一章中,我們將看一些被用來(lái)“切割”文本的程序。在下一章中, 我們將查看更多的文本處理程序,但主要集中于文本格式化輸出程序和其它一些人們需要的工具。
這一章會(huì)重新拜訪一些老朋友,并且會(huì)給我們介紹一些新朋友:
cat – 連接文件并且打印到標(biāo)準(zhǔn)輸出
sort – 給文本行排序
uniq – 報(bào)告或者省略重復(fù)行
cut – 從每行中刪除文本區(qū)域
paste – 合并文件文本行
join – 基于某個(gè)共享字段來(lái)聯(lián)合兩個(gè)文件的文本行
comm – 逐行比較兩個(gè)有序的文件
diff – 逐行比較文件
patch – 給原始文件打補(bǔ)丁
tr – 翻譯或刪除字符
sed – 用于篩選和轉(zhuǎn)換文本的流編輯器
到目前為止,我們已經(jīng)知道了一對(duì)文本編輯器(nano 和 vim),看過(guò)一堆配置文件,并且目睹了 許多命令的輸出都是文本格式。但是文本還被用來(lái)做什么? 它可以做很多事情。
許多人使用純文本格式來(lái)編寫(xiě)文檔。雖然很容易看到一個(gè)小的文本文件對(duì)于保存簡(jiǎn)單的筆記會(huì) 很有幫助,但是也有可能用文本格式來(lái)編寫(xiě)大的文檔。一個(gè)流行的方法是先用文本格式來(lái)編寫(xiě)一個(gè) 大的文檔,然后使用一種標(biāo)記語(yǔ)言來(lái)描述已完成文檔的格式。許多科學(xué)論文就是用這種方法編寫(xiě)的, 因?yàn)榛?Unix 的文本處理系統(tǒng)位于支持技術(shù)學(xué)科作家所需要的高級(jí)排版布局的一流系統(tǒng)之列。
世界上最流行的電子文檔類(lèi)型可能就是網(wǎng)頁(yè)了。網(wǎng)頁(yè)是文本文檔,它們使用 HTML(超文本標(biāo)記語(yǔ)言)或者是 XML (可擴(kuò)展的標(biāo)記語(yǔ)言)作為標(biāo)記語(yǔ)言來(lái)描述文檔的可視格式。
從本質(zhì)上來(lái)說(shuō),email 是一個(gè)基于文本的媒介。為了傳輸,甚至非文本的附件也被轉(zhuǎn)換成文本表示形式。 我們能看到這些,通過(guò)下載一個(gè) email 信息,然后用 less 來(lái)瀏覽它。我們將會(huì)看到這條信息開(kāi)始于一個(gè)標(biāo)題, 其描述了信息的來(lái)源以及在傳輸過(guò)程中它接受到的處理,然后是信息的正文內(nèi)容。
在類(lèi) Unix 的系統(tǒng)中,輸出會(huì)以純文本格式發(fā)送到打印機(jī),或者如果頁(yè)面包含圖形,其會(huì)被轉(zhuǎn)換成 一種文本格式的頁(yè)面描述語(yǔ)言,以 PostScript 著稱,然后再被發(fā)送給一款能產(chǎn)生圖形點(diǎn)陣的程序, 最后被打印出來(lái)。
在類(lèi) Unix 系統(tǒng)中會(huì)發(fā)現(xiàn)許多命令行程序被用來(lái)支持系統(tǒng)管理和軟件開(kāi)發(fā),并且文本處理程序也不例外。 許多文本處理程序被設(shè)計(jì)用來(lái)解決軟件開(kāi)發(fā)問(wèn)題。文本處理對(duì)于軟件開(kāi)發(fā)者來(lái)言至關(guān)重要是因?yàn)樗械能浖?都起始于文本格式。源代碼,程序員實(shí)際編寫(xiě)的一部分程序,總是文本格式。
回到第7章(重定向),我們已經(jīng)知道一些命令除了接受命令行參數(shù)之外,還能夠接受標(biāo)準(zhǔn)輸入。 那時(shí)候我們只是簡(jiǎn)單地介紹了它們,但是現(xiàn)在我們將仔細(xì)地看一下它們是怎樣被用來(lái)執(zhí)行文本處理的。
這個(gè) cat 程序具有許多有趣的選項(xiàng)。其中許多選項(xiàng)用來(lái)幫助更好的可視化文本內(nèi)容。一個(gè)例子是-A 選項(xiàng), 其用來(lái)在文本中顯示非打印字符。有些時(shí)候我們想知道是否控制字符嵌入到了我們的可見(jiàn)文本中。 最常用的控制字符是 tab 字符(而不是空格)和回車(chē)字符,在 MS-DOS 風(fēng)格的文本文件中回車(chē)符經(jīng)常作為 結(jié)束符出現(xiàn)。另一種常見(jiàn)情況是文件中包含末尾帶有空格的文本行。
讓我們創(chuàng)建一個(gè)測(cè)試文件,用 cat 程序作為一個(gè)簡(jiǎn)單的文字處理器。為此,我們將鍵入 cat 命令(隨后指定了 用于重定向輸出的文件),然后輸入我們的文本,最后按下 Enter 鍵來(lái)結(jié)束這一行,然后按下組合鍵 Ctrl-d, 來(lái)指示 cat 程序,我們已經(jīng)到達(dá)文件末尾了。在這個(gè)例子中,我們文本行的開(kāi)頭和末尾分別鍵入了一個(gè) tab 字符以及一些空格。
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox jumped over the lazy dog.
[me@linuxbox ~]$
下一步,我們將使用帶有-A 選項(xiàng)的 cat 命令來(lái)顯示這個(gè)文本:
[me@linuxbox ~]$ cat -A foo.txt
^IThe quick brown fox jumped over the lazy dog. $
[me@linuxbox ~]$
在輸出結(jié)果中我們看到,這個(gè) tab 字符在我們的文本中由^I 字符來(lái)表示。這是一種常見(jiàn)的表示方法,意思是 “Control-I”,結(jié)果證明,它和 tab 字符是一樣的。我們也看到一個(gè)$字符出現(xiàn)在文本行真正的結(jié)尾處, 表明我們的文本包含末尾的空格。
MS-DOS 文本 Vs. Unix 文本
可能你想用 cat 程序在文本中查看非打印字符的一個(gè)原因是發(fā)現(xiàn)隱藏的回車(chē)符。那么 隱藏的回車(chē)符來(lái)自于哪里呢?它們來(lái)自于 DOS 和 Windows!Unix 和 DOS 在文本文件中定義每行 結(jié)束的方式不相同。Unix 通過(guò)一個(gè)換行符(ASCII 10)來(lái)結(jié)束一行,然而 MS-DOS 和它的 衍生品使用回車(chē)(ASCII 13)和換行字符序列來(lái)終止每個(gè)文本行。
有幾種方法能夠把文件從 DOS 格式轉(zhuǎn)變?yōu)?Unix 格式。在許多 Linux 系統(tǒng)中,有兩個(gè) 程序叫做 dos2unix 和 unix2dos,它們能在兩種格式之間轉(zhuǎn)變文本文件。然而,如果你 的系統(tǒng)中沒(méi)有安裝 dos2unix 程序,也不要擔(dān)心。文件從 DOS 格式轉(zhuǎn)變?yōu)?Unix 格式的過(guò)程非常 簡(jiǎn)單;它只簡(jiǎn)單地涉及到刪除違規(guī)的回車(chē)符。通過(guò)隨后本章中討論的一些程序,這個(gè)工作很容易 完成。
cat 程序也包含用來(lái)修改文本的選項(xiàng)。最著名的兩個(gè)選項(xiàng)是-n,其給文本行添加行號(hào)和-s, 禁止輸出多個(gè)空白行。我們這樣來(lái)說(shuō)明:
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox
jumped over the lazy dog.
[me@linuxbox ~]$ cat -ns foo.txt
1 The quick brown fox
2
3 jumped over the lazy dog.
[me@linuxbox ~]$
在這個(gè)例子里,我們創(chuàng)建了一個(gè)測(cè)試文件 foo.txt 的新版本,其包含兩行文本,由兩個(gè)空白行分開(kāi)。 經(jīng)由帶有-ns 選項(xiàng)的 cat 程序處理之后,多余的空白行被刪除,并且對(duì)保留的文本行進(jìn)行編號(hào)。 然而這并不是多個(gè)進(jìn)程在操作這個(gè)文本,只有一個(gè)進(jìn)程。
這個(gè) sort 程序?qū)?biāo)準(zhǔn)輸入的內(nèi)容,或命令行中指定的一個(gè)或多個(gè)文件進(jìn)行排序,然后把排序 結(jié)果發(fā)送到標(biāo)準(zhǔn)輸出。使用與 cat 命令相同的技巧,我們能夠演示如何用 sort 程序來(lái)處理標(biāo)準(zhǔn)輸入:
[me@linuxbox ~]$ sort > foo.txt
c
b
a
[me@linuxbox ~]$ cat foo.txt
a
b
c
輸入命令之后,我們鍵入字母“c”,“b”,和“a”,然后再按下 Ctrl-d 組合鍵來(lái)表示文件的結(jié)尾。 隨后我們查看生成的文件,看到文本行有序地顯示。
因?yàn)?sort 程序能接受命令行中的多個(gè)文件作為參數(shù),所以有可能把多個(gè)文件合并成一個(gè)有序的文件。例如, 如果我們有三個(gè)文本文件,想要把它們合并為一個(gè)有序的文件,我們可以這樣做:
sort file1.txt file2.txt file3.txt > final_sorted_list.txt
sort 程序有幾個(gè)有趣的選項(xiàng)。這里只是一部分列表:
選項(xiàng) | 長(zhǎng)選項(xiàng) | 描述 |
---|---|---|
-b | --ignore-leading-blanks | 默認(rèn)情況下,對(duì)整行進(jìn)行排序,從每行的第一個(gè)字符開(kāi)始。這個(gè)選項(xiàng)導(dǎo)致 sort 程序忽略 每行開(kāi)頭的空格,從第一個(gè)非空白字符開(kāi)始排序。 |
-f | --ignore-case | 讓排序不區(qū)分大小寫(xiě)。 |
-n | --numeric-sort | 基于字符串的長(zhǎng)度來(lái)排序。使用此選項(xiàng)允許根據(jù)數(shù)字值執(zhí)行排序,而不是字母值。 |
-r | --reverse | 按相反順序排序。結(jié)果按照降序排列,而不是升序。 |
-k | --key=field1[,field2] | 對(duì)從 field1到 field2之間的字符排序,而不是整個(gè)文本行??聪旅娴挠懻?。 |
-m | --merge | 把每個(gè)參數(shù)看作是一個(gè)預(yù)先排好序的文件。把多個(gè)文件合并成一個(gè)排好序的文件,而沒(méi)有執(zhí)行額外的排序。 |
-o | --output=file | 把排好序的輸出結(jié)果發(fā)送到文件,而不是標(biāo)準(zhǔn)輸出。 |
-t | --field-separator=char | 定義域分隔字符。默認(rèn)情況下,域由空格或制表符分隔。 |
雖然以上大多數(shù)選項(xiàng)的含義是不言自喻的,但是有些也不是。首先,讓我們看一下 -n 選項(xiàng),被用做數(shù)值排序。 通過(guò)這個(gè)選項(xiàng),有可能基于數(shù)值進(jìn)行排序。我們通過(guò)對(duì) du 命令的輸出結(jié)果排序來(lái)說(shuō)明這個(gè)選項(xiàng),du 命令可以 確定最大的磁盤(pán)空間用戶。通常,這個(gè) du 命令列出的輸出結(jié)果按照路徑名來(lái)排序:
[me@linuxbox ~]$ du -s /usr/share/\* | head
252 /usr/share/aclocal
96 /usr/share/acpi-support
8 /usr/share/adduser
196 /usr/share/alacarte
344 /usr/share/alsa
8 /usr/share/alsa-base
12488 /usr/share/anthy
8 /usr/share/apmd
21440 /usr/share/app-install
48 /usr/share/application-registry
在這個(gè)例子里面,我們把結(jié)果管道到 head 命令,把輸出結(jié)果限制為前 10 行。我們能夠產(chǎn)生一個(gè)按數(shù)值排序的 列表,來(lái)顯示 10 個(gè)最大的空間消費(fèi)者:
[me@linuxbox ~]$ du -s /usr/share/* | sort -nr | head
509940 /usr/share/locale-langpack
242660 /usr/share/doc
197560 /usr/share/fonts
179144 /usr/share/gnome
146764 /usr/share/myspell
144304 /usr/share/gimp
135880 /usr/share/dict
76508 /usr/share/icons
68072 /usr/share/apps
62844 /usr/share/foomatic
通過(guò)使用此 -nr 選項(xiàng),我們產(chǎn)生了一個(gè)反向的數(shù)值排序,最大數(shù)值排列在第一位。這種排序起作用是 因?yàn)閿?shù)值出現(xiàn)在每行的開(kāi)頭。但是如果我們想要基于文件行中的某個(gè)數(shù)值排序,又會(huì)怎樣呢? 例如,命令 ls -l 的輸出結(jié)果:
[me@linuxbox ~]$ ls -l /usr/bin | head
total 152948
-rwxr-xr-x 1 root root 34824 2008-04-04 02:42 [
-rwxr-xr-x 1 root root 101556 2007-11-27 06:08 a2p
...
此刻,忽略 ls 程序能按照文件大小對(duì)輸出結(jié)果進(jìn)行排序,我們也能夠使用 sort 程序來(lái)完成此任務(wù):
[me@linuxbox ~]$ ls -l /usr/bin | sort -nr -k 5 | head
-rwxr-xr-x 1 root root 8234216 2008-04-0717:42 inkscape
-rwxr-xr-x 1 root root 8222692 2008-04-07 17:42 inkview
...
sort 程序的許多用法都涉及到處理表格數(shù)據(jù),例如上面 ls 命令的輸出結(jié)果。如果我們 把數(shù)據(jù)庫(kù)這個(gè)術(shù)語(yǔ)應(yīng)用到上面的表格中,我們會(huì)說(shuō)每行是一條記錄,并且每條記錄由多個(gè)字段組成, 例如文件屬性,鏈接數(shù),文件名,文件大小等等。sort 程序能夠處理獨(dú)立的字段。在數(shù)據(jù)庫(kù)術(shù)語(yǔ)中, 我們能夠指定一個(gè)或者多個(gè)關(guān)鍵字段,來(lái)作為排序的關(guān)鍵值。在上面的例子中,我們指定 n 和 r 選項(xiàng)來(lái)執(zhí)行相反的數(shù)值排序,并且指定 -k 5,讓 sort 程序使用第五字段作為排序的關(guān)鍵值。
這個(gè) k 選項(xiàng)非常有趣,而且還有很多特點(diǎn),但是首先我們需要講講 sort 程序怎樣來(lái)定義字段。 讓我們考慮一個(gè)非常簡(jiǎn)單的文本文件,只有一行包含作者名字的文本。
William Shotts
默認(rèn)情況下,sort 程序把此行看作有兩個(gè)字段。第一個(gè)字段包含字符:
和第二個(gè)字段包含字符:
意味著空白字符(空格和制表符)被當(dāng)作是字段間的界定符,當(dāng)執(zhí)行排序時(shí),界定符會(huì)被 包含在字段當(dāng)中。再看一下 ls 命令的輸出,我們看到每行包含八個(gè)字段,并且第五個(gè)字段是文件大小:
-rwxr-xr-x 1 root root 8234216 2008-04-07 17:42 inkscape
讓我們考慮用下面的文件,其包含從 2006 年到 2008 年三款流行的 Linux 發(fā)行版的發(fā)行歷史,來(lái)做一系列實(shí)驗(yàn)。 文件中的每一行都有三個(gè)字段:發(fā)行版的名稱,版本號(hào),和 MM/DD/YYYY 格式的發(fā)行日期:
SUSE 10.2 12/07/2006
Fedora 10 11/25/2008
SUSE 11.04 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
SUSE 10.3 10/04/2007
...
使用一個(gè)文本編輯器(可能是 vim),我們將輸入這些數(shù)據(jù),并把產(chǎn)生的文件命名為 distros.txt。
下一步,我們將試著對(duì)這個(gè)文件進(jìn)行排序,并觀察輸出結(jié)果:
[me@linuxbox ~]$ sort distros.txt
Fedora 10 11/25/2008
Fedora 5 03/20/2006
Fedora 6 10/24/2006
Fedora 7 05/31/2007
Fedora 8 11/08/2007
...
恩,大部分正確。問(wèn)題出現(xiàn)在 Fedora 的版本號(hào)上。因?yàn)樵谧址?“1” 出現(xiàn)在 “5” 之前,版本號(hào) “10” 在 最頂端,然而版本號(hào) “9” 卻掉到底端。
為了解決這個(gè)問(wèn)題,我們必須依賴多個(gè)鍵值來(lái)排序。我們想要對(duì)第一個(gè)字段執(zhí)行字母排序,然后對(duì) 第三個(gè)字段執(zhí)行數(shù)值排序。sort 程序允許多個(gè) -k 選項(xiàng)的實(shí)例,所以可以指定多個(gè)排序關(guān)鍵值。事實(shí)上, 一個(gè)關(guān)鍵值可能包括一個(gè)字段區(qū)域。如果沒(méi)有指定區(qū)域(如同之前的例子),sort 程序會(huì)使用一個(gè)鍵值, 其始于指定的字段,一直擴(kuò)展到行尾。下面是多鍵值排序的語(yǔ)法:
[me@linuxbox ~]$ sort --key=1,1 --key=2n distros.txt
Fedora 5 03/20/2006
Fedora 6 10/24/2006
Fedora 7 05/31/2007
...
雖然為了清晰,我們使用了選項(xiàng)的長(zhǎng)格式,但是 -k 1,1 -k 2n 格式是等價(jià)的。在第一個(gè) key 選項(xiàng)的實(shí)例中, 我們指定了一個(gè)字段區(qū)域。因?yàn)槲覀冎幌雽?duì)第一個(gè)字段排序,我們指定了 1,1, 意味著“始于并且結(jié)束于第一個(gè)字段?!痹诘诙€(gè)實(shí)例中,我們指定了 2n,意味著第二個(gè)字段是排序的鍵值, 并且按照數(shù)值排序。一個(gè)選項(xiàng)字母可能被包含在一個(gè)鍵值說(shuō)明符的末尾,其用來(lái)指定排序的種類(lèi)。這些 選項(xiàng)字母和 sort 程序的全局選項(xiàng)一樣:b(忽略開(kāi)頭的空格),n(數(shù)值排序),r(逆向排序),等等。
我們列表中第三個(gè)字段包含的日期格式不利于排序。在計(jì)算機(jī)中,日期通常設(shè)置為 YYYY-MM-DD 格式, 這樣使按時(shí)間順序排序變得容易,但是我們的日期為美國(guó)格式 MM/DD/YYYY。那么我們?cè)鯓幽馨凑?時(shí)間順序來(lái)排列這個(gè)列表呢?
幸運(yùn)地是,sort 程序提供了一種方式。這個(gè) key 選項(xiàng)允許在字段中指定偏移量,所以我們能在字段中 定義鍵值。
[me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt
Fedora 10 11/25/2008
Ubuntu 8.10 10/30/2008
SUSE 11.0 06/19/2008
...
通過(guò)指定 -k 3.7,我們指示 sort 程序使用一個(gè)排序鍵值,其始于第三個(gè)字段中的第七個(gè)字符,對(duì)應(yīng)于 年的開(kāi)頭。同樣地,我們指定 -k 3.1和 -k 3.4來(lái)分離日期中的月和日。 我們也添加了 n 和 r 選項(xiàng)來(lái)實(shí)現(xiàn)一個(gè)逆向的數(shù)值排序。這個(gè) b 選項(xiàng)用來(lái)刪除日期字段中開(kāi)頭的空格( 行與行之間的空格數(shù)迥異,因此會(huì)影響 sort 程序的輸出結(jié)果)。
一些文件不會(huì)使用 tabs 和空格做為字段界定符;例如,這個(gè) /etc/passwd 文件:
[me@linuxbox ~]$ head /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
這個(gè)文件的字段之間通過(guò)冒號(hào)分隔開(kāi),所以我們?cè)鯓邮褂靡粋€(gè) key 字段來(lái)排序這個(gè)文件?sort 程序提供 了一個(gè) -t 選項(xiàng)來(lái)定義分隔符。按照第七個(gè)字段(帳戶的默認(rèn) shell)來(lái)排序此 passwd 文件,我們可以這樣做:
[me@linuxbox ~]$ sort -t ':' -k 7 /etc/passwd | head
me:x:1001:1001:Myself,,,:/home/me:/bin/bash
root:x:0:0:root:/root:/bin/bash
dhcp:x:101:102::/nonexistent:/bin/false
gdm:x:106:114:Gnome Display Manager:/var/lib/gdm:/bin/false
hplip:x:104:7:HPLIP system user,,,:/var/run/hplip:/bin/false
klog:x:103:104::/home/klog:/bin/false
messagebus:x:108:119::/var/run/dbus:/bin/false
polkituser:x:110:122:PolicyKit,,,:/var/run/PolicyKit:/bin/false
pulse:x:107:116:PulseAudio daemon,,,:/var/run/pulse:/bin/false
通過(guò)指定冒號(hào)字符做為字段分隔符,我們能按照第七個(gè)字段來(lái)排序。
與 sort 程序相比,這個(gè) uniq 程序是個(gè)輕量級(jí)程序。uniq 執(zhí)行一個(gè)看似瑣碎的認(rèn)為。當(dāng)給定一個(gè) 排好序的文件(包括標(biāo)準(zhǔn)輸出),uniq 會(huì)刪除任意重復(fù)行,并且把結(jié)果發(fā)送到標(biāo)準(zhǔn)輸出。 它常常和 sort 程序一塊使用,來(lái)清理重復(fù)的輸出。
uniq 程序是一個(gè)傳統(tǒng)的 Unix 工具,經(jīng)常與 sort 程序一塊使用,但是這個(gè) GNU 版本的 sort 程序支持一個(gè) -u 選項(xiàng),其可以從排好序的輸出結(jié)果中刪除重復(fù)行。
讓我們創(chuàng)建一個(gè)文本文件,來(lái)實(shí)驗(yàn)一下:
[me@linuxbox ~]$ cat > foo.txt
a
b
c
a
b
c
記住輸入 Ctrl-d 來(lái)終止標(biāo)準(zhǔn)輸入?,F(xiàn)在,如果我們對(duì)文本文件執(zhí)行 uniq 命令:
[me@linuxbox ~]$ uniq foo.txt
a
b
c
a
b
c
輸出結(jié)果與原始文件沒(méi)有差異;重復(fù)行沒(méi)有被刪除。實(shí)際上,uniq 程序能完成任務(wù),其輸入必須是排好序的數(shù)據(jù),
[me@linuxbox ~]$ sort foo.txt | uniq
a
b
c
這是因?yàn)?uniq 只會(huì)刪除相鄰的重復(fù)行。uniq 程序有幾個(gè)選項(xiàng)。這里是一些常用選項(xiàng):
選項(xiàng) | 說(shuō)明 |
---|---|
-c | 輸出所有的重復(fù)行,并且每行開(kāi)頭顯示重復(fù)的次數(shù)。 |
-d | 只輸出重復(fù)行,而不是特有的文本行。 |
-f n | 忽略每行開(kāi)頭的 n 個(gè)字段,字段之間由空格分隔,正如 sort 程序中的空格分隔符;然而, 不同于 sort 程序,uniq 沒(méi)有選項(xiàng)來(lái)設(shè)置備用的字段分隔符。 |
-i | 在比較文本行的時(shí)候忽略大小寫(xiě)。 |
-s n | 跳過(guò)(忽略)每行開(kāi)頭的 n 個(gè)字符。 |
-u | 只是輸出獨(dú)有的文本行。這是默認(rèn)的。 |
這里我們看到 uniq 被用來(lái)報(bào)告文本文件中重復(fù)行的次數(shù),使用這個(gè)-c 選項(xiàng):
[me@linuxbox ~]$ sort foo.txt | uniq -c
2 a
2 b
2 c
下面我們將要討論的三個(gè)程序用來(lái)從文件中獲得文本列,并且以有用的方式重組它們。
這個(gè) cut 程序被用來(lái)從文本行中抽取文本,并把其輸出到標(biāo)準(zhǔn)輸出。它能夠接受多個(gè)文件參數(shù)或者 標(biāo)準(zhǔn)輸入。
從文本行中指定要抽取的文本有些麻煩,使用以下選項(xiàng):
選項(xiàng) | 說(shuō)明 |
---|---|
-c char_list | 從文本行中抽取由 char_list 定義的文本。這個(gè)列表可能由一個(gè)或多個(gè)逗號(hào) 分隔開(kāi)的數(shù)值區(qū)間組成。 |
-f field_list | 從文本行中抽取一個(gè)或多個(gè)由 field_list 定義的字段。這個(gè)列表可能 包括一個(gè)或多個(gè)字段,或由逗號(hào)分隔開(kāi)的字段區(qū)間。 |
-d delim_char | 當(dāng)指定-f 選項(xiàng)之后,使用 delim_char 做為字段分隔符。默認(rèn)情況下, 字段之間必須由單個(gè) tab 字符分隔開(kāi)。 |
--complement | 抽取整個(gè)文本行,除了那些由-c 和/或-f 選項(xiàng)指定的文本。 |
正如我們所看到的,cut 程序抽取文本的方式相當(dāng)不靈活。cut 命令最好用來(lái)從其它程序產(chǎn)生的文件中 抽取文本,而不是從人們直接輸入的文本中抽取。我們將會(huì)看一下我們的 distros.txt 文件,看看 是否它足夠 “整齊” 成為 cut 實(shí)例的一個(gè)好樣本。如果我們使用帶有 -A 選項(xiàng)的 cat 命令,我們能查看是否這個(gè) 文件符號(hào)由 tab 字符分離字段的要求。
[me@linuxbox ~]$ cat -A distros.txt
SUSE^I10.2^I12/07/2006$
Fedora^I10^I11/25/2008$
SUSE^I11.0^I06/19/2008$
Ubuntu^I8.04^I04/24/2008$
Fedora^I8^I11/08/2007$
SUSE^I10.3^I10/04/2007$
Ubuntu^I6.10^I10/26/2006$
Fedora^I7^I05/31/2007$
Ubuntu^I7.10^I10/18/2007$
Ubuntu^I7.04^I04/19/2007$
SUSE^I10.1^I05/11/2006$
Fedora^I6^I10/24/2006$
Fedora^I9^I05/13/2008$
Ubuntu^I6.06^I06/01/2006$
Ubuntu^I8.10^I10/30/2008$
Fedora^I5^I03/20/2006$
看起來(lái)不錯(cuò)。字段之間僅僅是單個(gè) tab 字符,沒(méi)有嵌入空格。因?yàn)檫@個(gè)文件使用了 tab 而不是空格, 我們將使用 -f 選項(xiàng)來(lái)抽取一個(gè)字段:
[me@linuxbox ~]$ cut -f 3 distros.txt
12/07/2006
11/25/2008
06/19/2008
04/24/2008
11/08/2007
10/04/2007
10/26/2006
05/31/2007
10/18/2007
04/19/2007
05/11/2006
10/24/2006
05/13/2008
06/01/2006
10/30/2008
03/20/2006
因?yàn)槲覀兊?distros 文件是由 tab 分隔開(kāi)的,最好用 cut 來(lái)抽取字段而不是字符。這是因?yàn)橐粋€(gè)由 tab 分離的文件, 每行不太可能包含相同的字符數(shù),這就使計(jì)算每行中字符的位置變得困難或者是不可能。在以上事例中,然而, 我們已經(jīng)抽取了一個(gè)字段,幸運(yùn)地是其包含地日期長(zhǎng)度相同,所以通過(guò)從每行中抽取年份,我們能展示怎樣 來(lái)抽取字符:
[me@linuxbox ~]$ cut -f 3 distros.txt | cut -c 7-10
2006
2008
2007
2006
2007
2006
2008
2006
2008
2006
通過(guò)對(duì)我們的列表再次運(yùn)行 cut 命令,我們能夠抽取從位置7到10的字符,其對(duì)應(yīng)于日期字段的年份。 這個(gè) 7-10 表示法是一個(gè)區(qū)間的例子。cut 命令手冊(cè)包含了一個(gè)如何指定區(qū)間的完整描述。
展開(kāi) Tabs
distros.txt 的文件格式很適合使用 cut 程序來(lái)抽取字段。但是如果我們想要 cut 程序 按照字符,而不是字段來(lái)操作一個(gè)文件,那又怎樣呢?這要求我們用相應(yīng)數(shù)目的空格來(lái) 代替 tab 字符。幸運(yùn)地是,GNU 的 Coreutils 軟件包有一個(gè)工具來(lái)解決這個(gè)問(wèn)題。這個(gè) 程序名為 expand,它既可以接受一個(gè)或多個(gè)文件參數(shù),也可以接受標(biāo)準(zhǔn)輸入,并且把 修改過(guò)的文本送到標(biāo)準(zhǔn)輸出。
如果我們通過(guò) expand 來(lái)處理 distros.txt 文件,我們能夠使用 cut -c 命令來(lái)從文件中抽取 任意區(qū)間內(nèi)的字符。例如,我們能夠使用以下命令來(lái)從列表中抽取發(fā)行年份,通過(guò)展開(kāi) 此文件,再使用 cut 命令,來(lái)抽取從位置 23 開(kāi)始到行尾的每一個(gè)字符:
[me@linuxbox ~]$ expand distros.txt | cut -c 23-
Coreutils 軟件包也提供了 unexpand 程序,用 tab 來(lái)代替空格。
當(dāng)操作字段的時(shí)候,有可能指定不同的字段分隔符,而不是 tab 字符。這里我們將會(huì)從/etc/passwd 文件中 抽取第一個(gè)字段:
[me@linuxbox ~]$ cut -d ':' -f 1 /etc/passwd | head
root
daemon
bin
sys
sync
games
man
lp
mail
news
使用-d 選項(xiàng),我們能夠指定冒號(hào)做為字段分隔符。
這個(gè) paste 命令的功能正好與 cut 相反。它會(huì)添加一個(gè)或多個(gè)文本列到文件中,而不是從文件中抽取文本列。 它通過(guò)讀取多個(gè)文件,然后把每個(gè)文件中的字段整合成單個(gè)文本流,輸入到標(biāo)準(zhǔn)輸出。類(lèi)似于 cut 命令, paste 接受多個(gè)文件參數(shù)和 / 或標(biāo)準(zhǔn)輸入。為了說(shuō)明 paste 是怎樣工作的,我們將會(huì)對(duì) distros.txt 文件 動(dòng)手術(shù),來(lái)產(chǎn)生發(fā)行版的年代表。
從我們之前使用 sort 的工作中,首先我們將產(chǎn)生一個(gè)按照日期排序的發(fā)行版列表,并把結(jié)果 存儲(chǔ)在一個(gè)叫做 distros-by-date.txt 的文件中:
[me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt
下一步,我們將會(huì)使用 cut 命令從文件中抽取前兩個(gè)字段(發(fā)行版名字和版本號(hào)),并把結(jié)果存儲(chǔ)到 一個(gè)名為 distro-versions.txt 的文件中:
[me@linuxbox ~]$ cut -f 1,2 distros-by-date.txt > distros-versions.txt
[me@linuxbox ~]$ head distros-versions.txt
Fedora 10
Ubuntu 8.10
SUSE 11.0
Fedora 9
Ubuntu 8.04
Fedora 8
Ubuntu 7.10
SUSE 10.3
Fedora 7
Ubuntu 7.04
最后的準(zhǔn)備步驟是抽取發(fā)行日期,并把它們存儲(chǔ)到一個(gè)名為 distro-dates.txt 文件中:
[me@linuxbox ~]$ cut -f 3 distros-by-date.txt > distros-dates.txt
[me@linuxbox ~]$ head distros-dates.txt
11/25/2008
10/30/2008
06/19/2008
05/13/2008
04/24/2008
11/08/2007
10/18/2007
10/04/2007
05/31/2007
04/19/2007
現(xiàn)在我們擁有了我們所需要的文本了。為了完成這個(gè)過(guò)程,使用 paste 命令來(lái)把日期列放到發(fā)行版名字 和版本號(hào)的前面,這樣就創(chuàng)建了一個(gè)年代列表。通過(guò)使用 paste 命令,然后按照期望的順序來(lái)安排它的 參數(shù),就能很容易完成這個(gè)任務(wù)。
[me@linuxbox ~]$ paste distros-dates.txt distros-versions.txt
11/25/2008 Fedora 10
10/30/2008 Ubuntu 8.10
06/19/2008 SUSE 11.0
05/13/2008 Fedora 9
04/24/2008 Ubuntu 8.04
11/08/2007 Fedora 8
10/18/2007 Ubuntu 7.10
10/04/2007 SUSE 10.3
05/31/2007 Fedora 7
04/19/2007 Ubuntu 7.04
在某些方面,join 命令類(lèi)似于 paste,它會(huì)往文件中添加列,但是它使用了獨(dú)特的方法來(lái)完成。 一個(gè) join 操作通常與關(guān)系型數(shù)據(jù)庫(kù)有關(guān)聯(lián),在關(guān)系型數(shù)據(jù)庫(kù)中來(lái)自多個(gè)享有共同關(guān)鍵域的表格的 數(shù)據(jù)結(jié)合起來(lái),得到一個(gè)期望的結(jié)果。這個(gè) join 程序執(zhí)行相同的操作。它把來(lái)自于多個(gè)基于共享 關(guān)鍵域的文件的數(shù)據(jù)結(jié)合起來(lái)。
為了知道在關(guān)系數(shù)據(jù)庫(kù)中是怎樣使用 join 操作的,讓我們想象一個(gè)很小的數(shù)據(jù)庫(kù),這個(gè)數(shù)據(jù)庫(kù)由兩個(gè) 表格組成,每個(gè)表格包含一條記錄。第一個(gè)表格,叫做 CUSTOMERS,有三個(gè)數(shù)據(jù)域:一個(gè)客戶號(hào)(CUSTNUM), 客戶的名字(FNAME)和客戶的姓(LNAME):
CUSTNUM FNAME ME
======== ===== ======
4681934 John Smith
第二個(gè)表格叫做 ORDERS,其包含四個(gè)數(shù)據(jù)域:訂單號(hào)(ORDERNUM),客戶號(hào)(CUSTNUM),數(shù)量(QUAN), 和訂購(gòu)的貨品(ITEM)。
ORDERNUM CUSTNUM QUAN ITEM
======== ======= ==== ====
3014953305 4681934 1 Blue Widget
注意兩個(gè)表格共享數(shù)據(jù)域 CUSTNUM。這很重要,因?yàn)樗贡砀裰g建立了聯(lián)系。
執(zhí)行一個(gè) join 操作將允許我們把兩個(gè)表格中的數(shù)據(jù)域結(jié)合起來(lái),得到一個(gè)有用的結(jié)果,例如準(zhǔn)備 一張發(fā)貨單。通過(guò)使用兩個(gè)表格 CUSTNUM 數(shù)字域中匹配的數(shù)值,一個(gè) join 操作會(huì)產(chǎn)生以下結(jié)果:
FNAME LNAME QUAN ITEM
===== ===== ==== ====
John Smith 1 Blue Widget
為了說(shuō)明 join 程序,我們需要?jiǎng)?chuàng)建一對(duì)包含共享鍵值的文件。為此,我們將使用我們的 distros.txt 文件。 從這個(gè)文件中,我們將構(gòu)建額外兩個(gè)文件,一個(gè)包含發(fā)行日期(其會(huì)成為共享鍵值)和發(fā)行版名稱:
[me@linuxbox ~]$ cut -f 1,1 distros-by-date.txt > distros-names.txt
[me@linuxbox ~]$ paste distros-dates.txt distros-names.txt > distros-key-names.txt
[me@linuxbox ~]$ head distros-key-names.txt
11/25/2008 Fedora
10/30/2008 Ubuntu
06/19/2008 SUSE
05/13/2008 Fedora
04/24/2008 Ubuntu
11/08/2007 Fedora
10/18/2007 Ubuntu
10/04/2007 SUSE
05/31/2007 Fedora
04/19/2007 Ubuntu
第二個(gè)文件包含發(fā)行日期和版本號(hào):
[me@linuxbox ~]$ cut -f 2,2 distros-by-date.txt > distros-vernums.txt
[me@linuxbox ~]$ paste distros-dates.txt distros-vernums.txt > distros-key-vernums.txt
[me@linuxbox ~]$ head distros-key-vernums.txt
11/25/2008 10
10/30/2008 8.10
06/19/2008 11.0
05/13/2008 9
04/24/2008 8.04
11/08/2007 8
10/18/2007 7.10
10/04/2007 10.3
05/31/2007 7
04/19/2007 7.04
現(xiàn)在我們有兩個(gè)具有共享鍵值( “發(fā)行日期” 數(shù)據(jù)域 )的文件。有必要指出,為了使 join 命令 能正常工作,所有文件必須按照關(guān)鍵數(shù)據(jù)域排序。
[me@linuxbox ~]$ join distros-key-names.txt distros-key-vernums.txt | head
11/25/2008 Fedora 10
10/30/2008 Ubuntu 8.10
06/19/2008 SUSE 11.0
05/13/2008 Fedora 9
04/24/2008 Ubuntu 8.04
11/08/2007 Fedora 8
10/18/2007 Ubuntu 7.10
10/04/2007 SUSE 10.3
05/31/2007 Fedora 7
04/19/2007 Ubuntu 7.04
也要注意,默認(rèn)情況下,join 命令使用空白字符做為輸入字段的界定符,一個(gè)空格作為輸出字段 的界定符。這種行為可以通過(guò)指定的選項(xiàng)來(lái)修改。詳細(xì)信息,參考 join 命令手冊(cè)。
通常比較文本文件的版本很有幫助。對(duì)于系統(tǒng)管理員和軟件開(kāi)發(fā)者來(lái)說(shuō),這個(gè)尤為重要。 一名系統(tǒng)管理員可能,例如,需要拿現(xiàn)有的配置文件與先前的版本做比較,來(lái)診斷一個(gè)系統(tǒng)錯(cuò)誤。 同樣的,一名程序員經(jīng)常需要查看程序的修改。
這個(gè) comm 程序會(huì)比較兩個(gè)文本文件,并且會(huì)顯示每個(gè)文件特有的文本行和共有的文把行。 為了說(shuō)明問(wèn)題,通過(guò)使用 cat 命令,我們將會(huì)創(chuàng)建兩個(gè)內(nèi)容幾乎相同的文本文件:
[me@linuxbox ~]$ cat > file1.txt
a
b
c
d
[me@linuxbox ~]$ cat > file2.txt
b
c
d
e
下一步,我們將使用 comm 命令來(lái)比較這兩個(gè)文件:
[me@linuxbox ~]$ comm file1.txt file2.txt
a
b
c
d
e
正如我們所見(jiàn)到的,comm 命令產(chǎn)生了三列輸出。第一列包含第一個(gè)文件獨(dú)有的文本行;第二列, 文本行是第二列獨(dú)有的;第三列包含兩個(gè)文件共有的文本行。comm 支持 -n 形式的選項(xiàng),這里 n 代表 1,2 或 3。這些選項(xiàng)使用的時(shí)候,指定了要隱藏的列。例如,如果我們只想輸出兩個(gè)文件共享的文本行, 我們將隱藏第一列和第二列的輸出結(jié)果:
[me@linuxbox ~]$ comm -12 file1.txt file2.txt
b
c
d
類(lèi)似于 comm 程序,diff 程序被用來(lái)監(jiān)測(cè)文件之間的差異。然而,diff 是一款更加復(fù)雜的工具,它支持 許多輸出格式,并且一次能處理許多文本文件。軟件開(kāi)發(fā)員經(jīng)常使用 diff 程序來(lái)檢查不同程序源碼 版本之間的更改,diff 能夠遞歸地檢查源碼目錄,經(jīng)常稱之為源碼樹(shù)。diff 程序的一個(gè)常見(jiàn)用例是 創(chuàng)建 diff 文件或者補(bǔ)丁,它會(huì)被其它程序使用,例如 patch 程序(我們一會(huì)兒討論),來(lái)把文件 從一個(gè)版本轉(zhuǎn)換為另一個(gè)版本。
如果我們使用 diff 程序,來(lái)查看我們之前的文件實(shí)例:
[me@linuxbox ~]$ diff file1.txt file2.txt
1d0
< a
4a4
> e
我們看到 diff 程序的默認(rèn)輸出風(fēng)格:對(duì)兩個(gè)文件之間差異的簡(jiǎn)短描述。在默認(rèn)格式中, 每組的更改之前都是一個(gè)更改命令,其形式為 range operation range , 用來(lái)描述要求更改的位置和類(lèi)型,從而把第一個(gè)文件轉(zhuǎn)變?yōu)榈诙€(gè)文件:
改變 | 說(shuō)明 |
---|---|
r1ar2 | 把第二個(gè)文件中位置 r2 處的文件行添加到第一個(gè)文件中的 r1 處。 |
r1cr2 | 用第二個(gè)文件中位置 r2 處的文本行更改(替代)位置 r1 處的文本行。 |
r1dr2 | 刪除第一個(gè)文件中位置 r1 處的文本行,這些文本行將會(huì)出現(xiàn)在第二個(gè)文件中位置 r2 處。 |
在這種格式中,一個(gè)范圍就是由逗號(hào)分隔開(kāi)的開(kāi)頭行和結(jié)束行的列表。雖然這種格式是默認(rèn)情況(主要是 為了服從 POSIX 標(biāo)準(zhǔn)且向后與傳統(tǒng)的 Unix diff 命令兼容), 但是它并不像其它可選格式一樣被廣泛地使用。最流行的兩種格式是上下文模式和統(tǒng)一模式。
當(dāng)使用上下文模式(帶上 -c 選項(xiàng)),我們將看到這些:
[me@linuxbox ~]$ diff -c file1.txt file2.txt
*** file1.txt 2008-12-23 06:40:13.000000000 -0500
--- file2.txt 2008-12-23 06:40:34.000000000 -0500
***************
*** 1,4 ****
- a
b
c
d
--- 1,4 ----
b
c
d
+ e
這個(gè)輸出結(jié)果以兩個(gè)文件名和它們的時(shí)間戳開(kāi)頭。第一個(gè)文件用星號(hào)做標(biāo)記,第二個(gè)文件用短橫線做標(biāo)記。 縱觀列表的其它部分,這些標(biāo)記將象征它們各自代表的文件。下一步,我們看到幾組修改, 包括默認(rèn)的周?chē)舷挛男袛?shù)。在第一組中,我們看到:
*** 1,4 ***
其表示第一個(gè)文件中從第一行到第四行的文本行。隨后我們看到:
--- 1,4 ---
這表示第二個(gè)文件中從第一行到第四行的文本行。在更改組內(nèi),文本行以四個(gè)指示符之一開(kāi)頭:
指示符 | 意思 |
---|---|
blank | 上下文顯示行。它并不表示兩個(gè)文件之間的差異。 |
- | 刪除行。這一行將會(huì)出現(xiàn)在第一個(gè)文件中,而不是第二個(gè)文件內(nèi)。 |
+ | 添加行。這一行將會(huì)出現(xiàn)在第二個(gè)文件內(nèi),而不是第一個(gè)文件中。 |
! | 更改行。將會(huì)顯示某個(gè)文本行的兩個(gè)版本,每個(gè)版本會(huì)出現(xiàn)在更改組的各自部分。 |
這個(gè)統(tǒng)一模式相似于上下文模式,但是更加簡(jiǎn)潔。通過(guò) -u 選項(xiàng)來(lái)指定它:
[me@linuxbox ~]$ diff -u file1.txt file2.txt
--- file1.txt 2008-12-23 06:40:13.000000000 -0500
+++ file2.txt 2008-12-23 06:40:34.000000000 -0500
@@ -1,4 +1,4 @@
-a
b
c
d
+e
上下文模式和統(tǒng)一模式之間最顯著的差異就是重復(fù)上下文的消除,這就使得統(tǒng)一模式的輸出結(jié)果要比上下文 模式的輸出結(jié)果簡(jiǎn)短。在我們上述實(shí)例中,我們看到類(lèi)似于上下文模式中的文件時(shí)間戳,其緊緊跟隨字符串 @@ -1,4 +1,4 @@。這行字符串表示了在更改組中描述的第一個(gè)文件中的文本行和第二個(gè)文件中的文本行。 這行字符串之后就是文本行本身,與三行默認(rèn)的上下文。每行以可能的三個(gè)字符中的一個(gè)開(kāi)頭:
字符 | 意思 |
---|---|
空格 | 兩個(gè)文件都包含這一行。 |
- | 在第一個(gè)文件中刪除這一行。 |
+ | 添加這一行到第一個(gè)文件中。 |
這個(gè) patch 程序被用來(lái)把更改應(yīng)用到文本文件中。它接受從 diff 程序的輸出,并且通常被用來(lái) 把較老的文件版本轉(zhuǎn)變?yōu)檩^新的文件版本。讓我們考慮一個(gè)著名的例子。Linux 內(nèi)核是由一個(gè) 大型的,組織松散的貢獻(xiàn)者團(tuán)隊(duì)開(kāi)發(fā)而成,這些貢獻(xiàn)者會(huì)提交固定的少量更改到源碼包中。 這個(gè) Linux 內(nèi)核由幾百萬(wàn)行代碼組成,雖然每個(gè)貢獻(xiàn)者每次所做的修改相當(dāng)少。對(duì)于一個(gè)貢獻(xiàn)者 來(lái)說(shuō),每做一個(gè)修改就給每個(gè)開(kāi)發(fā)者發(fā)送整個(gè)的內(nèi)核源碼樹(shù),這是沒(méi)有任何意義的。相反, 提交一個(gè) diff 文件。一個(gè) diff 文件包含先前的內(nèi)核版本與帶有貢獻(xiàn)者修改的新版本之間的差異。 然后一個(gè)接受者使用 patch 程序,把這些更改應(yīng)用到他自己的源碼樹(shù)中。使用 diff/patch 組合提供了 兩個(gè)重大優(yōu)點(diǎn):
一個(gè) diff 文件非常小,與整個(gè)源碼樹(shù)的大小相比較而言。
當(dāng)然,diff/patch 能工作于任何文本文件,不僅僅是源碼文件。它同樣適用于配置文件或任意其它文本。
準(zhǔn)備一個(gè) diff 文件供 patch 程序使用,GNU 文檔(查看下面的拓展閱讀部分)建議這樣使用 diff 命令:
diff -Naur old_file new_file > diff_file
old_file 和 new_file 部分不是單個(gè)文件就是包含文件的目錄。這個(gè) r 選項(xiàng)支持遞歸目錄樹(shù)。
一旦創(chuàng)建了 diff 文件,我們就能應(yīng)用它,把舊文件修補(bǔ)成新文件。
patch < diff_file
我們將使用測(cè)試文件來(lái)說(shuō)明:
[me@linuxbox ~]$ diff -Naur file1.txt file2.txt > patchfile.txt
[me@linuxbox ~]$ patch < patchfile.txt
patching file file1.txt
[me@linuxbox ~]$ cat file1.txt
b
c
d
e
在這個(gè)例子中,我們創(chuàng)建了一個(gè)名為 patchfile.txt 的 diff 文件,然后使用 patch 程序, 來(lái)應(yīng)用這個(gè)補(bǔ)丁。注意我們沒(méi)有必要指定一個(gè)要修補(bǔ)的目標(biāo)文件,因?yàn)?diff 文件(在統(tǒng)一模式中)已經(jīng) 在標(biāo)題行中包含了文件名。一旦應(yīng)用了補(bǔ)丁,我們能看到,現(xiàn)在 file1.txt 與 file2.txt 文件相匹配了。
patch 程序有大量的選項(xiàng),而且還有額外的實(shí)用程序可以被用來(lái)分析和編輯補(bǔ)丁。
我們對(duì)于文本編輯器的經(jīng)驗(yàn)是它們主要是交互式的,意思是我們手動(dòng)移動(dòng)光標(biāo),然后輸入我們的修改。 然而,也有非交互式的方法來(lái)編輯文本。有可能,例如,通過(guò)單個(gè)命令把一系列修改應(yīng)用到多個(gè)文件中。
這個(gè) tr 程序被用來(lái)更改字符。我們可以把它看作是一種基于字符的查找和替換操作。 換字是一種把字符從一個(gè)字母轉(zhuǎn)換為另一個(gè)字母的過(guò)程。例如,把小寫(xiě)字母轉(zhuǎn)換成大寫(xiě)字母就是 換字。我們可以通過(guò) tr 命令來(lái)執(zhí)行這樣的轉(zhuǎn)換,如下所示:
[me@linuxbox ~]$ echo "lowercase letters" | tr a-z A-Z
LOWERCASE LETTERS
正如我們所見(jiàn),tr 命令操作標(biāo)準(zhǔn)輸入,并把結(jié)果輸出到標(biāo)準(zhǔn)輸出。tr 命令接受兩個(gè)參數(shù):要被轉(zhuǎn)換的字符集以及 相對(duì)應(yīng)的轉(zhuǎn)換后的字符集。字符集可以用三種方式來(lái)表示:
一個(gè)枚舉列表。例如, ABCDEFGHIJKLMNOPQRSTUVWXYZ
一個(gè)字符域。例如,A-Z 。注意這種方法有時(shí)候面臨與其它命令相同的問(wèn)題,歸因于 語(yǔ)系的排序規(guī)則,因此應(yīng)該謹(jǐn)慎使用。
大多數(shù)情況下,兩個(gè)字符集應(yīng)該長(zhǎng)度相同;然而,有可能第一個(gè)集合大于第二個(gè),尤其如果我們 想要把多個(gè)字符轉(zhuǎn)換為單個(gè)字符:
[me@linuxbox ~]$ echo "lowercase letters" | tr [:lower:] A
AAAAAAAAA AAAAAAA
除了換字之外,tr 命令能允許字符從輸入流中簡(jiǎn)單地被刪除。在之前的章節(jié)中,我們討論了轉(zhuǎn)換 MS-DOS 文本文件為 Unix 風(fēng)格文本的問(wèn)題。為了執(zhí)行這個(gè)轉(zhuǎn)換,每行末尾的回車(chē)符需要被刪除。 這個(gè)可以通過(guò) tr 命令來(lái)執(zhí)行,如下所示:
tr -d '\r' < dos_file > unix_file
這里的 dos_file 是需要被轉(zhuǎn)換的文件,unix_file 是轉(zhuǎn)換后的結(jié)果。這種形式的命令使用轉(zhuǎn)義序列 \r 來(lái)代表回車(chē)符。查看 tr 命令所支持地完整的轉(zhuǎn)義序列和字符類(lèi)別列表,試試下面的命令:
[me@linuxbox ~]$ tr --help
ROT13: 不那么秘密的編碼環(huán)
tr 命令的一個(gè)有趣的用法是執(zhí)行 ROT13文本編碼。ROT13是一款微不足道的基于一種簡(jiǎn)易的替換暗碼的 加密類(lèi)型。把 ROT13稱為“加密”是大方的;“文本模糊處理”更準(zhǔn)確些。有時(shí)候它被用來(lái)隱藏文本中潛在的攻擊內(nèi)容。 這個(gè)方法就是簡(jiǎn)單地把每個(gè)字符在字母表中向前移動(dòng)13位。因?yàn)橐苿?dòng)的位數(shù)是可能的26個(gè)字符的一半, 所以對(duì)文本再次執(zhí)行這個(gè)算法,就恢復(fù)到了它最初的形式。通過(guò) tr 命令來(lái)執(zhí)行這種編碼:
echo "secret text" | tr a-zA-Z n-za-mN-ZA-M
frperg grkg
再次執(zhí)行相同的過(guò)程,得到翻譯結(jié)果:
_echo "frperg grkg" | tr a-zA-Z n-za-mN-ZA-M+
secret text
大量的 email 程序和 USENET 新聞讀者都支持 ROT13 編碼。Wikipedia 上面有一篇關(guān)于這個(gè)主題的好文章:
tr 也可以完成另一個(gè)技巧。使用-s 選項(xiàng),tr 命令能“擠壓”(刪除)重復(fù)的字符實(shí)例:
[me@linuxbox ~]$ echo "aaabbbccc" | tr -s ab
abccc
這里我們有一個(gè)包含重復(fù)字符的字符串。通過(guò)給 tr 命令指定字符集“ab”,我們能夠消除字符集中 字母的重復(fù)實(shí)例,然而會(huì)留下不屬于字符集的字符(“c”)無(wú)更改。注意重復(fù)的字符必須是相鄰的。 如果它們不相鄰:
[me@linuxbox ~]$ echo "abcabcabc" | tr -s ab
abcabcabc
那么擠壓會(huì)沒(méi)有效果。
名字 sed 是 stream editor(流編輯器)的簡(jiǎn)稱。它對(duì)文本流進(jìn)行編輯,要不是一系列指定的文件, 要不就是標(biāo)準(zhǔn)輸入。sed 是一款強(qiáng)大的,并且有些復(fù)雜的程序(有整本內(nèi)容都是關(guān)于 sed 程序的書(shū)籍), 所以在這里我們不會(huì)詳盡的討論它。
總之,sed 的工作方式是要不給出單個(gè)編輯命令(在命令行中)要不就是包含多個(gè)命令的腳本文件名, 然后它就按行來(lái)執(zhí)行這些命令。這里有一個(gè)非常簡(jiǎn)單的 sed 實(shí)例:
[me@linuxbox ~]$ echo "front" | sed 's/front/back/'
back
在這個(gè)例子中,我們使用 echo 命令產(chǎn)生了一個(gè)單詞的文本流,然后把它管道給 sed 命令。sed,依次, 對(duì)流文本執(zhí)行指令 s/front/back/,隨后輸出“back”。我們也能夠把這個(gè)命令認(rèn)為是相似于 vi 中的“替換” (查找和替代)命令。
sed 中的命令開(kāi)始于單個(gè)字符。在上面的例子中,這個(gè)替換命令由字母 s 來(lái)代表,其后跟著查找 和替代字符串,斜杠字符做為分隔符。分隔符的選擇是隨意的。按照慣例,經(jīng)常使用斜杠字符, 但是 sed 將會(huì)接受緊隨命令之后的任意字符做為分隔符。我們可以按照這種方式來(lái)執(zhí)行相同的命令:
[me@linuxbox ~]$ echo "front" | sed 's\_front\_back\_'
back
通過(guò)緊跟命令之后使用下劃線字符,則它變成界定符。sed 可以設(shè)置界定符的能力,使命令的可讀性更強(qiáng), 正如我們將看到的.
sed 中的大多數(shù)命令之前都會(huì)帶有一個(gè)地址,其指定了輸入流中要被編輯的文本行。如果省略了地址, 然后會(huì)對(duì)輸入流的每一行執(zhí)行編輯命令。最簡(jiǎn)單的地址形式是一個(gè)行號(hào)。我們能夠添加一個(gè)地址 到我們例子中:
[me@linuxbox ~]$ echo "front" | sed '1s/front/back/'
back
給我們的命令添加地址 1,就導(dǎo)致只對(duì)僅有一行文本的輸入流的第一行執(zhí)行替換操作。如果我們指定另一 個(gè)數(shù)字:
[me@linuxbox ~]$ echo "front" | sed '2s/front/back/'
front
我們看到?jīng)]有執(zhí)行這個(gè)編輯命令,因?yàn)槲覀兊妮斎肓鳑](méi)有第二行。地址可以用許多方式來(lái)表達(dá)。這里是 最常用的:
地址 | 說(shuō)明 |
---|---|
n | 行號(hào),n 是一個(gè)正整數(shù)。 |
$ | 最后一行。 |
/regexp/ | 所有匹配一個(gè) POSIX 基本正則表達(dá)式的文本行。注意正則表達(dá)式通過(guò) 斜杠字符界定。選擇性地,這個(gè)正則表達(dá)式可能由一個(gè)備用字符界定,通過(guò)\cregexpc 來(lái) 指定表達(dá)式,這里 c 就是一個(gè)備用的字符。 |
addr1,addr2 | 從 addr1 到 addr2 范圍內(nèi)的文本行,包含地址 addr2 在內(nèi)。地址可能是上述任意 單獨(dú)的地址形式。 |
first~step | 匹配由數(shù)字 first 代表的文本行,然后隨后的每個(gè)在 step 間隔處的文本行。例如 1~2 是指每個(gè)位于偶數(shù)行號(hào)的文本行,5~5 則指第五行和之后每五行位置的文本行。 |
addr1,+n | 匹配地址 addr1 和隨后的 n 個(gè)文本行。 |
addr! | 匹配所有的文本行,除了 addr 之外,addr 可能是上述任意的地址形式。 |
通過(guò)使用這一章中早前的 distros.txt 文件,我們將演示不同種類(lèi)的地址表示法。首先,一系列行號(hào):
[me@linuxbox ~]$ sed -n '1,5p' distros.txt
SUSE 10.2 12/07/2006
Fedora 10 11/25/2008
SUSE 11.0 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
在這個(gè)例子中,我們打印出一系列的文本行,開(kāi)始于第一行,直到第五行。為此,我們使用 p 命令, 其就是簡(jiǎn)單地把匹配的文本行打印出來(lái)。然而為了高效,我們必須包含選項(xiàng) -n(不自動(dòng)打印選項(xiàng)), 讓 sed 不要默認(rèn)地打印每一行。
下一步,我們將試用一下正則表達(dá)式:
[me@linuxbox ~]$ sed -n '/SUSE/p' distros.txt
SUSE 10.2 12/07/2006
SUSE 11.0 06/19/2008
SUSE 10.3 10/04/2007
SUSE 10.1 05/11/2006
通過(guò)包含由斜杠界定的正則表達(dá)式 \/SUSE\/,我們能夠孤立出包含它的文本行,和 grep 程序的功能 是相同的。
最后,我們將試著否定上面的操作,通過(guò)給這個(gè)地址添加一個(gè)感嘆號(hào):
[me@linuxbox ~]$ sed -n '/SUSE/!p' distros.txt
Fedora 10 11/25/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
Ubuntu 6.10 10/26/2006
Fedora 7 05/31/2007
Ubuntu 7.10 10/18/2007
Ubuntu 7.04 04/19/2007
Fedora 6 10/24/2006
Fedora 9 05/13/2008
Ubuntu 6.06 06/01/2006
Ubuntu 8.10 10/30/2008
Fedora 5 03/20/2006
這里我們看到期望的結(jié)果:輸出了文件中所有的文本行,除了那些匹配這個(gè)正則表達(dá)式的文本行。
目前為止,我們已經(jīng)知道了兩個(gè) sed 的編輯命令,s 和 p。這里是一個(gè)更加全面的基本編輯命令列表:
命令 | 說(shuō)明 |
---|---|
= | 輸出當(dāng)前的行號(hào)。 |
a | 在當(dāng)前行之后追加文本。 |
d | 刪除當(dāng)前行。 |
i | 在當(dāng)前行之前插入文本。 |
p | 打印當(dāng)前行。默認(rèn)情況下,sed 程序打印每一行,并且只是編輯文件中匹配 指定地址的文本行。通過(guò)指定-n 選項(xiàng),這個(gè)默認(rèn)的行為能夠被忽略。 |
q | 退出 sed,不再處理更多的文本行。如果不指定-n 選項(xiàng),輸出當(dāng)前行。 |
Q | 退出 sed,不再處理更多的文本行。 |
s/regexp/replacement/ | 只要找到一個(gè) regexp 匹配項(xiàng),就替換為 replacement 的內(nèi)容。 replacement 可能包括特殊字符 &,其等價(jià)于由 regexp 匹配的文本。另外, replacement 可能包含序列 \1到 \9,其是 regexp 中相對(duì)應(yīng)的子表達(dá)式的內(nèi)容。更多信息,查看 下面 back references 部分的討論。在 replacement 末尾的斜杠之后,可以指定一個(gè) 可選的標(biāo)志,來(lái)修改 s 命令的行為。 |
y/set1/set2 | 執(zhí)行字符轉(zhuǎn)寫(xiě)操作,通過(guò)把 set1 中的字符轉(zhuǎn)變?yōu)橄鄬?duì)應(yīng)的 set2 中的字符。 注意不同于 tr 程序,sed 要求兩個(gè)字符集合具有相同的長(zhǎng)度。 |
到目前為止,這個(gè) s 命令是最常使用的編輯命令。我們將僅僅演示一些它的功能,通過(guò)編輯我們的 distros.txt 文件。我們以前討論過(guò) distros.txt 文件中的日期字段不是“友好地計(jì)算機(jī)”模式。 文件中的日期格式是 MM/DD/YYYY,但如果格式是 YYYY-MM-DD 會(huì)更好一些(利于排序)。手動(dòng)修改 日期格式不僅浪費(fèi)時(shí)間而且易出錯(cuò),但是有了 sed,只需一步就能完成修改:
[me@linuxbox ~]$ sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
SUSE 10.2 2006-12-07
Fedora 10 2008-11-25
SUSE 11.0 2008-06-19
Ubuntu 8.04 2008-04-24
Fedora 8 2007-11-08
SUSE 10.3 2007-10-04
Ubuntu 6.10 2006-10-26
Fedora 7 2007-05-31
Ubuntu 7.10 2007-10-18
Ubuntu 7.04 2007-04-19
SUSE 10.1 2006-05-11
Fedora 6 2006-10-24
Fedora 9 2008-05-13
Ubuntu 6.06 2006-06-01
Ubuntu 8.10 2008-10-30
Fedora 5 2006-03-20
哇!這個(gè)命令看起來(lái)很丑陋。但是它起作用了。僅用一步,我們就更改了文件中的日期格式。 它也是一個(gè)關(guān)于為什么有時(shí)候會(huì)開(kāi)玩笑地把正則表達(dá)式稱為是“只寫(xiě)”媒介的完美的例子。我們 能寫(xiě)正則表達(dá)式,但是有時(shí)候我們不能讀它們。在我們恐懼地忍不住要逃離此命令之前,讓我們看一下 怎樣來(lái)構(gòu)建它。首先,我們知道此命令有這樣一個(gè)基本的結(jié)構(gòu):
sed 's/regexp/replacement/' distros.txt
我們下一步是要弄明白一個(gè)正則表達(dá)式將要孤立出日期。因?yàn)槿掌谑?MM/DD/YYYY 格式,并且 出現(xiàn)在文本行的末尾,我們可以使用這樣的表達(dá)式:
[0-9]{2}/[0-9]{2}/[0-9]{4}$
此表達(dá)式匹配兩位數(shù)字,一個(gè)斜杠,兩位數(shù)字,一個(gè)斜杠,四位數(shù)字,以及行尾。如此關(guān)心regexp, 那么_replacement_又怎樣呢?為了解決此問(wèn)題,我們必須介紹一個(gè)正則表達(dá)式的新功能,它出現(xiàn) 在一些使用 BRE 的應(yīng)用程序中。這個(gè)功能叫做逆參照,像這樣工作:如果序列\(zhòng)n 出現(xiàn)在_replacement_中 ,這里 n 是指從 1 到 9 的數(shù)字,則這個(gè)序列指的是在前面正則表達(dá)式中相對(duì)應(yīng)的子表達(dá)式。為了 創(chuàng)建這個(gè)子表達(dá)式,我們簡(jiǎn)單地把它們用圓括號(hào)括起來(lái),像這樣:
([0-9]{2})/([0-9]{2})/([0-9]{4})$
現(xiàn)在我們有了三個(gè)子表達(dá)式。第一個(gè)表達(dá)式包含月份,第二個(gè)包含某月中的某天,以及第三個(gè)包含年份。 現(xiàn)在我們就可以構(gòu)建replacement,如下所示:
\3-\1-\2
此表達(dá)式給出了年份,一個(gè)斜杠,月份,一個(gè)斜杠,和某天。
sed 's/([0-9]{2})/([0-9]{2})/([0-9]{4})$/\3-\1-\2/' distros.txt
我們還有兩個(gè)問(wèn)題。第一個(gè)是在我們表達(dá)式中額外的斜杠將會(huì)迷惑 sed,當(dāng) sed 試圖解釋這個(gè) s 命令 的時(shí)候。第二個(gè)是因?yàn)?sed,默認(rèn)情況下,只接受基本的正則表達(dá)式,在表達(dá)式中的幾個(gè)字符會(huì) 被當(dāng)作文字字面值,而不是元字符。我們能夠解決這兩個(gè)問(wèn)題,通過(guò)反斜杠的自由應(yīng)用來(lái)轉(zhuǎn)義 令人不快的字符:
sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
你掌握了吧!
s 命令的另一個(gè)功能是使用可選標(biāo)志,其跟隨替代字符串。一個(gè)最重要的可選標(biāo)志是 g 標(biāo)志,其 指示 sed 對(duì)某個(gè)文本行全范圍地執(zhí)行查找和替代操作,不僅僅是對(duì)第一個(gè)實(shí)例,這是默認(rèn)行為。 這里有個(gè)例子:
[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/'
aaaBbbccc
我們看到雖然執(zhí)行了替換操作,但是只針對(duì)第一個(gè)字母 “b” 實(shí)例,然而剩余的實(shí)例沒(méi)有更改。通過(guò)添加 g 標(biāo)志, 我們能夠更改所有的實(shí)例:
[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/g'
aaaBBBccc
目前為止,通過(guò)命令行我們只讓 sed 執(zhí)行單個(gè)命令。使用-f 選項(xiàng),也有可能在一個(gè)腳本文件中構(gòu)建更加復(fù)雜的命令。 為了演示,我們將使用 se