Emacs Lisp

1 List Processing #

List in Elisp #

Lists #

若列表前有 ' ,说明这是一个列表的引用(quote),当执行这段代码时,Elisp 不会做任何事,除了保持它如同其所写:

'(this list looks like this)
this list looks like this
'(+ 1 1)
+ 1 1

若没有 ' ,则会把第一个元素作为命令(函数):

(+ 1 1)
2

Elisp 解释器的执行 #

  1. 对一个列表,如果有引用,直接返回这个列表
  2. 对一个列表,如果没引用,把第一个元素作为函数来执行
  3. 对一个没有引用且没有括号的符号,把它当作变量
  4. 对一些特殊的函数,例如 if , defun ,有其特殊的意义
  5. 如果一个列表中含有子列表,从内往外执行
  6. 从左向右执行

变量 #

一个变量可以同时被绑定为一个函数和一个值,两个定义是分开的。

fill-column
80
(+ 2 fill-column)
82

set 设置变量的值

(set 'flowers '(rose violet daisy buttercup))
rose violet daisy buttercup

注意两个参数都带着 quote(除非需要真的 evaluate):

(set 'a 'b)
(set a (+ 1 1))
2

实际使用中第一个参数几乎总是需要 quote,所以有一个更常用的函数 setq ,它自动给第一个参数加 quote:

(setq carnivores '(lion tiger leopard))
lion tiger leopard

另外 setq 还可以同时给多个变量赋值:

(setq trees '(pine fir oak maple)
      herbivores '(gazelle antelope zebra))

2 Data Types #

判断一个值是什么类型 #

用以下几个判断函数(结尾的 p 意为 predicate):

(integerp 1)  ; 整型
(integerp 1.0)
(floatp -0.0e+NaN)  ; 浮点型
(numberp 1)  ;数字类型
(zerop 0)  ; 是否为0
(wholenump 1.2)  ; 是否是非负整数

数字的比较操作符 #

(< 1 1)
(<= 1 1)
(> 1 1)
(>= 1 1)
(= 1 1)   ; equal
(/= 1 1)  ; not equal

另有 eql 函数测试两个数不仅值是否相等,还测试其类型是否一致。

(= 1 1.0)
(eql 1 1.0)

数字的转换 #

常见的函数:

eqequal #

两个函数都被用来比较两个对象是否“相等”,但有区别,简单来说 eq 更加严格,如果两个对象 eq ,则它们肯定 equal ,反之不一定成立。

对于整数和符号, eqequal 是一样的,如果两个对象的值一样就返回 t

(eq 'foo 'foo)     ; => t
(eq 456 456)       ; => t
(equal 'foo 'foo)  ; => t
(equal 456 456)    ; => t

但对于其他类型(如列表,向量,字符串等), eq 返回 t 当且仅当两个对象是同一个对象(即改变其中一个对象时,另一个也会被改变)。

(eq "asdf" "asdf")              ; => nil
(eq '(1 (2 (3))) '(1 (2 (3))))  ; => nil
(setq a "asdf")
(setq b a)
(eq a b)  ; => t

equal 函数返回 t 当且仅当两个对象的值相同:

(equal "asdf" "asdf")              ; => t
(equal '(1 (2 (3))) '(1 (2 (3))))  ; => t

对于浮点数,比较相等无法保证返回正确的结果:

(eq 3.0 3.0)  ; => t or nil

3 Characters and Strings #

字符的表示 #

不同于大部分语言用单引号 ' 来表示字符,Elisp 中使用的语法是在字符前加一个问号:

?A  ; ASCII code of 'A' is 65
65

转义字符 #

部分字符前加反斜杠有特殊含义:

?\a   ; control-g, 'C-g'
?\b   ; backspace, <BS>, 'C-h'
?\t   ; tab, <TAB>, 'C-i'
?\n   ; newline, 'C-j'
?\v   ; vertical tab, 'C-k'
?\f   ; formfeed character, 'C-l'
?\r   ; carriage return, <RET>, 'C-m'
?\e   ; escape character, <ESC>, 'C-['
?\s   ; space character, <SPC>
?\\   ; backslash character, '\'
?\d   ; delete character, <DEL>

可以用 ?\M- 代表 Meta 键,再加上别的字符构成组合键的新字符:

?\M-A
134217793

测试字符串 #

没有字符的测试函数,因为字符就是整数。测试字符串使用 stringpstring-or-null-p 当对象是一个字符串或 nil 时返回 tchar-or-string-p 测试是否是字符或字符串。

构造字符串 #

用例子来说明常用的字符串构造函数:

(make-string 5 ?x)             ; => "xxxxx"
(string ?a ?b ?c)              ; => "abc"
(substring "0123456789" 3)     ; => "3456789"
(substring "0123456789" 3 5)   ; => "34"
(substring "0123456789" -3 -1) ; => "78"
(concat "abc" "def")           ; => "abcdef"

比较字符串 #

用例子来说明,注意空字符串小于所有其他字符串。

(string= "abc" "abc")  ; => t
(string< "abc" "abcd") ; => t
(length "abc")         ; => 3

字符串转换 #

字符转字符串: char-to-string ,字符串转字符(只返回首字符): string-to-char

数字与字符串互转,以及输出为八进制或十六进制字符串:

(string-to-number "256") ; => 256
(number-to-string 256)   ; => "256"
(format "%#o" 256)       ; => "0400"
(format "%#x" 256)       ; => "0x100"

与列表或向量间的转换:

(concat '(?a ?b ?c ?d ?e)) ; => "abcde"
(concat [?a ?b ?c ?d ?e])  ; => "abcde"
(vconcat "abdef")          ; => [97 98 100 101 102]
(append "abcdef" nil)      ; => (97 98 99 100 101 102)

大小写转换:

(downcase "The cat in the hat")        ; => "the cat in the hat"
(downcase ?X)                          ; => 120
(upcase "The cat in the hat")          ; => "THE CAT IN THE HAT"
(upcase ?x)                            ; => 88
(capitalize "The CAT in tHe hat")      ; => "The Cat In The Hat"
(upcase-initials "The CAT in the hAt") ; => "The CAT In The HAt"

格式化字符串 #

语法与 C 语言差不多:

(format "%d is a number" 1)

4 Function Definitions #

defun 来定义函数,后面可跟最多五个部分: #

  1. 函数名
  2. 参数列表,如果没有参数则为 ()
  3. 文档来描述这个函数(可选,但强烈推荐)
  4. 指定其是否是 interactive 的(可选)
  5. 函数体
1
2
3
4
5
(defun multiply-by-seven (number)
  "Multiply NUMBER by seven."
  (* 7 number))

(multiply-by-seven 3)
21

interactive 关键字来使得函数可以被 M-x 执行或绑定到某个键上: #

1
2
3
4
(defun multiply-by-seven (number)
  "Multiply NUMBER by seven."
  (interactive "p")
  (message "The result is %d" (* 7 number)))

执行的时候用 C-u 传递参数: C-u 3 M-x multiply-by-seven ,或者先按 Meta 再加数字也是同样的效果。

其中 "p" 的意思是使用前缀参数(prefix)

let 语句定义局部变量,防止重名 #

这些局部变量只在 let 语句范围内有效。

1
2
3
(let ((zebra "stripes")
      (tiger "fierce"))
  (message "One kind of animal has %s and another is %s." zebra tiger))
One kind of animal has stripes and another is fierce.

还有一个 let* ,使用方法和 let 完全一样,区别在于 let* 的声明列表中后面的变量可以使用前面声明的变量。

1
2
3
4
5
(defun circle-area (radix)
  (let* ((pi 3.1415926)
         (area (* pi radix radix)))
    (message "直径为%.2f的圆面积是%.2f" radix area)))
(circle-area 5)
直径为 5.00 的圆面积是 78.54

条件语句 if #

(if (> 5 4)
    (message "5 is greater than 4!"))
5 is greater than 4!

条件语句 if-else

(if (> 4 5)
    (message "4 falsely greater than 5!")
  (message "4 is not greater than 5!"))
4 is not greater than 5!

假就是 nil() (两者其实是等价的),其他任何东西都是真。

条件语句 cond #

cond 后可以跟多个条件,类似多个 if-else 或者 switch case 语句:

1
2
3
4
5
(defun fib (n)
  (cond ((= n 0) 0)
        ((= n 1) 1)
        (t (+ (fib (- n 1))
              (fib (- n 2))))))

save-excursion 记住当前光标的位置,执行完程序后重置光标 #

1
2
3
4
5
(message "We are %d characters into this buffer."
         (- (point)
            (save-excursion
              (goto-char (point-min))
              (point))))
We are 3680 characters into this buffer.

执行函数 #

M-: 可执行一条 elisp 语句。

Lambda 函数 #

定义并执行一个 lambda 函数:

(setq foo (lambda (name)
            (message "Hello, %s!" name)))
(funcall foo "Emacser")
Hello, Emacser!
1
2
3
4
5
6
(defun simplified-beginning-of-buffer ()
  "Move point to the beginning of the buffer;
leave mark at previous position."
  (interactive)
  (push-mark)
  (goto-char (point-min)))
1
2
3
4
5
6
7
8
9
(defun mark-whole-buffer ()
  "Put point at beginning and mark at end of buffer.
You probably should not use this function in Lisp programs;
it is usually a mistake for a Lisp function to use any subroutine
that uses or sets the mark."
  (interactive)
  (push-mark (point))
  (push-mark (point-max) nil t)
  (goto-char (point-min)))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(defun append-to-buffer (buffer start end)
  "Append to specified buffer the text of the region.
It is inserted into that buffer before its point.
When calling from a program, give three arguments:
BUFFER (or buffer name), START and END.
START and END specify the portion of the current buffer to be copied."
  (interactive
   (list (read-buffer "Append to buffer: " (other-buffer
                                            (current-buffer) t))
         (region-beginning) (region-end)))
  (let ((oldbuf (current-buffer)))
    (save-excursion
      (let* ((append-to (get-buffer-create buffer))
             (windows (get-buffer-window-list append-to t t))
             point)
        (set-buffer append-to)
        (setq point (point))
        (barf-if-buffer-read-only)
        (insert-buffer-substring oldbuf start end)
        (dolist (window windows)
          (when (= (window-point window) point)
            (set-window-point window (point))))))))

6 car,cdr,cons: Fundamental Functions #

缩写: cons: construct car: Contents of the Address part of the Register cdr: Contents of the Decrement part of the Register

car: 取列表的第一个元素

(car '(rose violet daisy buttercup))

更好的名字是 first ,它是 car 的同义词。

(first '(rose violet daisy buttercup))
rose

cdr 返回除第一个元素外剩下的列表

(cdr '(rose violet daisy buttercup))
violet daisy buttercup

更好的名字是 rest ,它是 cdr 的同义词。

(rest '(rose violet daisy buttercup))
violet daisy buttercup

carcdr 也可以用在 cell 上:

(car '(1 . 2))
1
(cdr '(1 . 2))
2

conscarcdr 的逆操作:

(cons 'pine '(fir oak maple))
pine fir oak maple

上面是在列表前端增加元素,用 append 可以在后端添加元素:

(append '(a b) '(c))
a b c

length 返回列表的长度

(length '(pine fir oak maple))
4

nthcdr 多次做 cdr 并返回结果

(nthcdr 2 '(pine fir oak maple))
oak maple

nthnthcdr 的结果取 car:

(nth 1 '("one" "two" "three"))
two

last 返回倒数 n 个长度的列表:

(last '(0 1 2 3 4 5) 2)
4 5

butlastlast 返回的东西恰好相反:返回除去了倒数 n 个元素的列表。

(butlast '(0 1 2 3 4 5) 2)
0 1 2 3

上面的这些函数都是“只读”的,不改变原本的列表,只是从中读出值或构建新的列表。下面介绍一些会修改原来列表的函数。

setcarsetcdr 将列表的 CAR 和 CDR 替换为新值:

(setq animals (list 'antelope 'giraffe 'lion 'tiger))
(setcar animals 'hippopotamus)
animals
hippopotamus giraffe lion tiger
(setq domesticated-animals (list 'horse 'cow 'sheep 'goat))
(setcdr domesticated-animals '(cat dog))
domesticated-animals
horse cat dog

pushpop 顾名思义:

(setq foo nil) ; => nil
(push 'a foo)  ; => (a)
(push 'b foo)  ; => (b a)
(pop foo)      ; => b
foo            ; => (a)

reverse 反向列表:

(setq foo '(a b c))
(reverse foo)
c b a

sort 排序列表:

(setq foo '(3 2 4 1 5))
(sort foo '<)

注意 sort 函数会破坏原来的列表:

(setq foo '(3 2 4 1 5))
(sort foo '<)
foo
3 4 5

copy-sequence 复制列表,注意仅仅使用 setq 给列表定义新的变量绑定并不会复制列表,仅仅是用一个新的指针指向了同一个列表而已。

(setq foo '(3 2 4 1 5))
(setq foo_copy (copy-sequence foo))
(sort foo '<)
foo_copy
3 2 4 1 5

测试一个元素是否在列表中: memqmember ,前者是用 eq 来测试,后者是用 equal

用 key 查找关联表: assqeq 来比较, assocequal 来比较:

(assq 'a '((a 97) (b 98)))        ; => (a 97)
(assoc "a" '(("a" 97) ("b" 98)))  ; => ("a" 97)

assoc-default 可以直接对结果取 cdr

(assoc-default "a" '(("a" 97) ("b" 98)))
97

也可以用 value 查找关联表:

(rassq '97 '((a . 97) (b . 98)))     ; => (a . 97)
(rassoc '(97) '(("a" 97) ("b" 98)))  ; => ("a" 97)

7 Cutting and Storing Text #

术语 kill 表示将文字删掉,但并没有完全删掉,而是放进了一个缓存,叫 kill buffer。

kill 这个术语并不好,更准确的应该是 clip

通过 zap-to-char 函数来了解如何 kill 一段文字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(defun zap-to-char (arg char)
  "Kill up to and including ARG'th occurrence of CHAR.
Case is ignored if `case-fold-search' is non-nil in the current buffer.
Goes backward if ARG is negative; error if CHAR not found."
  (interactive "p\ncZap to char: ")
  (if (char-table-p translation-table-for-input)
      (setq char (or (aref translation-table-for-input char) char)))
  (kill-region (point) (progn
                         (search-forward (char-to-string char)
                                         nil nil arg)
                         (point))))

理解 interactive 表达式: p 表示函数的第一个参数由前缀参数(prefix argument)传递,且如果没有前缀参数,则默认值为 1。 \n 用来分隔两个参数。 c 表示第二个参数为从输入获取一个字符,且命令行会显示提示"Zap to char: " 。

if 语句不用管,只是为了处理奇怪的字符集。

progn 将其参数顺序执行,并返回最后一个的结果。

kill-region 的实现中,用到了 condition-case ,有点类似于 try-catch 错误处理。

两个宏: when 就是没有 else 的 if,实际上可以将 when 替换为 if。 unless 相反,是没有 then 的 if。

以下三个语句效果类似,都是将一个值加入到列表的开头:(但实际上有一些区别)

(setq string "abc")
(setq list '("a" "b" "c"))

;; 以下三个语句是一样的
(push string list)
(setq list (cons string list))
(add-to-list 'list string)

defvar 不同于 setq 的地方:它只对未赋值的变量赋值,且它可以加文档。

8 How Lists are Implemented #

列表实现是一个链表,节点储存的是地址,指向该元素的值。

变量就像一个指针,用 setq 给变量赋值一个列表,相当于把列表的链表头节点的地址赋给了这个变量指针。

cdr 只是直接返回链表第一个节点指向后序节点的地址,而求 car 也只是得到了第一个节点的地址。

链表可以看做是嵌套的 cell:

例如只有一个元素的列表 '(1 . nil) 实际就是 '(1).

'(1 . (2 . (3 . nil)))  ; 实际就是 '(1 2 3)
1 2 3

9 Loops and Recursion #

while Loop:

(setq animals '(gazelle giraffe lion tiger))

(while animals
  (print animals)
  (setq animals (cdr animals)))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(defun triangle (number-of-rows)
  "Add up the number of pebbles in a triangle.
The first row has one pebble, the second row two pebbles,
the third row three pebbles, and so on.
The argument is NUMBER-OF-ROWS."
  (let ((total 0)
        (row-number 1))
    (while (<= row-number number-of-rows)
      (setq total (+ total row-number))
      (setq row-number (1+ row-number)))
    total))
(triangle 4)

dolist 宏可以不用每次都写 cdr:

1
2
3
4
5
6
7
(setq animals '(gazelle giraffe lion tiger))
(defun reverse-list-with-dolist (list)
  "Using dolist, reverse the order of LIST."
  (let (value) ; make sure list starts empty
    (dolist (element list value)
      (setq value (cons element value)))))
(reverse-list-with-dolist animals)
tiger lion giraffe gazelle

类似有 dotimes:

(let (value)
  (dotimes (number 3)
    (setq value (cons number value)))
  value)
2 1 0

递归:

1
2
3
4
5
6
(defun factorial (n) :exports both
  "Compute the factorial of 'n'"
  (if (<= n 0)
      1
    (* n (factorial (- n 1)))))
(factorial 5)

10 Regular Expression Searches #

Emacs 的正则表达式并不十分好用,有一些反人类的设计。

调试正则表达式的工具 re-builder

特殊字符: $, ^, ., *, +, ?, [, ], \.

特殊字符如果要表达其符号本身,需要加反斜杠来转义,但问题是 Elisp 中使用正则表达式时,往往是在字符串中,此时又需要一层转义,因此 Emacs 的正则表达式往往有很多双反斜杠。

更麻烦的是,对于普通字符如 (, ), | ,想要用其特殊含义时(括号用于 Capture,竖线用于表达“或”)需要用反斜杠转义。而单个反斜杠 \( 在字符串中被解读为 ( ,因此需要两个反斜杠。

几个正则表达式的例子:

\([0-9] [0-9]\) 匹配例如 1 1, 1 2 ,其写成字符串的时候,需要加反斜杠: "\\([0-9] [0-9]\\)"

[0-9]\|[abc] 匹配如 1, a ,其写成字符串的时候为: "[0-9]\\|[abc]"

x\{4\} 匹配 xxxx ,因为 } 是普通字符,故这里要加反斜杠来使用其特殊含义(指定重复次数),写成字符串的时候为: x\\{4\\}

方括号匹配一个字符集,通常在字符集中的特殊字符不再特殊。而在字符集中的 ] , -^ 有特殊规则:

寻找句子的结尾:

(sentence-end)
\([.?!…‽][]"'”’)}»›]*\($\|[	  ]\)\|[。.?!]+\)[
]*

其中, [.?!…‽] 匹配句号,问号,感叹号等。 []"'”’)}»›] 匹配各种右括号以及引号(可以是多个)。 $\(\|[  ]\) 匹配行尾或 TAB,空格。最后奇怪的换行其实是匹配任意多个换行符。

11 Counting via Repetition and Regexps #

学习用循环,正则表达式来实现一个数单词个数的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(defun count-words-example (beginning end)
  "Print number of words in the region.
Words are defined as at least one word-constituent
character followed by at least one character that
is not a word-constituent. The buffer's syntax
table determines which characters these are."
  (interactive "r")
  (message "Counting words in region ... ")
;;; 1. Set up appropriate conditions.
  (save-excursion
    (goto-char beginning)
    (let ((count 0))
;;; 2. Run the while loop.
      (while (and (< (point) end) (re-search-forward "\\w+\\W*" end t))
        (setq count (1+ count)));;; 3. Send a message to the user.
      (cond ((zerop count)
             (message
              "The region does NOT have any words."))
            ((= 1 count)
             (message
              "The region has 1 word."))
            (t
             (message
              "The region has %d words." count))))))

12 Your .emacs File #

设置 major mode: #

(setq major-mode 'text-mode)

添加钩子函数(hook): #

(add-hook 'text-mode-hook 'turn-on-auto-fill)

添加按键绑定: #

(global-set-key "\C-cw" 'compare-windows)

C-c w 绑定为函数 compare-windows

解除绑定:

(global-unset-key "\C-xf")

以上都是全局绑定,会被记录到变量 global-map 中。

还可以给特定的 mode 绑定按键,其值会被记录在这个 mode 特定的 map 中:

(define-key texinfo-mode-map "\C-c\C-cg" 'texinfo-insert-@group)

define-key 的第一个参数制定了按键绑定会被记录到的 keymap 变量名。

加载文件 #

加载一个文件,即 evaluate 它的内容:

(load "~/emacs/slowsplit")

不需要后缀名,它会自动去找 slowsplit.elc 文件,不过不存在,再去找 slowsplit.el

可以通过设置 load-path 来避免指定加载文件的路径:

(setq load-path (cons "~/emacs" load-path))

另有两个 interactive 的命令用来加载文件: load-libraryload-file

Autoloading #

在函数被加载后,它只是可见而不是真的被加载了,只会在第一次被调用的时候再真正加载。

一般用在很少被调用的函数上,以提高 Emacs 启动速度。

(autoload 'html-helper-mode
  "html-helper-mode" "Edit HTML documents" t)

13 Debugging #

debug-on-entry 命令来调试,按 d 键单步执行。

cancel-debug-on-entry 命令来退出调试。

也可以在程序中插入 (debug) 函数:

1
2
3
4
5
6
7
8
(defun triangle-bugged (number)
  "Return sum of numbers 1 through NUMBER inclusive."
  (let ((total 0))
    (while (> number 0)
      (setq total (+ total number))
      (debug)                    ; Start debugger.
      (setq number (1= number))) ; Error here.
    total))

Links to this note