假若將 Elisp 的應用場景固定爲文本處理,學習 Elisp,我認爲無需像學習其餘任何一門編程語言那樣亦步亦趨,因此本章直接從文件讀寫開始入手,經過一些小程序,創建對 Elisp 語言的初步感覺。編程
雖然我已決定從文件讀寫開始學習 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
這是由於,princ
函數僅僅是將字符串類型的數據原樣輸出。若讓終端保持有序,輸出的文本末尾需附加一個換行符 \n
。雖然修改 princ
的定義完成此事彷佛甚爲困難,可是站在它的肩膀上定義一個新的函數完成此事,則甚爲簡單:
(defun princ\' (x) (princ x) (princ "\n")) (princ\' "Hello world!")
princ\'
即是我定義的新函數。我本來想使用 princ'
這個名字,可是符號 '
已被 Elisp 語言做爲一個有特殊語意的符號,所以我不得不使用 Elisp 語言的字符轉義符 \
對 '
進行轉義,代表後者無特殊含義,僅僅是一個符號。
定義一個函數,遵循的格式是
(defun 函數名 (參數) 函數體)
函數體由一個或一組表達式構成。在 princ\'
的定義中,函數體由兩個表達式構成。
運行新的 hello-world.el 程序,結果以下圖所示:
假設存在文本文件 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-min
和 point-max
能夠得到緩衝區的起止位置,所以可基於它們將插入點移動到緩衝區的開頭或結尾。例如,將 |||
插入到 Hello world!
的尾部:
(find-file "foo.txt") (goto-char (point-max)) (insert "|||")
在此,也許應該提出一個疑問,爲什麼須要用 point-min
得到緩衝區起始位置?難道這個位置不是 1 嗎?由於在緩衝區內部能夠建立更小的局部區域,而它也是 Elisp 的一種數據類型,它的名字叫 Narrowing。對於位於 Narrowing 區域的文本,也能夠用 point-min
和 point-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 便會打開該函數的文檔。在文檔裏,函數的用途、參數以及返回結果皆有詳細的說明。一開始,看不懂,也不大要緊,關鍵是要去看。