如何寫 Emacs 命令

我用 Emacs 寫文檔,敲代碼,彷佛已有十五年了,期間曾有兩三次叛逃到了 Vim 陣營,最長的一次,約長達兩年。html

這兩年,據說很多 Emacs 用戶叛逃到了 VS Code 陣營。我安之若素。不過,我並不是 Emacs 資深用戶。我駕馭 Emacs 的能力,僅僅比新手更知道 Emacs 的配置文件在哪兒,怎樣抄別人的配置,以及不甚畏懼 Lisp 代碼。node

Emacs 可能已經再也不是世上最好的文本編輯器了,但它多是世界上惟一支持用戶使用 Lisp 語言爲它寫擴展的文本編輯器。Lisp 語言頗有趣,Emacs 也就頗有趣。最好的,沒必要有趣。有趣的,沒必要最好。再者,客觀而言,別的編輯器能作的事,有什麼是 Emacs 作不到的麼?莫要忘記,Emacs 是假裝成文本編輯器的操做系統。正則表達式

今天我要給像我這樣的假裝成 Emacs 老手的新手寫一篇關於編寫 Emacs 命令的入門文章。由於我昨天寫了一個 Emacs 命令。它不是我人生中所寫的最初的 Emacs 命令。在它們以前,我也寫過一些別的命令。只是假若此次再不認真記錄一下,之後遇到相似的問題,個人表現依然像是一個新手,一時居然不知如何下手。編程

問題

寫 Emacs 命令,就是用 Emacs Lisp——下文一直將其簡稱爲 ELisp——編寫程序。markdown

編程是爲了解決問題。若是沒有問題,就不須要學習編程,不然,學會的只是某種編程語言的語法,而不是用這種語言編程的技藝。網絡

我有問題。由於我在 Emacs 裏曾經用了一個我根本就記不住的正則表達式,將編程語言

![foo.png](https://upload-images.jianshu.io/upload_images/11203728-22a5ea9d16a8c1da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![bar.png](https://upload-images.jianshu.io/upload_images/11203728-a26c53d305a61e9d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![xxx.png](https://upload-images.jianshu.io/upload_images/11203728-50bc5a679b288b7a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

... ... ...

轉化爲編輯器

[foo]: https://upload-images.jianshu.io/upload_images/11203728-22a5ea9d16a8c1da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240

[bar]: https://upload-images.jianshu.io/upload_images/11203728-a26c53d305a61e9d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240

[xxx]: https://upload-images.jianshu.io/upload_images/11203728-50bc5a679b288b7a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
... ...

熟悉 Markdown 的人,或許能辨識出,我是將圖片列表轉化爲圖片的引用列表。不熟悉 Markdown 的人,就對比一下上述的代碼塊,看看它們有什麼不一樣。函數

區域選擇

假設存在文件 foo.md,其內容以下:學習

... 正文 ...

圖片列表:

![農夫和-T21.jpg](https://upload-images.jianshu.io/upload_images/11203728-0e9cadcfda448325.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![干將.jpg](https://upload-images.jianshu.io/upload_images/11203728-c2f31785a97068f6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![齒刃.jpg](https://upload-images.jianshu.io/upload_images/11203728-79f77acea166aa65.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![獵刀.jpg](https://upload-images.jianshu.io/upload_images/11203728-589e82eb51688e52.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

... ... ...

用 Emacs 打開 foo.md 以後,知道如何選中上述的圖片列表區域麼?若是不知道,就用鼠標去劃拉吧,只要能選出它們便可,可是我仍是建議用 Emacs 預綁定的鍵,不知道,學一下又不難。只需 C-@C-a, C-nC-e 這幾個鍵就足以實現下圖所示的文本區域選擇:

區域選擇結果

文本匹配與替換

對於選中的文本區域,可以使用 Emacs 的命令 M-x replace-regexp,使用正則表達式捕捉文本,並對其進行替換。可解決個人問題的正則表達式的匹配和替換命令爲

匹配:\!\[\(.*\)\..+\](\(.*\))
替換:[\1]: \2

因爲用於文本匹配的正則表達式使用了分組捕獲,因此在替換命令裏可基於捕獲的結果構造替換文本。

上述文本匹配和替換命令可表示爲

M-x replace-regexp RET \!\[\(.*\)\..+\](\(.*\)) RET [\1]: \2 RET

其中,RET 表示敲回車鍵。

若是不會編寫 Emacs 命令,或者由於懶惰而不肯去編寫 Emacs 命令,那麼每次遇到此類問題時,能一鼓作氣地將文本匹配和替換的正則表達式寫正確,不知作別人如何,我常常作不到。

能否以將上述操做也寫成一個命令呢?

能夠。

在哪兒寫?

若是是本身寫 Emacs 命令,在哪都行。我是在 myfun.el 文件裏寫,該文件位於 $HOME/.myscript/emacs。爲了讓 Emacs 可以發現我寫的命令,我須要將 myfun.el 文件的加載路徑在 Emacs 的配置文件裏告訴 Emacs。

個人 Emacs 配置文件是 $HOME/.emacs.d/init.el,由於我用的是 Linux。其餘操做系統,有各自的路徑。在 init.el 文件中增長:

(load "~/.my-scripts/emacs/myfun")

而後,我即可以將 myfun.el 文件看成個人試驗室了。

一個什麼都不能作的命令

在 myfun.el 中寫下

(defun foo ()
  (interactive))

而後,打開一個新的 Emacs 窗口 1,執行命令 M-x foo,亦即摁住 Alt 鍵,再摁 x 鍵,而後鬆開,Emacs 底部的微緩衝區(Minibuffer)會接受輸入,在其中輸入 foo,回車。

foo 命令什麼都沒作,因此 Emacs 執行了它,什麼變化都不會出現。可是,假若執行命令 C-h f foo,Emacs 會顯示如下信息:

foo is an interactive Lisp function in
‘~/.my-scripts/emacs/myfun.el’.

(foo)

  Probably introduced at or before Emacs version 1.2.

Not documented.

上述信息裏說,foo 是具備交互性的 Lisp 函數。

在 Emacs 環境裏,具備交互性的 Lisp 函數就是 Emacs 的命令。那麼,有不具備交互性的 Lisp 函數嗎?試試將 foo 修改成

(defun foo ())

而後,再開啓一個新的 Emacs 窗口,再次執行 M-x foo,這一次 Emacs 發生了變化,至少在 Emacs 27.1 版本里,它自覺得是,認爲我是要執行 foonote-mode 命令……亦即,此時 foo 已再也不是命令了。它是什麼呢?再度執行 C-h f foo,Emacs 顯示

foo is a Lisp function in ‘~/.my-scripts/emacs/myfun.el’.

(foo)

  Probably introduced at or before Emacs version 1.2.

Not documented.

此時,foo 單純是一個 Lisp 函數了。

defun 不是消除快樂,而是 define function 的縮寫。我如此輕易地就寫出了一個 Lisp 函數,而且執行了它,我以爲挺快樂。

習題:理解 C-h k C-@

提示: C-h 是摁住 Ctrl 鍵,再摁 h 鍵,而後鬆開。 C-@ 是摁住 Ctrl 鍵,再摁住 Shift,最後摁下數字 2 鍵,而後鬆開,這是由於 @ 在 2 上面,須要 Shift 切換。

區域的起點和終點

在 Emacs 裏選取的文本區域,它有起點和終點,可分別經過 Emacs 提供的函數 region-beginningregion-end 獲取。爲何不是 region-beginregion-end 呢?由於 begin 不具名詞性。藉助 Emacs 提供的 message 函數,我可讓 foo 命令在微緩衝區中顯示文本區域的起點和終點。

新的 foo 命令的定義以下:

(defun foo ()
  (interactive)
  (message "文本區域 [%d,%d)" (region-beginning) (region-end)))

若是徹底不懂 Lisp,可是略懂 C 語言,那麼上述代碼我能夠大體翻譯爲 C 代碼:

void foo(void) {
    printf("文本區域 [%d,%d)", region-beginning(), region-end());
}

當我在 Emacs 裏執行 M-x foo 時,至關於 C 代碼的主函數調用了 foo

int main(void) {
    foo();
    return 0;
}

文本匹配和替換函數

既然我能夠執行 M-x replace-regexp 命令對選中的區域內的文本進行匹配和替換,那麼在 foo 函數是否也能調用它呢?

固然能。可是,須要先了解一下這個函數,執行 C-h f replace-regexp,即可查閱關於它的文檔:

replace-regexp is an interactive compiled Lisp function in
‘replace.el’.

(replace-regexp REGEXP TO-STRING &optional DELIMITED START END
BACKWARD REGION-NONCONTIGUOUS-P)

  This function is for interactive use only;
  in Lisp code use `re-search-forward' and `replace-match' instead.
  Probably introduced at or before Emacs version 21.1.

Replace things after point matching REGEXP with TO-STRING.
Preserve case in each match if ‘case-replace’ and ‘case-fold-search’
are non-nil and REGEXP has no uppercase letters.

... ... ...

Third arg DELIMITED (prefix arg if interactive), if non-nil, means replace
only matches surrounded by word boundaries.  A negative prefix arg means
replace backward.

雖然文檔是英文的,但即便翻譯成中文,我也是看不太懂。我能看懂的部分,寫成僞代碼就是

(replace-regexp
   "\\!\\[\\(.*\\)\\..+\\](\\(.*\\))"
   "[\\1]: \\2"
    nil (region-beginning) (region-end) ...接下來的我不懂...)

其中的 nil 對應的那個參數,它的含義我也是不太懂,我只是試着將它的值設爲 nil,也不知結果如何。可是,我在 Emacs Lisp 手冊上曾經看到過函數幫助文檔 2 給出的格式標記說明,凡出如今 &optional 記號以後的參數,它們是可選參數。這意味着,我能保證

(replace-regexp
   "\\!\\[\\(.*\\)\\..+\\](\\(.*\\))"
   "[\\1]: \\2"
    nil (region-beginning) (region-end))

至少是合法的代碼。將它置入 foo 函數的定義,即

(defun foo ()
  (interactive)
  (replace-regexp
   "\\!\\[\\(.*\\)\\..+\\](\\(.*\\))"
   "[\\1]: \\2"
    nil (region-beginning) (region-end)))

試了一下,M-x foo 工做得很好。在此,須要解釋一下,爲何 foo 函數調用的 replace-regexp 比直接 M-x replace-regexp 多了不少 \ 。由於在 Lisp 語言中,\ 有它自身的做用,即轉義符,要將它做爲字面值使用,須要用它對它自己進行轉義,就成了 \\

M-x foo

後記

問題獲得了很好的解決。事實上,我解決的遠比上文最後給出的 foo 函數還要好,但我不想在這篇文章上浪費太多時間。由於網絡上不只有很好的 Emacs 教程,也有很好的 ELisp 入門教程 3 。還有個怪人,叫李殺,他爲 Emacs 和 ELisp 寫了許多優秀的教程 4 。這篇文章真正想表達的是,一旦有了想去解決的問題,就不那麼畏懼或輕視 Lisp 了,哪怕它是 Emacs Lisp。

對於願意去解決問題的人,從不會落入形而上學的窠臼。由於他們會千方百計讓本身可以走下去,直至達到目的。問題解決以後,假若沒有新的問題,形而上學一下也無妨,就如我此刻。

附錄

更全面的 foo 函數的定義:

(defun jianshu-image-refs ()
  (interactive)
  (save-restriction
    (let (x y text)
      (setq x (region-beginning))
      (setq y (region-end))
      (setq text (buffer-substring-no-properties x y))
      (setq text (replace-regexp-in-string
                  "\\!\\[\\(.*\\)\\..+\\](\\(.*\\))"
                  "[\\1]: \\2"
                  text))
      (delete-region x y)
      (insert text)
      (flush-lines "^$" x (+ x (length text))))))

  1. 確切地說,應該是開啓一個新的 Emacs 進程。「窗口」在 Emacs 環境裏,另具深意。
  2. https://www.gnu.org/software/...
  3. https://bzg.fr/en/learn-emacs...
  4. http://ergoemacs.org>
相關文章
相關標籤/搜索