基础知识 - 如何自定义编辑器
文章目录
入门介绍
主要了解 Elisp 语言的基础语法,好处就是可以自己写程序解决遇到的一些使用问题,包括读懂别人的配置。让自己对自己的编辑器了如指掌。
注意: 本章只介绍基础使用知识,如果需要深入了解学习,建议参考官方手册。
Hello,World!
按照编程惯例,都喜欢使用这句话作为编程的第一次试验,这里也就不免俗了^_^!。
首先启动 Emacs,在没有任何配置的环境下,在*scratch*缓冲区输入以下代码,并移动光标至当前行尾按下 C-x C-e
1 快捷键执行以下代码。同时可以用 eval-region 高级命令,首先按下 C-S-@
1 快捷键标记,可以按方向键或者使用 C-p
、 C-n
、 C-f
、 C-b
上下左右移动选中以下代码,再按下 M-x
2 输入 eval-region 命令即可执行,默认有一个 C-M-x
快捷键可以直接运行,前提是光标要在标记的内容最后一个字符后面。
|
|
在最下面当前 Minibuffer 会显示这条字符串。如果显示该字符串,则成功执行,否则会提示相应错误。也可以切换至*Messages*缓冲区查看,通过 C-x b
1 切换。
如图:
数据类型
内建的 Elisp 数据类型称为 primitive types
,包括整数、浮点数、 cons
、符号(symbol
)、字符串、向量(vector
)、散列表
(hash-table
)。每一种数据类型基本上有对应的 syntax-rules
,让解释器解析这种数据类型。
number
数字分为整数和浮点数 注意: elisp
中没有双精度数。1, 1.,+1, -1, 536870913, 0, -0 这些都是整数。1500.0, 15e2, 15.0e2,
+1500000e-3, 和 .15e4 都可以用来表示一个浮点数 1500.。遵循 IEEE
标准, elisp
也有一个特殊类型的值称为 NaN
(not-a-number
)。你可以用 (/ 0.0 0.0)
产生这个数。
数字的表示支持36进制的数字,因为只有 0-9
和 a-z
36 个字符来表示数字。
|
|
判断是否为数字等,可以使用一些内置的数字测试函数,如下:
|
|
判断两个数字值是否相等,使用 =
号,不能使用 eql
, equal
, sxhash-eql
, sxhash-equal
和 gethash
函数。当使用 equal
比较判断 x
和 y
是相同的 NaN
时,返回 t
,使用 =
比较判断则返回 nil
,相反使用 equal
比较判断数值时,返回 nil
,而使用 =
则返回 t
, eql
测试数字的值是否相等,还测试数字类型是否一致。例如:
|
|
常用的比较操作符号是我们在其它语言中都很熟悉的,比如 <
, >
, >=
, <=
,不一样的是,由于赋值是使用 set
函数,所以 =
不再是一个赋值运算符了,而是测试数字相等符号。和其它语言类似,对于浮点数的相等测试都是不可靠的,取反使用的是 not
而不是 !
,不等于可以使用 /=
判断。
|
|
整数向浮点数转换是通过 float
函数进行的。而浮点数转换成整数,内置有以下几个函数:
truncate
转换成靠近0
的整数floor
转换成最接近的不比本身大的整数ceiling
转换成最接近的不比本身小的整数round
四舍五入后的整数,换句话说和它的差绝对值最小的整数
三角运算有函数: sin
, cos
, tan
, asin
, acos
, atan
。开方函数是 sqrt
。
exp
是以 e
为底的指数运算, expt
可以指定底数的指数运算。 log
默认底数是 e
,但是也可以指定底数。 log10
就是 (log x 10)
。logb 是以 2 为底数运算,但是返回的是一个整数。这个函数是用来计算数的位。
random
可以生成随机数。可以用 (random t)
来产生一个新种子。注意: emacs
每次启动后调用 random
总是产生相同的随机数,在运行过程中,可以调用多次,不需要重新通过 (random t)
来产生新的种子。
string
字符串是有序的字符数组,与其他语言不同的是, elisp
中字符串可以是任意字符,包括 \0
。
|
|
字符串的字符其实就是一个整数。一个字符 A
就是一个整数 65。但是目前字符串中的字符被限制在 0-524287 之间。字符的读入语法是在字符前加上一个问号,比如 ?A
代表字符 A
。
|
|
字符同样可以是一些符号,控制字符,退格、制表符,换行符,垂直制表符,换页符,空格,回车,删除和 escape
。注意有些字符有歧义,所以需要使用 ~\~转义。如:
|
|
控制字符可以有多种表示方式,比如 C-i
,可以使用以下表示:
|
|
字符串测试使用 stringp
。 string-or-null-p
当对象是一个字符或 nil
时返回 t
。 char-or-string-p
测试是否是字符串或者字符类型。
同一个字符构造一个字符串,可以使用 make-string
。 不同字符的字符串可以使用 string
。
|
|
字符串切片操作可以使用 substring
, 支持正序和倒序(使用负数)。
|
|
字符串分割使用 split-string
默认使用空格分割,同时可以指定 split-string-default-separators
保留一个空字符,同时支持指定某个字符或者使用正则表达式来作为分割符。例如:
|
|
拼接字符串使用 concat
,可以使两个字符串拼接成一个。
|
|
字符串比较:
char-equal
比较两个字符是否相等,区分大小写。string=
字符串比较,string-equal
是一个别名。string<
是按字典序比较两个字符串,string-less
是它的别名。string>
最新版本包含该函数,旧版本没有。文档里面只有string-greaterp
函数。string-prefix-p
string-suffix-p
字符串转换,字符串与数字之间的相互转换。
string-to-number
字符串转数字number-to-string
数字转字符串
字符串转换其他进制可以使用 format
函数,该函数主要用于格式化字符串。
|
|
字符串与向量、列表互相转换。
|
|
大小写转换使用的是 downcase
和 upcase
两个函数。这两个函数的参数既可以字符串,也可以是字符。 capitalize
可以使字符串中单词的第一个字符大写,其它字符小写。 upcase-initials
只使第一个单词的第一个字符大写,其它字符小写。
|
|
string-match
查找字符串,支持正则表达式字符串。 string-match
在查找的同时,还会记录下每个匹配到的字符串的位置。这个位置可以在匹配后用 match-data
、 match-beginning
和 match-end
等函数来获得相应数据。
|
|
替换使用的函数是 replace-match
,字符串替换相关操作有一点复杂,需要重新计算,查找和替换字符串用到了,目前还没学习到的基本语法,可以跳过这部分。
|
|
解析文本时一个很常用的操作是把字符串按分隔符分解,可以用 split-string
函数:
|
|
与 split-string
对应是把几个字符串用一个分隔符连接起来,这可以用 mapconcat
高阶函数完成。比如:
|
|
identity
是一个特殊的函数,它会直接返回参数。
list
cons cell
就是两个有顺序的元素。第一个叫 CAR
,第二个就 CDR
。 CAR
和 CDR
名字来自于 Lisp
。 cons cell
也就是
construction of cells
。 car
函数用于取得 cons cell
的 CAR
部分, cdr
取得 cons cell
的 CDR
部分。 cons cell
非常简单,但是它却能衍生出许多高级的数据结构,比如链表、树、关联表等等。 使用 .
分隔两个部分,如下:
|
|
'
别名是 quote
表示自求值, quote
函数返回参数本身不做求值计算。列表包括了 cons cell
。但是列表中有一个特殊的元素──空表
nil
。注意, nil
没有 car
和 cdr
部分,即使对其取值,返回还是 nil
,类似其他语言,如C语言 NULL
表示空值,如下:
|
|
测试一个 cons cell
用 consp
,是否是列表用 listp
。
|
|
生成一个 cons cell
使用 cons
,列表用 list
或者直接使用 quote
。
|
|
向 list
追加元素并改变原列表可以使用 push
弹出一个元素使用 pop
,这两个宏可以看作堆栈,都是先进后出。拼接列表直接使用 cons
或者在列表末尾拼接使用 append
。
|
|
访问列表的第 n
个元素,使用 nth
函数:
|
|
获得列表一个区间的函数有 nthcdr
、 last
和 butlast
。 nthcdr
和 last
比较类似,它们都是返回第 n
个元素后面的所有元素。 nthcdr
函数返回第 n
个元素后的列表:
|
|
last
函数返回倒数第 n
个长度的列表:
|
|
butlast
和前两个函数不同,返回的除了倒数第 n
个元素的列表。
|
|
反转列表使用 reverse
函数,该函数返回新的列表,还有一个 nreverse
具有破坏性的反转列表函数,使用该函数会修改原列表顺序。
|
|
列表排序使用 sort
,注意该函数同样是破坏性的操作,如果需要保留原来的列表,需要进行值拷贝。
|
|
判断元素是否在列表中使用 memq
或 member
。
|
|
列表元素去重使用 delete-dups
。
|
|
删除指定元素使用 remq
、 remove
、 delq
和 delete
。
|
|
关联列表(association list
),类似其他语言的 hash table
, elisp
中也有 hash table
,不过 hash table
相比于 association list
至少以下缺点:
hash table
里的关键字(key
)是无序的,而association list
的关键字 可以按想要的顺序排列。hash table
没有列表那样丰富的函数,只有一个maphash
函数可以遍历列表。而association list
就是一个列表,所有列表函数都能适用。hash table
没有读入语法和输入形式,这对于调试和使用都带来很多不便。
|
|
可以 rassoc
和 rassq
来根据数据查找键值,也就是使用键值( key
)对应的数据查找键值。
|
|
用 setcdr
来更改键值对应的数据,注意 progn
用来执行多条语句,暂时不用理会,后面会介绍。
|
|
遍历列表最常用的函数就是 mapc
和 mapcar
、 dolist
。
|
|
注意: memq
、 member
、 delq
、 delete
、 assq
和 assoc
这些函数,它们分别使用 eq
和 equal
两种方法。
symbol
符号主要具有以下特性:
- 符号名字
- 变量
- 函数
- 属性列表
测试是否是符号使用 symbolp
。符号名字可以含有任何字符。大多数的符号名字只含有字母、数字和标点 -+=*/
。这样的名字不需要其它标点。名字前缀要足够把符号名和数字区分开来,如果需要的话,可以在前面用 \
表示为符号,比如:
|
|
符号名是区分大小写的。
|
|
符号名必須是唯一的,所以一定会有一个表与名字关联,这个表在 elisp
里称为 obarray
。创建一个符号时,首先会对这个名字求
hash
值,当 elisp
读入一个符号时,通常会先查找这个符号是否在 obarray
里,如果没有则会把这个符号加入到 obarray
。这样查找并加入一个符号的过程称为是 intern
。 intern
函数可以查找或加入到 obarray
,返回对应的符号。默认是全局的 obarray
,也可以指定一个 obarray
。 intern-soft
与 intern
不同的是,当名字不在 obarray
里时, intern-soft
会返回 nil
,而 intern
会加入到 obarray
里,除去 obarray
里的符号,可以用 unintern
函数。
|
|
符号的名字,可以用 symbol-name
, 符号的值使用 symbol-value
,符号函數使用 symbol-function
, 可以用 fboundp
测试一个符号是否绑定函数,属性列表( property list
)。通常属性列表用于存储和符号相关的信息,比如变量和函数的文档,定义的文件名和位置,语法类型,可以使用 put
和 get
设置获取,用 symbol-plist
得到所有的属性列表。
|
|
变量
elisp
的变量有三种,分别是全局变量和局部变量和一个 buffer-local
变量。全局变量通常使用 defvar
来定义,局部变量常用的是
let
和 let*
来绑定, let*
与 let
的区别是绑定的变量,即可使用。 setq
用来改变变量的值(注意, setq
改变的最里层的值,不会影响最外层变量的值)。
|
|
声明一个 buffer-local
的变量可以用 make-variable-buffer-local
或用 make-local-variable
。这两个函数的区别在于前者是相当于在所有变量中都产生一个 buffer-local
的变量。而后者只在声明时所在的缓冲区内产生一个局部变量。
|
|
用 make-local-variable
声明为 buffer-local
变量时,这个变量的值还是全局变量的值。这时候全局的值也称为缺省值。你可以用
default-value
来访问这个符号的全局变量的值。
|
|
如果一个变量是 buffer-local
,那么在这个缓冲区内使用用 setq
就只能用改变当前缓冲区里这个变量的值。 setq-default
可以修改符号作为全局变量的值。测试一个变量是不是 buffer-local
可以用 local-variable-p
。
|
|
测试一个变量是否绑定值,或是否定义,可以使用 boundp
。对于一个 buffer-local
变量,它的缺省值可能是没有定义的,可以用
default-boundp
先进行测试。使一个变量的值重新为空,可以用 makunbound
。要取消一个 buffer-local
变量用函数
kill-local-variable
。可以用 kill-all-local-variables
取消所有的 buffer-local
变量。但是有属性 permanent-local
的不会消除,带有这些标记的变量一般都是和缓冲区模式无关的。
|
|
顺序执行
按表达式顺序依次执行的。在 defun
等特殊环境中是自动进行的。由于无法使用 eval-last-sexp
同时执行两个以上的表达式,在 if
表达式中的条件为真时执行的部分也只能运行一个表达式。这时就需要用 progn
这个特殊表达式。定义如下:
|
|
使用例子:
|
|
条件语句
条件判断表达式 if
和 cond
。还有两个宏 when
和 unless
,使用这两个宏的好处是使代码可读性提高, when
能省去 if
里的 progn
结构, unless
省去条件为真子句需要的的 nil
表达式。定义形式分别如下:
|
|
使用例子:
|
|
循环迭代
循环迭代使用的是 while
表达式。它的形式是:
|
|
例如:
|
|
在函数式编程过程中,常用的是递归。
逻辑运算
条件的逻辑运算和其它语言都是很类似的,使用 and
、 or
、 not
。
|
|
函数、命令与lambda表达式
elisp
中的函数和类函数对象:
lambda
表达式用
Lisp
编写的函数(严格意义上的函数对象)。primitive
(内建函数)一种可以从
Lisp
中调用但实际上是用C
语言编写的函数。原语也称为内建函数或subr
。通常,函数作为原语实现是因为它是Lisp
的基本部分(例如car
),或者因为它提供了操作系统服务的低级接口,或者因为它需要快速运行。与Lisp
中定义的函数不同,原语只能通过更改C源代码和重新编译Emacs
来修改或添加。special form
(特殊形式)一种类似于函数的原语,但不以通常的方式计算其所有参数。它可能只计算一些参数,也可能以不寻常的顺序或多次计算它们。示例包括
if
、and
和while
。见特殊表格。macro
(宏)在
Lisp
语言中定义的一种结构,它与函数的不同之处在于它把一个Lisp
表达式转换成另一个要计算的表达式,而不是原来的表达式。元编程的实现基本上都是通过宏实现的,能通过宏定义实现各种高阶函数。command
(命令)一种可以通过命令执行原语调用的对象,通常是由于用户键入了一个绑定到该命令的键序列。
closure
闭包与
lambda
表达式非常相似的函数对象,只是它还包含词法变量绑定的环境。
定义函数使用 defun
关键字,函数参数把必须提供的参数写在前面, &optional
可选的参数写在后面,最后用 &rest
一个符号表示剩余的所有参数。函数参数语法形式:
|
|
定义函数:
|
|
文档字符串,通常用于介绍函数功能。调用函数,运行时根据状态调用函数,可以用 funcall
和 apply
两个函数。
|
|
elisp
编写的命令都含有一个 interactive
表达式,它有一个字符串的可选参数。如果需要用户输入,即需要指定可选参数。字符串的第一个字符(也称为代码字符)代表参数的类型。例如:
|
|
通过 M-x
输入 hello-world
调用命令,会提示 Say:
输入 Tom
即提示 Hello, Tom
。
interactive
可以使用的代码字符
代码字符 | 代替的表达式 |
---|---|
a | (completing-read prompt obarray ‘fboundp t) |
b | (read-buffer prompt nil t) |
B | (read-buffer prompt) |
c | (read-char prompt) |
C | (read-command prompt) |
d | (point) |
D | (read-directory-name prompt) |
e | (read-event) |
f | (read-file-name prompt nil nil t) |
F | (read-file-name prompt) |
k | (read-key-sequence prompt) |
K | (read-key-sequence prompt nil t) |
m | (mark) |
n | (read-number prompt) |
N | (if current-prefix-arg (prefix-numeric-value current-prefix-arg) (read-number prompt)) |
p | (prefix-numeric-value current-prefix-arg) |
P | current-prefix-arg |
r | (region-beginning) (region-end) |
s | (read-string prompt) |
S | (completing-read prompt obarray nil t) |
v | (read-variable prompt) |
x | (read-from-minibuffer prompt nil nil t) |
X | (eval (read-from-minibuffer prompt nil nil t)) |
z | (read-coding-system prompt) |
Z | (and current-prefix-arg (read-coding-system prompt)) |
函数与 lambda
表达式是等价的,它只是没有一个具体的名字,定义形式与参数和函数相同。 lambda
表达式定义如下:
|
|
调用 lambda
使用 funcall
:
|
|
宏定义使用的是 defmacro
。可以使用 macroexpand
进行宏展开。例如:
|
|
以上都可以使用 functionp
测试一个符号是函数对象。
装饰函数
Elisp
提供一个 advice
系统,它类似与 pyhton
中的装饰器,用于在不修改原函数的情况下,对函数增加额外的功能,如修改函数参数与返回值。当然 advice
功能更强大。旧版本中使用 defadvice
来定义它,使用的过程需要使用 ad-*
开头的系列宏来配合工作(目前已经弃用了),新的程序基本上使用全新的 advice-add
来添加,也可以使用 define-advice
定义一个 advice
。例如添加:
|
|
advice
系统提供了两组原语:一组是核心原语,用于保存在变量和对象字段中的函数值(对应的原语是 add function
和 remove function
),另一组是分层的,用于命名函数(主要原语是 advice add
和 advice remove
)。例如移除:
|
|
使用 define-advice
定义一个 advice
。不过 define-advice
定义时的命名给人感觉很奇怪,常见还是直接使用 advice-add
。 如:
|
|