Emacs Lisp 编程

Table of Contents

1 开发环境

ELisp 代码通常保存在 .el 为后缀的文件中,也可切换到“*scratch*” buffer 里编代码。

当处于 lisp-mode 时,光标移到 Lisp 表达式尾部:

  1. 按 C-x C-e 执行 Lisp 表达式。
  2. 按 C-j 执行 Lisp 表达式,并插入返回值在当前 buffer 中。

还有一些工具帮助你开发 Emacs Lisp 程序:

  • paredit,自行安装,可用于补全括号等操作。
  • ielm,Emacs 自带的 Emacs Lisp 交互式环境,M-x ielm。

Emacs Lisp 的大多函数都有说明文字,调用 documentation 函数可以查看对应的文档,如查看 message 的说明:

(documentation 'message)

当然,更常用的是使用快捷键 C-h f 或 M-x describe-function。

2 基础

; 这是注释

; Lisp 的语法很简单,就三种:
; - (hello):表示调用 hello 这个函数(或宏)。
; - '(hello):表示一个列表。
; - hello:返回符号对应的值,等价于求变量的值。

(defvar hello "hello world")            ; 定义一个变量
(setf hello "Hello world")   ; 调用 setf 来改变变量的值
hello                      ; 返回 hello 对应的值

;; 以下改变 hello 的值,和 setf 的区别是 set 需要对符号加上引用符“'”
(set 'hello "Hello")

;; 表示这是一个列表,有三个元素:
;; 第一个是符号 setf
;; 第二个是符号 hello
;; 第三个是字符串:Hello world
'(setf hello "Hello world")

'hello                                  ; 表示返回 hello 这个符号,等同于 (quote hello)

3 基本类型

;; 通过 type-of 函数获得对象的类型:
(type-of 1)                             ; => integer
(type-of "test")                        ; => string
(type-of 1.)                            ; => integer
(type-of 1.0)                           ; => float
(type-of 'a)                            ; => symbol

;; symbol 是对象的名字
;; Common Lisp 程序员注意,Common Lisp 会把 symbol 名称转成大写,Emacs Lisp 不会。
;; 设置符号的值
(setq hello "hello world")              ; => "hello world"
(setf hello "hello world")              ; => "hello world"
(set 'hello "hello world")              ; => "hello world"

(symbolp 'hello)                  ; => t,判断对象是不是符号
(symbol-value 'hello)             ; => "hello world",获得符号对应的值
(symbol-name 'hello)              ; => "hello",获得符号的名字

;; t 为真,nil 和 () 为假,除此之外都为真,并且 nil 和 () 是相等的。
;; 术语“谓词函数”指函数返回值是 t 或者 nil,这类函数名一般都以“p”结尾,比如 booleanp,用来判断对象是不是布尔类型:
(booleanp t)                            ; => t
(booleanp nil)                          ; => t
(booleanp 1)                            ; => nil
(booleanp '())                          ; => t

;; 字符串由双引号包围
"hello world"

;; 实质上字符串由不可变的字符序列组成——用字符组成的数组。所以可以使用操作数组的函数操作字符串。如"hello"表示字符串 hello,而这个字符串由“h”、“e”、“l”、“l”和“o”这几个字符组成。
;; 可以使用 string 函数手动创建字符串:
(string ?\h ?\i)                        ; => "hi"

;; 创建重复 n 次的字符串
(make-string 5 ?a)                      ; => "aaaaa"

;; 取子字符串
(substring "hello world" 0 5)    ; => "hello",取下标 0~5 之间的字符
(substring "hello world" 6)      ; => "world",从下标 6 开始取剩余字符
(substring "hello world" -5)   ; => "world",倒序取字符
(substring "hello world" -5 -1)         ; => "worl"

;; 连接字符串
(concat "hello " "world")               ; => "hello world"

;; 切割字符串
(split-string "hello world" " ")        ; => ("hello" "world")

;; 获得字符串长度
(length "a string")                   ; => 8

;; 谓词函数
(stringp "abc")                   ; => t,判断是不是字符串

;; 判断对象是否是字符串或者 nil
(string-or-null-p "abc")                ; => t
(string-or-null-p nil)                  ; => t
(string-or-null-p t)                    ; => nil

;; 判断对象是否是字符串或者字符
(char-or-string-p ?\a)            ; => t
(char-or-string-p "a")            ; => t
(char-or-string-p 1)              ; => t,实质上每个字符都有对应的数字

;; 判断字符串是否为空的三种方法
(eq "" "")                              ; => t
(= 0 (length ""))                       ; => t
(equal "" "")                           ; => t
(string-equal "" "")                    ; => t

;; 字符串转列表
(string-to-list "hello world") ; => (104 101 108 108 111 32 119 111 114 108 100)

;; 删除空白字符
(replace-regexp-in-string "" "" "astring") ; => "astring"

;; 在 Emacs Lisp中,运算符其实也是函数:
(+ 1 1 1)                               ; => 3
(- 3 2)                                 ; => 1
(* 2 2)                                 ; => 4
(/ 4 3) ; => 1,注意这里和 Common Lisp 不一样的是,这里不是返回有理数(4/3),而是返回近似值。
(/ 4 3.0)                               ; => 1.3333333333333333
(% 4 3)                                 ; => 1,求余
(expt 2 3)                              ; => 8,次方
(lsh 2 2)                               ; => 8,左移操作
(lsh 4 -2)                              ; => 1,右移操作

;; 整数
1
1.                                      ; 类似1
-1                                      ; 负数表示
+1

;; 数字和字符串互转
(string-to-number "10")                 ; => 10
(number-to-string 10)                   ; => "10"

;; 判断是否是整数
(integerp 1)                            ; => t
(integerp 1.0)                          ; => nil

;; 除此之外还有浮点数:
+1.0
1.0

;; 科学计数
+1e10
100e-10
1e-08

;; 判断对象是不是浮点数
(floatp 1)                              ; => nil
(floatp 1.0)                            ; => t

;; 与浮点数做运算,返回的是浮点类型
(+ 1 1.0)                               ; => 2.0,
(* 1 1.0)                               ; => 1.0
(- 10 1.0)                              ; => 9.0
(/ 4 2.0)                               ; => 2.0

;; Emacs Lisp 有列表(list)和数组(array)两种序列,一个 list 就是一组 cons cell 组成,一个 cons cell 包含两个元素,car 函数取第一个,cdr 取第二个,如:
(defvar a-cons-cell (cons 1 2))
(car a-cons-cell) ; => 1
(cdr a-cons-cell) ; => 2

;; dotted pair:
(1 . 2)
;; 表示 car 是 1,cdr 是 2。

;; association list(alist):
((a . 1) (b . 2) (c . 3))

;; add-to-list,将指定的元素添加到列表头部:
(defvar a '(1 2 3 4))
(add-to-list 'a 5) ;; => (5 1 2 3 4)

;; 经验1,对数据做“shadow”:*
;; Org mode 导出 HTML 文件时,如果源文件中有 Latex 表达式,就在 HTML 中外部引用 MathJax 这个 JavaScript 库来展现。引用的 URL 是外部链接,默认值存储在列表 org-html-mathjax-options 中。如果想自定义 URL,就可用 add-to-list 向列表头部插入自定义的 URL,在导出时会优先使用这个 URL:
(add-to-list 'org-html-mathjax-options '(path "https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"))

;; 数组(array),固定长度的序列,字符串、向量、字符表(chars table)和布尔向量(bool-vector)都是数组。
;; 向量(vector)表示如下:
[1 2 3]

;; 布尔向量(bool-vector):
(make-bool-vector 3 t)
(make-bool-vector 3 nil)

4 循环

;; do,基本语法:
(do ((变量名 初始值 [累加]))
    (结束条件)
  body)

;; 例,输出 1 ~ 10:
(do ((i 0 (+ i 1)))
    ((> i 10))
  (princ i))

5 函数

;; 函数用 defun 定义,如:
(defun hello ()
  (message "hello world"))

;; 实质上我们在 M-x 中执行的命令,都是定义的 Emacs Lisp 函数。要想让函数能在 M-x 中执行,需定义成如下:
(defun hello ()
  (interactive)
  (message "hello world"))
;; 便可在 M-x 输入 hello 来调用。

6 Buffer

;; 打印输出
;; 调用 message 将会在“*Messages*” Buffer 中打印出字符串,所有的打印记录可以到 M-x view-echo-area-messages 中查看:
(message "hi")

;; insert 函数可以在当前 buffer 里插入内容
(insert "hello world")

;; mode-name 变量存储了当前 mode 的名字,按下 M-x eval-expression,输入 mode-name 即可。

;; switch-to-buffer,切换 buffer
;; 不仅可以切换到已有的 buffer,还可以新建 buffer。下面这句代码将切换到一个叫“test”新的 buffer:
(switch-to-buffer "test")

;; with-current-buffer,获得某个 buffer 的内容:
(with-current-buffer "a-buffer"
  (buffer-string))

;; 按行处理 buffer 内容
(dolist (line (split-string (with-current-buffer "a-buffer"
                              (buffer-string))))
  (print line))

7 钩子(hook)

;; Emacs 中的钩子是一种在一定情况下才会触发的机制。

;; add-hook,使 Emacs 在进入某个模式的时候才执行,如:
(add-hook 'text-mode-hook
          (message "Welcome to Text mode"))
;; 当 Emacs 进入 text-mode 的时候,就会提示“Welcome to Text mode”。如果要执行多条语句,可以用 lambda 或者 progn,按李杀的说法,应该尽量避免在 add-hook 使用 lambda,因为这样无法 remove-hook。请见:http://ergoemacs.org/emacs/emacs_avoid_lambda_in_hook.html

8 绑定键

;; kbd,将字符串形式的快捷键描述转换成 Emacs 内部的表示法:
(kbd "C-c C-c") ; 表示两次连按“Ctrl+c”

;; global-set-key 可以将某个函数绑定至指定的快捷键:
(defun say-hello ()
  (interactive)
  (message "hello world"))

;; 将快捷键 C-c h 绑定到了 say-hello 函数上,当按下组合键时,会调用 say-hello 函数
(global-set-key (kbd "C-c h") 'say-hello)

;; 如果要查看某组快捷键绑定在哪个函数上,可以执行 describe-key 或者使用快捷键 C-h k。
;; 比如想查看 C-b 绑定在哪个函数上,首先按下 C-h k,然后 Emacs 会提示你按下要查看的快捷键,最后会打开一个临时 buffer,显示相关信息。

9 系统编程

;; 执行系统命令:
(call-process "/bin/uname" nil t nil)

;; shell-command,执行 shell:
(shell-command "uname")

;; shell-command-to-string,获得命令执行的输出:
(let ((output (shell-command-to-string "uname")))
  (message output))

;; 在使用 shell-command 系列函数时,一定要注意参数是否可控,如果没严格限制输入内容,就能通过构造非法的 shell 语法来控制命令的执行。

10 单元测试

;; 自带的单元测试框架——ERT,详细请见:https://www.gnu.org/software/emacs/manual/html_node/ert/index.html
;; 定义一个测试用例:

(ert-deftest test ()
  (should (= 2 (+ 1 1))))

;; 通过快捷键 M-x ert 运行测试用例,执行结果如下:
;; Selector: test
;; Passed: 1
;; Failed: 0
;; Skipped: 0
;; Total: 1/1
;; Started at: 2015-01-25 11:40:02+0800
;; Finished.
;; Finished at: 2015-01-25 11:40:02+0800
;; .

11 调试

使用自带的 edebug。