Elisp Tutorial
目录
基础
- 分号是注释开始的标志。
- Elisp 是由符号表达式构成的(即“S-表达式”或"S 式")。s 式周围有括号,而且也可以嵌套:
(+ 2 (+ 1 1))
- 一个 s 式可以包含原子符号或者其他 s 式。在上面的例子中,1 和 2 是原子符号,(+ 2 (+ 1 1)) 和 (+ 1 1) 是 s 式。
;; `setq' 可以将一个值赋给一个变量 (setq my-name "Bastien") ;; `insert' 会在光标处插入字符串: (insert "Hello!") ;; 可以把s式嵌入函数中 (defun hello () (insert "Hello, I am " my-name)) ;; 可以用 `progn'命令将s式结合起来: (progn (switch-to-buffer-other-window "*test*") (hello "you")) ;; 格式化字符串的方法: (format "Hello %s!\n" "visitor") ;; 可以用 `let' 将一个值和一个局部变量绑定: (let ((local-name "you")) (switch-to-buffer-other-window "*test*") (erase-buffer) (hello local-name) (other-window 1)) ;; 这里我们就不需要使用 `progn' 了, 因为 `let' 也可以将很多s式组合起来。 ;; 定义变量 (setq name "username") (Message:q name) ; -> "username" : ;; 定义函数 (defun func () (message "Hello, %s" name)) ;; 执行函数 (func) ; -> Hello, username ;; 设置快捷键 (global-set-key (kbd "<f1>") 'func) ;; 使函数可直接被调用可添加 (interactive) (defun func () (interactive) (message "Hello, %s" name))
操作符前置
程序的本质是什么,无非是数据本身和操作数据过程。lisp 把操作数据的“操作符”前置了,比如平常计算多个数加法的时候用 1+2+3+4+5
,lisp 这样做: (+ 1 2 3 4 5)
,括号是 lisp 的特性,左括号的往右的第一个字符就是所谓的“操作符”,用空格来区别不同的参数。这样做有什么好处?现代语言因为更偏向自然语言,操作符的位置更加灵活,比如调用一个方法的时候: object.method(args)
,方法需要一个对象支撑,自己变成了附庸品,后置了;几乎所有连接字符串都可以这么操作: "HE"+"LLO"+"WORLD"
,操作符+位于中间;还有最后一种情况,常见的 Assert statement
或者内置函数调用 ABS(num)
又把操作符前置了,在操作符使用过程中混乱不协调,容易迷惑然后出错,所以灵活也有它的弊端。括号和操作符前置两个特性让 lisp 独特而优雅,组成了为人称道的 S-expression(S 表达式),以后还会提及它的好处。
数据结构
将操作符前置是高度统一又美好理想的理念,因为纯数据的存在导致不可能所有括号里面的最左边都是操作符。lisp 其实叫做列表处理语言,这里就引出了 lisp 里面最基础的数据结构——list,还有它的组成元素 atom(原子)。其实我们已经见过了 list 就是 (+ 1 2 3 4 5)
,只有在括号括起来的表达式就叫做一个 list,里面的元素只要不是另外一个 list 就叫做原子(atom),意为不能再分解的意思。那么纯数据该怎么表示?像这样 (1 2 3 4 5 )
,显然不符合操作符前置的原则,会报错说找不到名为 1 的函数,于是考虑再三就把操作符扩充到了括号之外,变成 '(1 2 3 4 5)
表示整个括号里面不执行,直接返回整个 list。像 (+ 1 2 3 4 5)
这样的 list 会执行,返回值为 15;而 '(+ 1 2 3 4 5)
会直接返回 (+ 1 2 3 4 5)
本身。
返回机制
上面提到了返回,在其他大部分的语言里需要一个关键字 return 来实现,而 lisp 只要遇到括号就代表自动返回值,可以执行括号内容后返回,也可以根据括号前面的'单引号只返回括号本身。这时候就有人想在返回的时候部分内容能够执行就好了,比如 (5 (* 2 5) 15 20)
,如果返回的时候里面的表达式 (* 2 5)
能直接执行的话整个表达式就能返回 (5 10 15 20)
这样的数列。于是就有人想出了用外层的括号来控制内层的括号,变成 `(5 ,(* 2 5) 15 20),
外层加个反引号内层加个逗号就能返回以 5 为倍数的数列了。
常见关键字
nil
和 t
nil 和 t 在 elisp 里面分别代表着 false 和 true,用单个字母 t
来代表 true 让人措手不及。和其他语言一样,正整数也表示 true,负整数表示 false。
require
顾名思义它的作用就是引用包,后面一般加着包名,至于具体的设置和用法会在下一篇文章详细分析,只要知道它和其他语言中的 import,require 一样把其他模块或者包的上下文空间引入当前文件下。
setq
setq
作用是赋值,一般这么用 (setq variable value)
,比如 (setq kill-ring-max 200)
让 emacs 剪切板存放的条目数上限为 200。
add-to-list
添加一个元素到某个 list 里面,这样用 (add-to-list LIST ELEMENT)
。也可以看出除了用单个变量来控制某种配置外,emacs 里面还存在各种 list 这种容器式数据结构,用来映射或者存放一组变量。
add-hook
添加钩子,比如 (add-hook ‘window-setup-hook ‘toggle-frame-maximized)
,作用是当 window 启动的时候 frame 最大化。在添加钩子的时候要查看是否有以-hook 结尾的函数。
global-set-key
、 define-key
以及 kbd
kbd
是一个宏,至于什么是宏,我们先暂按不说,它的存在就是为简化定义一系列按键输入(键序列),比如 (kbd “C-x C-f”)
会返回 control+c 然后 control+f 的输入操作。 global-set-key
用来定义按键绑定的函数,之前提到过 emacs 里面的任何操作都是由一个或者多个函数组成,结合上面的 kbd
宏,emacs 这样定义的一个按键的功能—— (global-set-key (kbd "C-x C-f") 'find-file)
。那么 define-key
又是什么? global-set-key
显而易见是用来定义全局的,针对于整个 emacs 而言的,污染性太大, define-key
则用来定义针对某个 mode 才有的按键,也就是说当我们进入到某个 mode 的时候这个按键才生效。常见用法 (define-key keymap function)
,keymap 保管了 key 和 function 的映射表,一般每个 mode 都会提供了一个 keymap 用于用户自定义。 如果不知道自己输入的键序列在 kbd
里面是怎么写的,可以使用 C-h k
来查看有没有此键序列的绑定,如果没有绑定任何的函数会返回这个键序列本身,就是写入 kbd
里面的内容。
setq/setq-default
我们需要区分 setq
与 setq-default
: setq
设置当前缓冲区(Buffer)中的变量值 , setq-default
设 置的为全局的变量的值(具体内容可以在StackOverflow 找到)
require
require
的意思为从文件中加载特性,你可以在杀哥的网站读到关于 Emacs Lisp 库系统 的更多内容,文章在这里。
在进行美化之前我们需要配置插件的源(默认的源非常有限),最常使用的是 MELPA (Milkypostman's Emacs Lisp Package Archive)。它有非常多的插件(3000 多个插件)。 一个插件下载的次数多并不能说明它非常有用,也许这个插件是其他的插件的依赖。在 这里 你可以找到其安装使用方法。添加源后,我们就可以使用 M-x package-list-packages 来查看所有 MELPA 上的插件了。在表单中可以使用 I 来标记安装 D 来标记删除, U 来更新,并用 X 来确认。
quote
;; 下面两行的效果完全相同的 (quote foo) 'foo
quote
的意思是不要执行后面的内容,返回它原本的内容(具体请参考下面的例子)
(print '(+ 1 1)) ;; -> (+ 1 1) (print (+ 1 1)) ;; -> 2
更多关于 quote
的内容可以在 这里 找到,或者在 这里这里找到 StackOverflow 上对于它的讨论。
;; 第一种 (setq package-selected-packages my/packages) ;; 第二种 (setq package-selected-packages 'my/packages) ;; 第三种 (setq package-selected-packages (quote my/packages))
第一种设置是在缓冲区中设置一个名为 package-selected-packages 的变量,将其的值 设定为 my/packages 变量的值。第二种和第三种其实是完全相同的,将一个名为 package-selected-packages 的变量设置为 my/packages 。
auto-mode-alist(major mode)
你可以在这里(How Emacs Chooses a Major Mode)找到 Emacs 是如何选择何时该选用何 种 Major Mode 的方法。
在这里我们需要知道 auto-mode-alist 的作用,这个变量是一个AssociationList,它 使用正则表达式(REGEXP)的规则来匹配不同类型文件应使用的 Major Mode。 下面是几个 正则表达式匹配的例子,
(("\\`/tmp/fol/" . text-mode) ("\\.texinfo\\'" . texinfo-mode) ("\\.texi\\'" . texinfo-mode) ("\\.el\\'" . emacs-lisp-mode) ("\\.c\\'" . c-mode) ("\\.h\\'" . c-mode) …)
下面是如何添加新的模式与对应文件类型的例子(与我们配置 js2-mode 时相似的例子),
(setq auto-mode-alist (append ;; File name (within directory) starts with a dot. '(("/\\.[^/]*\\'" . fundamental-mode) ;; File name has no dot. ("/[^\\./]*\\'" . fundamental-mode) ;; File name ends in ‘.C’. ("\\.C\\'" . c++-mode)) auto-mode-alist))
命名规则
Elisp 中并没有命名空间(Namespace),换句话说就是所有的变量均为全局变量,所以其 命名方法就变的非常重要。下面是一个简单的命名规则,
#自定义变量可以使用自己的名字作为命名方式(可以是变量名或者函数名) my/XXXX #模式命名规则 ModeName-mode #模式内的变量则可以使用 ModeName-VariableName 遵守上面的命名规则可以最大程度的减少命名冲突发生的可能性。
Major 与 Minor Mode
每一个文件类型都对应一个 Major Mode,它提供语法高亮以及缩进等基本的编辑支持功能,然后而 Minor Mode 则提供 其余的增强性的功能(例如 linum-mode )。
在 Emacs 中,Major Mode 又分为三种,
- text-mode ,用于编辑文本文件
- special-mode ,特殊模式(很少见)
- prog-mode ,所有的编程语言的父模式
在每一个模式(mode)中它的名称与各个变量还有函数都是有特定的命名规则,比如所有的 模式都被命名为 ModeName-mode ,里面所设置的快捷键则为 ModeName-mode-key-map ,而所有的钩子则会被命名为 ModeName-mode-hook 。
模块化
在这一部分我们首先需要知道的是什么是 features
。在 Emacs 中每一个 feature
都 是一个 Elisp 符号,用于代表一个 Lisp 插件(Package)。
当一个插件调用 (provide 'symbol_name)
函数时,Emacs 就会将这个符号加入到 features
的列表中去。你可以在这里读到更多关于 feature 的内容。
接着我们需要弄明白的是 load-file
, load
, require
, autoload
之间的区别。(他们之间区别的链接已经再前面贴过了,你也可以在这里找到之前同样的链接)
简单来说, load-file
用于打开某一个指定的文件,用于当你不想让 Emacs 来去决定加 载某个配置文件时( .el 或者 .elc 文件)。
load
搜索 load-path
中的路径并打开第一个所找到的匹配文件名的文件。此方法用于 你预先不知道文件路径的时候。
require
加载还未被加载的插件。首先它会查看变量 features
中是否存在所要加载的 符号如果不存在则使用上面提到的 load
将其载入。(有点类似于其他编程语言中的 import
)
autoload
用于仅在函数调用时加载文件,使用此方法可以大大节省编辑器的启动时间。
Dired Mode
使用 C-x d 就可以进入 Dired Mode
- + 创建目录
- g 刷新目录
- C 拷贝
- D 删除
- R 重命名
- d 标记删除
- u 取消标记
- x 执行所有的标记
启用 dired-x 可以让每一次进入 Dired 模式时,使用新的快捷键 C-x C-j 就可以进 入当前文件夹的所在的路径。
宏
Lisp 的宏(Macro)类似于 C++ 中的模板,并可以生产新的代码(你可以在这里找到更多 关于宏的讨论)。使用它,我们可以增强某个函数的功能而不去更改这个函数的代码。
(defmacro inc (var) (list 'setq var (list '1+ var))) (setq my-var 1) (setq my-var (+ 1 my-var)) (macroexpand '(inc my-var))
以上这个宏的作用是将变量的值+1. 执行以上代码之后, my-var 的结果为 2.
可以使用 macroexpand 获得宏展开的结果, 如以上代码结果为:
(setq my-var (1+ my-var))
函数与宏的区别:
- 宏的参数并不会被马上求值, 解释器会先展开宏, 宏展开之后解释器才会执行宏展开的 结果; 而函数的参数会马上求值
- 宏的执行结果是一个表达式, 该表达式会立即被解释器执行; 而函数的结果是一个值
backquote
backquote 的作用与 quote 相似, 同样不对后面的表达式求值, 但是当 backquote 在宏中 与逗号(,)一起使用时, 用逗号修饰的变量将进行求值.
例如以下代码:
(defmacro my-print-2 (number) `(message "This is a number: %d" ,number)) (pp (macroexpand '(my-print-2 (+ 2 3)))) (my-print-2 (+ 2 3))
当输出 message 且 number 不带逗号时, my-print-2 的执行将提示错误. 因为宏不对参 数进行求值, 所以以上宏展开相当于:
(message "This is a number:" number)
因为我们没有定义 number 变量, 所以执行出错.
而如果加入逗号, 则在宏展开时会对变量 number 进行求值, 展开结果为:
(message "This is a number: %d" (+ 2 3))
在调试宏的过程中, 可以使用 macroexpand 和 macroexpand-all 获取宏展开的结果.
关于 backquote 的更多讨论, 可以见以下地址: lisp 中的`与,是怎么用的?
use-package
use-package 是一个宏, 它能让你将一个包的 require 和它的相关的初始化等配置组织 在一起, 避免对同一个包的配置代码散落在不同的文件中.
use-package 的更多信息参见以下地址: use-package
更安全的 require
在 Emacs 中, 当我们要引入一个包时, 通常会使用以下代码:
(require 'package-name)
但是当 package-name 不在 load-path 中时, 以上代码会抛出错误. 使用 Use-package 可以避免:
(use-package package-name)
以上代码展开的结果如下:
(if (not (require 'package-name nil 't)) (ignore (message (format "Cannot load %s" 'package-name))))
可以看到, Use-package 使用 ignore 来避免抛出错误, 这样当某个包不存在时, eamcs 也能够正常启动.
将配置集中
当我们引入某个包时, 有可能需要定义一些与这个包相关的变量, 使用 Use-package 实 现这个需求如下:
(use-package package-name :init (setq my-var1 "xxx") :config (progn (setq my-var2 "xxx") (setq my-var3 "xxx") ) )
在上例中, init 后的代码在包的 require 之前执行, 如果这段代码出错则跳过包的 require.
config 后的代码在包的 require 之后执行.
init 与 config 之后只能接单个表达式语句, 如果需要执行多个语句, 可以用 progn .
autoload
使用 require 时会引入这个包, 但是当你的包很多时会影响启动速度. 而使用 autoload 则可以在真正需要这个包时再 require, 提高启动速度, 避免无谓的 require.
使用 Use-package 可以轻松的实现这个功能:
(use-package package-name :commands (global-company-mode) :defer t )
使用 commands 可以让 package 延迟加载, 如以上代码会首先判断 package 的符号是否 存在, 如果存在则在 package-name 的路径下加载. defer 也可以让 package-name 进行延迟加载.
键绑定
在之前的代码中, 如果我们需要绑定一个键, 需要使用 global-key-bind 或 define-key 实现, 而使用*Use-package* 实现更简单:
(use-package color-moccur :commands (isearch-moccur isearch-all) :bind (("M-s O" . moccur) :map isearch-mode-map ("M-o" . isearch-moccur) ("M-O" . isearch-moccur-all)) :init (setq isearch-lazy-highlight t) :config (use-package moccur-edit))
为什么使用 Use-package
- Use-package 能让相关的配置更为集中, 避免配置分散带来的维护困难
- Use-package 有完善的错误处理, 使配置代码更为健壮
org-babel-load-file
org 是和 markdown 一样是一种格式标记文本形式,可以用来编写文档的,但它比 markdown 厉害得多,提供功能更齐全,其中一种功能是将 org 里面编写的程序放到 org-babel(org 文件的某个块区域)里面,和 markdown 仅仅用来展示程序不同,org-babel 里面的程序可以用 org-babel-load-file
来执行。
(org-babel-load-file "~/.emacs.d/config.org")
意味着你可以在一个 org 文件里面写出 emacs 的配置和相应的说明,是一种高效管理 emacs 配置的方式,初次听说还蛮刷三观的。不过因为加载速度的原因没能完全流行起来,不过还是有一些 emacser 坚持使用这种方式。