所有類 Unix 的操作系統都非常依賴于被用于幾種數據類型存儲的文本文件。所以這很有道理, 有許多用于處理文本的工具。在這一章中,我們將看一些被用來“切割”文本的程序。在下一章中, 我們將查看更多的文本處理程序,但主要集中于文本格式化輸出程序和其它一些人們需要的工具。
這一章會重新拜訪一些老朋友,并且會給我們介紹一些新朋友:
> * cat – 連接文件并且打印到標準輸出
> * sort – 給文本行排序
> * uniq – 報告或者省略重復行
> * cut – 從每行中刪除文本區域
> * paste – 合并文件文本行
> * join – 基于某個共享字段來聯合兩個文件的文本行
> * comm – 逐行比較兩個有序的文件
> * diff – 逐行比較文件
> * patch – 給原始文件打補丁
> * tr – 翻譯或刪除字符
> * sed – 用于篩選和轉換文本的流編輯器
> * aspell – 交互式拼寫檢查器
## 文本應用程序
到目前為止,我們已經知道了一對文本編輯器(nano 和 vim),看過一堆配置文件,并且目睹了 許多命令的輸出都是文本格式。但是文本還被用來做什么? 它可以做很多事情。
### 文檔
許多人使用純文本格式來編寫文檔。雖然很容易看到一個小的文本文件對于保存簡單的筆記會 很有幫助,但是也有可能用文本格式來編寫大的文檔。一個流行的方法是先用文本格式來編寫一個 大的文檔,然后使用一種標記語言來描述已完成文檔的格式。許多科學論文就是用這種方法編寫的, 因為基于 Unix 的文本處理系統位于支持技術學科作家所需要的高級排版布局的一流系統之列。
### 網頁
世界上最流行的電子文檔類型可能就是網頁了。網頁是文本文檔,它們使用 HTML(超文本標記語言)或者是 XML (可擴展的標記語言)作為標記語言來描述文檔的可視格式。
### 電子郵件
從本質上來說,email 是一個基于文本的媒介。為了傳輸,甚至非文本的附件也被轉換成文本表示形式。 我們能看到這些,通過下載一個 email 信息,然后用 less 來瀏覽它。我們將會看到這條信息開始于一個標題, 其描述了信息的來源以及在傳輸過程中它接受到的處理,然后是信息的正文內容。
### 打印輸出
在類 Unix 的系統中,輸出會以純文本格式發送到打印機,或者如果頁面包含圖形,其會被轉換成 一種文本格式的頁面描述語言,以 PostScript 著稱,然后再被發送給一款能產生圖形點陣的程序, 最后被打印出來。
### 程序源碼
在類 Unix 系統中會發現許多命令行程序被用來支持系統管理和軟件開發,并且文本處理程序也不例外。 許多文本處理程序被設計用來解決軟件開發問題。文本處理對于軟件開發者來言至關重要是因為所有的軟件 都起始于文本格式。源代碼,程序員實際編寫的一部分程序,總是文本格式。
## 回顧一些老朋友
回到第7章(重定向),我們已經知道一些命令除了接受命令行參數之外,還能夠接受標準輸入。 那時候我們只是簡單地介紹了它們,但是現在我們將仔細地看一下它們是怎樣被用來執行文本處理的。
### cat
這個 cat 程序具有許多有趣的選項。其中許多選項用來幫助更好的可視化文本內容。一個例子是-A 選項, 其用來在文本中顯示非打印字符。有些時候我們想知道是否控制字符嵌入到了我們的可見文本中。 最常用的控制字符是 tab 字符(而不是空格)和回車字符,在 MS-DOS 風格的文本文件中回車符經常作為 結束符出現。另一種常見情況是文件中包含末尾帶有空格的文本行。
讓我們創建一個測試文件,用 cat 程序作為一個簡單的文字處理器。為此,我們將鍵入 cat 命令(隨后指定了 用于重定向輸出的文件),然后輸入我們的文本,最后按下 Enter 鍵來結束這一行,然后按下組合鍵 Ctrl-d, 來指示 cat 程序,我們已經到達文件末尾了。在這個例子中,我們文本行的開頭和末尾分別鍵入了一個 tab 字符以及一些空格。
~~~
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox jumped over the lazy dog.
[me@linuxbox ~]$
~~~
下一步,我們將使用帶有-A 選項的 cat 命令來顯示這個文本:
~~~
[me@linuxbox ~]$ cat -A foo.txt
^IThe quick brown fox jumped over the lazy dog. $
[me@linuxbox ~]$
~~~
在輸出結果中我們看到,這個 tab 字符在我們的文本中由^I 字符來表示。這是一種常見的表示方法,意思是 “Control-I”,結果證明,它和 tab 字符是一樣的。我們也看到一個$字符出現在文本行真正的結尾處, 表明我們的文本包含末尾的空格。
> MS-DOS 文本 Vs. Unix 文本
>
> 可能你想用 cat 程序在文本中查看非打印字符的一個原因是發現隱藏的回車符。那么 隱藏的回車符來自于哪里呢?它們來自于 DOS 和 Windows!Unix 和 DOS 在文本文件中定義每行 結束的方式不相同。Unix 通過一個換行符(ASCII 10)來結束一行,然而 MS-DOS 和它的 衍生品使用回車(ASCII 13)和換行字符序列來終止每個文本行。
>
> 有幾種方法能夠把文件從 DOS 格式轉變為 Unix 格式。在許多 Linux 系統中,有兩個 程序叫做 dos2unix 和 unix2dos,它們能在兩種格式之間轉變文本文件。然而,如果你 的系統中沒有安裝 dos2unix 程序,也不要擔心。文件從 DOS 格式轉變為 Unix 格式的過程非常 簡單;它只簡單地涉及到刪除違規的回車符。通過隨后本章中討論的一些程序,這個工作很容易 完成。
cat 程序也包含用來修改文本的選項。最著名的兩個選項是-n,其給文本行添加行號和-s, 禁止輸出多個空白行。我們這樣來說明:
~~~
[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 ~]$
~~~
在這個例子里,我們創建了一個測試文件 foo.txt 的新版本,其包含兩行文本,由兩個空白行分開。 經由帶有-ns 選項的 cat 程序處理之后,多余的空白行被刪除,并且對保留的文本行進行編號。 然而這并不是多個進程在操作這個文本,只有一個進程。
### sort
這個 sort 程序對標準輸入的內容,或命令行中指定的一個或多個文件進行排序,然后把排序 結果發送到標準輸出。使用與 cat 命令相同的技巧,我們能夠演示如何用 sort 程序來處理標準輸入:
~~~
[me@linuxbox ~]$ sort > foo.txt
c
b
a
[me@linuxbox ~]$ cat foo.txt
a
b
c
~~~
輸入命令之后,我們鍵入字母“c”,“b”,和“a”,然后再按下 Ctrl-d 組合鍵來表示文件的結尾。 隨后我們查看生成的文件,看到文本行有序地顯示。
因為 sort 程序能接受命令行中的多個文件作為參數,所以有可能把多個文件合并成一個有序的文件。例如, 如果我們有三個文本文件,想要把它們合并為一個有序的文件,我們可以這樣做:
~~~
sort file1.txt file2.txt file3.txt > final_sorted_list.txt
~~~
sort 程序有幾個有趣的選項。這里只是一部分列表:
表21-1: 常見的 sort 程序選項
| 選項 | 長選項 | 描述 |
|-------|-------|-------|
| -b | --ignore-leading-blanks | 默認情況下,對整行進行排序,從每行的第一個字符開始。這個選項導致 sort 程序忽略 每行開頭的空格,從第一個非空白字符開始排序。 |
| -f | --ignore-case | 讓排序不區分大小寫。 |
| -n | --numeric-sort | 基于字符串的長度來排序。使用此選項允許根據數字值執行排序,而不是字母值。 |
| -r | --reverse | 按相反順序排序。結果按照降序排列,而不是升序。 |
| -k | --key=field1[,field2] | 對從 field1到 field2之間的字符排序,而不是整個文本行。看下面的討論。 |
| -m | --merge | 把每個參數看作是一個預先排好序的文件。把多個文件合并成一個排好序的文件,而沒有執行額外的排序。 |
| -o | --output=file | 把排好序的輸出結果發送到文件,而不是標準輸出。 |
| -t | --field-separator=char | 定義域分隔字符。默認情況下,域由空格或制表符分隔。 |
雖然以上大多數選項的含義是不言自喻的,但是有些也不是。首先,讓我們看一下 -n 選項,被用做數值排序。 通過這個選項,有可能基于數值進行排序。我們通過對 du 命令的輸出結果排序來說明這個選項,du 命令可以 確定最大的磁盤空間用戶。通常,這個 du 命令列出的輸出結果按照路徑名來排序:
~~~
[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
~~~
在這個例子里面,我們把結果管道到 head 命令,把輸出結果限制為前 10 行。我們能夠產生一個按數值排序的 列表,來顯示 10 個最大的空間消費者:
~~~
[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 選項,我們產生了一個反向的數值排序,最大數值排列在第一位。這種排序起作用是 因為數值出現在每行的開頭。但是如果我們想要基于文件行中的某個數值排序,又會怎樣呢? 例如,命令 ls -l 的輸出結果:
~~~
[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 程序能按照文件大小對輸出結果進行排序,我們也能夠使用 sort 程序來完成此任務:
~~~
[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 程序的許多用法都涉及到處理表格數據,例如上面 ls 命令的輸出結果。如果我們 把數據庫這個術語應用到上面的表格中,我們會說每行是一條記錄,并且每條記錄由多個字段組成, 例如文件屬性,鏈接數,文件名,文件大小等等。sort 程序能夠處理獨立的字段。在數據庫術語中, 我們能夠指定一個或者多個關鍵字段,來作為排序的關鍵值。在上面的例子中,我們指定 n 和 r 選項來執行相反的數值排序,并且指定 -k 5,讓 sort 程序使用第五字段作為排序的關鍵值。
這個 k 選項非常有趣,而且還有很多特點,但是首先我們需要講講 sort 程序怎樣來定義字段。 讓我們考慮一個非常簡單的文本文件,只有一行包含作者名字的文本。
~~~
William Shotts
~~~
默認情況下,sort 程序把此行看作有兩個字段。第一個字段包含字符:
和第二個字段包含字符:
意味著空白字符(空格和制表符)被當作是字段間的界定符,當執行排序時,界定符會被 包含在字段當中。再看一下 ls 命令的輸出,我們看到每行包含八個字段,并且第五個字段是文件大小:
~~~
-rwxr-xr-x 1 root root 8234216 2008-04-07 17:42 inkscape
~~~
讓我們考慮用下面的文件,其包含從 2006 年到 2008 年三款流行的 Linux 發行版的發行歷史,來做一系列實驗。 文件中的每一行都有三個字段:發行版的名稱,版本號,和 MM/DD/YYYY 格式的發行日期:
~~~
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
...
~~~
使用一個文本編輯器(可能是 vim),我們將輸入這些數據,并把產生的文件命名為 distros.txt。
下一步,我們將試著對這個文件進行排序,并觀察輸出結果:
~~~
[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
...
~~~
恩,大部分正確。問題出現在 Fedora 的版本號上。因為在字符集中 “1” 出現在 “5” 之前,版本號 “10” 在 最頂端,然而版本號 “9” 卻掉到底端。
為了解決這個問題,我們必須依賴多個鍵值來排序。我們想要對第一個字段執行字母排序,然后對 第三個字段執行數值排序。sort 程序允許多個 -k 選項的實例,所以可以指定多個排序關鍵值。事實上, 一個關鍵值可能包括一個字段區域。如果沒有指定區域(如同之前的例子),sort 程序會使用一個鍵值, 其始于指定的字段,一直擴展到行尾。下面是多鍵值排序的語法:
~~~
[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
...
~~~
雖然為了清晰,我們使用了選項的長格式,但是 -k 1,1 -k 2n 格式是等價的。在第一個 key 選項的實例中, 我們指定了一個字段區域。因為我們只想對第一個字段排序,我們指定了 1,1, 意味著“始于并且結束于第一個字段。”在第二個實例中,我們指定了 2n,意味著第二個字段是排序的鍵值, 并且按照數值排序。一個選項字母可能被包含在一個鍵值說明符的末尾,其用來指定排序的種類。這些 選項字母和 sort 程序的全局選項一樣:b(忽略開頭的空格),n(數值排序),r(逆向排序),等等。
我們列表中第三個字段包含的日期格式不利于排序。在計算機中,日期通常設置為 YYYY-MM-DD 格式, 這樣使按時間順序排序變得容易,但是我們的日期為美國格式 MM/DD/YYYY。那么我們怎樣能按照 時間順序來排列這個列表呢?
幸運地是,sort 程序提供了一種方式。這個 key 選項允許在字段中指定偏移量,所以我們能在字段中 定義鍵值。
~~~
[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 程序使用一個排序鍵值,其始于第三個字段中的第七個字符,對應于 年的開頭。同樣地,我們指定 -k 3.1和 -k 3.4來分離日期中的月和日。 我們也添加了 n 和 r 選項來實現一個逆向的數值排序。這個 b 選項用來刪除日期字段中開頭的空格( 行與行之間的空格數迥異,因此會影響 sort 程序的輸出結果)。
一些文件不會使用 tabs 和空格做為字段界定符;例如,這個 /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
~~~
這個文件的字段之間通過冒號分隔開,所以我們怎樣使用一個 key 字段來排序這個文件?sort 程序提供 了一個 -t 選項來定義分隔符。按照第七個字段(帳戶的默認 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
~~~
通過指定冒號字符做為字段分隔符,我們能按照第七個字段來排序。
### uniq
與 sort 程序相比,這個 uniq 程序是個輕量級程序。uniq 執行一個看似瑣碎的認為。當給定一個 排好序的文件(包括標準輸出),uniq 會刪除任意重復行,并且把結果發送到標準輸出。 它常常和 sort 程序一塊使用,來清理重復的輸出。
* * *
uniq 程序是一個傳統的 Unix 工具,經常與 sort 程序一塊使用,但是這個 GNU 版本的 sort 程序支持一個 -u 選項,其可以從排好序的輸出結果中刪除重復行。
* * *
讓我們創建一個文本文件,來實驗一下:
~~~
[me@linuxbox ~]$ cat > foo.txt
a
b
c
a
b
c
~~~
記住輸入 Ctrl-d 來終止標準輸入。現在,如果我們對文本文件執行 uniq 命令:
~~~
[me@linuxbox ~]$ uniq foo.txt
a
b
c
a
b
c
~~~
輸出結果與原始文件沒有差異;重復行沒有被刪除。實際上,uniq 程序能完成任務,其輸入必須是排好序的數據,
~~~
[me@linuxbox ~]$ sort foo.txt | uniq
a
b
c
~~~
這是因為 uniq 只會刪除相鄰的重復行。uniq 程序有幾個選項。這里是一些常用選項:
表21-2: 常用的 uniq 選項
| 選項 | 說明 |
|-------|-----|
| -c | 輸出所有的重復行,并且每行開頭顯示重復的次數。 |
| -d | 只輸出重復行,而不是特有的文本行。 |
| -f n | 忽略每行開頭的 n 個字段,字段之間由空格分隔,正如 sort 程序中的空格分隔符;然而, 不同于 sort 程序,uniq 沒有選項來設置備用的字段分隔符。 |
| -i | 在比較文本行的時候忽略大小寫。 |
| -s n | 跳過(忽略)每行開頭的 n 個字符。 |
| -u | 只是輸出獨有的文本行。這是默認的。 |
這里我們看到 uniq 被用來報告文本文件中重復行的次數,使用這個-c 選項:
~~~
[me@linuxbox ~]$ sort foo.txt | uniq -c
2 a
2 b
2 c
~~~
## 切片和切塊
下面我們將要討論的三個程序用來從文件中獲得文本列,并且以有用的方式重組它們。
### cut
這個 cut 程序被用來從文本行中抽取文本,并把其輸出到標準輸出。它能夠接受多個文件參數或者 標準輸入。
從文本行中指定要抽取的文本有些麻煩,使用以下選項:
表21-3: cut 程序選擇項
| 選項 | 說明 |
|-----|------|
| -c char_list | 從文本行中抽取由 char_list 定義的文本。這個列表可能由一個或多個逗號 分隔開的數值區間組成。 |
| -f field_list | 從文本行中抽取一個或多個由 field_list 定義的字段。這個列表可能 包括一個或多個字段,或由逗號分隔開的字段區間。 |
| -d delim_char | 當指定-f 選項之后,使用 delim_char 做為字段分隔符。默認情況下, 字段之間必須由單個 tab 字符分隔開。 |
| --complement | 抽取整個文本行,除了那些由-c 和/或-f 選項指定的文本。 |
正如我們所看到的,cut 程序抽取文本的方式相當不靈活。cut 命令最好用來從其它程序產生的文件中 抽取文本,而不是從人們直接輸入的文本中抽取。我們將會看一下我們的 distros.txt 文件,看看 是否它足夠 “整齊” 成為 cut 實例的一個好樣本。如果我們使用帶有 -A 選項的 cat 命令,我們能查看是否這個 文件符號由 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$
~~~
看起來不錯。字段之間僅僅是單個 tab 字符,沒有嵌入空格。因為這個文件使用了 tab 而不是空格, 我們將使用 -f 選項來抽取一個字段:
~~~
[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
~~~
因為我們的 distros 文件是由 tab 分隔開的,最好用 cut 來抽取字段而不是字符。這是因為一個由 tab 分離的文件, 每行不太可能包含相同的字符數,這就使計算每行中字符的位置變得困難或者是不可能。在以上事例中,然而, 我們已經抽取了一個字段,幸運地是其包含地日期長度相同,所以通過從每行中抽取年份,我們能展示怎樣 來抽取字符:
~~~
[me@linuxbox ~]$ cut -f 3 distros.txt | cut -c 7-10
2006
2008
2007
2006
2007
2006
2008
2006
2008
2006
~~~
通過對我們的列表再次運行 cut 命令,我們能夠抽取從位置7到10的字符,其對應于日期字段的年份。 這個 7-10 表示法是一個區間的例子。cut 命令手冊包含了一個如何指定區間的完整描述。
> 展開 Tabs
>
> distros.txt 的文件格式很適合使用 cut 程序來抽取字段。但是如果我們想要 cut 程序 按照字符,而不是字段來操作一個文件,那又怎樣呢?這要求我們用相應數目的空格來 代替 tab 字符。幸運地是,GNU 的 Coreutils 軟件包有一個工具來解決這個問題。這個 程序名為 expand,它既可以接受一個或多個文件參數,也可以接受標準輸入,并且把 修改過的文本送到標準輸出。
>
> 如果我們通過 expand 來處理 distros.txt 文件,我們能夠使用 cut -c 命令來從文件中抽取 任意區間內的字符。例如,我們能夠使用以下命令來從列表中抽取發行年份,通過展開 此文件,再使用 cut 命令,來抽取從位置 23 開始到行尾的每一個字符:
>
> [me@linuxbox ~]$ expand distros.txt | cut -c 23-
>
> Coreutils 軟件包也提供了 unexpand 程序,用 tab 來代替空格。
當操作字段的時候,有可能指定不同的字段分隔符,而不是 tab 字符。這里我們將會從/etc/passwd 文件中 抽取第一個字段:
~~~
[me@linuxbox ~]$ cut -d ':' -f 1 /etc/passwd | head
root
daemon
bin
sys
sync
games
man
lp
mail
news
~~~
使用-d 選項,我們能夠指定冒號做為字段分隔符。
### paste
這個 paste 命令的功能正好與 cut 相反。它會添加一個或多個文本列到文件中,而不是從文件中抽取文本列。 它通過讀取多個文件,然后把每個文件中的字段整合成單個文本流,輸入到標準輸出。類似于 cut 命令, paste 接受多個文件參數和 / 或標準輸入。為了說明 paste 是怎樣工作的,我們將會對 distros.txt 文件 動手術,來產生發行版的年代表。
從我們之前使用 sort 的工作中,首先我們將產生一個按照日期排序的發行版列表,并把結果 存儲在一個叫做 distros-by-date.txt 的文件中:
~~~
[me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt
~~~
下一步,我們將會使用 cut 命令從文件中抽取前兩個字段(發行版名字和版本號),并把結果存儲到 一個名為 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
~~~
最后的準備步驟是抽取發行日期,并把它們存儲到一個名為 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
~~~
現在我們擁有了我們所需要的文本了。為了完成這個過程,使用 paste 命令來把日期列放到發行版名字 和版本號的前面,這樣就創建了一個年代列表。通過使用 paste 命令,然后按照期望的順序來安排它的 參數,就能很容易完成這個任務。
~~~
[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,它會往文件中添加列,但是它使用了獨特的方法來完成。 一個 join 操作通常與關系型數據庫有關聯,在關系型數據庫中來自多個享有共同關鍵域的表格的 數據結合起來,得到一個期望的結果。這個 join 程序執行相同的操作。它把來自于多個基于共享 關鍵域的文件的數據結合起來。
為了知道在關系數據庫中是怎樣使用 join 操作的,讓我們想象一個很小的數據庫,這個數據庫由兩個 表格組成,每個表格包含一條記錄。第一個表格,叫做 CUSTOMERS,有三個數據域:一個客戶號(CUSTNUM), 客戶的名字(FNAME)和客戶的姓(LNAME):
~~~
CUSTNUM FNAME ME
======== ===== ======
4681934 John Smith
~~~
第二個表格叫做 ORDERS,其包含四個數據域:訂單號(ORDERNUM),客戶號(CUSTNUM),數量(QUAN), 和訂購的貨品(ITEM)。
~~~
ORDERNUM CUSTNUM QUAN ITEM
======== ======= ==== ====
3014953305 4681934 1 Blue Widget
~~~
注意兩個表格共享數據域 CUSTNUM。這很重要,因為它使表格之間建立了聯系。
執行一個 join 操作將允許我們把兩個表格中的數據域結合起來,得到一個有用的結果,例如準備 一張發貨單。通過使用兩個表格 CUSTNUM 數字域中匹配的數值,一個 join 操作會產生以下結果:
~~~
FNAME LNAME QUAN ITEM
===== ===== ==== ====
John Smith 1 Blue Widget
~~~
為了說明 join 程序,我們需要創建一對包含共享鍵值的文件。為此,我們將使用我們的 distros.txt 文件。 從這個文件中,我們將構建額外兩個文件,一個包含發行日期(其會成為共享鍵值)和發行版名稱:
~~~
[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
~~~
第二個文件包含發行日期和版本號:
~~~
[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
~~~
現在我們有兩個具有共享鍵值( “發行日期” 數據域 )的文件。有必要指出,為了使 join 命令 能正常工作,所有文件必須按照關鍵數據域排序。
~~~
[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
~~~
也要注意,默認情況下,join 命令使用空白字符做為輸入字段的界定符,一個空格作為輸出字段 的界定符。這種行為可以通過指定的選項來修改。詳細信息,參考 join 命令手冊。
## 比較文本
通常比較文本文件的版本很有幫助。對于系統管理員和軟件開發者來說,這個尤為重要。 一名系統管理員可能,例如,需要拿現有的配置文件與先前的版本做比較,來診斷一個系統錯誤。 同樣的,一名程序員經常需要查看程序的修改。
### comm
這個 comm 程序會比較兩個文本文件,并且會顯示每個文件特有的文本行和共有的文把行。 為了說明問題,通過使用 cat 命令,我們將會創建兩個內容幾乎相同的文本文件:
~~~
[me@linuxbox ~]$ cat > file1.txt
a
b
c
d
[me@linuxbox ~]$ cat > file2.txt
b
c
d
e
~~~
下一步,我們將使用 comm 命令來比較這兩個文件:
~~~
[me@linuxbox ~]$ comm file1.txt file2.txt
a
b
c
d
e
~~~
正如我們所見到的,comm 命令產生了三列輸出。第一列包含第一個文件獨有的文本行;第二列, 文本行是第二列獨有的;第三列包含兩個文件共有的文本行。comm 支持 -n 形式的選項,這里 n 代表 1,2 或 3。這些選項使用的時候,指定了要隱藏的列。例如,如果我們只想輸出兩個文件共享的文本行, 我們將隱藏第一列和第二列的輸出結果:
~~~
[me@linuxbox ~]$ comm -12 file1.txt file2.txt
b
c
d
~~~
### diff
類似于 comm 程序,diff 程序被用來監測文件之間的差異。然而,diff 是一款更加復雜的工具,它支持 許多輸出格式,并且一次能處理許多文本文件。軟件開發員經常使用 diff 程序來檢查不同程序源碼 版本之間的更改,diff 能夠遞歸地檢查源碼目錄,經常稱之為源碼樹。diff 程序的一個常見用例是 創建 diff 文件或者補丁,它會被其它程序使用,例如 patch 程序(我們一會兒討論),來把文件 從一個版本轉換為另一個版本。
如果我們使用 diff 程序,來查看我們之前的文件實例:
~~~
[me@linuxbox ~]$ diff file1.txt file2.txt
1d0
< a
4a4
> e
~~~
我們看到 diff 程序的默認輸出風格:對兩個文件之間差異的簡短描述。在默認格式中, 每組的更改之前都是一個更改命令,其形式為?range operation range?, 用來描述要求更改的位置和類型,從而把第一個文件轉變為第二個文件:
表21-4: diff 更改命令
| 改變 | 說明 |
|-----|--------|
| r1ar2 | 把第二個文件中位置 r2 處的文件行添加到第一個文件中的 r1 處。 |
| r1cr2 | 用第二個文件中位置 r2 處的文本行更改(替代)位置 r1 處的文本行。 |
| r1dr2 | 刪除第一個文件中位置 r1 處的文本行,這些文本行將會出現在第二個文件中位置 r2 處。 |
在這種格式中,一個范圍就是由逗號分隔開的開頭行和結束行的列表。雖然這種格式是默認情況(主要是 為了服從 POSIX 標準且向后與傳統的 Unix diff 命令兼容), 但是它并不像其它可選格式一樣被廣泛地使用。最流行的兩種格式是上下文模式和統一模式。
當使用上下文模式(帶上 -c 選項),我們將看到這些:
~~~
[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
~~~
這個輸出結果以兩個文件名和它們的時間戳開頭。第一個文件用星號做標記,第二個文件用短橫線做標記。 縱觀列表的其它部分,這些標記將象征它們各自代表的文件。下一步,我們看到幾組修改, 包括默認的周圍上下文行數。在第一組中,我們看到:
~~~
*** 1,4 ***
~~~
其表示第一個文件中從第一行到第四行的文本行。隨后我們看到:
~~~
--- 1,4 ---
~~~
這表示第二個文件中從第一行到第四行的文本行。在更改組內,文本行以四個指示符之一開頭:
表21-5: diff 上下文模式更改指示符
| 指示符 | 意思 |
|-------|---------|
| blank | 上下文顯示行。它并不表示兩個文件之間的差異。 |
| - | 刪除行。這一行將會出現在第一個文件中,而不是第二個文件內。 |
| + | 添加行。這一行將會出現在第二個文件內,而不是第一個文件中。 |
| ! | 更改行。將會顯示某個文本行的兩個版本,每個版本會出現在更改組的各自部分。 |
這個統一模式相似于上下文模式,但是更加簡潔。通過 -u 選項來指定它:
~~~
[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
~~~
上下文模式和統一模式之間最顯著的差異就是重復上下文的消除,這就使得統一模式的輸出結果要比上下文 模式的輸出結果簡短。在我們上述實例中,我們看到類似于上下文模式中的文件時間戳,其緊緊跟隨字符串 @@ -1,4 +1,4 @@。這行字符串表示了在更改組中描述的第一個文件中的文本行和第二個文件中的文本行。 這行字符串之后就是文本行本身,與三行默認的上下文。每行以可能的三個字符中的一個開頭:
表21-6: diff 統一模式更改指示符
| 字符 | 意思 |
|-----|-------|
| 空格 | 兩個文件都包含這一行。 |
| - | 在第一個文件中刪除這一行。 |
| + | 添加這一行到第一個文件中。 |
### patch
這個 patch 程序被用來把更改應用到文本文件中。它接受從 diff 程序的輸出,并且通常被用來 把較老的文件版本轉變為較新的文件版本。讓我們考慮一個著名的例子。Linux 內核是由一個 大型的,組織松散的貢獻者團隊開發而成,這些貢獻者會提交固定的少量更改到源碼包中。 這個 Linux 內核由幾百萬行代碼組成,雖然每個貢獻者每次所做的修改相當少。對于一個貢獻者 來說,每做一個修改就給每個開發者發送整個的內核源碼樹,這是沒有任何意義的。相反, 提交一個 diff 文件。一個 diff 文件包含先前的內核版本與帶有貢獻者修改的新版本之間的差異。 然后一個接受者使用 patch 程序,把這些更改應用到他自己的源碼樹中。使用 diff/patch 組合提供了 兩個重大優點:
1. 一個 diff 文件非常小,與整個源碼樹的大小相比較而言。
2. 一個 diff 文件簡潔地顯示了所做的修改,從而允許程序補丁的審閱者能快速地評估它。
當然,diff/patch 能工作于任何文本文件,不僅僅是源碼文件。它同樣適用于配置文件或任意其它文本。
準備一個 diff 文件供 patch 程序使用,GNU 文檔(查看下面的拓展閱讀部分)建議這樣使用 diff 命令:
~~~
diff -Naur old_file new_file > diff_file
~~~
old_file 和 new_file 部分不是單個文件就是包含文件的目錄。這個 r 選項支持遞歸目錄樹。
一旦創建了 diff 文件,我們就能應用它,把舊文件修補成新文件。
~~~
patch < diff_file
~~~
我們將使用測試文件來說明:
~~~
[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
~~~
在這個例子中,我們創建了一個名為 patchfile.txt 的 diff 文件,然后使用 patch 程序, 來應用這個補丁。注意我們沒有必要指定一個要修補的目標文件,因為 diff 文件(在統一模式中)已經 在標題行中包含了文件名。一旦應用了補丁,我們能看到,現在 file1.txt 與 file2.txt 文件相匹配了。
patch 程序有大量的選項,而且還有額外的實用程序可以被用來分析和編輯補丁。
## 運行時編輯
我們對于文本編輯器的經驗是它們主要是交互式的,意思是我們手動移動光標,然后輸入我們的修改。 然而,也有非交互式的方法來編輯文本。有可能,例如,通過單個命令把一系列修改應用到多個文件中。
### tr
這個 tr 程序被用來更改字符。我們可以把它看作是一種基于字符的查找和替換操作。 換字是一種把字符從一個字母轉換為另一個字母的過程。例如,把小寫字母轉換成大寫字母就是 換字。我們可以通過 tr 命令來執行這樣的轉換,如下所示:
~~~
[me@linuxbox ~]$ echo "lowercase letters" | tr a-z A-Z
LOWERCASE LETTERS
~~~
正如我們所見,tr 命令操作標準輸入,并把結果輸出到標準輸出。tr 命令接受兩個參數:要被轉換的字符集以及 相對應的轉換后的字符集。字符集可以用三種方式來表示:
1. 一個枚舉列表。例如, ABCDEFGHIJKLMNOPQRSTUVWXYZ
2. 一個字符域。例如,A-Z 。注意這種方法有時候面臨與其它命令相同的問題,歸因于 語系的排序規則,因此應該謹慎使用。
3. POSIX 字符類。例如,[:upper:]
大多數情況下,兩個字符集應該長度相同;然而,有可能第一個集合大于第二個,尤其如果我們 想要把多個字符轉換為單個字符:
~~~
[me@linuxbox ~]$ echo "lowercase letters" | tr [:lower:] A
AAAAAAAAA AAAAAAA
~~~
除了換字之外,tr 命令能允許字符從輸入流中簡單地被刪除。在之前的章節中,我們討論了轉換 MS-DOS 文本文件為 Unix 風格文本的問題。為了執行這個轉換,每行末尾的回車符需要被刪除。 這個可以通過 tr 命令來執行,如下所示:
~~~
tr -d '\r' < dos_file > unix_file
~~~
這里的 dos_file 是需要被轉換的文件,unix_file 是轉換后的結果。這種形式的命令使用轉義序列 \r 來代表回車符。查看 tr 命令所支持地完整的轉義序列和字符類別列表,試試下面的命令:
~~~
[me@linuxbox ~]$ tr --help
~~~
> ROT13: 不那么秘密的編碼環
>
> tr 命令的一個有趣的用法是執行 ROT13文本編碼。ROT13是一款微不足道的基于一種簡易的替換暗碼的 加密類型。把 ROT13稱為“加密”是大方的;“文本模糊處理”更準確些。有時候它被用來隱藏文本中潛在的攻擊內容。 這個方法就是簡單地把每個字符在字母表中向前移動13位。因為移動的位數是可能的26個字符的一半, 所以對文本再次執行這個算法,就恢復到了它最初的形式。通過 tr 命令來執行這種編碼:
>
> | _echo “secret text” | tr a-zA-Z n-za-mN-ZA-M_ |
>
> frperg grkg
>
> 再次執行相同的過程,得到翻譯結果:
>
> | _echo “frperg grkg” | tr a-zA-Z n-za-mN-ZA-M+ |
>
> secret text
>
> 大量的 email 程序和 USENET 新聞讀者都支持 ROT13 編碼。Wikipedia 上面有一篇關于這個主題的好文章:
>
> [http://en.wikipedia.org/wiki/ROT13](http://en.wikipedia.org/wiki/ROT13)
tr 也可以完成另一個技巧。使用-s 選項,tr 命令能“擠壓”(刪除)重復的字符實例:
~~~
[me@linuxbox ~]$ echo "aaabbbccc" | tr -s ab
abccc
~~~
這里我們有一個包含重復字符的字符串。通過給 tr 命令指定字符集“ab”,我們能夠消除字符集中 字母的重復實例,然而會留下不屬于字符集的字符(“c”)無更改。注意重復的字符必須是相鄰的。 如果它們不相鄰:
~~~
[me@linuxbox ~]$ echo "abcabcabc" | tr -s ab
abcabcabc
~~~
那么擠壓會沒有效果。
### sed
名字 sed 是 stream editor(流編輯器)的簡稱。它對文本流進行編輯,要不是一系列指定的文件, 要不就是標準輸入。sed 是一款強大的,并且有些復雜的程序(有整本內容都是關于 sed 程序的書籍), 所以在這里我們不會詳盡的討論它。
總之,sed 的工作方式是要不給出單個編輯命令(在命令行中)要不就是包含多個命令的腳本文件名, 然后它就按行來執行這些命令。這里有一個非常簡單的 sed 實例:
~~~
[me@linuxbox ~]$ echo "front" | sed 's/front/back/'
back
~~~
在這個例子中,我們使用 echo 命令產生了一個單詞的文本流,然后把它管道給 sed 命令。sed,依次, 對流文本執行指令 s/front/back/,隨后輸出“back”。我們也能夠把這個命令認為是相似于 vi 中的“替換” (查找和替代)命令。
sed 中的命令開始于單個字符。在上面的例子中,這個替換命令由字母 s 來代表,其后跟著查找 和替代字符串,斜杠字符做為分隔符。分隔符的選擇是隨意的。按照慣例,經常使用斜杠字符, 但是 sed 將會接受緊隨命令之后的任意字符做為分隔符。我們可以按照這種方式來執行相同的命令:
~~~
[me@linuxbox ~]$ echo "front" | sed 's\_front\_back\_'
back
~~~
通過緊跟命令之后使用下劃線字符,則它變成界定符。sed 可以設置界定符的能力,使命令的可讀性更強, 正如我們將看到的.
sed 中的大多數命令之前都會帶有一個地址,其指定了輸入流中要被編輯的文本行。如果省略了地址, 然后會對輸入流的每一行執行編輯命令。最簡單的地址形式是一個行號。我們能夠添加一個地址 到我們例子中:
~~~
[me@linuxbox ~]$ echo "front" | sed '1s/front/back/'
back
~~~
給我們的命令添加地址 1,就導致只對僅有一行文本的輸入流的第一行執行替換操作。如果我們指定另一 個數字:
~~~
[me@linuxbox ~]$ echo "front" | sed '2s/front/back/'
front
~~~
我們看到沒有執行這個編輯命令,因為我們的輸入流沒有第二行。地址可以用許多方式來表達。這里是 最常用的:
表21-7: sed 地址表示法
| 地址 | 說明 |
|-----|-------|
| n | 行號,n 是一個正整數。 |
| $ | 最后一行。 |
| /regexp/ | 所有匹配一個 POSIX 基本正則表達式的文本行。注意正則表達式通過 斜杠字符界定。選擇性地,這個正則表達式可能由一個備用字符界定,通過\cregexpc 來 指定表達式,這里 c 就是一個備用的字符。 |
| addr1,addr2 | 從 addr1 到 addr2 范圍內的文本行,包含地址 addr2 在內。地址可能是上述任意 單獨的地址形式。 |
| first~step | 匹配由數字 first 代表的文本行,然后隨后的每個在 step 間隔處的文本行。例如 1~2 是指每個位于偶數行號的文本行,5~5 則指第五行和之后每五行位置的文本行。 |
| addr1,+n | 匹配地址 addr1 和隨后的 n 個文本行。 |
| addr! | 匹配所有的文本行,除了 addr 之外,addr 可能是上述任意的地址形式。 |
通過使用這一章中早前的 distros.txt 文件,我們將演示不同種類的地址表示法。首先,一系列行號:
~~~
[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
~~~
在這個例子中,我們打印出一系列的文本行,開始于第一行,直到第五行。為此,我們使用 p 命令, 其就是簡單地把匹配的文本行打印出來。然而為了高效,我們必須包含選項 -n(不自動打印選項), 讓 sed 不要默認地打印每一行。
下一步,我們將試用一下正則表達式:
~~~
[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
~~~
通過包含由斜杠界定的正則表達式 \/SUSE\/,我們能夠孤立出包含它的文本行,和 grep 程序的功能 是相同的。
最后,我們將試著否定上面的操作,通過給這個地址添加一個感嘆號:
~~~
[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
~~~
這里我們看到期望的結果:輸出了文件中所有的文本行,除了那些匹配這個正則表達式的文本行。
目前為止,我們已經知道了兩個 sed 的編輯命令,s 和 p。這里是一個更加全面的基本編輯命令列表:
表21-8: sed 基本編輯命令
| 命令 | 說明 |
|-----|-------|
| = | 輸出當前的行號。 |
| a | 在當前行之后追加文本。 |
| d | 刪除當前行。 |
| i | 在當前行之前插入文本。 |
| p | 打印當前行。默認情況下,sed 程序打印每一行,并且只是編輯文件中匹配 指定地址的文本行。通過指定-n 選項,這個默認的行為能夠被忽略。 |
| q | 退出 sed,不再處理更多的文本行。如果不指定-n 選項,輸出當前行。 |
| Q | 退出 sed,不再處理更多的文本行。 |
| s/regexp/replacement/ | 只要找到一個 regexp 匹配項,就替換為 replacement 的內容。 replacement 可能包括特殊字符 &,其等價于由 regexp 匹配的文本。另外, replacement 可能包含序列 \1到 \9,其是 regexp 中相對應的子表達式的內容。更多信息,查看 下面 back references 部分的討論。在 replacement 末尾的斜杠之后,可以指定一個 可選的標志,來修改 s 命令的行為。 |
| y/set1/set2 | 執行字符轉寫操作,通過把 set1 中的字符轉變為相對應的 set2 中的字符。 注意不同于 tr 程序,sed 要求兩個字符集合具有相同的長度。 |
到目前為止,這個 s 命令是最常使用的編輯命令。我們將僅僅演示一些它的功能,通過編輯我們的 distros.txt 文件。我們以前討論過 distros.txt 文件中的日期字段不是“友好地計算機”模式。 文件中的日期格式是 MM/DD/YYYY,但如果格式是 YYYY-MM-DD 會更好一些(利于排序)。手動修改 日期格式不僅浪費時間而且易出錯,但是有了 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
~~~
哇!這個命令看起來很丑陋。但是它起作用了。僅用一步,我們就更改了文件中的日期格式。 它也是一個關于為什么有時候會開玩笑地把正則表達式稱為是“只寫”媒介的完美的例子。我們 能寫正則表達式,但是有時候我們不能讀它們。在我們恐懼地忍不住要逃離此命令之前,讓我們看一下 怎樣來構建它。首先,我們知道此命令有這樣一個基本的結構:
~~~
sed 's/regexp/replacement/' distros.txt
~~~
我們下一步是要弄明白一個正則表達式將要孤立出日期。因為日期是 MM/DD/YYYY 格式,并且 出現在文本行的末尾,我們可以使用這樣的表達式:
~~~
[0-9]{2}/[0-9]{2}/[0-9]{4}$
~~~
此表達式匹配兩位數字,一個斜杠,兩位數字,一個斜杠,四位數字,以及行尾。如此關心_regexp, 那么_replacement_又怎樣呢?為了解決此問題,我們必須介紹一個正則表達式的新功能,它出現 在一些使用 BRE 的應用程序中。這個功能叫做_逆參照,像這樣工作:如果序列\n 出現在_replacement_中 ,這里 n 是指從 1 到 9 的數字,則這個序列指的是在前面正則表達式中相對應的子表達式。為了 創建這個子表達式,我們簡單地把它們用圓括號括起來,像這樣:
~~~
([0-9]{2})/([0-9]{2})/([0-9]{4})$
~~~
現在我們有了三個子表達式。第一個表達式包含月份,第二個包含某月中的某天,以及第三個包含年份。 現在我們就可以構建_replacement_,如下所示:
~~~
\3-\1-\2
~~~
此表達式給出了年份,一個斜杠,月份,一個斜杠,和某天。
~~~
sed 's/([0-9]{2})/([0-9]{2})/([0-9]{4})$/\3-\1-\2/' distros.txt
~~~
我們還有兩個問題。第一個是在我們表達式中額外的斜杠將會迷惑 sed,當 sed 試圖解釋這個 s 命令 的時候。第二個是因為 sed,默認情況下,只接受基本的正則表達式,在表達式中的幾個字符會 被當作文字字面值,而不是元字符。我們能夠解決這兩個問題,通過反斜杠的自由應用來轉義 令人不快的字符:
~~~
sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
~~~
你掌握了吧!
s 命令的另一個功能是使用可選標志,其跟隨替代字符串。一個最重要的可選標志是 g 標志,其 指示 sed 對某個文本行全范圍地執行查找和替代操作,不僅僅是對第一個實例,這是默認行為。 這里有個例子:
~~~
[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/'
aaaBbbccc
~~~
我們看到雖然執行了替換操作,但是只針對第一個字母 “b” 實例,然而剩余的實例沒有更改。通過添加 g 標志, 我們能夠更改所有的實例:
~~~
[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/g'
aaaBBBccc
~~~
目前為止,通過命令行我們只讓 sed 執行單個命令。使用-f 選項,也有可能在一個腳本文件中構建更加復雜的命令。 為了演示,我們將使用 sed 和 distros.txt 文件來生成一個報告。我們的報告以開頭標題,修改過的日期,以及 大寫的發行版名稱為特征。為此,我們需要編寫一個腳本,所以我們將打開文本編輯器,然后輸入以下文字:
~~~
# sed script to produce Linux distributions report
1 i\
\
Linux Distributions Report\
s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
~~~
我們將把 sed 腳本保存為 distros.sed 文件,然后像這樣運行它:
~~~
[me@linuxbox ~]$ sed -f distros.sed distros.txt
Linux Distributions Report
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
~~~
正如我們所見,我們的腳本文件產生了期望的結果,但是它是如何做到的呢?讓我們再看一下我們的腳本文件。 我們將使用 cat 來給每行文本編號:
~~~
[me@linuxbox ~]$ cat -n distros.sed
1 # sed script to produce Linux distributions report
2
3 1 i\
4 \
5 Linux Distributions Report\
6
7 s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
8 y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
~~~
我們腳本文件的第一行是一條注釋。如同 Linux 系統中的許多配置文件和編程語言一樣,注釋以#字符開始, 然后是人類可讀的文本。注釋可以被放到腳本中的任意地方(雖然不在命令本身之中),且對任何 可能需要理解和/或維護腳本的人們都很有幫助。
第二行是一個空行。正如注釋一樣,添加空白行是為了提高程序的可讀性。
許多 sed 命令支持行地址。這些行地址被用來指定對輸入文本的哪一行執行操作。行地址可能被 表示為單獨的行號,行號范圍,以及特殊的行號“$”,它表示輸入文本的最后一行。
從第三行到第六行所包含地文本要被插入到地址 1 處,也就是輸入文本的第一行中。這個 i 命令 之后是反斜杠回車符,來產生一個轉義的回車符,或者就是所謂的連行符。這個序列能夠 被用在許多環境下,包括 shell 腳本,從而允許把回車符嵌入到文本流中,而沒有通知 解釋器(在這是指 sed 解釋器)已經到達了文本行的末尾。這個 i 命令,同樣地,命令 a(追加文本, 而不是插入文本)和 c(取代文本)命令都允許多個文本行,只要每個文本行,除了最后一行,以一個 連行符結束。實際上,腳本的第六行是插入文本的末尾,它以一個普通的回車符結尾,而不是一個 連行符,通知解釋器 i 命令結束了。
* * *
注意:一個連行符由一個斜杠字符其后緊跟一個回車符組成。它們之間不允許有空白字符。
* * *
第七行是我們的查找和替代命令。因為命令之前沒有添加地址,所以輸入流中的每一行文本 都得服從它的操作。
第八行執行小寫字母到大寫字母的字符替換操作。注意不同于 tr 命令,這個 sed 中的 y 命令不 支持字符區域(例如,[a-z]),也不支持 POSIX 字符集。再說一次,因為 y 命令之前不帶地址, 所以它會操作輸入流的每一行。
> 喜歡 sed 的人們也會喜歡。。。
>
> sed 是一款非常強大的程序,它能夠針對文本流完成相當復雜的編輯任務。它最常 用于簡單的行任務,而不是長長的腳本。許多用戶喜歡使用其它工具,來執行較大的工作。 在這些工具中最著名的是 awk 和 perl。它們不僅僅是工具,像這里介紹的程序,且延伸到 完整的編程語言領域。特別是 perl,經常被用來代替 shell 腳本,來完成許多系統管理任務, 同時它也是一款非常流行網絡開發語言。awk 更專用一些。其具體優點是其操作表格數據的能力。 awk 程序通常逐行處理文本文件,這點類似于 sed,awk 使用了一種方案,其與 sed 中地址 之后跟隨編輯命令的概念相似。雖然關于 awk 和 perl 的內容都超出了本書所討論的范圍, 但是對于 Linux 命令行用戶來說,它們都是非常好的技能。
### aspell
我們要查看的最后一個工具是 aspell,一款交互式的拼寫檢查器。這個 aspell 程序是早先 ispell 程序 的繼承者,大多數情況下,它可以被用做一個替代品。雖然 aspell 程序大多被其它需要拼寫檢查能力的 程序使用,但它也可以作為一個獨立的命令行工具使用。它能夠智能地檢查各種類型的文本文件, 包括 HTML 文件,C/C++ 程序,電子郵件和其它種類的專業文本。
拼寫檢查一個包含簡單的文本文件,可以這樣使用 aspell:
~~~
aspell check textfile
~~~
這里的 textfile 是要檢查的文件名。作為一個實際例子,讓我們創建一個簡單的文本文件,叫做 foo.txt, 包含一些故意的拼寫錯誤:
~~~
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox jimped over the laxy dog.
~~~
下一步我們將使用 aspell 來檢查文件:
~~~
[me@linuxbox ~]$ aspell check foo.txt
~~~
因為 aspell 在檢查模式下是交互的,我們將看到像這樣的一個屏幕:
~~~
The quick brown fox jimped over the laxy dog.
1)jumped 6)wimped
2)gimped 7)camped
3)comped 8)humped
4)limped 9)impede
5)pimped 0)umped
i)Ignore I)Ignore all
r)Replace R)Replace all
a)Add l)Add Lower
b)Abort x)Exit
?
~~~
在顯示屏的頂部,我們看到我們的文本中有一個拼寫可疑且高亮顯示的單詞。在中間部分,我們看到 十個拼寫建議,序號從 0 到 9,然后是一系列其它可能的操作。最后,在最底部,我們看到一個提示符, 準備接受我們的選擇。
如果我們按下 1 按鍵,aspell 會用單詞 “jumped” 代替錯誤單詞,然后移動到下一個拼寫錯的單詞,就是 “laxy”。如果我們選擇替代物 “lazy”,aspell 會替換 “laxy” 并且終止。一旦 aspell 結束操作,我們 可以檢查我們的文件,會看到拼寫錯誤的單詞已經更正了。
~~~
[me@linuxbox ~]$ cat foo.txt
The quick brown fox jumped over the lazy dog.
~~~
除非由命令行選項 --dont-backup 告訴 aspell,否則通過追加擴展名.bak 到文件名中, aspell 會創建一個包含原始文本的備份文件。
為了炫耀 sed 的編輯本領,我們將還原拼寫錯誤,從而能夠重用我們的文件:
~~~
[me@linuxbox ~]$ sed -i 's/lazy/laxy/; s/jumped/jimped/' foo.txt
~~~
這個 sed 選項-i,告訴 sed 在適當位置編輯文件,意思是不要把編輯結果發送到標準輸出中。sed 會把更改應用到文件中, 以此重新編寫文件。我們也看到可以把多個 sed 編輯命令放在同一行,編輯命令之間由分號分隔開來。
下一步,我們將看一下 aspell 怎樣來解決不同種類的文本文件。使用一個文本編輯器,例如 vim(膽大的人可能想用 sed), 我們將添加一些 HTML 標志到文件中:
~~~
<html>
<head>
<title>Mispelled HTML file</title>
</head>
<body>
<p>The quick brown fox jimped over the laxy dog.</p>
</body>
</html>
~~~
現在,如果我們試圖拼寫檢查我們修改的文件,我們會遇到一個問題。如果我們這樣做:
~~~
[me@linuxbox ~]$ aspell check foo.txt
~~~
我們會得到這些:
~~~
<html>
<head>
<title>Mispelled HTML file</title>
</head>
<body>
<p>The quick brown fox jimped over the laxy dog.</p>
</body>
</html>
1) HTML 4) Hamel
2) ht ml 5) Hamil
3) ht-ml 6) hotel
i) Ignore I) Ignore all
r) Replace R) Replace all
a) Add l) Add Lower
b) Abort x) Exit
?
~~~
aspell 會認為 HTML 標志的內容是拼寫錯誤。通過包含-H(HTML)檢查模式選項,這個問題能夠 解決,像這樣:
~~~
[me@linuxbox ~]$ aspell -H check foo.txt
~~~
這會導致這樣的結果:
~~~
<html>
<head>
<title><b>Mispelled</b> HTML file</title>
</head>
<body>
<p>The quick brown fox jimped over the laxy dog.</p>
</body>
</html>
1) HTML 4) Hamel
2) ht ml 5) Hamil
3) ht-ml 6) hotel
i) Ignore I) Ignore all
r) Replace R) Replace all
a) Add l) Add Lower
b) Abort x) Exit
?
~~~
這個 HTML 標志被忽略了,并且只會檢查文件中非標志部分的內容。在這種模式下,HTML 標志的 內容被忽略了,不會進行拼寫檢查。然而,ALT 標志的內容,會被檢查。
* * *
注意:默認情況下,aspell 會忽略文本中 URL 和電子郵件地址。通過命令行選項,可以重寫此行為。 也有可能指定哪些標志進行檢查及跳過。詳細內容查看 aspell 命令手冊。
* * *
## 總結歸納
在這一章中,我們已經查看了一些操作文本的命令行工具。在下一章中,我們會再看幾個命令行工具。 誠然,看起來不能立即顯現出怎樣或為什么你可能使用這些工具為日常的基本工具, 雖然我們已經展示了一些半實際的命令用法的例子。我們將在隨后的章節中發現這些工具組成 了解決實際問題的基本工具箱。這將是確定無疑的,當我們學習 shell 腳本的時候, 到時候這些工具將真正體現出它們的價值。
## 拓展閱讀
GNU 項目網站包含了本章中所討論工具的許多在線指南。
* 來自 Coreutils 軟件包:
[http://www.gnu.org/software/coreutils/manual/coreutils.html#Output-of-entire-files](http://www.gnu.org/software/coreutils/manual/coreutils.html#Output-of-entire-files)
[http://www.gnu.org/software/coreutils/manual/coreutils.html#Operating-on-sorted-files](http://www.gnu.org/software/coreutils/manual/coreutils.html#Operating-on-sorted-files)
[http://www.gnu.org/software/coreutils/manual/coreutils.html#Operating-on-fields-within-a-line](http://www.gnu.org/software/coreutils/manual/coreutils.html#Operating-on-fields-within-a-line)
[http://www.gnu.org/software/coreutils/manual/coreutils.html#Operating-on-characters](http://www.gnu.org/software/coreutils/manual/coreutils.html#Operating-on-characters)
* 來自 Diffutils 軟件包:
[http://www.gnu.org/software/diffutils/manual/html_mono/diff.html](http://www.gnu.org/software/diffutils/manual/html_mono/diff.html)
* sed 工具
[http://www.gnu.org/software/sed/manual/sed.html](http://www.gnu.org/software/sed/manual/sed.html)
* aspell 工具
[http://aspell.net/man-html/index.html](http://aspell.net/man-html/index.html)
* 尤其對于 sed 工具,還有很多其它的在線資源:
[http://www.grymoire.com/Unix/Sed.html](http://www.grymoire.com/Unix/Sed.html)
[http://sed.sourceforge.net/sed1line.txt](http://sed.sourceforge.net/sed1line.txt)
* 試試用 google 搜索 “sed one liners”, “sed cheat sheets” 關鍵字
### 友情提示
有一些更有趣的文本操作命令值得。在它們之間有:split(把文件分割成碎片), csplit(基于上下文把文件分割成碎片),和 sdiff(并排合并文件差異)。
- 第一章:引言
- 第二章:什么是shell
- 第三章:文件系統中跳轉
- 第四章:研究操作系統
- 第五章:操作文件和目錄
- 第六章:使用命令
- 第七章:重定向
- 第八章:從shell眼中看世界
- 第九章:鍵盤高級操作技巧
- 第十章:權限
- 第十一章:進程
- 第十二章:shell環境
- 第十三章:VI簡介
- 第十四章:自定制shell提示符
- 第十五章:軟件包管理
- 第十六章:存儲媒介
- 第十七章:網絡系統
- 第十八章:查找文件
- 第十九章:歸檔和備份
- 第二十章:正則表達式
- 第二十一章:文本處理
- 第二十二章:格式化輸出
- 第二十三章:打印
- 第二十四章:編譯程序
- 第二十五章:編寫第一個shell腳本
- 第二十六章:啟動一個項目
- 第二十七章:自頂向下設計
- 第二十八章:流程控制 if分支結構
- 第二十九章:讀取鍵盤輸入
- 第三十章:流程控制 while/until 循環
- 第三十一章:疑難排解
- 第三十二章:流程控制 case分支
- 第三十三章:位置參數
- 第三十四章:流程控制 for循環
- 第三十五章:字符串和數字
- 第三十六章:數組
- 第三十七章:奇珍異寶