on
Emacs Lisp
- 1 List Processing
- 2 Data Types
- 3 Characters and Strings
- 4 Function Definitions
- 5 Buffer-Related Functions
- 6 car,cdr,cons: Fundamental Functions
- 7 Cutting and Storing Text
- 8 How Lists are Implemented
- 9 Loops and Recursion
- 10 Regular Expression Searches
- 11 Counting via Repetition and Regexps
- 12 Your .emacs File
- 13 Debugging
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 解释器的执行 #
- 对一个列表,如果有引用,直接返回这个列表
- 对一个列表,如果没引用,把第一个元素作为函数来执行
- 对一个没有引用且没有括号的符号,把它当作变量
- 对一些特殊的函数,例如
if
,defun
,有其特殊的意义 - 如果一个列表中含有子列表,从内往外执行
- 从左向右执行
变量 #
一个变量可以同时被绑定为一个函数和一个值,两个定义是分开的。
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)
数字的转换 #
常见的函数:
truncate
转换成靠近 0 的整数floor
,ceiling
向下、向上取整round
四舍五入取整
eq
和 equal
#
两个函数都被用来比较两个对象是否“相等”,但有区别,简单来说 eq
更加严格,如果两个对象 eq
,则它们肯定 equal
,反之不一定成立。
对于整数和符号, eq
和 equal
是一样的,如果两个对象的值一样就返回 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
测试字符串 #
没有字符的测试函数,因为字符就是整数。测试字符串使用 stringp
, string-or-null-p
当对象是一个字符串或 nil
时返回 t
。 char-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
来定义函数,后面可跟最多五个部分: #
- 函数名
- 参数列表,如果没有参数则为
()
- 文档来描述这个函数(可选,但强烈推荐)
- 指定其是否是 interactive 的(可选)
- 函数体
|
|
21
用 interactive
关键字来使得函数可以被 M-x
执行或绑定到某个键上: #
|
|
执行的时候用 C-u
传递参数: C-u 3 M-x multiply-by-seven
,或者先按 Meta 再加数字也是同样的效果。
其中 "p"
的意思是使用前缀参数(prefix)
用 let
语句定义局部变量,防止重名 #
这些局部变量只在 let
语句范围内有效。
|
|
One kind of animal has stripes and another is fierce.
还有一个 let*
,使用方法和 let
完全一样,区别在于 let*
的声明列表中后面的变量可以使用前面声明的变量。
|
|
直径为 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 语句:
|
|
save-excursion
记住当前光标的位置,执行完程序后重置光标 #
|
|
We are 3680 characters into this buffer.
执行函数 #
M-:
可执行一条 elisp 语句。
Lambda 函数 #
定义并执行一个 lambda 函数:
(setq foo (lambda (name)
(message "Hello, %s!" name)))
(funcall foo "Emacser")
Hello, Emacser!
5 Buffer-Related Functions #
|
|
|
|
|
|
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 |
car
和 cdr
也可以用在 cell 上:
(car '(1 . 2))
1
(cdr '(1 . 2))
2
cons
是 car
和 cdr
的逆操作:
(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 |
nth
对 nthcdr
的结果取 car
:
(nth 1 '("one" "two" "three"))
two
last
返回倒数 n 个长度的列表:
(last '(0 1 2 3 4 5) 2)
4 | 5 |
---|---|
butlast
跟 last
返回的东西恰好相反:返回除去了倒数 n 个元素的列表。
(butlast '(0 1 2 3 4 5) 2)
0 | 1 | 2 | 3 |
上面的这些函数都是“只读”的,不改变原本的列表,只是从中读出值或构建新的列表。下面介绍一些会修改原来列表的函数。
setcar
和 setcdr
将列表的 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 |
push
和 pop
顾名思义:
(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 |
测试一个元素是否在列表中: memq
或 member
,前者是用 eq
来测试,后者是用 equal
。
用 key 查找关联表: assq
用 eq
来比较, assoc
用 equal
来比较:
(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
一段文字。
|
|
理解 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)))
|
|
dolist
宏可以不用每次都写 cdr:
|
|
tiger | lion | giraffe | gazelle |
类似有 dotimes
:
(let (value)
(dotimes (number 3)
(setq value (cons number value)))
value)
2 | 1 | 0 |
递归:
|
|
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\\}
方括号匹配一个字符集,通常在字符集中的特殊字符不再特殊。而在字符集中的 ]
, -
和 ^
有特殊规则:
- 如果想在集中包括
]
,必须把它作为字符集中的第一个字符。例如,[]a]
匹配]
或a
。 - 想包括
-
,它要是字符集中的第一个或最后一个字符,或者放在一个范围的后面。如[]-]
匹配]
和-
。 - 如果想在集中包括字符
^
,它可以在除第一个位置以外的任何地方。
寻找句子的结尾:
(sentence-end)
\([.?!…‽][]"'”’)}»›]*\($\|[ ]\)\|[。.?!]+\)[
]*
其中, [.?!…‽]
匹配句号,问号,感叹号等。 []"'”’)}»›]
匹配各种右括号以及引号(可以是多个)。 $\(\|[ ]\)
匹配行尾或 TAB,空格。最后奇怪的换行其实是匹配任意多个换行符。
11 Counting via Repetition and Regexps #
学习用循环,正则表达式来实现一个数单词个数的函数
|
|
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-library
和 load-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)
函数:
|
|