上一章:緩衝區變換html
不少程序是有圖形界面的,就是平常所見的那些有菜單和按鈕的窗口以及對話框之類。在終端裏運行的程序,一般也叫命令行程序,它們也有界面,即一組選項和參數。這兩種程序,各有所長,也各有所短。segmentfault
我之因此學習 Elisp 語言,是由於感受它的長處適合編寫文本處理程序,例如上一章所寫的一個簡單的文本處理程序,它能夠將文本由 Markdown 格式翻譯爲 HTML 格式。像這樣的文本處理程序,它們的運行一般並不須要圖形界面,不然我爲什麼不直接爲 Emacs 寫一個插件呢?數組
如同函數能夠有參數,命令行程序也能夠有一些參數。凡是函數或程序沒法決斷的一些因素,可抽象爲一組參數,交由函數或程序的使用者決斷。命令行選項本質上也是命令行參數,只不過它至關於程序的一些功能開關,可用於開啓或關閉程序的一些功能,也可用於修飾其餘參數。函數
選項傾向於定性,而參數傾向於定量。當兩者統一爲程序的參數時,即可使得程序可以明確咱們要用它解決什麼問題。有些問題只須要定性的角度去解決。有些問題只須要從量化的角度去解決,所以對兩者做區分,也是有意義的。學習
在 Linux 系統裏,命令行程序佔據了半壁甚至更多的江山。這些命令行程序的選項,一般以 -
或 --
做爲前綴,參數則沒有前綴,因而在形式上對於程序的使用者而言,兩者有着明顯的區別。測試
在上一章裏,我寫了個可將文本由 Markdown 格式變換爲 HTML 格式的程序。這個程序雖然在功能上遠不健全,可是已經到了要爲它設計選項和參數的時候了。插件
假設這個程序名爲 mdc.el,執行這個程序時,它支持 -i
和 -o
兩個選項。-i
選項用於指定輸入文件名,-o
選項用於指定輸出文件名,其中輸入文件名和輸入文件名都是與選項對應的參數。例如命令行
$ emacs -Q --script mdc.el -i foo.md -o foo.html
假若不向 mdc.el 提供任何選項和參數,或者提供了它不認識的選項和參數,它也不表示任何不滿意,僅僅是在終端輸出:翻譯
用法:emacs -Q --script mdc.el -i 輸入文件 -o 輸出文件
嵌入在 Emacs 內部的 Elisp 解釋器,它可以從終端裏得到全部的選項和參數,將結果保存爲一個列表變量 argv
,這是個全局變量。因而,在 mdc.el 程序裏,只需訪問這個列表,即可以得到所需的選項和參數。固然,這須要對 argv
進行遍歷,而後作一些文本匹配方面的工做。這些工做再也不有任何難度,所須要的知識,在前面的章節裏已經運用得很熟練了吧。設計
假設 mdc.el 的實現以下:
(defun princ\' (x) (princ x) (princ "\n")) (while (not (null argv)) (princ\' (car argv)) (setq argv (cdr argv)))
注意,在判斷列表是否爲空,我一直是使用 (not (null 列表對象))
的方式,由於我一直不想認可 Elisp 語言裏非 nil
即爲真的規矩。可是,如今以爲,入鄉仍是隨俗吧,認可 (not (null 列表對象))
等價於 列表對象
。
執行 mdc.el,
$ emacs -Q --script mdc.el a b foo bar blab blab
結果在終端輸出如下信息:
a b foo bar blab blab
這意味着遍歷 argv
的程序是正確的。假若在遍歷過程當中增長文本匹配和參數獲取功能,即可以獲得輸入文件名和輸出文件名了。例如,
(let (x input output) (while argv (setq x (car argv)) (setq argv (cdr argv)) (cond ((string= x "-i") (setq input (car argv))) ((string= x "-o") (setq output (car argv))))) (if (or (not input) (not output)) (princ\' "emacs -Q --script mdc.el -i 輸入文件 -o 輸出文件")))
在上述代碼中,遍歷 argv
過程結束後,基於邏輯「或」運算 or
,對 input
和 output
變量進行了基本的有效性檢測。該檢測僅能保證它們已經獲得了賦值,可是所賦之值是否正確,例如在命令行裏輸入了錯誤的文件名,這種狀況,程序沒法判斷。
上一節最後的那段代碼裏,檢測變量 input
和 output
的有效性的條件表達式只含有邏輯表達式爲真時對應的程序分支,另外一個分支不存在,如今能夠爲將 mdc.el 的功能部分做爲該分支。
mdc.el 的功能部分,即上一章所實現的緩衝區變換程序,其中可與界面代碼進行結合的部分是
(let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file "foo.md") html-buffer) (with-current-buffer html-buffer (write-region nil nil "foo.html")))
如今能夠將這段代碼中的 foo.md
和 foo.html
替換爲字符串變量 input
和 output
,而後將這段代碼嵌入到界面代碼裏,結果爲
(let (x input output) (while argv (setq x (car argv)) (setq argv (cdr argv)) (cond ((string= x "-i") (setq input (car argv))) ((string= x "-o") (setq output (car argv))))) (if (or (not input) (not output)) (princ\' "emacs -Q --script mdc.el -i 輸入文件 -o 輸出文件") (let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file input) html-buffer) (with-current-buffer html-buffer (write-region nil nil output)))))
上一節最後給出的代碼有些繁冗,不妨將命令行選項解析部分以及程序功能部分處理出來,封裝爲函數。
若將命令行解析部分封裝爲函數,那麼該函數的求值結果應該包含着 input
和 output
的值。可以包含多個值的求值結果,在 Elisp 語言裏,只有表。能夠是列表,也能夠是 Hash 表,後者更適合存儲命令行程序的選項和參數,由於它能夠將選項以及它修飾的參數組成鍵值對結構。
使用 Elisp 函數 make-hash-table
可建立 Hash 表實例,例如
(make-hash-table :test 'equal)
其中,:test 'equal
用於指定使用 equal
函數判斷用於從 Hash 表檢索數據的鍵與 Hash 表的鍵是否相等。我不知道爲何 string=
不能夠。equal
能夠比較兩個對象是否相同,應用範圍要比 =
和 string=
更爲普遍,例如它也能夠判斷兩個列表是否相等。除 equal
外,Elisp 的 Hash 表還有兩個可選的鍵相等測試函數,eq
和 eql
,假若不指定測試函數,make-hash-table
默認使用 eql
,僅適用於建立以數字做爲鍵的 Hash 表。
將一個符號與 Hash 表綁定,便有了一個 Hash 表變量:
(setq mdc-args (make-hash-table :test 'equal))
Elisp 函數 puthash
可向 Hash 表添加鍵值對,例如:
(puthash "-i" "foo.md" mdc-args)
Elisp 函數 gethash
可以使用鍵,從 Hash 表裏得到與鍵對應的值,例如
(gethash "-i" mdc-args)
掌握了上述函數的用法,即可實現一個解析命令行,並將解析結果存儲到 Hash 表的函數了,例如:
(defun mdc-get-args (mdc-args) (let (x) (while argv (setq x (car argv)) (setq argv (cdr argv)) (if (string-match "-i\\|-o" x) (progn (puthash x (car argv) mdc-args) (setq argv (cdr argv)))))))
mdc-get-args
函數的用法以下:
(let ((mdc-args (make-hash-table :test 'equal))) (mdc-get-args mdc-args) (let ((input (gethash "-i" mdc-args)) (output (gethash "-o" mdc-args))) (if (or (not input) (not output)) (princ\' "emacs -Q --script mdc.el -i 輸入文件 -o 輸出文件") (let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file input) html-buffer) (with-current-buffer html-buffer (write-region nil nil output))))))
Elisp 的關聯列表也可用於存儲選項和參數,用法與 Hash 表相似,只是數據訪問效率遠低於後者。不過,對於存儲命令行選項和參數這樣的工做,關聯列表足以勝任。
關聯列表的每一個元素是序對。cons
可構造序對,例如:
(cons "-i" "foo.md")
也能夠用 .
語法構造序對,例如:
("-i" . "foo.md")
事實上,Elisp 的列表的本質就是一組級聯的序對結構,例如 '(1 2 3 4)
,在 Elisp 解釋器看來,它的真正結構是
(1 . (2 . (3 . (4 . ()))))
car
能夠取序對的第一個元素。cdr
則用於取序對的第二個元素。
構造關聯列表,能夠像普通列表那樣使用 cons
函數。例如:
(setq mdc-args '()) (setq mdc-args (cons ("-i" . "foo.md") mdc-args)))
Elisp 函數 assoc
可根據給定的鍵,可從關聯列表裏獲取第一個同鍵的序對,例如:
(assoc "-i" mdc-args)
求值結果爲 ("-i" . "foo.md")
。爲何要強調「第一個」呢?由於關聯列表裏,容許多個序對有相同的鍵。
要得到鍵對應的值,須要使用 cdr
,例如
(cdr (assoc "-i" mdc-args))
至於如何使用關聯列表保存命令行選項和參數,這個任務能夠做爲本章的練習題。
這個教程,更確切地說,是我學習 Elisp 所做的筆記,第一部分可就此落幕了。至於這個教程,會不會有第二部分,這取解決於我是否遇到了新的文本處理問題。
完整的 mdc.el 程序代碼以下:
(defun princ\' (x) (princ x) (princ "\n")) (defun section-n (level name) (let ((n (length level))) (format "<h%d>%s</h%d>" n name n))) (defun empty-line (text) "") (defun paragraph (text) (format "<p>%s</p>" text)) (defun translate (text) (cond ((string-match "^\\(#+\\)[[:blank:]]+\\(.+\\)$" text) (section-n (match-string 1 text) (match-string 2 text))) ((string-match "^$" text) (empty-line text)) (t (paragraph text)))) (defun current-line () (buffer-substring (line-beginning-position) (line-end-position))) (defun translate-buffer (source target) (with-current-buffer source (let (text) (while (< (point) (point-max)) (setq text (translate (current-line))) (with-current-buffer target (insert text) (insert "\n")) (forward-line 1))))) (defun mdc-get-args (mdc-args) (let (x) (while argv (setq x (car argv)) (setq argv (cdr argv)) (if (string-match "-i\\|-o" x) (progn (puthash x (car argv) mdc-args) (setq argv (cdr argv))))))) (let ((mdc-args (make-hash-table :test 'equal))) (mdc-get-args mdc-args) (let ((input (gethash "-i" mdc-args)) (output (gethash "-o" mdc-args))) (if (or (not input) (not output)) (princ\' "emacs -Q --script mdc.el -i 輸入文件 -o 輸出文件") (let ((html-buffer (generate-new-buffer "html"))) (translate-buffer (find-file input) html-buffer) (with-current-buffer html-buffer (write-region nil nil output))))))