走在 Elisp 的歧路上 · 01 緩衝區和文件

假若將 Elisp 的應用場景固定爲文本處理,學習 Elisp,我認爲無需像學習其餘任何一門編程語言那樣亦步亦趨,因此本章直接從文件讀寫開始入手,經過一些小程序,創建對 Elisp 語言的初步感覺。編程

Hello world!

雖然我已決定從文件讀寫開始學習 Elisp,可是我仍是但願對初學者友好一點,畢竟我也是初學者。這種友好應該像學習任何一門編程語言那樣,從寫一個可以輸出 Hello world! 的程序。小程序

用 Emacs 新建一份文本文件,名曰 hello-world.el。固然,也可使用其餘文本編輯器完成此事,可是要保證系統已安裝了 Emacs 且可用。hello-world.el 的內容只有一行:數組

(princ "Hello world!")

在終端(或命令行窗口)裏,將工做目錄(當前目錄)切換至 hello-world.el 文件所在的目錄,而後執行網絡

$ emacs -Q --script hello-world.el

終端會隨即顯示編程語言

Hello world!

從這個 Hellow world 程序裏,能學到哪些 Elisp 知識呢?編輯器

首先,princ 是一個函數,確切地說,是 Elisp 的內建函數。什麼是函數?在數學裏,y=f(x) 是函數,f 可將 x 映射爲 y。princ 也是這樣的函數,它將 "Hello world! 這個對象映射爲顯示於終端的對象。函數

其次,"Hello world!" 是 Elisp 的字符串類型,用於表示一段文本。文本是數據。數據未必是文本。若將 Elisp 做爲用於處理文本的語言,字符串就是基本且核心的數據類型。學習

最後,這個做爲示例的 Elisp 程序的最小單位是一個函數調用。我向 princ 函數提供一個字符串類型的值,即可令其工做,且足以構成一個程序。Emacs 裏有 Elisp 解釋器。Elisp 程序是由 Elisp 解釋器解釋運行的,相似於計算機程序是由計算機的 CPU 「解釋」運行。換言之,Elisp 解釋器可以讀懂 Elisp 程序,並完成這個程序所描述的那些工做,例如,在終端裏輸出 Hello world!搜索引擎

定義一個新的函數

hello-world.el 程序雖然能在終端裏輸出 Hello world!,可是它的輸出很容易令終端有所錯亂,例如將個人終端弄成了下面這幅樣子:spa

Hello world!

這是由於,princ 函數僅僅是將字符串類型的數據原樣輸出。若讓終端保持有序,輸出的文本末尾需附加一個換行符 \n。雖然修改 princ 的定義完成此事彷佛甚爲困難,可是站在它的肩膀上定義一個新的函數完成此事,則甚爲簡單:

(defun princ\' (x)
  (princ x)
  (princ "\n"))

(princ\' "Hello world!")

princ\' 即是我定義的新函數。我本來想使用 princ' 這個名字,可是符號 ' 已被 Elisp 語言做爲一個有特殊語意的符號,所以我不得不使用 Elisp 語言的字符轉義符 \' 進行轉義,代表後者無特殊含義,僅僅是一個符號。

定義一個函數,遵循的格式是

(defun 函數名 (參數)
  函數體)

函數體由一個或一組表達式構成。在 princ\' 的定義中,函數體由兩個表達式構成。

運行新的 hello-world.el 程序,結果以下圖所示:

princ'

緩衝區

假設存在文本文件 foo.txt,其內容爲

Hello world!

如何寫一個 Elisp 程序,從 foo.txt 讀取所有內容並輸出到終端?

讀取文件,這個操做意味着什麼?意味着從計算機輔存(硬盤)中獲取數據,放入主存(內存)。緣由在於,計算機 CPU 訪問主存的速度遠快於輔存。

爲了簡化文件的讀寫,Elisp 提供一種數據類型——緩衝區(Buffer)。緩衝區對象(也可稱爲緩衝區實例)本質上是計算機主存裏的一段空間。文件的內容被讀取後,存入緩衝區實例裏,在後者中可進行文件內容的編輯工做。編輯完畢後,緩衝區實例包含的信息能夠再存迴文件。爲了便於描述,在不至於引發誤解的前提下,我會將緩衝區實例簡稱爲緩衝區。相似的稱謂也適用於 Elisp 的其餘數據類型上,例如數字、字符串、列表、數組、哈希表等。

使用 Elisp 函數 generate-new-buffer 能夠建立一個有名字的緩衝區。例如,建立一個名曰 foo 的緩衝區:

(generate-new-buffer "foo")

能建立一個,就能建立多個,可是不管建立了多少個,其中只可能有一個是激活的,亦即當前緩衝區。在讀取文件時,從文件獲取的數據老是存放在當前緩衝區內。Elisp 函數 buffer-name 能夠得到當前緩衝區的名字。如下程序可查看當前緩衝區的名字:

(princ\' (buffer-name))

Elisp 解釋器有一個默認的緩衝區,名字叫 *scratch*。假若沒有建立新的緩衝區並將其激活爲當前緩衝區,那麼上述程序的輸出就是 *scratch*

Elisp 函數 set-buffer 可將指定的緩衝區設爲當前緩衝區。例如,下面這個程序可將上文建立的 foo 緩衝區設爲當前緩衝區,並經過輸出當前緩衝區的名字它是否爲當前緩衝區:

(set-buffer "foo")
(princ\' (buffer-name))

set-buffer 的參數除了能夠是緩衝區的名字,也能夠是緩衝區自己。因爲 generate-new-buffer 可以返回它建立的新緩衝區,所以它能夠與 set-buffer 函數複合,用於建立一個緩衝區並將其設爲當前的緩衝區,例如

(set-buffer (generate-new-buffer "foo"))

將上述代碼綜合一下,能夠放在一個名字叫 foo.el 的文本文件裏。foo.el 內容爲

(defun princ\' (x)
  (princ x)
  (princ "\n"))

(princ\' (buffer-name))

(set-buffer (generate-new-buffer "foo"))
(princ\' (buffer-name))

在終端裏,若以 foo.el 所在目錄爲工做目錄,執行

$ emacs -Q --script ./foo.el

輸出爲

*scratch*
foo

這是我寫的第二個 Elisp 程序,感受還不錯。別的編程語言裏,可能沒有緩衝區這種設施。沒有對比,就沒有傷害。沒有傷害,就沒有自豪。

文件讀取

對於上一節一開始所提出的問題,事實上並不須要我去爲待讀取的文件建立一個緩衝區,並將其設爲當前緩衝區。Elisp 提供的 find-file 能夠替我完成這項工做。例如,

(find-file "foo.txt")
(princ\' (buffer-name))

所產生的輸出爲

foo.txt

這個名曰 foo.txt 的緩衝區,即是 find-file 函數爲 foo.txt 文件而建立的。

如何確認 foo.txt 文件裏的內容真的被讀取後存放到 foo.txt 緩衝區呢?可經過 buffer-string 函數以字符串的形式得到當前緩衝區存儲的數據,而後將所得結果顯示於終端,例如

(princ\' (buffer-string))

所以,讀取 foo.txt 文件裏的內容,並將其顯示於終端的程序至此便完成了。完整的程序以下:

(defun princ\' (x)
  (princ x)
  (princ "\n"))

(find-file "foo.txt")
(princ\' (buffer-string))

代碼風格

Elisp 代碼,只要不破壞名字,它的風格是很隨意的。例如 princ\'函數的定義,寫成

(defun princ\' (x) (princ x) (princ "\n"))

也是能夠的。

寫成

(defun
    princ\'
    (x)
  (princ
   x)(princ
       "\n"))

也不是不行。可是,最好不要寫怪異的代碼。畢竟,那層層括號的嵌套,人生已經很不容易了。

括號不管是內層的,仍是外層的,它們老是成對出現。Lisp 語言最大特色就是,不管是函數的定義,仍是函數的調用,仍是其餘的一些表達式,在形式上是由括號構成的嵌套結構。這種結構,Lisp 語言稱爲列表。

若是使用 Emacs 編寫 Elisp 代碼或其餘 Lisp 方言的代碼,要記得安裝 paredit 包。我不想浪費時間去講如何安裝和使用這個包。不徹底是由於沒人給我發稿費,主要是每一個人都應該會用網絡搜索引擎。

在緩衝區內插入文本

不管是用 find-file 函數自動建立的緩衝區,仍是基於 generate-new-buffer 建立的緩衝區,一旦它們被設定爲當前緩衝區,即可以使用 Elisp 提供的一些函數,將數據寫入其中。

insert 函數可將字符串類型的數據寫入當前緩衝區,例如:

(defun princ\' (s)
  (princ (concat s "\n")))
  
(find-file "foo.txt")
(insert "|||")
(princ\' (buffer-string))

輸出結果爲

|||Hello world!

可見 insert 函數將 ||| 插入到了當前緩衝區存儲的文本數據的首部。這是由於,當前緩衝區內存在這一個不可見的光標,我將其稱爲插入點,它對應於 Emacs 圖形窗口裏不斷閃動的那個光標,表示文本的插入點。在使用 find-file 打開一份文件時,插入點會自動定位在文件的開頭,座標爲 1。爲了理解插入點,就須要將緩衝區想像成一維數組,存放的元素爲字符,這個一維數組就像一根很長的紙帶那樣,插入點的座標就是插入點位於第幾個字符以前。

point 函數能夠得到插入點的座標。例如

(find-file "foo.txt")
(princ (point))

輸出 1

goto-char 函數可將插入點移動到緩衝區內的任何位置。例如,假若將 ||| 插入 Hello world! 的兩個單詞的中間,只需

(find-file "foo.txt")
(goto-char 6)
(insert "|||")

因爲函數 point-minpoint-max 能夠得到緩衝區的起止位置,所以可基於它們將插入點移動到緩衝區的開頭或結尾。例如,將 ||| 插入到 Hello world! 的尾部:

(find-file "foo.txt")
(goto-char (point-max))
(insert "|||")

在此,也許應該提出一個疑問,爲什麼須要用 point-min 得到緩衝區起始位置?難道這個位置不是 1 嗎?由於在緩衝區內部能夠建立更小的局部區域,而它也是 Elisp 的一種數據類型,它的名字叫 Narrowing。對於位於 Narrowing 區域的文本,也能夠用 point-minpoint-max 獲取起止位置,故而 point-min 得到的結果未必是 1。關於 Narrowing,它在 Emacs 圖形界面裏較爲有用,在使用 Elisp 編寫文本處理程序方面,我暫時還沒思考出它的應用場景。

在緩衝區內刪除文本

Elisp 函數 delete-char 能夠刪除插入點以後的字符。例如,如下程序將 foo.txt 讀入緩衝區後,插入點尚在緩衝區起始位置時,刪除它後面 5 個字符,

(find-file "foo.txt")
(delete-char 5)

Elisp 也提供了一些與插入點位置無關的緩衝區文本刪除函數,其中 delete-region 能夠刪除落入指定區間的文本。例如,刪除緩衝區內第 6 個字符到第 12 個字符之間的字符,被刪除的字符包括前者,但不包括後者,

(find-file "foo.txt")
(delete-region 6 12)

可使用 (princ\' (buffer-string)) 查看緩衝區內容的變化。

將緩衝區內容寫入文件

如今,已經基本掌握了從文件讀取內容到緩衝區,在緩衝區內寫入一些內容,接下來,須要考慮的一個問題是,緩衝區的內容該如何保存到文件裏。保存方式天然有兩種,一種是保存到與當前緩衝區關聯的文件,另外一種是保存到其餘文件。

save-buffer 可將當前緩衝區保存到與之關聯的文件裏。例如

(find-file "foo.txt")
(goto-char (point-max))
(insert "|||")
(save-buffer)

運行上述程序後,可打開 foo.txt 文件查看其內容,是否在 Hello world! 以後多出了 |||

write-file 可將當前緩衝區保存到其餘文件。例如

(find-file "foo.txt")
(goto-char (point-max))
(insert "|||")
(write-file "bar.txt")

結語

本章的內容雖然較爲簡單,可是已經隱約觸及了 Emacs 的一些本質。假若理解並熟悉了本文出現的全部 Elisp 已經提供的函數的用法,至關於掌握了 Emacs 最樸素的功能,即打開一份文件,添加一些內容,刪除一些內容,而後保存,並不須要一個圖形界面幫助咱們完成這些事。

文中所出現的函數,除 princ\' 以外,我將其餘全部函數說成 Elisp 提供的,甚至一度想將它們稱爲 Elisp 標準庫裏的函數。但事實上,Elisp 只是一門語言,並且也不存在這個標準庫。這些函數皆來自於 Emacs 的核心功能——數量龐大的函數集,分散於衆多 Elisp 源程序文件。我將這些函數統稱爲 Elisp 函數。

在 Emacs 裏默認的鍵綁定 C-h f,而後輸入其中的某個函數名,回車,Emacs 便會打開該函數的文檔。在文檔裏,函數的用途、參數以及返回結果皆有詳細的說明。一開始,看不懂,也不大要緊,關鍵是要去看。

相關文章
相關標籤/搜索