Table of Contents
Vim進階索引[3]::指令碼
在這一系列的進階教程中我們會不時地遇到Vim指令碼的內容,也許是配置檔案也許是在“宏”中使用的自定義函式。雖然我假設Vim使用者會隨著這系列的教程,慢慢地掌握指令碼的知識。但實際上很多人在學習Vim的過程中“本能地”避開Vim指令碼語言的內容;習慣性地認為指令碼語言是洪水猛獸。但是卻沒意識到他們在學習的Vim命令就是指令碼語言的主要部分,並且這一部分已經足以讓他們寫出功能完整的指令碼了。本文的目的不在於讓使用者看完後就掌握了Vim指令碼語言,而在於引導讀者對Vim指令碼語言有個認識:知道怎麼使用已有的知識來寫指令碼;知道怎麼執行指令碼檔案;知道Vim指令碼語言中還有哪些主要的概念。最理想的情況下讀者從今天開始能隨著這系列教程的深入自然而然地掌握Vim的指令碼語言。本中提供了相關的幫助索引,使用者可以用這些幫助命令找到更完整的資訊。
對了這一系列教程使用的Vim版本是6.3。
實際上本文假定讀者有一定的Vim基礎,當然也包括掌握了一部分的命令列命令。為了能更好地掌握Vim的進階技巧我們這篇中將涉及Vim的指令碼。
:help usr_41.txt :help eval.txt
1 什麼是指令碼?指令碼的作用是什麼?
狹義的指令碼指含一條或多條編輯命令的獨立(指令碼檔案)檔案。透過在編輯器中呼叫指令碼執行在指令碼中定義的一條或多條編輯命令。如無明確說明,本文的指令碼指的是可在指令碼中執行的一條或多條命令而不是指檔案本身。當我說“寫指令碼”時,我指的是寫出以某一特定編輯任務為目的而組織起來的一條或多條命令1。
既然你掌握了一些命令你當然會寫指令碼。我們一起來寫一個指令碼吧。將下面的命令放入一檔案中,假設檔名為myscript.vim2。
" 在檔案的最後一行加入簽名 :$a 月下獨酌 ======================== 暫伴月將影,行樂須及春。 我歌月徘徊,我舞影零亂。 醒時同交歡,醉後各分散。 永結無情遊,相期邈雲漢。 ======================== .
這個指令碼的作用是在檔案的最後一行加入簽名檔。現在用Vim隨便開啟一檔案,輸入
:so myscript.vim。
完成。
可以看到上面的“指令碼”實際上只是一行註釋和一條命令列命令,註釋在這裡並不是必需的3,所以只要你會命令就可以寫指令碼。在指令碼中以`"‘開始的行都是註釋。當然指令碼很少會只有一條命令,我們看一下指令碼還能為我們做什麼?現在我們還想為我們的指令碼加入日期怎麼辦?在指令碼的最後一行加入`:$r !date‘4。
通常我們使用指令碼是為了減少重複的勞動,如果前面例子中的簽名你只用一次,你當然不會想為此寫指令碼檔案。但這並不是絕對的。事實上,很難具體地說明指令碼有什麼作用,因為每個人都有自己獨特的需要,指令碼對每個人有不同的作用。總的來說:指令碼賦予了一些人滿足自己需要的能力。
2 兩種型別的指令碼
Vim中指令碼有兩種基本的型別即Ex指令碼和一般模式(normal mode)指令碼。
2.1 Ex指令碼
在Ex指令碼中所有的命令都被預設為Ex命令,而命令前面的:是可有可無的。我們開頭的例子,就是一Ex指令碼。如果在Ex指令碼中使用一般模式命令時,需要使用:normal命令。這是Vi的傳統指令碼格式。
2.2 一般模式指令碼。
與Ex指令碼相反一般模式指令碼。Vim在一般模式中執行這種指令碼,所以指令碼中的命令就如同你在一般模式中輸入指令一樣。Ex命令需要在前面加上:。下面是一個例子:
3G02f,lct,家庭地址^[:w
這條命令/指令碼將檔案中第三行的第二及第三個,之間的內容更改為“家庭地址”。分解一下就是:
3G 跳到第三行 0 跳到本行開始處 2f, 找到第二個, l 右移一位 ct, 修改第二個,和第三個,之間的內容 家庭地址 改為“家庭地址” ^[ 按回到一般模式。這個符號是透過輸入 指令序列 產生的, Windows是 。表示這裡按了 鍵。 :w 儲存修改
這種指令碼緊湊但不易讀,也不適合寫複雜的指令碼,一般只在宏5中使用。
3 執行指令碼
所以在繼續之前我們先學會如何使用指令碼。之所以在本篇的開始處就寫如何執行指令碼是因為:
- 你會一些命令所以你已經具備寫指令碼的能力了
- 執行指令碼大多數情況下要比寫指令碼容易
- 這樣你才能邊根據你對Vim指令碼語言掌握的程式使用指令碼,而不用學完全部指令碼知識後才知道怎麼執行指令碼。
記住,你並不需要一次掌握所有的指令碼知識,而是需要多少學多少,學會多少用多少。
實際上我們一開始的時候就執行過開始的示例指令碼了。這裡總結一下在Vim中執行指令碼的幾種方式。
3.1 Vim環境
先看一下下面四個命令:
:source
- 這是最傳統的執行方式。讀入Ex指令碼檔案並執行。我們一開始的例子就是一Ex指令碼檔案。一次只能執行一個檔案。
:source!
- 讀入Vim的一般模式命令。注意這個!號並不表示強制執行指令碼,而是讀入不同的指令碼。
:runtime
- 從'runtimepath'設定的路徑中讀取指令碼檔案。可使用萬用字元或多個檔名引數,但只有第一個符合的檔案被執行。
:runtime!
- 跟上一命令一樣但執行所有的符合檔案。慎用!
一些例子:
:so! c:/normal_mode_script.vim
- [Windows環境]Vim中路徑可以使用/或。執行一般模式指令碼。
:so ../vscript/samp1.vim
- [windows或Unix環境]執行Ex模式指令碼。
:ru samp[0-9].vim
- [Unix環境]在runtimepath中搜尋以形如samp0.vim、samp1.vim、……、samp9.vim的指令碼並執行找到的第一個指令碼檔案。
3.2 命令列
Vim的指令碼也可以在命令列中使用,使用者不需要經過執行Vim、開啟檔案、執行指令碼這三個步驟,相反的同戶同要在命令列上給出合適的引數Vim就可以自動處理。在命令列執行Vim的指令碼可透過以下幾種方式:
引數 | 作用 | 示例 |
---|---|---|
-s | 讀入一般模式指令碼 | vim -s 指令碼檔名 檔案 |
-e -s -E -s | 使用vim的ex命令 | vim -e -s vim -es vim -E -s |
現在我們可以在命令列執行本文開頭的指令碼了,但在如果你現在執行的話可能會發現什麼也沒發生。這是因為指令碼中沒有儲存的命令,我們前還需要對指令碼進行一些修改——加入命令`wq!‘6。這是修改後的指令碼檔案(前面的:號是多餘的已經去掉了):
" 在檔案的最後一行加入簽名 $a 月下獨酌 ======================== 暫伴月將影,行樂須及春。 我歌月徘徊,我舞影零亂。 醒時同交歡,醉後各分散。 永結無情遊,相期邈雲漢。 ======================== . $r !date wq!
這個簡單的指令碼完成的任務就是在檔案的末尾加入簽名和日期。現在在命令列執行試試:
$ vim -es透過在命令列執行指令碼我們還可以批次修改檔案:
for a in `ls *.txt` do vim -es如果指令碼檔案使用MS-Dos的'fileformat'可能會出現不理想的結果,最好將'fileformat'設為Unix。當使用`-e -s‘和`-E -s‘在Windows會失去響應7,不過`-s‘不會。
一般模式指令碼除了自已寫以外,Vim提供了兩個命令列引數來“錄製”命令8:
引數 作用 示例 -w 將操作錄製起來,如果指令碼檔案已存在則覆蓋 vim -w script.vim 檔案 -W 同上但是如果指令碼檔案已存在則新增到末尾 vim -W script.vim 檔案 現在跟我做以下實驗,先找一文字文件如 f1.txt 複製為f2.txt。使用以下命令執行Vim:
gvim -w mysc.vim f1.txt
然後,進行幾個簡單的編輯操作並儲存退出。現在f1.txt是編輯過的而f2.txt是未經編輯的。用以下命令執行vim:
gvim -s mysc.vim f2.txt
再開啟f2.txt看看是不是與f1.txt進行了一樣的操作了呢。現在再打mysc.vim看看,可以看到剛才的所以鍵盤操作都被“錄”下來了。你也可以對指令碼檔案mysc.vim進行進一步的修改以適應實際需要。好了到目前為止你已經完全可以使用指令碼了:你已經掌握了一些命令,你也知道如何執行指令碼了。當你掌握的編輯命令越來越多時,你寫出來的指令碼也就越強大。如果你還想學更多指令碼的技巧時那就往下看吧。
4 指令碼進階
學習一門程式設計指令碼語言最有效的方法就是動手進行實驗。下面的內容中有相當多簡單的例子,建議讀者跟著例子做。當然你不用儲存為指令碼檔案再執行,一般直接在Vim中輸入命令就行了。如果有多條命令可以進入Ex模式執行,方法是按在一般模式中按Q——如果接Q不行的話那就按gQ,輸入visual退出Ex模式。
Vim5.0以前的版本與Vi(ex)的指令碼是一到多條編輯命令的集合,指令碼的功能就是依序執行指令碼中的各種編輯命令。那時的指令碼沒有變數也沒流程控制等程式語言所具有的要素。
對於重複或者複雜的編輯工作Vi通常使用指令碼、宏和外部工具來完成。而且一般也認為透過使用宏及外部工具對於大部分的編輯任務而言就以足夠了,然而無論是宏還是指令碼都只是一系列的編輯指令組成,它們無法與Vi互動並且缺乏足夠的靈活性。
從Vim5開始,Vim引入了內建的“指令碼語言”,其實就是加入了變數、表示式及流程控制的命令。同時Vim提供了許多的內建函式和內建變數,這些函式和變數我們在後面的教程中會慢慢接觸到。透過這些程式設計特性使用者可以寫出更靈活更強大的指令碼9。現在來看一下都有哪些內容。
4.1 變數
:h variables :h internal-variablesVim的變數有兩種型別,一是字串型別,一是數字型別。這沒什麼好解釋的了,需要注意的是在我們為變數賦字串型別值時用單引號和雙引號表示的字串值是不一樣。在雙引號字串中是可以使用跳脫字元的,而單引號字串中不進行任何的轉義。下面是兩條命令及他們的執行結果:
:echo "|t|" | | :echo '|t|' |t|變數名。Vim中合法的變數名由下劃線_、大小字母和數字構成,但變數名不能由數字開頭。
賦值語句。
:help 41.2 :help :let :help :unletVim中為變數賦值需要使用:let命令,要釋放變數則使用:unlet命令。這是一些例子:
let a=1 let b=3+1 let c='你好' let d="blahblah" unlet c變數字首:
在Vim在可使用不同的變數字首10,來區別不同變數的作用範圍。g:全域性變數 s:指令碼檔案內部的變數 l:只在函式內部使用的變數 a:變數變數 v:預定義變數完整列表見`:help internal-variables‘。
預定義變數是Vim為使用者提供的變數,這些變數包括了各種有用的資訊,如語言設定、Vim版本、當前行號……等等。這裡有祥細列表`:help v:var‘可訪問的特殊變數:
&設定項 @暫存器 $環境變數看一下一下下面的例子:
:let a="abc"
- 定義一個全域性變數a
:let g:a="abc"
- 同上
:let l:a=a:marg
- 自定義函式的區域性變數。將引數marg的值傳給變數a。因為只在自定義函式內部使用所以l:可以省略。
:echo v:lang
- 顯示預定義變數lang的值。預定義變數是隻讀的。
:echo &tabstop
- 顯示設定項'tabstop'(製表符寬度)的值。
:echo $home
- 顯示環境變數$home。
:echo @3
- 顯示數字暫存器3的值
:let &l:tabstop=4
- 將'tabstop'設為本地變數,並賦值4。見`:help let-star‘與`:help :setlocal‘
4.2 表示式
:help expression-syntax :help eval表示式是比較抽象的概念,在Vim中所有可以表示字串/數值或返回字串/數值的變數名11、函式、算術運算式、邏輯運算式都是表示式——當然還有字串/數字本身也是。而它們所表達的字串/數字就是表示式的值。下面這些都是表示式:
表示式 型別 值 "123木頭人" 字串 "123木頭人" 123 數值 123 mode() 函式 表示當前模式的字串 abc 變數 變數的值。如果變數未賦值則表示式錯誤。 "123"==123 邏輯表示式 1。這裡發生了型別轉換,見:help expr6 "123"=="321" 邏輯表示式 0 "123木頭人"+3 算術表示式 126。這裡發生了型別轉換 32 / 2 算術表示式 16 name . '你好' 字串 假設name的值為'小明',則值為'小明你好' 怎樣判斷一個表示式是不是合法的呢?一個技巧是使用:echo命令。:echo後接表示式會顯示錶達式的值當表示式不合法是則顯示錯誤資訊。
Vim支援的算術運算及邏輯運算與C語言相似。要注意的是Vim不支援浮點運算,算術運算結果的小數點將被省略。所以像7/2的結果會是3而不是3.5。Vim多了一些字串比較的運算子。其中最重要的是正則匹配的運算子:=~ !~另外在這些運算子後加上#表示區分大小寫,而?表示不區分大小寫。如:
==? 大小寫不敏感 ==# 大小寫敏感在沒有#或?時則,根據'ignorecase'設定項決定是否區分大小寫。
詳細說明請參閱Vim文件
:h expr44.3 流程控制
Vim中的控制語句有兩種形式分別是選擇語句和迴圈語句。它們相應的命令是:if與:while,結束命令分別是:endif與:endwhile。
它們都根據條件表示式的值來決定是否執行選擇體或迴圈體中的命令,條件表示式的值是數值——0表示條件不成立,非0數值表示條件成立12。其中條件表示式可以是任意合法的表示式,因為條件表示式的值是數值,所以如果表示式的值是字串時會進行自動型別轉換,見:help expr6。選擇語句。這是選擇語句的形式:
:if 條件表示式 "Ex命令 :endif例:
" 如果變數user_name的值為"小明"則顯示“小明你好”否則不做動作。 :if user_name=='小明' | echo '小明你好' | endif選擇語句與迴圈語句都支援巢狀使用。除了巢狀外選擇語句還有一種形式可以進行多分支的選擇。下面是多分支選擇語句的一種形式,表示的是當表示式為真時執行第一組命令否則轉到第二組表示式,當第二組表示式為真時執行第二組命令,……當前面的表示式全為假時執行其他命令:
:if 條件表示式 " 第一組命令 :elseif 第二組條件表示式 " 第二組命令 :elseif 第三組條件表示式 " 第三組命令 " ...... :else " 其他命令 :endif上面的:elseif及:else命令都是可選的,可視情況新增。下面再看個例子:
:if user_name=='小明' : echo '小明你好' :elseif user_name=~'^李' : echo '李先生/女士你好' :else : echo '你好' :endif迴圈語句:
迴圈語句在形式上與選擇語句相似,所不同的是當條件表示式的值為真時選擇語句中的命令會被執行一次然後退出而迴圈語句會命令命令並回到迴圈的開始處重新計算表示式如果為真就再次執行……如此重複。:while 條件表示式 " 命令 :endwhile假設有變數user_1、user_2、… 、user_10分別存了10個不同的使用者名稱,我們可以用這種方式將10個使用者名稱列出來13。
:let num=1 :while num<=10 " 使用了{}模擬陣列下標。:help curly-braces-names : echo user_{num} : let num=num+1 :endwhile這是兩個與:while相關的命令`:help :continue‘、`:help :break‘
4.4 函式
Vim中的函式可以分為兩種一種是Vim提供的內建函式。一種是自定義函式。
內建函式可以認為是有特殊用法的命令。不同的是命令不能做為表示式而函式可以。Vim提供了相當多的內建函式,具體列表見:help function-list。內建函式一般提供了返回值可以在表示式的環境中使用如:" 例一、函式line(".")的作用是返回當前行的行號。函式的結果(行號)將儲存在變數line_num中。 :let line_num=line(".") " 例二、echo接受的引數是字串表示式。而toupper()返回的正是大寫字串。 :echo toupper("abc") ABC自定義函式是使用者根據自己需要寫的函式。形式如下:
:function! 函式名()
" 指令碼
:return
" 指令碼
:endfunction
函式名的命令規則與變數名一樣,但只有大寫字母開頭的函式名可以從指令碼外部訪問。:function命令後的!號表示如果也存在同名函式則覆蓋原函式——!號是可選的。:return語句不是必須的,如果沒有:return語句則函式的返回值總為0。沒有:return語句的函式一般不在表示式中使用,要呼叫這些函式的可以使用:call命令。" 例,定義兩個函式 :function! Sayhello( words ) : return a:words :endfunction :function! Sayhello2( words ) : echo a:words :endfunction然後分別執行以下命令,觀察結果看有什麼不同。
:call Sayhello("abc") :call Sayhello2("abc") :echo Sayhello("abc") :echo Sayhello2("abc")4.5 零散內容
.運算子。字串可以用.運算子連線。
" 例1,將'你 好'值給變數hello let hello = '你' . "t" . '好' " 例2 :echo 'ab' . (3+4) ab7 " 例3,不支援浮點運算+自動型別轉換 :echo 123.4 1234 :echo 9/4.4 24 " 例4 :let name="小明" :let addr="龍門客棧" :echo "姓名:t".name."r地址:t".addr 姓名: 小明 地址: 龍門客棧[]運算子。雖然Vim不直接支援陣列,但允許透過[]析取字串中的某個字母(Vim7已經加入了陣列的支援)。這跟C語言也是一樣的,例:
:echo 'ab'[ 0 ] a :echo 'ab'[1] b :echo 1234[2] 3 :let a="abcd" | echo a[0] . a[2] ac " 一箇中文有兩個位元組因而 :echo '中文'[0].'中文'[1] 中`|‘
連線多條命令這樣就可以在同一行中執行多條命令——實際上我們前面已經多次用過這種技巧了。注意有些命令以|為引數因而不能使用|來分隔,祥細列表見`:help :bar‘`:exec‘
以表示式的結果為命令執行。這條命令經常做為一些複雜技巧的一部分,建議使用者詳細瞭解這條命令。let cmd='echo' | exec cmd . ' "abc"' abc至此Vim指令碼的主要內容已經講完了。這些內容目前還比較零散,我們會在以後的教程中將今天學的這些知識一點一點拼起來。
5 小結
對沒有程式設計基礎的人來講學習一門程式語言尤其是通用程式語言最難的有兩點其一是使用流程控制語句和各種表示式。二是他們學到的東西不能在日常生活中運用,這讓他們很容易就將辛苦學來的知識忘得一乾二淨。好在在Vim中你很容易就可以將所學的指令碼語言知識用上——只要你會用命令就能用指令碼。
Footnotes
[1] 事實上命令與指令碼是一體兩面,當你在編輯器作使用命令它就是命令,當你把命令以某種方式儲存起來以便再次使用時它就成了指令碼。
[2] 檔名也可以是中文,但也許用英文檔名會更符合一般人的閱讀習慣。另外Vim指令碼檔案一般是以Vim為字尾名,但實際上可以使用任意字尾名
[3] 但寫註釋是個好習慣,至少可以練習打字。
[4] Windows的使用者需要在date後面加入選項`/t‘。另外用Vim內建的strftime()也可以達到同樣的目的,但!date顯然是更好也更簡便的方法——除非你需要跨平臺執行
[5] Vi中的宏一般是指以map命令定義的指令
[6] `q‘對Vim來說不是必需的,不過多打一個字母也不費什麼事
[7] Cygwin中可以正常執行
[8] 你可能已經想到了,這跟q命令的作用相近。不同的是使用這兩個引數錄製的命令將寫入到檔案中而不是暫存器中
[9] 一個副作用是使用者可能會傾向於以Vim來完成所有任務,而不考慮是否有更好的或現在的方式。
[10] 就是名稱空間,不過我想抽象的詞還是少用點
[11] 必須是已經賦過值的變數名
[12] 其實就是邏輯值,0為假,非0為真。但Vim中邏輯值不是獨立的資料型別
[13] Vim7之前不能使用陣列,但是可以用模擬的方式。這就是一個模擬陣列的方式。