UP | HOME

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 为倍数的数列了。

常见关键字

nilt

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-keydefine-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

我们需要区分 setqsetq-defaultsetq 设置当前缓冲区(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))

函数与宏的区别:

  1. 宏的参数并不会被马上求值, 解释器会先展开宏, 宏展开之后解释器才会执行宏展开的 结果; 而函数的参数会马上求值
  2. 宏的执行结果是一个表达式, 该表达式会立即被解释器执行; 而函数的结果是一个值

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

  1. Use-package 能让相关的配置更为集中, 避免配置分散带来的维护困难
  2. 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 坚持使用这种方式。

作者: Petrus

Created: 2021-09-01 Wed 00:38