外部程式::Vim進階索引[6]
Table of Contents:
Vim進階索引[6]::外部程式
Windows的使用者可能會因為這一篇裡面用了大量的Unix工具而倍感沮喪。但大可不必如此,這一篇裡面我們更多的是講一種使用Vim的理念──使用者可以依據自身對外部工具的掌握程度適時地使用這些工具來減少工作量。常的工具都可以找到對應的Winodws版本,而且通常很小。
unix工具箱哲學的一個核心思想是每個工具只完成各自相對簡單的任務,這些工具的真正威力來自於它們之間關係。你可以組合這些工具來完成複雜的工作。Vim雖然是一個互動式的編輯器,但這種思想仍在它身上得了很好的體現。它能很好地與其他工具配合來擴充套件功能或完成相對複雜的編輯工作。
這一篇教程我們將討論與Vim中與外部程式有關的議題。
1 使用外部程式的Vim命令
Vim中有一些功能是透過外部程式來實現的。比如:make命令,Vim沒有內建make工具。但是使用者可以為:make命令指定一個外部程式,比如:gnu make或Windows下的nmake。這樣我們在Vim中就可以使用:make命令了。設定make工具所用的設定項是'makeprg'。我們先看一下跟外部程式有關命令和其對應設定項。
:cscope
|
=
|
gq
|
:grep
|
:make
|
K
|
:shell
|
提示:這些設定的一個共同點是它們都不能在模式行中進行設定。出於安全的原因在模式行中不能使用這些設定項。
Vim的Diff模式就是使用了工具GNU diff,這個程式是Vim的重要組成,Vim並未提供相應的設定項讓使用者更改。與vim相關的外部程式還有ctags類的程式,這類程式用來生成關鍵字的索引。但Vim用到的是tags檔案所以也沒有提供相應的設定項。(即使要生成tags,ctags也不是必需的)
這些就是使用或可設定為外部程式的命令。總的來說這些命令在某些方面提供了方便。但從功能性來說它們並非是必不可少的。不過既然有了我們還是利用起來吧。
由於這些命令的特性我們可以用來做一些有用的事情,現在來看一下我們還能怎麼用這些命令。
=
gq
- 這兩條命令都是在一般模式中使用的過濾命令(一般的過濾在行模式中使用)。它們的工作方式都是一樣的:對一定的範圍使用過濾程式/內部命令,所以我們放在一起。透過指定合適的過濾程式我們就能決定這命令的作用。
比如:設定為sort可以用來排序`se equalprg=sort‘,設定為`se equalprg=sort|uniq‘,排序並去除重複。
總之任何過濾程式都可以在這裡使用──而決不僅僅可以使用縮排或文字格式化的工具。 K
- 與前面兩個命令不同,這個命令的目的並非改變當前文字的內容或格式。這個命令以當前字為或選區(至多一行)為引數執行命令 。自動傳遞引數給相應的程式。利用它自動傳遞引數的特點我們可以執行一些需要引數的程式。
除了用來查詢Vim文件,man文件外最直覺的一個用法就是用來查字典了。只要是以一個字詞為輸入的所有程式都可以定義為'keywordprg'。如:stardict、locate、which等。下面是用K來執行當前圈選1的網址的例子(windows平臺):
se kp=start c:progra~1operaopera.exe
:grep
:make
- 這兩條命令的真正有用的/有效的特性在於Vim可以根據執行的結果生成quickfix視窗。而且你可以用:cn, :clist, cw…等quickfix命令在不同的位置間跳轉。要用好兩條命令首先要了解這兩個設定項:grepformat errorformat。這兩個設定項的作用是捕捉並分析輸出。在使用make工具時由於在使用不同編譯器時輸出的錯誤資訊的格式也不盡相同,所以在設定好了grepprg或makeprg後還要教它“讀懂輸出”。
在預設情況下Unix平臺的Vim的'grepprg'是'grep -n'而Windows下則是'findstr /n',不過它們執行的結果都一樣。假設你是命令列下(shell中)用前面的命令在當前目錄查詢:grep -n goes *.txtl,返回資訊是以下面的格式出現的:
a_text_file.txt:52: something goes here .........
這條資訊中包括了:檔名a_text_file,行號52,該行的內容共三個部分。這三個部分是由:隔開的。
我們再看一下預設情況下的gfm::se gfm? grepformat=%f:%l:%m, ....
同時設定的多種不同格式的grepformat間用,隔開。其中%f、%l、%m分別表示檔名,行號和提示資訊(這個例子中就是該行的內容)。中間的:是幫助對應不同部分的關鍵,Vim在輸出的資訊中查詢:並將冒號分隔起來的三個部分與gfm中的三部分對應起來。這樣我們是Vim中使用grep後,它就能自動解讀輸出的資訊並帶我們到不同檔案的特定行去了。要了解%f、%l等更多佔位符的意義及用法見*error-file-format*
提示:在Vim中透過程式的輸出資訊,使用cn cl cw等命令在檔案中快速移動的這一特性稱為quickfix。使用quickfix時,檔名是必需的——這樣才能開啟正確的檔案,有行號的話更好——精確地定位到行。
可以看到在設定好程式的前提下,要用好這兩條命令的關鍵是設定好合適的gfm/efm。Vim文件中提供了相當多的現成的gfm和efm,幾乎含蓋了各種型別的編譯器。所以在你設定"合適的gfm/efm"之前先看一看有沒有現成的。
這裡再來看一下makeinfo的輸出格式,
somethingwrong.texi:5: Unknown command `blah'.
與我為它設定的“錯誤資訊格式”:
se efm=%f:%l: %m se makeprg=makeinfo
很眼熟不是?它跟grep是一樣的只是剛才的查詢結果現在成了“錯誤資訊”,本質上:grep跟:make是一樣的。除了命令名稱和設定項名稱的不同它們的其他地方是一樣。在設定這些後我只要使用:make,Vim就會帶我到出錯的行去——如果有錯的話。
提示:se makeprg=makeinfo這條命令不是必需的,你可以將命令放到makefile中,然後使用make工具。當然直接用makeinfo編譯文件還是使用make工具由你自己決定。當然efm是要設定的。同樣的還有tex文件,你一樣可以使用make工具但efm要設定正確。
這兩條命令的作用可不僅侷限於使用各種make工具或grep類工具。想想下面的設定會產生什麼樣的效果:
:se gfm=%f :se grepprg=find
上面的設定讓你可以快速定位特定的檔案並開啟。將find改為ls就有了一個簡易的Explorer外掛了。
看到下面的命令,你可能會一頭霧水,
:se grepprg=cat :se gfm=%f :grep blah.txt
但如果你知道blah.txt有著下面的內容後,你就會知道了——這是個專案檔案。
farsi.c farsi.h feature.h glbl_ime.cpp proto/buffer.pro proto/charset.pro
再想一下如果blah.txt的內容是一些網址,上面的命令又會產生什麼結果?就像這樣:
ftp://ftp.vim.org/pub/vim/
動手實驗一下,然後:h netrw。
最後提供用來除錯sed/awk指令碼用的'efm':
" sed se efm=sed: file %f line %l: %m " awk 僅限於使用指令碼檔案時。 se efm=awk: %f:%l: %t: %m
對於make和grep的設定可以在ft檔案中設定或是在vimrc中使用au(:help :autocmd)進行設定。K,gq,=的功能也可以用map的方式來達到(這就是它們並非必不可少的原因),並且map也帶來了靈活性。但使用keywordprg你不能指定Vim的內部命令,同時使用外部命令時map通常意味著更復雜的設定過程。有利有弊。
另外,Vim處理make和grep和cscope的方式是內建一個命令的介面(quickfix),要達到同樣的效果又需要使用者寫相當長度的指令碼。可以說這點來說使用內建的介面還是要方便些的。
儘管我們可以透過這些Vim命令靈活地使用外部程式,但Vim與外部程式的互動更多地來自:!命令。
2 !命令
Vim為使用者提供了極大的可能性,讓使用者可以自由的操縱他們的文字。尤其是透過內建的指令碼直譯器,使用者只要寫Vim指令碼就可以擴充套件Vim的功能。但是Vim知道大量經得住時間考驗的Unix工具如果閒置起來就太可惜了。透過提供方便的介面,Vim在保持體積小巧的同時也讓使用者有選擇的餘地。
Vim的外掛機制是以兩種方式實現的:一是透過內建的指令碼直譯器。一是透過保留與外部程式協作的介面。本篇我們會看看如何在Vim中使用外部程式或shell指令碼。
一般而言我們可以透過使用多個軟體來滿足我們的特殊要求:先用A程式處理一部分儲存,再由B程式接著處理,……但是Vim提供的介面讓我們在使用外部程式就象在使用內建的功能一樣方便快捷,你不必在程式之間來回切換。
Vim呼叫外部程式主要是透過!實現的。!命令可以在行模式下也可以在一般模式中使用。帶地址/範圍地使用這條命令時(即作為過濾命令時),當前地址範圍內的內容會被命令的輸出所替代。否則只是在shell中執行該命令。詳細用法見:! 和 :! 。
在你決定花點時間寫Vim指令碼前也許你應該先考慮以下這幾個問題
- 我需要什麼樣的結果?
- 有這樣的外部程式存在並且能更好或更簡單地滿足我的要求嗎?
- 最後就是你對哪種方式(Vim指令碼或外部工具程式)更熟悉。
比如你可能在整理一份軟體列表,你需要某個目錄的檔案列表。毫無疑問你會選擇用外部程式(或命令):
:r !dir ... :r !ls ...
這是另一種情況,你要將文字區中每一行文字複製一行在該行下面,你可以使用下面的任意一條命令:
%!sed p g/^/norm Yp
使用兩命令看上去沒什麼差別你只需要照自己的習慣來決定使用哪條命令。但如果你要對文字排序或是更復雜的工作呢?這時寫Vim指令碼會是很累人的工作。
2.1 !的兩種使用風格
!命令在shell中執行命令,所有能在命令列中執行的程式/工具都可以。對Linux來說這代表了幾乎所有的可執行程式都可以從vim中啟動執行。而Windows中則與路徑的設定有關,但通常來說在windows目錄下和system32目錄下的程式都可以直接在Vim中執行。此外還有命令直譯器的內部命令。能在shell中執行的程式指令碼或批處理檔案都能在Vim中使用。但我們可以作一些簡單的分類。
在Vim中使用命令不外乎有兩種情況。
- 是把Vim當成一個shell介面(這種情況下要注意命令引數中的特殊字元。見*cmdline-special*)。如
!touch abbcc !rm abbcc
當然你也可以執行圖型介面程式:
" 用Opera預覽效果 !opera % " 或者你想要後臺執行firefox !firefox % &
簡單說這種使用方式就是把Vim當成shell用。但是在Vim中方便地執行其他程式不是我們的首要目標。Vim是一個編輯器,所以我會著重在編輯相關的內容──就是下面要講的第二。
- 是為了獲取程式的輸出。又可細分為兩種。過濾程式(filter)產生的輸出;非過濾程式的輸出。
- 獲取非過濾程式的輸出。前面的讀入目錄的命令就是在Vim中使用非過濾命令的例子,這裡再舉幾個例子:
r !date "當前行下插入日期 " 在當前位置插入指令碼執行時間的測試結果 .!time myscript " 執行使用者的shell指令碼或批處理程式,並獲取輸出 r !myshellscript
能夠在命令列產生某種輸出的程式(批處理/指令碼)都能與Vim配合(如前面例子中的date),而其中一些從stdin讀取資料處理後輸出到stdout的程式,即filter程式,在Vim中的應用範圍要更大一點。相對而言一個圖形介面程式在這種情況幾乎毫無用處可言,無法在命令列下執行也就代表著難以與其他程式配合。因為當一個程式在處理的結果是在對話方塊中顯示的話,表示這個結果無法直接為Vim所用。我們必須至少要經過一個複製貼上的過程:-(
- 獲取過濾程式輸出。與非過濾程式不同的是過濾程式對給定範圍內的文字進行“加工”,然後才產生輸出。下面是一些使用過濾命令的例子。
" gggqG %!fmt " 選擇列印部分“列”資料 8,20!cut .. " 刪除選區中的重複行,改變次序 ''!sort |uniq " 同上,但不改變行的次序 ''!awk '{if ($0 in a) next}{a[$0]++}8'
- 獲取非過濾程式的輸出。前面的讀入目錄的命令就是在Vim中使用非過濾命令的例子,這裡再舉幾個例子:
Unix下有相當多豐富的過濾程式資源,Windows下這類的工具較少(sort、more和findstr)。好在Unix下的最好的命令列工具(尤其是Gnu版本)通常都有Windows版本。它們通常很小——可以在這裡下載。其中最強大的兩個工具是awk、sed和CoreUtils工具包,稍大一點的選擇是perl。
2.2 例項
由於這一篇更多地是涉及到工具軟體的使用而非Vim本身,所以不會對這些例子進行很詳細的解說。這裡演示一下實際應用中外部程式的用法。這一次仍是以成績單為例,這是其中的三條資料:
姓名 期中 期末 李阿月 72 70 林小麗 91 93 王小明 46 56
共三欄欄與欄之間用一個或多個空格隔開。
看一下外部命令是如何逐一完成任務的,思考一下用純Vim的方式要怎麼達到同樣的目的。
- 首先是成績單排序。按期末成績,然後期中成績:
:2,$!sort -k3n -k2,2n
- 根據前面排序的結果,寫上排名(在Unix上可以輸出序號的工具很多如nl cat等,不過這次我們要用的是grep):
" 工具之間也可以協作 :2,$!grep -n .|tr : ' '
- 計算平均成績。在暫存器篇中我們用了下面的命令來更新目錄中的行號:
:1,25s/[0-9]+$/=submatch(0)+25/
我們可以用同樣的方式計處平均成績,但Vim不支援浮點運算。
而成績通常會精確到0.5,所以我們使用了下面的指令碼::2,$!gawk '{$0=$0 " " ($3+$4)/2}8'
- 如果我們打算在下面增加一欄計算總人數。總平均成績,用Vim指令碼就沒那麼容易了。
:2,$!gawk '{sm+=$2;sf+=$3}8;END{print "人數:" NR " 期中平均:" sm/NR " 期末平均:" sf/NR}'
- 我們還能在成績單後加入簡單的統計,
:2,$--!gawk -f myawkscript.awk
上面的命令後會在成績單後面加入下面的內容:
-- 期末考成績分佈圖示 -- 優 4人 %16■■■■ 良 8人 %32■■■■■■■■ ....
提示:對myawkscript.awk有興趣的話可以見附錄。
- 匯出為CVS(這樣就方便在其他資料處理分析工具中開啟):
" 選擇要匯出的範圍,然後輸入…… :'w !tr -s ' ' , >成績.cvs
3 相關議題
- 要以某一部分的文字,如圈選區的文字作為某個外部程式的輸入但又不捕捉該程式的輸出要怎麼做呢?我們需要用到:w命令,格式如下:
:3,5w !cmd
我們前面匯出為cvs用的就是這種方法。
- 很多情況下我們需要在Vim指令碼中執行並捕捉外部程式的輸出。這時我們可以用system()函式(*system()*)。
:let files = system("ls")
上面的例子是文件中帶的例子。看了這個例子大家應該就知道這個函式的用用法了。
4 小結
Unix的這些工具軟體有著比Vim指令碼更多的應用,對於個人的使用目的來講寫Vim指令碼不應做為首選。別忘了從Vi誕生之始開始工具軟體就一直是擴充套件Vi功能的主要途徑。充分利用現有的外部程式資源一直是用好Vi的關鍵之一,除了節省大量時間外,使用對的工具正是Unix風格的一貫體現。如果你使用Linux/Unix的話花點時間在這些工具上會是很好的投資。
最後一點補充:在寫完這一篇後,我才發現關於!的部分的篇幅與其在實際應用中的廣泛和重要程度完全不符(這當然是因為我前面說過的“這更多是涉及外部程式和而非Vim的使用)。所以我想我得說明一下:對於相當一部分的老手來說!是他們解決一些棘手編輯問題的第一個選擇,其次才是指令碼。而其中有些人(尤其是perl老手)根本不願意寫超過5行的Vim指令碼。
Appendix A
myawkscript.awk的程式碼:
# /usr/bin/gawk -f # 從期末成績計算各成績段的人數、比例及圖示 # 計算各成績段的人數 {$3>=90?++a[1]:$3>=80?++a[2]:$3>=70?++a[3]:$3>=60?++a[4]:++a[5]} # 顯示輸入的資料 8888 END{ split("優 良 中 及 不",rank) print "n-- 期末考成績分佈圖示 --" # 顯示統計資訊 for(i=1;i<=5;i++){ if(a[i]=="")a[i]=0 printf "%st%d人t%d%%",rank[i],a[i],a[i]*100/NR # 顯示圖示 while(a[i]-->0)printf "■"; print "" } }