使用脚本编写 Vim 编辑器,第 5 部分: 事件驱动的脚本编写和自动化
Vim 的事件模型
Vim 編輯功能的運行方式是事件驅動的。但由于性能上的原因,實際的實現要遠比這個復雜,還需要進行許多事件處理優化或者處理事件循環下面的幾層,但是您仍然可以將編輯器看成一個簡單循環,響應一系列的編輯事件。
無論您何時開始一個 Vim 會話,打開一個文件,編輯一個緩沖區,修改您的編輯模式,切換窗口,或者和周圍的文件系統交互,您正在有效地排列 Vim 能迅速接受和處理的事件。
例如,如果您啟動 Vim,編輯一個名為 demo.txt 的文件,切換到 Insert 模式,輸入一段文檔,保存文件,然后退出,您的 Vim 對話就接收到一系列的事件,如清單 1 所示。
清單 1. 一個簡單 Vim 編輯對話中的事件序列
> vimBufWinEnter (create a default window)BufEnter (create a default buffer)VimEnter (start the Vim session):edit example.txtBufNew (create a new buffer to contain demo.txt)BufAdd (add that new buffer to the session’s buffer list)BufLeave (exit the default buffer)BufWinLeave (exit the default window)BufUnload (remove the default buffer from the buffer list)BufDelete (deallocate the default buffer)BufReadCmd (read the contexts of demo.txt into the new buffer)BufEnter (activate the new buffer)BufWinEnter (activate the new buffer's window)iInsertEnter (swap into Insert mode) HelloCursorMovedI (insert a character)CursorMovedI (insert a character)CursorMovedI (insert a character)CursorMovedI (insert a character)CursorMovedI (insert a character)<ESC>InsertLeave (swap back to Normal mode):wqBufWriteCmd (save the buffer contents back to disk)BufWinLeave (exit the buffer's window)BufUnload (remove the buffer from the buffer list)VimLeavePre (get ready to quit Vim)VimLeave (quit Vim)更有趣的是,Vim 提供允許您攔截任何此類編輯事件的 “掛鉤”。這樣,您就可以創建一個特殊的 Vimscript 指令或者函數,在一個特定事件發生時,進行執行:每次 Vim 啟動時,每次加載文件時,每次退出 Insert 模式……甚至是每次您移動指針時。這樣就可以在整個編輯器中的任何地方添加自動的行為。
Vim 提供 78 種編輯事件的通知,分為八大類:對話開始和清理事件,文件讀取事件,文件編輯事件,緩沖區修改事件,選項設置事件,窗口相關事件,用戶交互事件,以及異步通知。
要查看此類事件的全部清單,在 Vim 命令行輸入?:help autocmd-events。要查看各個事件的詳細描述,請看?:help autocmd-events-abc。
本文解釋了事件在 Vim 中如何運行,然后介紹了一系列自動編輯事件和行為的腳本。
回頁首
使用自動命令處理事件
Vim 中用于攔截事件的機制就是?自動命令。每個自動命令都指定攔截某種類型的事件,攔截此類事件中被編輯的文件名,當它們被檢測到時,命令行模式就會采取行動。所有這些的關鍵字就是?autocmd(通常縮寫為?au)。常用的語法是:
autocmd EventName filename_pattern :command事件名稱是 78 種有效 Vim 事件名稱(如?:help autocmd-events?所列示)之一。文件名稱模式的語法和普通的 shell 模式(詳見?:help autocmd-patterns)很相似 —— 但又有所區別。該命令可以是任何有效的 Vim 命令,包括 Vimscript 函數的調用。命令開始處的冒號是可選的,但是最好加上;這么做能夠讓命令更輕松地在一個(往往很復雜的)?autocmd?參數清單中定位。
例如,您可以放下最后的顧慮,將下述內容添加到您的?.vimrc?文件中,來指定一個?FocusGained?事件的事件處理器:
autocmd FocusGained *.txt :echo 'Welcome back, ' . $USER . '! You look great!'當一個 Vim 窗口變為窗口系統的輸入焦點時,FocusGained?事件就會進行排列。所以現在無論您何時切換回您的 Vim 對話,如果您當前正在編輯任何名稱符合文件名稱模式?*.txt?的文件,那么 Vim 將會自動執行指定的?echo?命令。
您可以按照自己的意愿對同一個事件設置任意數量的處理器,所有這些處理器都會按照其最初的指定順序來執行。例如,FocusGained?事件的一個非常有用的的自動化可能是:無論您何時切換回您正在編輯的對話,都會使 Vim 簡潔地強調指針所在行,如清單 2 所示。
清單 2. FocusGained 事件的一個有用的自動化
autocmd FocusGained *.txt :set cursorline autocmd FocusGained *.txt :redraw autocmd FocusGained *.txt :sleep 1 autocmd FocusGained *.txt :set nocursorline這 4 個自動命令使 Vim 自動地強調包含指針的行(set cursorline),指示高亮強調點(redraw),等待一秒鐘(sleep 1),然后取消強調(set nocursorline)。
您可以按這種方法來使用任何系列的命令;您甚至可以使用多個自動命令來分解一個單一的控制結構。例如,您可以設置一個全局變量(g:autosave_on_focus_change)來控制一個 “自動保存” 機制,它可以自動地寫入任何修改過的?.txt?文件,無論用戶何時從 Vim 切換到其它窗口(調用一個?FocusLost?事件進行排列):
清單 3. 退出一個編輯器窗口時自動保存的自動命令
autocmd FocusLost *.txt : if &modified && g:autosave_on_focus_change autocmd FocusLost *.txt : write autocmd FocusLost *.txt : echo "Autosaved file while you were absent" autocmd FocusLost *.txt : endif像這樣的多行自動命令需要您多次重復關鍵的事件選擇規范(例如,FocusLost *.txt)。這樣一來,它們通常就不易維護,也更容易出錯。更簡單、更安全的方法是,將任意控制結構,或者其它命令隊列,析出到一個獨立的函數,然后再用一個單獨的自動命令來調用這個函數。例如:
清單 4. 更簡潔的處理多行自動命令的方法
function! Highlight_cursor ()set cursorlineredrawsleep 1set nocursorline endfunction function! Autosave ()if &modified && g:autosave_on_focus_changewriteecho "Autosaved file while you were absent" endif endfunctionautocmd FocusGained *.txt :call Highlight_cursor() autocmd FocusLost *.txt :call Autosave()通用自動命令和單文件適用的自動命令
目前為止,所有列出的例子都局限在處理符合模式?*.txt?的事件。很顯然,這就意味著您可以使用任何文件通配符模式來指定一個特殊自動命令所適用的文件。例如,您只需簡單地使用通用文件匹配模式?*?作為文件名過濾器,就可以將之前的指針強調?FocusGained?自動應用到任何文件:
" Cursor-highlight any file when context-switching ... autocmd FocusGained * :call Highlight_cursor()您也可以有選擇地將命令限制在一個單一文件中:
" Only cursor-highlight for my .vimrc ... autocmd FocusGained ~/.vimrc :call Highlight_cursor()要注意的是,這也意味著您可以對同一個事件指定不同的行為,這取決于當前正在編輯哪個文件。例如,當用戶把注意力放在其它地方時,您可以選擇自動地保存文本文件,或者使 Perl 或者 Python 腳本自檢, 構建一個文檔文件來對當前段落重新格式化,如清單 5 所示。
清單 5. 當用戶的注意力在其它地方時可以做什么
autocmd FocusLost *.txt :call Autosave() autocmd FocusLost *.p[ly] :call Checkpoint_sourcecode() autocmd FocusLost *.doc :call Reformat_current_para()自動命令組
自動命令有一個相關的命名機制,允許它們集成一個自動命令組,這樣就可以對它們進行集體操作。
為了指定一個自動命令組,您可以使用?augroup?命令。該命令的通用語法是:
augroup GROUPNAME" autocommand specifications here ... augroup END這個組的名稱可以是一系列非空白的字符,除了 “end” 或者 “END”,這是保留用于指定一個組的結束的。
使用自動命令組,您就可以一次實施任意數量的自動命令。特別是,您可以將響應同一事件的命令集成在一個組中,如清單 6 所示。
清單 6. 定義響應 FocusLost 事件的自動命令組
augroup Defocusautocmd FocusLost *.txt :call Autosave()autocmd FocusLost *.p[ly] :call Checkpoint_sourcecode()autocmd FocusLost *.doc :call Reformat_current_para() augroup END或者您可以把和某種單獨文件類型相關的自動命令集成,例如:
清單 7. 定義處理文本文件的自動命令組
augroup TextEvents autocmd FocusGained *.txt :call Highlight_cursor()autocmd FocusLost *.txt :call Autosave() augroup END停用自動命令
您可以使用?autocmd!?命令(即,使用一個感嘆號),刪除指定事件的處理器。該命令通用的語法是:
autocmd! [group] [EventName [filename_pattern]]為了刪除一個單獨的事件處理器,需要指定所有的三個參數。例如,要從?Unfocussed?組中刪除?.txt?文件的處理器,使用:
autocmd! Unfocussed FocusLost *.txt若不使用一個指定事件名稱,您可以使用一個星號來表示從特殊組和文件名模式中要刪除的事件。如果您想要刪除?Unfocussed?組里?.txt?文件中的全部事件,可以使用:
autocmd! Unfocussed * *.txt如果您離開文件名模式,那么指定事件類型的各個處理器都會被刪除。您可以像這樣刪除?Unfocussed?組的所有?FocusLost?處理器:
autocmd! Unfocussed FocusLost如果您還遺漏了事件名稱,那么在指定組中的每個事件處理器也會被刪除。這樣就能關閉?Unfocussed?組中指定的所有事件處理:
autocmd! Unfocussed最后,如果您省略了組名稱,自動命令刪除就會適用當前激活狀態的組中。這個選項的典型應用就是在設置一系列自動命令之前,在組內 “清理桌面”。例如,Unfocussed?組最好可以像這樣進行指定:
清單 8. 在添加新自動命令之前保證這個組是空的
augroup Unfocussedautocmd!autocmd FocusLost *.txt :call Autosave()autocmd FocusLost *.p[ly] :call Checkpoint_sourcecode()autocmd FocusLost *.doc :call Reformat_current_para() augroup END在每個組開始的地方添加一個?autocmd!?是非常重要的,因為自動命令不會靜態地聲明事件的處理器;它們是動態地創建處理器。如果您兩次執行相同的?autocmd,您就可以獲得兩個事件處理器,這兩個處理器將會由那一點上相同的事件和文件名組合分別激活。通過用?autocmd!?開始每個自動命令組,您就可以清除組內所有現存的處理器,這樣隊列?autocmd?語句會替換任何現存的處理器,而不是對它們進行參數設置。反過來,這就意味著您的腳本可以按需要執行任意次(或者是您的?.vimrc?可以反復被?source?),無需增加不必要的事件處理實體。
一些實踐例子
適當使用自動命令可以使您的編輯工作容易很多。讓我們看一些您使用自動命令簡化編輯過程,消除現有問題的例子。
管理同步編輯
Vim 最有用的特性之一就是當您打算編輯一個當前正在由其它 Vim 實例編輯的文件時,它會進行自動檢測。這經常發生在一個多窗口環境中,在這個環境中,您正在一個終端編輯一個文件;或者在一個多用戶設置中,在這個環境下他人正在一個共享文件上工作。當 Vim 檢測到第二個編輯某特殊文件的企圖時,您可以得到以下請求:
Swap file ".filename.swp" already exists! [O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: _根據您工作的環境,無需您的仔細思考,您的手指可能每次就會自動地點擊這些選項。例如,如果您很少在共享文件上工作,您可能只用點擊?q來終止會話,然后開始搜索您正在編輯的文件的終端窗口。另一方面,如果您常常編輯共享資源,或許您的手指會訓練有素地立刻點擊<ENTER>?來選擇默認選項,打開只讀文件。
然而,使用自動命令,通過簡單地自動響應觸發它的?SwapExists?事件,您完全無需查看、識別和響應那個消息。例如,如果您從不想編輯一個正在其它地方進行編輯的文件,您可以添加以下命令到您的?.vimrc:
清單 9. 自動退出同步編輯
augroup NoSimultaneousEditsautocmd!autocmd SwapExists * :let v:swapchoice = 'q' augroup END這會設置一個自動命令組,并刪除所有之前的處理器(通過?autocmd!?命令)。然后它會在任意文件上安裝一個?SwapExists?事件的處理器(使用通用文件模式:*)。那個處理器會簡單地分配?'q'?來響應指定的?v:swapchoice?變量。Vim 會提前查詢這個變量,顯示 “swapfile exists” 消息。如果這個變量已經被設置過,它就使用這個值作為自動響應,而不顯示這個消息。那么現在您就永遠不會看到?swapfile?消息;您的 Vim 會話將會自動退出,如果您試圖編輯一個正在其它地方進行編輯的文件。
換一種情況,如果您想要在只讀模式打開已編輯的文件,您可以簡單地改變?NoSimultaneousEdits?組:
清單 10. 自動只讀訪問現有文件
augroup NoSimultaneousEditsautocmd!autocmd SwapExists * :let v:swapchoice = 'o' augroup END更有趣的是,您可以根據當前正在考慮的文件的位置,安排在這兩個可替換的選項間選擇。例如,您可以選擇在您自己的子目錄中自動退出文件,但是在?/dev/shared/?下,以只讀的方式打開共享文件。您可以按如下進行操作:
清單 11. 自動返回環境敏感響應
augroup NoSimultaneousEditsautocmd!autocmd SwapExists ~/* :let v:swapchoice = 'q'autocmd SwapExists /dev/shared/* :let v:swapchoice = 'o' augroup END即:如果文件全名以主目錄開始,以后內容不限(~/*),那么預選 “退出” 行為;但是如果文件全名以共享目錄開頭(/dev/shared/*),那么就預選 “只讀” 行為。
統一自動格式化代碼
Vim 很好地支持編輯時自動代碼布局(見?:help indent.txt?和?:help filter)。例如,您可以選擇?'autoindent'?和'smartindent'?選項,然后根據您的輸入自動地使 Vim 重新縮進您的代碼塊。或者您可以設置?'equalprg'?選項,將您特定語言的代碼重新格式化程序連接到標準?=?命令。
不幸的是,Vim 沒有一個選項或者命令來處理最常見的代碼格式化程序:您就不得不閱讀其他人難懂的?malformatted?代碼。尤其是,沒有內置的選項來告訴 Vim 自動地對您打開的任何代碼文件的格式程序進行殺毒。
這是沒有問題的,因為不用這種方法,而是設置一個自動命令來實現它是非常簡單的。
例如,您可以將以下的自動命令組添加到您的?.vimrc,這樣當您打開相應類型的文件時,C、Python、Perl,和 XML 文件就能自動地在適合的代碼格式程序中運行,如清單 12 所示。
清單 12. 自動命令上的完美代碼
augroup CodeFormattersautocmd!autocmd BufReadPost,FileReadPost *.py :silent %!PythonTidy.pyautocmd BufReadPost,FileReadPost *.p[lm] :silent %!perltidy -qautocmd BufReadPost,FileReadPost *.xml :silent %!xmlpp –t –c –nautocmd BufReadPost,FileReadPost *.[ch] :silent %!indent augroup END組中的所有自動命令在結構上是相同的,只是在它們適用的文件名擴展和它們相應激活的美化打印機方面有所不同。
要注意的是,自動命令不會命名一個單一的待處理事件。相反地,它都會指定一個事件清單。任何?autocmd?都可以由一個用逗號分隔的事件類型清單指定,這里所列出來的任一事件都能調用處理器。
在這種情況下,每個處理器列出的事件都是?BufReadPost(它在當前文件被加載到新的緩沖區時進行排列)和?FileReadPost?(它在任何:read?命令執行之后進行排列)。這兩個事件往往同時被指定,因為在兩者之間,它們覆蓋了將當前文件的內容加載到一個緩沖區的最常用方法。
在事件清單之后,每個自動命令指定它適用的文件后綴:Python 的?.py,Perl 的?.pl?和?.pm,XML 的?.xml,或者 C 的?.c?和?.h?文件。要注意的是,通過這些事件,這些文件名模式也可以被指定為一個逗號分隔的清單,而不是一個單一模式。例如,Perl 處理器可以這樣寫:
autocmd BufReadPost,FileReadPost *.pl,*.pm :silent %!perltidy -q或者 C 的處理器也可以被擴展到處理常用的 C++ 變體(.C,.cc,.cxx,等等。),像這樣:
autocmd BufReadPost,FileReadPost *.[chCH],*.cc,*.hh,*.[ch]xx :silent %!indent通常,每個自動命令的最后組件是需要執行的命令。在各種情況下,它是一個全局過濾器命令(%!filter_program),它會接收文件的全部內容(%),把它排出(!)到指定的外部程序(PythonTidy.py,perltidy,xmlpp,或者?indent?之一)。然后將每個程序的輸出粘貼回緩沖區,替換原有的內容。
一般情況下,當使用像這樣的過濾器命令時,Vim 會在命令完成后,自動地顯示一個通知,像這樣:
42 lines filtered Press ENTER or type command to continue_為了避免這種煩惱,每個自動命令用一個?:silent?來對其行動添加前綴,它會中和任何普通的信息消息,但仍允許顯示錯誤消息。
隨機對代碼自動格式化
Vim 對自動地格式化您所輸入的 C 代碼有良好的支持,但它并未對其它語言提供支持。那并不完全是 Vim 的錯誤;一部分語言 —— 對,Perl,我就在說它 —— 很難實時正確地格式化。
如果 Vim 沒有為用您喜歡語言編寫的源代碼提供足夠的支持,您可以簡單地用您的編輯器調用一個外部工具來為您完成這個任務。
最簡單的方法就是利用?InsertLeave?事件。無論您何時退出?Insert?模式(最常見的是在您敲擊?<ESC>?鍵之后),這個事件都會被排列。您可以輕松地設置一個處理器,它可以在您完成其添加之后重新格式化您的代碼,就像這樣:
清單 13. 在每個編輯之后激活 PerlTidy
function! TidyAndResetCursor ()let cursor_pos = getpos('.')%!perltidy -qcall setpos('.', cursor_pos) endfunctionaugroup PerlTidyautocmd!autocmd InsertLeave *.p[lm] :call TidyAndResetCursor() augroup ENDTidyAndResetCursor()?函數可以通過存儲在變量?cursor_pos?中內置的?getpos()?返回的指針信息,來記錄當前指針的位置。 然后它在整個文件上(%!perltidy -q)運行外部?perltidy?工具,最后通過把存儲的指針信息發送到內置的?setpos()?函數,把指針恢復到原來的位置。
在?PerlTidy?組內,每次用戶離開任意 Perl 文件中的?Insert?模式時,您只需要設置一個單一調用?TidyAndResetCursor()?的自動命令。
每當您插入文本時,這個相同的代碼模式可以適用于執行任何適當的行為。例如,如果您正在一個非常不可靠的系統中工作,希望能最大化自身的能力來恢復文件(見?:help usr_11.txt),如果出現故障,每次您離開?Insert?模式,可以安排 Vim 來升級它的交換文件,像這樣:
augroup UpdateSwapautocmd!autocmd InsertLeave * :preserve augroup END時間戳文件
另一組有用的事件是?BufWritePre,FileWritePre,和?FileAppendPre。在您的 Vim 會話把一個緩沖區寫回磁盤之前(作為一個命令的結果,例如?:write,:update,或者?:saveas),就會對這些 “Pre” 事件進行排列。一個?BufWritePre?事件發生在整個緩沖區被寫入之前,一個?FileWritePre?發生在部分緩沖區被寫入之前(即,當您指定寫入的行范圍::1,10write)。一個?FileAppendPre?發生在一個:write?命令被用于追加而不是替換之前;例如:
:write >> logfile.log).對所有這三類事件,Vim 在正在被編寫的代碼行范圍內,設置了特殊的行編號別名?'[?and?']。那么這些別名可以在任意序列命令的范圍指定符中使用,保證自動命令行為只應用于相關行。
通常,您可以設置一個單一的覆蓋所有三種預寫事件的處理器。例如,每當一個文件被寫入(或者追加)到磁盤,您可以使 Vim 自動地升級一個內部的時間戳,如清單 14 所示。
清單 14. 當文件保存時自動更新內部時間戳
function! UpdateTimestamp ()'[,']s/^This file last updated: \zs.*/\= strftime("%c") / endfunctionaugroup TimeStampingautocmd!autocmd BufWritePre,FileWritePre,FileAppendPre * :call UpdateTimestamp() augroup ENDUpdateTimestamp()?函數,通過專門將替換的范圍限制在?'[?和?']?之間,在每個正在被編寫的代碼行上執行一個替代(s/.../.../),像這樣:'[,']s/.../.../。這個替代會查找以 “This file last updated:” 開始的代碼行,無論其后內容是什么(.*)。在?.*?之前的?\zs?會導致這個替代看起來只是從冒號后開始匹配,所以只有實際的時間戳被替換。
為了更新時間戳,這個替代在替換的文本中使用特殊的?\= escape?序列。這個轉義序列告訴 Vim 把替換文本作為一個 Vimscript 表達式來處理,對它進行評估來獲取實際的替換字符串。在這種情況下,這個表達式調用內置的?strftime()?函數,這個函數會返回一個標準時間戳字符串:“Fri Oct 23 14:51:01 2009”。這個字符串然后被替代命令寫回時間戳。
剩下的工作就是設置在任意文件(*)中的所有三種事件類型(BufWritePre、FileWritePre、FileAppendPre)的事件處理器(autocmd),然后讓它調用適合的時間戳函數(:call UpdateTimestamp())。現在,無論何時編寫文件,被保存代碼行的時間戳都會被更新到當前時間。
要注意的是,Vim 還提供了另外兩組您可以用來修改寫操行為的事件。要讓寫操作之后的某些行為自動化,您可以使用BufWritePost,FileWritePost?和?FileAppendPost。要在您的腳本中完全替換標準的寫操作,您可以使用BufWriteCmd、FileWriteCmd?和?FileAppendCmd(但是首先要查詢?:help Cmd-event?來獲得一些重要的提示)。
表格驅動的時間戳
當然,您也可以創建更巧妙的機制來處理有不同時間戳約定的文件。例如,您可能喜歡指定各種時間戳簽名和它們在一個 Vim 字典中的替換(見該系列的上一篇文章),然后在各對中循環決定時間戳應該怎樣升級。這個方法如清單 15 所示。
清單 15. 表格驅動的自動時間戳
let s:timestamps = { \ 'This file last updated: \zs.*' : 'strftime("%c")', \ 'Last modification: \zs.*' : 'strftime("%Y%m%d.%H%M%S")', \ 'Copyright (c) .\{-}, \d\d\d\d-\zs\d\d\d\d' : 'strftime("%Y")', \}function! UpdateTimestamp ()for [signature, replacement] in items(s:timestamps)silent! execute "'[,']s/" . signature . '/\= ' . replacement . '/'endfor endfunction這里,這個 for 循環會遍歷?s:timestamps?字典中的每個時間戳的簽名/替換對,就像這樣:
for [signature, replacement] in items(s:timestamps)然后它產生一個包含相應替代命令的字符串。以下的替代命令在結構上和之前例子中的一樣,但是在這里它是由篡改字符串的簽名/替換對來構建的:
"'[,']s/" . signature . '/\= ' . replacement . '/'最后,它以靜默方式執行產生的命令:
silent! execute "'[,']s/" . signature . '/\= ' . replacement . '/'silent!?的使用是很重要的,因為它保證了任何不匹配的替換不會導致令人討厭的?Pattern not found?錯誤消息。
需要注意的是,s:timestamps?中的最后條目是一個極為有用的例子:它會自動地更新任何嵌入式版權聲明的年份范圍,只要包含它們的文件被編寫。
文件名驅動的時間戳
您可能喜歡用參數表示?UpdateTimestamp()?函數,然后對不同文件類型創建一系列不同的?autocmds,而不是列出一個表格中所有可能的時間戳格式,如清單 16 所示。
清單 16. 不同文件類型的環境敏感時間戳
點擊查看代碼清單
在這個版本中,簽名和替換組件被明確地傳遞給?UpdateTimestamp(),然后它可以產生一個包含相應替換命令的字符串,并執行該字符串。在?Timestamping?組中,您可以為每個所需的文件類型設置獨立的自動命令,分別為它們傳遞適合的時間戳簽名和替換文本。
請求目錄
在您開始編輯之前,自動命令也是很有用的。例如,當您開始編輯一個新文檔時,您可能偶爾會看到這樣的信息:
"dir/subdir/filename" [New DIRECTORY]這就表示您所指定的文件(這里是?filename)不存在,它所應該在的目錄(這里是?dir/subdir?)也不存在。
Vim 將很樂意允許您忽略這個警告(很多用戶甚至沒有意識到這是一個警告),繼續編輯文件。但是當您想要保存它時,您會遇到以下沒有幫助的錯誤信息:
"dir/subdir/filename" E212: Can't open file for writing.現在,為了保存您的工作,您不得不在編寫文件寫入之前,創建一個缺失的目錄。您可以在 Vim 中這么做:
:write "dir/subdir/filename" E212: Can't open file for writing. :call mkdir(expand("%:h"),"p") :write這里,內置的?expand()?函數調用應用于?"%:h",其中?%?表示當前文件路徑(這里是?dir/subdir/filename?),并且?:h?只提取了這個路徑的 “頭”,刪除了文件名稱,留下了預期目錄(dir/subdir)。然后,對 Vim 的內置?mkdir()?的調用讀取這個目錄路徑,按這個路徑創建所有臨時目錄(按第二參數要求,"p")。
事實上,雖然大多數 Vim 用戶更愿意簡單地轉到 shell 來創建所需的目錄。例如:
:write "dir/subdir/filename" E212: Can't open file for writing. :! mkdir -p dir/subdir/ :write任何一種方法都很麻煩。如果您最終不得不創建缺失的目錄,那么為什么不提前讓 Vim 通知它不存在,然后在您開始之前簡單完成創建?這樣,您就永遠不會遇到意味不明的 [New DIRECTORY] 提示;您的工作流程稍后也不會被一個同樣神秘的?E212?錯誤所中斷。
要讓 Vim 負責提前創建不存在的目錄,您可以將一個處理器連接到?BufNewFile?事件,當您開始編輯一個不存在的文檔時,它就會進行排列。清單 17 顯示了您需要添加到?.vimrc?文件來實現此功能的代碼。
清單 17. 無條件自動創建不存在的目錄
augroup AutoMkdirautocmd!autocmd BufNewFile * :call EnsureDirExists() augroup END function! EnsureDirExists ()let required_dir = expand("%:h")if !isdirectory(required_dir)call mkdir(required_dir, 'p')endif endfunctionAutoMkdir?組設置了任意種類文件上?BufNewFile?事件的單一自動命令,當編輯一個新文件時,調用?EnsureDirExists()?函數。EnsureDirExists()?首先通過展開當前路徑的 “頭” 來確定正被請求的目錄:expand("%:h")。然后它使用內置的?isdirectory()?函數來檢查請求的路徑是否存在。如果不存在,它就會試著使用 Vim 內置的?mkdir()?創建目錄。
要注意的是,如果?mkdir()?調用由于某種原因不能創建請求的目錄,它就會生成一個更準確,信息更豐富的錯誤信息:
E739: Cannot create directory: dir/subdir更謹慎地請求目錄
這個解決方案唯一的問題就是,有時,自動創建不存在的子目錄真是最不應該做的事情。例如,假設您進行如下請求:
> vim /share/sites/corporate/root/.htaccess您打算在現存的子目錄?/share/corporate/website/root/?中創建一個新的訪問控制文件。當然,由于您弄錯了路徑,您實際所作是在之前不存在的子目錄?/share/website/corporate/root/?中創建一個新的訪問控制文件。因為那是自動發生的,沒有任何警告,至少在誤用的訪問控制導致某些在線災難之前,您甚至可能沒有意識到這個錯誤。
為了避免這樣的問題,您或許希望 Vim 在自動創建缺失的目錄方面不要那么有用。清單 18 顯示了?EnsureDirExists()?一個更細致的版本,它會繼續檢測缺失的目錄,但現在詢問用戶要對其進行何種操作。要注意的是,自動命令設置和清單 17 中的一摸一樣;只有EnsureDirExists()?函數發生了改變。
清單 18. 有條件地自動創建不存在的目錄
augroup AutoMkdirautocmd!autocmd BufNewFile * :call EnsureDirExists() augroup END function! EnsureDirExists ()let required_dir = expand("%:h")if !isdirectory(required_dir)call AskQuit("Directory '" . required_dir . "' doesn't exist.", "&Create it?")trycall mkdir( required_dir, 'p' )catchcall AskQuit("Can't create '" . required_dir . "'", "&Continue anyway?")endtryendif endfunctionfunction! AskQuit (msg, proposed_action)if confirm(a:msg, "&Quit?\n" . a:proposed_action) == 1exitendif endfunctionEnsureDirExists()?函數的這個版本像之前一樣,對所需的目錄進行定位,并檢測它是否存在。然而,如果目錄不存在,EnsureDirExists()?現在就調用一個助手函數:AskQuit()。這個函數使用內置的?confirm()?函數來詢問您是否想要退出會話或者自動創建目錄。"Quit?"?作為第一選項顯示,這也使它成為默認值,如果您只是敲擊?<ENTER>?鍵的話。
如果您選擇了?"Quit?"?選項,助手函數就立即終止 Vim 會話。否則,助手函數就簡單返回。在這種情況下,EnsureDirExists()?繼續執行,并試著調用?mkdir()。
然而,要注意的是,mkdir()?調用現在在一個?try...endtry?結構中。這就是 —— 如您所期待的 —— 一個異常處理器,它現在將會捕捉如果?mkdir()?不能創建請求的目錄而被拋出的?E739?錯誤。
當那個錯誤被拋出,catch?塊將會攔截它,并再次調用?AskQuit(),通知您不能創建目錄,并詢問您是否依然想要繼續。想要更詳細地了解 Vim 的大量異常處理機制,請看::help exception-handling。
EnsureDirExists()?第二版的總體效果是強調不存在、但是需要您明確請求創建的目錄(通過在提示時輸入一個?‘c’?創建)。如果不能創建目錄,您就會被再次警告,并讓您選擇是否無論如何都要繼續會話(當被詢問時再次輸入一個?'c')。這也相當輕松地避免了錯誤編輯(只需在任意提示時敲擊?<ENTER>?鍵來選擇默認的?"Quit?"?選項)。
當然,您或許喜歡將繼續作為默認值,在這種情況下,您只需要改變?AskQuit()?的第一行:
if confirm(a:msg, a:proposed_action . "\n&Quit?") == 2這樣,您提議的行為就會是第一選擇,也就是默認行為。要注意,"Quit?"?現在是第二選擇,所以這個響應現在需要和值 2 進行比較。
展望未來
通過自動化重復操作,您就不必親力親為,自動命令能夠幫您節省大量的努力,避免大量的錯誤。著手開始的一個有效方法就是在編輯時心理上退后一步,注意通過使用 Vim 的事件處理機制,讓使用的重復模式適當地自動化。將這些模式腳本化到自動命令或許需要一些預先的準備,但是自動化的行為每天都會回報您的投資。通過自動化每天的操作,您將會節省時間和精力,避免錯誤,使工作流程更順暢,清除瑣碎的壓力,從而提高您的效率。
雖然您的自動命令開始時可能只能進行一些簡單的單行自動操作,隨著您思考使用 Vim 更好地完成更多繁瑣的工作,您很快就會發現您能重新設計和定制它們。通過這種方式,您的事件處理器逐步會變得更智能、更安全、更完美地適應您的工作方式。
然而,隨著這些類似的 Vim 腳本變得更復雜,您還需要更好的工具來管理它們。每當您設計一個聰明的新鍵盤映射或者自動命令,在您的.vimrc?中添加 10 至 20 行代碼將會最終產生一個長達千行……完全不可維護的配置文件。
所以,在這個系列的下一篇文章中,我們會探討 Vim 的簡單插件架構,它允許您析出?.vimrc?的一部分,并將其隔離在單獨的模塊中。我們將會了解那個插件系統如何通過開發一個獨立模塊來運行,改善使用 XML 的一些擔心。
參考資料
學習
- 從本系列第一篇文章:“使用腳本編寫 Vim 編輯器,第 1 部分:變量、值和表達式”(developerWorks,2009 年 5 月)開始學習 Vimscript 和擴展 Vim 編輯器的嵌入式語言。
- 查看以下資源繼續學習有關 Vim 編輯器和它的很多命令:
- Vim 主頁
- 在線圖書?A Byte of Vim
- 關于 Vim 的各種圖書
- Vim 手冊
- Steve Oualline 的?Vim Cookbook
- 要獲得 Vimscript 腳本的大量例子,請查看:
- Vim Tips wiki
- Vimscript 歸檔
- from:?https://www.ibm.com/developerworks/cn/linux/l-vim-script-5/
總結
以上是生活随笔為你收集整理的使用脚本编写 Vim 编辑器,第 5 部分: 事件驱动的脚本编写和自动化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用脚本编写 Vim 编辑器,第 4 部
- 下一篇: vi 技巧和诀窍:令人刮目相看的 10