vim-logo.gif​當你對 vim 瞭解越多的時候,有時候你會覺得自己變得很貪婪,常常會問:如果 vim 連這個都做得到,那它可不可以怎麼怎麼?就像我們之前介紹的置換功能,你可能會覺得 vim 真的可以幫你做到很多事情。但是它能不能做更多事情?

舉例來說,有時候一篇文章裡面你可能不想要它的空白行,但是你用「:s」的話都是以一行一行當作單位,沒有辦法讓你砍掉這些空白行。雖然說你其實可以把換行符號取代掉,讓文章變成一行,然後再做兩三個取代命令來達成。但是萬一遇到要把包含某個字串的那行搬移或是刪除的時候又該怎麼辦呢?那這就是今天要講的「 :g 」全域指令

我們先來看前面說的怎麼用 :g 砍掉空白行:

:g/^[ \t]*$/d

\t 是用來代表<Tab>鍵,:g 後面跟著的就是要尋找的字串,而最後就是跟著命令,像上述命令中的 d 就表示刪除該行的意思。所以套同樣的方式,我們也可以砍掉包含某特殊字元的行數。

不過你可能會想,難道 :g 就這樣了嗎?

當然不是!

還記得我們在做 search 的時候嗎?有時候連我都會覺得:拜託, vim 難道就不能把所有比對的地方一次給我看就好了?我為什麼還要一個個按 n n n n … 好麻煩。或是我能不能把不包含某個字串的行列出來呢?在原來講 vim 的 search 指令裡面,我們能做的就是針對想要的字串作比對,但是對於上述的要求,我們就必須透過 :g 來完成。

比方說在一篇文章中,我只想找有「小柴胡湯」的條文,可是用「/小柴胡湯」只能出現像下面的結果:

vim slash search example

我還是會看到其他不包含「小柴胡湯」條文,但是用 :g 就不一樣了。假如我用這樣的指令:

:g/小柴胡湯/

就會看到變成這個樣子:

vim :global search example

那如果我還想看到含比對字串的行數的話,就可以下:

:g/小柴胡湯/nu

那就會看到變成這樣子:

vim :global search with nu command

黃色部分就代表包含的行數。

那如果你想看到附近的條文的話,也可以用:

:g/小柴胡湯/z#=4

這樣就是只看比對的附近四條條文並用虛線標記起來。(你可以用 :h :z 看到詳細的說明)

那這時候也許你還會想:那我可不可以找不包含某字串的條文呢? vim 用 :g 也很容易可以做到。比方說我想要找不包含[湯散丸]的條文,也就是說你只想看到只有出證而沒有出方的條文:

:g!/[湯散丸]/

驚嘆號「!」就代表不包含後面的條文。

又假如一份文件長成這樣(像信件可能就會長成這樣):

vim :global pattern range delete example

那我想要把 From: xxx@blah.blah.com 的內容 content second 都砍掉的話怎麼辦?當然你可以用之前的選擇模式的方式,用上下左右或是用滑鼠把它砍掉,但是有了 :g ,你何必作這麼麻煩的事情呢?

我們要先把游標放在第二次出現的 From: xxx@blah.blah.com 上,然後用「 .,」先指定範圍,如果沒有這個範圍指定的話,那連第一個 content… first 都會被清掉。指令下成像下面這個樣子:

:.,g/xxx@blah.blah.com/;/yyy@blah2.blah2.com/-1d

yyy@blah2.blah2.com後面跟上的「/-1」表示是到 yyy@blah2.blah2.com 的上一行,如果沒用 -1 的話,那連 yyy@blah2.blah2.com 這行都會因為其後跟上的命令「d」而被砍掉。同理,+n 就表示到比對到的那行(本例是 yyy@blah2.blah2.com 這行)之後,還要再往下 n 行才執行後面的命令。上面的指令我們可以把格式寫成這樣:

:{range1}g/pattern1/{offset1};/pattern2/{offset2}{command}

這樣就是說你可以指定範圍{range1}作 :g ,而在此範圍下,又只作 /pattern1/ 和 /pattern2/ 之間的範圍(包含 pattern1 和 pattern2)加減後面的範圍{offset1}及{offset2},然後在這個已經被比對出來的區域中執行後面的指令{command}。

vim :global pattern range delete with comamnd example

所以做完那個指令之後,原來的文件內容就會變成:

vim :global pattern range delete done

那你可能又想了(誰叫我們總是會想猜 vim 到底可以做到什麼程度?),那我是不是也可以指定區域範圍之後,並同時搬移到某個我指定的地方去?

當然可以!

比方說拿原來的信件範例來說,我可不可以把從 xxx@blah.blah.com 的信件放到文件的最後面去?我們可以下這樣的指令:

g/^From: xxx@blah.blah.com/;/^From:/-1move $

所以本來長成這樣:

vim :global pattern range move

執行之後就會變成:

vim :global pattern range move done

所以我們可以看到它就真的按照我們想要的跑到文件的最後面「$」去了。而如果你想要的指令不是 move (搬移)的話,你也可以用 copy (複製)的方式。指令就是把剛剛的 -1move 改成 -1copy 就行啦。

不過如果你只是想要讓包含某字串的行搬到另外包含一個字串的話,上面這些可能又不能滿足你的需要了。

舉例來說像我今天把 BBS 的文章寄回去給自己,可是讀信的時候,它只會按照寄信的時間而不是根據 BBS 的文章 — 時間: 來排序。因為對信件來說,那都是算內文了。

如果我想要按照 BBS post 的順序來排序,難道就只好一篇篇改嗎?還是自己要寫一個判讀信件內容的程式呢?

:g/時間: /m?From .(email address) .*200.

這告訴 vim 把內文當中含有「時間: 」這東西移到往上尋找看到的第一個From .(email address) .*200. 這行底下

m?

m 就是指 move 的意思,「?」表示要 match 後面的才可以。

然後我們把時間拿掉:

:%s/時間: //g

把一封信件的最開頭那行的最後面有關時間的部分砍掉:

:%s/\(^From: .*tw\) .*/\1/g

告訴 vim 說把 From: 的底下那行連接起來 j 表示連接, 1 表示 1 行:

:g/^From: /j1

好啦,這樣你就把內文的時間移到信件的格式裡面去了。輕輕鬆鬆。

上面這些都是我個人自己會用到的,你可以用 vim 輸入 :h :g 看到相關的說明。

為什麼我會喜歡用 vim 呢?因為我怎麼想它就怎麼做,很直覺。不過,如果你不是拿 vim 當作你日常的編輯器的話,其實指令看來看去還是會忘記的。快裝一個 vim 來玩玩看吧!

Updated 2005-09-15:
今天在做區域的一些置換的時候,我用了像下面這樣的指令:

:g/^<s.*$/+1;/^\%(([0-9]\+)\|<u\|<s\).*$/s/^\%(([0-9]\+)\|<u\|<s\).*$/<\/ul>&/g

你可以看到在 :g/pattern1/ 後面跟了 /+1;。注意那個分號「;」,在我原來的文章裡面是用逗點「,」,我後來發現如果你不給{offset1}的話,也就是說不調整比對的起始行的話,用逗點「,」或是分號「;」在我今天測試的情況是沒有差別的。但是如果你會改變起使的比對位置的話,也就是用到{offset1}的話,你就必須使用分號「;」。所以在文章裡面我已經做了修正,把原來用逗點的地方一律改成分號「;」。