鍍金池/ 教程/ Linux/ 文本處理
網(wǎng)絡(luò)系統(tǒng)
打印
重定向
使用命令
位置參數(shù)
權(quán)限
文本處理
疑難排解
layout: book-zh title: 自定制 shell 提示符
查找文件
layout: book-zh title: vi 簡介
shell 環(huán)境
什么是 shell
編譯程序
鍵盤高級(jí)操作技巧
流程控制:case 分支
流程控制:if 分支結(jié)構(gòu)
layout: book-zh title: 軟件包管理
進(jìn)程
存儲(chǔ)媒介
格式化輸出
編寫第一個(gè) Shell 腳本
啟動(dòng)一個(gè)項(xiàng)目
流程控制:while/until 循環(huán)
文件系統(tǒng)中跳轉(zhuǎn)
字符串和數(shù)字
讀取鍵盤輸入
歸檔和備份
探究操作系統(tǒng)
流程控制:for 循環(huán)
自頂向下設(shè)計(jì)
數(shù)組
操作文件和目錄
奇珍異寶
從 shell 眼中看世界
正則表達(dá)式

文本處理

所有類 Unix 的操作系統(tǒng)都非常依賴于被用于幾種數(shù)據(jù)類型存儲(chǔ)的文本文件。所以這很有道理, 有許多用于處理文本的工具。在這一章中,我們將看一些被用來“切割”文本的程序。在下一章中, 我們將查看更多的文本處理程序,但主要集中于文本格式化輸出程序和其它一些人們需要的工具。

這一章會(huì)重新拜訪一些老朋友,并且會(huì)給我們介紹一些新朋友:

  • cat – 連接文件并且打印到標(biāo)準(zhǔn)輸出

  • sort – 給文本行排序

  • uniq – 報(bào)告或者省略重復(fù)行

  • cut – 從每行中刪除文本區(qū)域

  • paste – 合并文件文本行

  • join – 基于某個(gè)共享字段來聯(lián)合兩個(gè)文件的文本行

  • comm – 逐行比較兩個(gè)有序的文件

  • diff – 逐行比較文件

  • patch – 給原始文件打補(bǔ)丁

  • tr – 翻譯或刪除字符

  • sed – 用于篩選和轉(zhuǎn)換文本的流編輯器

  • aspell – 交互式拼寫檢查器

文本應(yīng)用程序

到目前為止,我們已經(jīng)知道了一對(duì)文本編輯器(nano 和 vim),看過一堆配置文件,并且目睹了 許多命令的輸出都是文本格式。但是文本還被用來做什么? 它可以做很多事情。

文檔

許多人使用純文本格式來編寫文檔。雖然很容易看到一個(gè)小的文本文件對(duì)于保存簡單的筆記會(huì) 很有幫助,但是也有可能用文本格式來編寫大的文檔。一個(gè)流行的方法是先用文本格式來編寫一個(gè) 大的文檔,然后使用一種標(biāo)記語言來描述已完成文檔的格式。許多科學(xué)論文就是用這種方法編寫的, 因?yàn)榛?Unix 的文本處理系統(tǒng)位于支持技術(shù)學(xué)科作家所需要的高級(jí)排版布局的一流系統(tǒng)之列。

網(wǎng)頁

世界上最流行的電子文檔類型可能就是網(wǎng)頁了。網(wǎng)頁是文本文檔,它們使用 HTML(超文本標(biāo)記語言)或者是 XML (可擴(kuò)展的標(biāo)記語言)作為標(biāo)記語言來描述文檔的可視格式。

電子郵件

從本質(zhì)上來說,email 是一個(gè)基于文本的媒介。為了傳輸,甚至非文本的附件也被轉(zhuǎn)換成文本表示形式。 我們能看到這些,通過下載一個(gè) email 信息,然后用 less 來瀏覽它。我們將會(huì)看到這條信息開始于一個(gè)標(biāo)題, 其描述了信息的來源以及在傳輸過程中它接受到的處理,然后是信息的正文內(nèi)容。

打印輸出

在類 Unix 的系統(tǒng)中,輸出會(huì)以純文本格式發(fā)送到打印機(jī),或者如果頁面包含圖形,其會(huì)被轉(zhuǎn)換成 一種文本格式的頁面描述語言,以 PostScript 著稱,然后再被發(fā)送給一款能產(chǎn)生圖形點(diǎn)陣的程序, 最后被打印出來。

程序源碼

在類 Unix 系統(tǒng)中會(huì)發(fā)現(xiàn)許多命令行程序被用來支持系統(tǒng)管理和軟件開發(fā),并且文本處理程序也不例外。 許多文本處理程序被設(shè)計(jì)用來解決軟件開發(fā)問題。文本處理對(duì)于軟件開發(fā)者來言至關(guān)重要是因?yàn)樗械能浖?都起始于文本格式。源代碼,程序員實(shí)際編寫的一部分程序,總是文本格式。

回顧一些老朋友

回到第7章(重定向),我們已經(jīng)知道一些命令除了接受命令行參數(shù)之外,還能夠接受標(biāo)準(zhǔn)輸入。 那時(shí)候我們只是簡單地介紹了它們,但是現(xiàn)在我們將仔細(xì)地看一下它們是怎樣被用來執(zhí)行文本處理的。

cat

這個(gè) cat 程序具有許多有趣的選項(xiàng)。其中許多選項(xiàng)用來幫助更好的可視化文本內(nèi)容。一個(gè)例子是-A 選項(xiàng), 其用來在文本中顯示非打印字符。有些時(shí)候我們想知道是否控制字符嵌入到了我們的可見文本中。 最常用的控制字符是 tab 字符(而不是空格)和回車字符,在 MS-DOS 風(fēng)格的文本文件中回車符經(jīng)常作為 結(jié)束符出現(xiàn)。另一種常見情況是文件中包含末尾帶有空格的文本行。

讓我們創(chuàng)建一個(gè)測試文件,用 cat 程序作為一個(gè)簡單的文字處理器。為此,我們將鍵入 cat 命令(隨后指定了 用于重定向輸出的文件),然后輸入我們的文本,最后按下 Enter 鍵來結(jié)束這一行,然后按下組合鍵 Ctrl-d, 來指示 cat 程序,我們已經(jīng)到達(dá)文件末尾了。在這個(gè)例子中,我們文本行的開頭和末尾分別鍵入了一個(gè) tab 字符以及一些空格。

[me@linuxbox ~]$ cat > foo.txt
    The quick brown fox jumped over the lazy dog.
[me@linuxbox ~]$

下一步,我們將使用帶有-A 選項(xiàng)的 cat 命令來顯示這個(gè)文本:

[me@linuxbox ~]$ cat -A foo.txt
^IThe quick brown fox jumped over the lazy dog.       $
[me@linuxbox ~]$

在輸出結(jié)果中我們看到,這個(gè) tab 字符在我們的文本中由^I 字符來表示。這是一種常見的表示方法,意思是 “Control-I”,結(jié)果證明,它和 tab 字符是一樣的。我們也看到一個(gè)$字符出現(xiàn)在文本行真正的結(jié)尾處, 表明我們的文本包含末尾的空格。

MS-DOS 文本 Vs. Unix 文本

可能你想用 cat 程序在文本中查看非打印字符的一個(gè)原因是發(fā)現(xiàn)隱藏的回車符。那么 隱藏的回車符來自于哪里呢?它們來自于 DOS 和 Windows!Unix 和 DOS 在文本文件中定義每行 結(jié)束的方式不相同。Unix 通過一個(gè)換行符(ASCII 10)來結(jié)束一行,然而 MS-DOS 和它的 衍生品使用回車(ASCII 13)和換行字符序列來終止每個(gè)文本行。

有幾種方法能夠把文件從 DOS 格式轉(zhuǎn)變?yōu)?Unix 格式。在許多 Linux 系統(tǒng)中,有兩個(gè) 程序叫做 dos2unix 和 unix2dos,它們能在兩種格式之間轉(zhuǎn)變文本文件。然而,如果你 的系統(tǒng)中沒有安裝 dos2unix 程序,也不要擔(dān)心。文件從 DOS 格式轉(zhuǎn)變?yōu)?Unix 格式的過程非常 簡單;它只簡單地涉及到刪除違規(guī)的回車符。通過隨后本章中討論的一些程序,這個(gè)工作很容易 完成。

cat 程序也包含用來修改文本的選項(xiàng)。最著名的兩個(gè)選項(xiàng)是-n,其給文本行添加行號(hào)和-s, 禁止輸出多個(gè)空白行。我們這樣來說明:

[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è)測試文件 foo.txt 的新版本,其包含兩行文本,由兩個(gè)空白行分開。 經(jīng)由帶有-ns 選項(xiàng)的 cat 程序處理之后,多余的空白行被刪除,并且對(duì)保留的文本行進(jìn)行編號(hào)。 然而這并不是多個(gè)進(jìn)程在操作這個(gè)文本,只有一個(gè)進(jìn)程。

sort

這個(gè) sort 程序?qū)?biāo)準(zhǔn)輸入的內(nèi)容,或命令行中指定的一個(gè)或多個(gè)文件進(jìn)行排序,然后把排序 結(jié)果發(fā)送到標(biāo)準(zhǔn)輸出。使用與 cat 命令相同的技巧,我們能夠演示如何用 sort 程序來處理標(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 組合鍵來表示文件的結(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)。這里只是一部分列表:

表21-1: 常見的 sort 程序選項(xiàng)
選項(xiàng) 長選項(xiàng) 描述
-b --ignore-leading-blanks 默認(rèn)情況下,對(duì)整行進(jìn)行排序,從每行的第一個(gè)字符開始。這個(gè)選項(xiàng)導(dǎo)致 sort 程序忽略 每行開頭的空格,從第一個(gè)非空白字符開始排序。
-f --ignore-case 讓排序不區(qū)分大小寫。
-n --numeric-sort 基于字符串的長度來排序。使用此選項(xiàng)允許根據(jù)數(shù)字值執(zhí)行排序,而不是字母值。
-r --reverse 按相反順序排序。結(jié)果按照降序排列,而不是升序。
-k --key=field1[,field2] 對(duì)從 field1到 field2之間的字符排序,而不是整個(gè)文本行。看下面的討論。
-m --merge 把每個(gè)參數(shù)看作是一個(gè)預(yù)先排好序的文件。把多個(gè)文件合并成一個(gè)排好序的文件,而沒有執(zhí)行額外的排序。
-o --output=file 把排好序的輸出結(jié)果發(fā)送到文件,而不是標(biāo)準(zhǔn)輸出。
-t --field-separator=char 定義域分隔字符。默認(rèn)情況下,域由空格或制表符分隔。

雖然以上大多數(shù)選項(xiàng)的含義是不言自喻的,但是有些也不是。首先,讓我們看一下 -n 選項(xiàng),被用做數(shù)值排序。 通過這個(gè)選項(xiàng),有可能基于數(shù)值進(jìn)行排序。我們通過對(duì) du 命令的輸出結(jié)果排序來說明這個(gè)選項(xiàng),du 命令可以 確定最大的磁盤空間用戶。通常,這個(gè) du 命令列出的輸出結(jié)果按照路徑名來排序:

[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ù)值排序的 列表,來顯示 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

通過使用此 -nr 選項(xiàng),我們產(chǎn)生了一個(gè)反向的數(shù)值排序,最大數(shù)值排列在第一位。這種排序起作用是 因?yàn)閿?shù)值出現(xiàn)在每行的開頭。但是如果我們想要基于文件行中的某個(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 程序來完成此任務(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ù)庫這個(gè)術(shù)語應(yīng)用到上面的表格中,我們會(huì)說每行是一條記錄,并且每條記錄由多個(gè)字段組成, 例如文件屬性,鏈接數(shù),文件名,文件大小等等。sort 程序能夠處理獨(dú)立的字段。在數(shù)據(jù)庫術(shù)語中, 我們能夠指定一個(gè)或者多個(gè)關(guān)鍵字段,來作為排序的關(guān)鍵值。在上面的例子中,我們指定 n 和 r 選項(xiàng)來執(zhí)行相反的數(shù)值排序,并且指定 -k 5,讓 sort 程序使用第五字段作為排序的關(guān)鍵值。

這個(gè) k 選項(xiàng)非常有趣,而且還有很多特點(diǎn),但是首先我們需要講講 sort 程序怎樣來定義字段。 讓我們考慮一個(gè)非常簡單的文本文件,只有一行包含作者名字的文本。

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ā)行歷史,來做一系列實(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
...

恩,大部分正確。問題出現(xiàn)在 Fedora 的版本號(hào)上。因?yàn)樵谧址?“1” 出現(xiàn)在 “5” 之前,版本號(hào) “10” 在 最頂端,然而版本號(hào) “9” 卻掉到底端。

為了解決這個(gè)問題,我們必須依賴多個(gè)鍵值來排序。我們想要對(duì)第一個(gè)字段執(zhí)行字母排序,然后對(duì) 第三個(gè)字段執(zhí)行數(shù)值排序。sort 程序允許多個(gè) -k 選項(xiàng)的實(shí)例,所以可以指定多個(gè)排序關(guān)鍵值。事實(shí)上, 一個(gè)關(guān)鍵值可能包括一個(gè)字段區(qū)域。如果沒有指定區(qū)域(如同之前的例子),sort 程序會(huì)使用一個(gè)鍵值, 其始于指定的字段,一直擴(kuò)展到行尾。下面是多鍵值排序的語法:

[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)的長格式,但是 -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è)鍵值說明符的末尾,其用來指定排序的種類。這些 選項(xiàng)字母和 sort 程序的全局選項(xiàng)一樣:b(忽略開頭的空格),n(數(shù)值排序),r(逆向排序),等等。

我們列表中第三個(gè)字段包含的日期格式不利于排序。在計(jì)算機(jī)中,日期通常設(shè)置為 YYYY-MM-DD 格式, 這樣使按時(shí)間順序排序變得容易,但是我們的日期為美國格式 MM/DD/YYYY。那么我們?cè)鯓幽馨凑?時(shí)間順序來排列這個(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
...

通過指定 -k 3.7,我們指示 sort 程序使用一個(gè)排序鍵值,其始于第三個(gè)字段中的第七個(gè)字符,對(duì)應(yīng)于 年的開頭。同樣地,我們指定 -k 3.1和 -k 3.4來分離日期中的月和日。 我們也添加了 n 和 r 選項(xiàng)來實(shí)現(xiàn)一個(gè)逆向的數(shù)值排序。這個(gè) b 選項(xiàng)用來刪除日期字段中開頭的空格( 行與行之間的空格數(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è)文件的字段之間通過冒號(hào)分隔開,所以我們?cè)鯓邮褂靡粋€(gè) key 字段來排序這個(gè)文件?sort 程序提供 了一個(gè) -t 選項(xiàng)來定義分隔符。按照第七個(gè)字段(帳戶的默認(rèn) shell)來排序此 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

通過指定冒號(hào)字符做為字段分隔符,我們能按照第七個(gè)字段來排序。

uniq

與 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 程序一塊使用,來清理重復(fù)的輸出。


uniq 程序是一個(gè)傳統(tǒng)的 Unix 工具,經(jīng)常與 sort 程序一塊使用,但是這個(gè) GNU 版本的 sort 程序支持一個(gè) -u 選項(xiàng),其可以從排好序的輸出結(jié)果中刪除重復(fù)行。


讓我們創(chuàng)建一個(gè)文本文件,來實(shí)驗(yàn)一下:

[me@linuxbox ~]$ cat > foo.txt
a
b
c
a
b
c

記住輸入 Ctrl-d 來終止標(biāo)準(zhǔn)輸入?,F(xiàn)在,如果我們對(duì)文本文件執(zhí)行 uniq 命令:

[me@linuxbox ~]$ uniq foo.txt
a
b
c
a
b
c

輸出結(jié)果與原始文件沒有差異;重復(fù)行沒有被刪除。實(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):

表21-2: 常用的 uniq 選項(xiàng)
選項(xiàng) 說明
-c 輸出所有的重復(fù)行,并且每行開頭顯示重復(fù)的次數(shù)。
-d 只輸出重復(fù)行,而不是特有的文本行。
-f n 忽略每行開頭的 n 個(gè)字段,字段之間由空格分隔,正如 sort 程序中的空格分隔符;然而, 不同于 sort 程序,uniq 沒有選項(xiàng)來設(shè)置備用的字段分隔符。
-i 在比較文本行的時(shí)候忽略大小寫。
-s n 跳過(忽略)每行開頭的 n 個(gè)字符。
-u 只是輸出獨(dú)有的文本行。這是默認(rèn)的。

這里我們看到 uniq 被用來報(bào)告文本文件中重復(fù)行的次數(shù),使用這個(gè)-c 選項(xiàng):

[me@linuxbox ~]$ sort foo.txt | uniq -c
        2 a
        2 b
        2 c

切片和切塊

下面我們將要討論的三個(gè)程序用來從文件中獲得文本列,并且以有用的方式重組它們。

cut

這個(gè) cut 程序被用來從文本行中抽取文本,并把其輸出到標(biāo)準(zhǔn)輸出。它能夠接受多個(gè)文件參數(shù)或者 標(biāo)準(zhǔn)輸入。

從文本行中指定要抽取的文本有些麻煩,使用以下選項(xiàng):

表21-3: cut 程序選擇項(xiàng)
選項(xiàng) 說明
-c char_list 從文本行中抽取由 char_list 定義的文本。這個(gè)列表可能由一個(gè)或多個(gè)逗號(hào) 分隔開的數(shù)值區(qū)間組成。
-f field_list 從文本行中抽取一個(gè)或多個(gè)由 field_list 定義的字段。這個(gè)列表可能 包括一個(gè)或多個(gè)字段,或由逗號(hào)分隔開的字段區(qū)間。
-d delim_char 當(dāng)指定-f 選項(xiàng)之后,使用 delim_char 做為字段分隔符。默認(rèn)情況下, 字段之間必須由單個(gè) tab 字符分隔開。
--complement 抽取整個(gè)文本行,除了那些由-c 和/或-f 選項(xiàng)指定的文本。

正如我們所看到的,cut 程序抽取文本的方式相當(dāng)不靈活。cut 命令最好用來從其它程序產(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$

看起來不錯(cuò)。字段之間僅僅是單個(gè) tab 字符,沒有嵌入空格。因?yàn)檫@個(gè)文件使用了 tab 而不是空格, 我們將使用 -f 選項(xiàng)來抽取一個(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 分隔開的,最好用 cut 來抽取字段而不是字符。這是因?yàn)橐粋€(gè)由 tab 分離的文件, 每行不太可能包含相同的字符數(shù),這就使計(jì)算每行中字符的位置變得困難或者是不可能。在以上事例中,然而, 我們已經(jīng)抽取了一個(gè)字段,幸運(yùn)地是其包含地日期長度相同,所以通過從每行中抽取年份,我們能展示怎樣 來抽取字符:

[me@linuxbox ~]$ cut -f 3 distros.txt | cut -c 7-10
2006
2008
2007
2006
2007
2006
2008
2006
2008
2006

通過對(duì)我們的列表再次運(yùn)行 cut 命令,我們能夠抽取從位置7到10的字符,其對(duì)應(yīng)于日期字段的年份。 這個(gè) 7-10 表示法是一個(gè)區(qū)間的例子。cut 命令手冊(cè)包含了一個(gè)如何指定區(qū)間的完整描述。

展開 Tabs

distros.txt 的文件格式很適合使用 cut 程序來抽取字段。但是如果我們想要 cut 程序 按照字符,而不是字段來操作一個(gè)文件,那又怎樣呢?這要求我們用相應(yīng)數(shù)目的空格來 代替 tab 字符。幸運(yùn)地是,GNU 的 Coreutils 軟件包有一個(gè)工具來解決這個(gè)問題。這個(gè) 程序名為 expand,它既可以接受一個(gè)或多個(gè)文件參數(shù),也可以接受標(biāo)準(zhǔn)輸入,并且把 修改過的文本送到標(biāo)準(zhǔn)輸出。

如果我們通過 expand 來處理 distros.txt 文件,我們能夠使用 cut -c 命令來從文件中抽取 任意區(qū)間內(nèi)的字符。例如,我們能夠使用以下命令來從列表中抽取發(fā)行年份,通過展開 此文件,再使用 cut 命令,來抽取從位置 23 開始到行尾的每一個(gè)字符:

[me@linuxbox ~]$ expand distros.txt | cut -c 23-

Coreutils 軟件包也提供了 unexpand 程序,用 tab 來代替空格。

當(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)做為字段分隔符。

paste

這個(gè) paste 命令的功能正好與 cut 相反。它會(huì)添加一個(gè)或多個(gè)文本列到文件中,而不是從文件中抽取文本列。 它通過讀取多個(gè)文件,然后把每個(gè)文件中的字段整合成單個(gè)文本流,輸入到標(biāo)準(zhǔn)輸出。類似于 cut 命令, paste 接受多個(gè)文件參數(shù)和 / 或標(biāo)準(zhǔn)輸入。為了說明 paste 是怎樣工作的,我們將會(huì)對(duì) distros.txt 文件 動(dòng)手術(shù),來產(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è)過程,使用 paste 命令來把日期列放到發(fā)行版名字 和版本號(hào)的前面,這樣就創(chuàng)建了一個(gè)年代列表。通過使用 paste 命令,然后按照期望的順序來安排它的 參數(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

在某些方面,join 命令類似于 paste,它會(huì)往文件中添加列,但是它使用了獨(dú)特的方法來完成。 一個(gè) join 操作通常與關(guān)系型數(shù)據(jù)庫有關(guān)聯(lián),在關(guān)系型數(shù)據(jù)庫中來自多個(gè)享有共同關(guān)鍵域的表格的 數(shù)據(jù)結(jié)合起來,得到一個(gè)期望的結(jié)果。這個(gè) join 程序執(zhí)行相同的操作。它把來自于多個(gè)基于共享 關(guān)鍵域的文件的數(shù)據(jù)結(jié)合起來。

為了知道在關(guān)系數(shù)據(jù)庫中是怎樣使用 join 操作的,讓我們想象一個(gè)很小的數(shù)據(jù)庫,這個(gè)數(shù)據(jù)庫由兩個(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), 和訂購的貨品(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é)合起來,得到一個(gè)有用的結(jié)果,例如準(zhǔn)備 一張發(fā)貨單。通過使用兩個(gè)表格 CUSTNUM 數(shù)字域中匹配的數(shù)值,一個(gè) join 操作會(huì)產(chǎn)生以下結(jié)果:

FNAME       LNAME       QUAN ITEM
=====       =====       ==== ====
John        Smith       1    Blue Widget

為了說明 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è)空格作為輸出字段 的界定符。這種行為可以通過指定的選項(xiàng)來修改。詳細(xì)信息,參考 join 命令手冊(cè)。

比較文本

通常比較文本文件的版本很有幫助。對(duì)于系統(tǒng)管理員和軟件開發(fā)者來說,這個(gè)尤為重要。 一名系統(tǒng)管理員可能,例如,需要拿現(xiàn)有的配置文件與先前的版本做比較,來診斷一個(gè)系統(tǒng)錯(cuò)誤。 同樣的,一名程序員經(jīng)常需要查看程序的修改。

comm

這個(gè) comm 程序會(huì)比較兩個(gè)文本文件,并且會(huì)顯示每個(gè)文件特有的文本行和共有的文把行。 為了說明問題,通過使用 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 命令來比較這兩個(gè)文件:

[me@linuxbox ~]$ comm file1.txt file2.txt
a
        b
        c
        d
    e

正如我們所見到的,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

diff

類似于 comm 程序,diff 程序被用來監(jiān)測文件之間的差異。然而,diff 是一款更加復(fù)雜的工具,它支持 許多輸出格式,并且一次能處理許多文本文件。軟件開發(fā)員經(jīng)常使用 diff 程序來檢查不同程序源碼 版本之間的更改,diff 能夠遞歸地檢查源碼目錄,經(jīng)常稱之為源碼樹。diff 程序的一個(gè)常見用例是 創(chuàng)建 diff 文件或者補(bǔ)丁,它會(huì)被其它程序使用,例如 patch 程序(我們一會(huì)兒討論),來把文件 從一個(gè)版本轉(zhuǎn)換為另一個(gè)版本。

如果我們使用 diff 程序,來查看我們之前的文件實(shí)例:

[me@linuxbox ~]$ diff file1.txt file2.txt
1d0
< a
4a4
> e

我們看到 diff 程序的默認(rèn)輸出風(fēng)格:對(duì)兩個(gè)文件之間差異的簡短描述。在默認(rèn)格式中, 每組的更改之前都是一個(gè)更改命令,其形式為 range operation range , 用來描述要求更改的位置和類型,從而把第一個(gè)文件轉(zhuǎn)變?yōu)榈诙€(gè)文件:

表21-4: diff 更改命令
改變 說明
r1ar2 把第二個(gè)文件中位置 r2 處的文件行添加到第一個(gè)文件中的 r1 處。
r1cr2 用第二個(gè)文件中位置 r2 處的文本行更改(替代)位置 r1 處的文本行。
r1dr2 刪除第一個(gè)文件中位置 r1 處的文本行,這些文本行將會(huì)出現(xiàn)在第二個(gè)文件中位置 r2 處。

在這種格式中,一個(gè)范圍就是由逗號(hào)分隔開的開頭行和結(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í)間戳開頭。第一個(gè)文件用星號(hào)做標(biāo)記,第二個(gè)文件用短橫線做標(biāo)記。 縱觀列表的其它部分,這些標(biāo)記將象征它們各自代表的文件。下一步,我們看到幾組修改, 包括默認(rèn)的周圍上下文行數(shù)。在第一組中,我們看到:

*** 1,4 ***

其表示第一個(gè)文件中從第一行到第四行的文本行。隨后我們看到:

--- 1,4 ---

這表示第二個(gè)文件中從第一行到第四行的文本行。在更改組內(nèi),文本行以四個(gè)指示符之一開頭:

表21-5: diff 上下文模式更改指示符
指示符 意思
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)一模式相似于上下文模式,但是更加簡潔。通過 -u 選項(xiàng)來指定它:

[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é)果簡短。在我們上述實(shí)例中,我們看到類似于上下文模式中的文件時(shí)間戳,其緊緊跟隨字符串 @@ -1,4 +1,4 @@。這行字符串表示了在更改組中描述的第一個(gè)文件中的文本行和第二個(gè)文件中的文本行。 這行字符串之后就是文本行本身,與三行默認(rèn)的上下文。每行以可能的三個(gè)字符中的一個(gè)開頭:

表21-6: diff 統(tǒng)一模式更改指示符
字符 意思
空格 兩個(gè)文件都包含這一行。
- 在第一個(gè)文件中刪除這一行。
+ 添加這一行到第一個(gè)文件中。

patch

這個(gè) patch 程序被用來把更改應(yīng)用到文本文件中。它接受從 diff 程序的輸出,并且通常被用來 把較老的文件版本轉(zhuǎn)變?yōu)檩^新的文件版本。讓我們考慮一個(gè)著名的例子。Linux 內(nèi)核是由一個(gè) 大型的,組織松散的貢獻(xiàn)者團(tuán)隊(duì)開發(fā)而成,這些貢獻(xiàn)者會(huì)提交固定的少量更改到源碼包中。 這個(gè) Linux 內(nèi)核由幾百萬行代碼組成,雖然每個(gè)貢獻(xiàn)者每次所做的修改相當(dāng)少。對(duì)于一個(gè)貢獻(xiàn)者 來說,每做一個(gè)修改就給每個(gè)開發(fā)者發(fā)送整個(gè)的內(nèi)核源碼樹,這是沒有任何意義的。相反, 提交一個(gè) diff 文件。一個(gè) diff 文件包含先前的內(nèi)核版本與帶有貢獻(xiàn)者修改的新版本之間的差異。 然后一個(gè)接受者使用 patch 程序,把這些更改應(yīng)用到他自己的源碼樹中。使用 diff/patch 組合提供了 兩個(gè)重大優(yōu)點(diǎn):

  1. 一個(gè) diff 文件非常小,與整個(gè)源碼樹的大小相比較而言。

  2. 一個(gè) diff 文件簡潔地顯示了所做的修改,從而允許程序補(bǔ)丁的審閱者能快速地評(píng)估它。

當(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)支持遞歸目錄樹。

一旦創(chuàng)建了 diff 文件,我們就能應(yīng)用它,把舊文件修補(bǔ)成新文件。

patch < diff_file

我們將使用測試文件來說明:

[me@linuxbox ~]$ diff -Naur file1.txt file2.txt &gt; patchfile.txt
[me@linuxbox ~]$ patch &lt; patchfile.txt
patching file file1.txt
[me@linuxbox ~]$ cat file1.txt
b
c
d
e

在這個(gè)例子中,我們創(chuàng)建了一個(gè)名為 patchfile.txt 的 diff 文件,然后使用 patch 程序, 來應(yīng)用這個(gè)補(bǔ)丁。注意我們沒有必要指定一個(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í)用程序可以被用來分析和編輯補(bǔ)丁。

運(yùn)行時(shí)編輯

我們對(duì)于文本編輯器的經(jīng)驗(yàn)是它們主要是交互式的,意思是我們手動(dòng)移動(dòng)光標(biāo),然后輸入我們的修改。 然而,也有非交互式的方法來編輯文本。有可能,例如,通過單個(gè)命令把一系列修改應(yīng)用到多個(gè)文件中。

tr

這個(gè) tr 程序被用來更改字符。我們可以把它看作是一種基于字符的查找和替換操作。 換字是一種把字符從一個(gè)字母轉(zhuǎn)換為另一個(gè)字母的過程。例如,把小寫字母轉(zhuǎn)換成大寫字母就是 換字。我們可以通過 tr 命令來執(zhí)行這樣的轉(zhuǎn)換,如下所示:

[me@linuxbox ~]$ echo "lowercase letters" | tr a-z A-Z
LOWERCASE LETTERS

正如我們所見,tr 命令操作標(biāo)準(zhǔn)輸入,并把結(jié)果輸出到標(biāo)準(zhǔn)輸出。tr 命令接受兩個(gè)參數(shù):要被轉(zhuǎn)換的字符集以及 相對(duì)應(yīng)的轉(zhuǎn)換后的字符集。字符集可以用三種方式來表示:

  1. 一個(gè)枚舉列表。例如, ABCDEFGHIJKLMNOPQRSTUVWXYZ

  2. 一個(gè)字符域。例如,A-Z 。注意這種方法有時(shí)候面臨與其它命令相同的問題,歸因于 語系的排序規(guī)則,因此應(yīng)該謹(jǐn)慎使用。

  3. POSIX 字符類。例如,[:upper:]

大多數(shù)情況下,兩個(gè)字符集應(yīng)該長度相同;然而,有可能第一個(gè)集合大于第二個(gè),尤其如果我們 想要把多個(gè)字符轉(zhuǎn)換為單個(gè)字符:

[me@linuxbox ~]$ echo "lowercase letters" | tr [:lower:] A
AAAAAAAAA AAAAAAA

除了換字之外,tr 命令能允許字符從輸入流中簡單地被刪除。在之前的章節(jié)中,我們討論了轉(zhuǎn)換 MS-DOS 文本文件為 Unix 風(fēng)格文本的問題。為了執(zhí)行這個(gè)轉(zhuǎn)換,每行末尾的回車符需要被刪除。 這個(gè)可以通過 tr 命令來執(zhí)行,如下所示:

tr -d '\r' < dos_file > unix_file

這里的 dos_file 是需要被轉(zhuǎn)換的文件,unix_file 是轉(zhuǎn)換后的結(jié)果。這種形式的命令使用轉(zhuǎn)義序列 \r 來代表回車符。查看 tr 命令所支持地完整的轉(zhuǎn)義序列和字符類別列表,試試下面的命令:

[me@linuxbox ~]$ tr --help

ROT13: 不那么秘密的編碼環(huán)

tr 命令的一個(gè)有趣的用法是執(zhí)行 ROT13文本編碼。ROT13是一款微不足道的基于一種簡易的替換暗碼的 加密類型。把 ROT13稱為“加密”是大方的;“文本模糊處理”更準(zhǔn)確些。有時(shí)候它被用來隱藏文本中潛在的攻擊內(nèi)容。 這個(gè)方法就是簡單地把每個(gè)字符在字母表中向前移動(dòng)13位。因?yàn)橐苿?dòng)的位數(shù)是可能的26個(gè)字符的一半, 所以對(duì)文本再次執(zhí)行這個(gè)算法,就恢復(fù)到了它最初的形式。通過 tr 命令來執(zhí)行這種編碼:

echo "secret text" | tr a-zA-Z n-za-mN-ZA-M

frperg grkg

再次執(zhí)行相同的過程,得到翻譯結(jié)果:

_echo "frperg grkg" | tr a-zA-Z n-za-mN-ZA-M+

secret text

大量的 email 程序和 USENET 新聞讀者都支持 ROT13 編碼。Wikipedia 上面有一篇關(guān)于這個(gè)主題的好文章:

http://en.wikipedia.org/wiki/ROT13

tr 也可以完成另一個(gè)技巧。使用-s 選項(xiàng),tr 命令能“擠壓”(刪除)重復(fù)的字符實(shí)例:

[me@linuxbox ~]$ echo "aaabbbccc" | tr -s ab
abccc

這里我們有一個(gè)包含重復(fù)字符的字符串。通過給 tr 命令指定字符集“ab”,我們能夠消除字符集中 字母的重復(fù)實(shí)例,然而會(huì)留下不屬于字符集的字符(“c”)無更改。注意重復(fù)的字符必須是相鄰的。 如果它們不相鄰:

[me@linuxbox ~]$ echo "abcabcabc" | tr -s ab
abcabcabc

那么擠壓會(huì)沒有效果。

sed

名字 sed 是 stream editor(流編輯器)的簡稱。它對(duì)文本流進(jìn)行編輯,要不是一系列指定的文件, 要不就是標(biāo)準(zhǔn)輸入。sed 是一款強(qiáng)大的,并且有些復(fù)雜的程序(有整本內(nèi)容都是關(guān)于 sed 程序的書籍), 所以在這里我們不會(huì)詳盡的討論它。

總之,sed 的工作方式是要不給出單個(gè)編輯命令(在命令行中)要不就是包含多個(gè)命令的腳本文件名, 然后它就按行來執(zhí)行這些命令。這里有一個(gè)非常簡單的 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 中的命令開始于單個(gè)字符。在上面的例子中,這個(gè)替換命令由字母 s 來代表,其后跟著查找 和替代字符串,斜杠字符做為分隔符。分隔符的選擇是隨意的。按照慣例,經(jīng)常使用斜杠字符, 但是 sed 將會(huì)接受緊隨命令之后的任意字符做為分隔符。我們可以按照這種方式來執(zhí)行相同的命令:

[me@linuxbox ~]$ echo "front" | sed 's\_front\_back\_'
back

通過緊跟命令之后使用下劃線字符,則它變成界定符。sed 可以設(shè)置界定符的能力,使命令的可讀性更強(qiáng), 正如我們將看到的.

sed 中的大多數(shù)命令之前都會(huì)帶有一個(gè)地址,其指定了輸入流中要被編輯的文本行。如果省略了地址, 然后會(huì)對(duì)輸入流的每一行執(zhí)行編輯命令。最簡單的地址形式是一個(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)槲覀兊妮斎肓鳑]有第二行。地址可以用許多方式來表達(dá)。這里是 最常用的:

表21-7: sed 地址表示法
地址 說明
n 行號(hào),n 是一個(gè)正整數(shù)。
$ 最后一行。
/regexp/ 所有匹配一個(gè) POSIX 基本正則表達(dá)式的文本行。注意正則表達(dá)式通過 斜杠字符界定。選擇性地,這個(gè)正則表達(dá)式可能由一個(gè)備用字符界定,通過\cregexpc 來 指定表達(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 可能是上述任意的地址形式。

通過使用這一章中早前的 distros.txt 文件,我們將演示不同種類的地址表示法。首先,一系列行號(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è)例子中,我們打印出一系列的文本行,開始于第一行,直到第五行。為此,我們使用 p 命令, 其就是簡單地把匹配的文本行打印出來。然而為了高效,我們必須包含選項(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

通過包含由斜杠界定的正則表達(dá)式 \/SUSE\/,我們能夠孤立出包含它的文本行,和 grep 程序的功能 是相同的。

最后,我們將試著否定上面的操作,通過給這個(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è)更加全面的基本編輯命令列表:

表21-8: sed 基本編輯命令
命令 說明
= 輸出當(dāng)前的行號(hào)。
a 在當(dāng)前行之后追加文本。
d 刪除當(dāng)前行。
i 在當(dāng)前行之前插入文本。
p 打印當(dāng)前行。默認(rèn)情況下,sed 程序打印每一行,并且只是編輯文件中匹配 指定地址的文本行。通過指定-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)志,來修改 s 命令的行為。
y/set1/set2 執(zhí)行字符轉(zhuǎn)寫操作,通過把 set1 中的字符轉(zhuǎn)變?yōu)橄鄬?duì)應(yīng)的 set2 中的字符。 注意不同于 tr 程序,sed 要求兩個(gè)字符集合具有相同的長度。

到目前為止,這個(gè) s 命令是最常使用的編輯命令。我們將僅僅演示一些它的功能,通過編輯我們的 distros.txt 文件。我們以前討論過 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è)命令看起來很丑陋。但是它起作用了。僅用一步,我們就更改了文件中的日期格式。 它也是一個(gè)關(guān)于為什么有時(shí)候會(huì)開玩笑地把正則表達(dá)式稱為是“只寫”媒介的完美的例子。我們 能寫正則表達(dá)式,但是有時(shí)候我們不能讀它們。在我們恐懼地忍不住要逃離此命令之前,讓我們看一下 怎樣來構(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_又怎樣呢?為了解決此問題,我們必須介紹一個(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á)式,我們簡單地把它們用圓括號(hào)括起來,像這樣:

([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è)問題。第一個(gè)是在我們表達(dá)式中額外的斜杠將會(huì)迷惑 sed,當(dāng) sed 試圖解釋這個(gè) s 命令 的時(shí)候。第二個(gè)是因?yàn)?sed,默認(rèn)情況下,只接受基本的正則表達(dá)式,在表達(dá)式中的幾個(gè)字符會(huì) 被當(dāng)作文字字面值,而不是元字符。我們能夠解決這兩個(gè)問題,通過反斜杠的自由應(yīng)用來轉(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í)例沒有更改。通過添加 g 標(biāo)志, 我們能夠更改所有的實(shí)例:

[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/g'
aaaBBBccc

目前為止,通過命令行我們只讓 sed 執(zhí)行單個(gè)命令。使用-f 選項(xiàng),也有可能在一個(gè)腳本文件中構(gòu)建更加復(fù)雜的命令。 為了演示,我們將使用 se