Emacs 完全指南 / 第 11 章:Elisp 编程
第 11 章:Elisp 编程
11.1 Elisp 简介
Emacs Lisp(Elisp)是 Emacs 的扩展语言。Emacs 本身几乎完全由 Elisp 编写。学会 Elisp,你就能定制 Emacs 的一切。
为什么学 Elisp
| 场景 | 收益 |
|---|
| 自定义快捷键 | 将常用操作绑定到任意按键 |
| 自动化任务 | 批量处理文件、文本 |
| 开发插件 | 创建并发布到 MELPA |
| 深度定制 | 修改 Emacs 的任何行为 |
| 理解 Emacs | 阅读和理解内置代码 |
评估 Elisp 的方式
;; 方式 1: *scratch* 缓冲区
;; 输入表达式,C-x C-e 评估
;; 方式 2: M-: (eval-expression)
;; 在 minibuffer 中输入并评估
;; 方式 3: C-x C-e
;; 在任意 Elisp 表达式末尾按 C-x C-e
;; 方式 4: M-x eval-buffer
;; 评估整个缓冲区
;; 方式 5: C-M-x (eval-defun)
;; 评估当前 defun
;; 方式 6: C-c C-c (emacs-lisp-mode)
;; 评估选区
11.2 基本数据类型
数据类型表
| 类型 | 示例 | 说明 |
|---|
| 整数 | 42, -7, 0 | 精确整数 |
| 浮点数 | 3.14, 1.0e5 | 浮点数 |
| 字符串 | "hello", "你好" | 文本 |
| 字符 | ?a, ?A, ?\n | 单个字符 |
| 符号 | 'foo, nil, t | 标识符 |
| 布尔 | t (真), nil (假) | 布尔值 |
| 列表 | (1 2 3), ("a" "b") | 有序集合 |
| 向量 | [1 2 3] | 固定长度数组 |
| 哈希表 | #s(hash-table …)` | 键值映射 |
| 函数 | (lambda (x) x) | 匿名函数 |
| 缓冲区 | 当前缓冲区对象 | Emacs 特有 |
| 进程 | 子进程对象 | Emacs 特有 |
基本操作
;; 算术运算
(+ 1 2 3) ; → 6
(- 10 3) ; → 7
(* 2 3 4) ; → 24
(/ 10 3) ; → 3 (整数除法)
(/ 10.0 3) ; → 3.3333333 (浮点除法)
(% 10 3) ; → 1 (取模)
;; 字符串操作
(concat "hello" " " "world") ; → "hello world"
(length "hello") ; → 5
(substring "hello" 1 3) ; → "el"
(upcase "hello") ; → "HELLO"
(downcase "HELLO") ; → "hello"
(format "Hello, %s! Age: %d" "Alice" 30) ; → "Hello, Alice! Age: 30"
;; 比较
(= 1 1) ; → t
(/= 1 2) ; → t
(< 1 2) ; → t
(> 2 1) ; → t
(string= "a" "a") ; → t
(string< "a" "b") ; → t
11.3 列表与序列
列表操作
;; 创建列表
'(1 2 3 4 5) ; 引用列表
(list 1 2 3 4 5) ; 构造列表
(cons 1 '(2 3)) ; → (1 2 3) 前插
(cons '(1 2) '(3 4)) ; → ((1 2) 3 4)
;; 访问元素
(car '(1 2 3)) ; → 1 第一个元素
(cdr '(1 2 3)) ; → (2 3) 剩余元素
(nth 2 '(1 2 3 4 5)) ; → 3 第 N 个元素(0 索引)
(last '(1 2 3)) ; → (3) 最后一个元素
(butlast '(1 2 3)) ; → (1 2) 除最后一个
;; 列表长度
(length '(1 2 3)) ; → 3
;; 列表操作
(append '(1 2) '(3 4)) ; → (1 2 3 4)
(reverse '(1 2 3)) ; → (3 2 1)
(sort '(3 1 4 1 5) '<) ; → (1 1 3 4 5)
(member 3 '(1 2 3 4)) ; → (3 4)
(memq 'b '(a b c)) ; → (b c) 使用 eq 比较
;; 序列通用函数
(seq-contains '(1 2 3) 2) ; → t
(seq-filter 'oddp '(1 2 3 4 5)) ; → (1 3 5)
(seq-map '1+ '(1 2 3)) ; → (2 3 4)
(seq-reduce '+ '(1 2 3 4) 0) ; → 10
(seq-find 'oddp '(2 3 4)) ; → 3
(seq-take '(1 2 3 4 5) 3) ; → (1 2 3)
(seq-drop '(1 2 3 4 5) 2) ; → (3 4 5)
向量和哈希表
;; 向量
[1 2 3 4]
(aref [1 2 3] 1) ; → 2
(vconcat '(1 2) [3 4]) ; → [1 2 3 4]
;; 哈希表
(setq ht (make-hash-table :test 'equal))
(puthash "name" "Alice" ht)
(puthash "age" 30 ht)
(gethash "name" ht) ; → "Alice"
(gethash "age" ht) ; → 30
(gethash "unknown" ht) ; → nil
(gethash "unknown" ht "default") ; → "default"
(remhash "age" ht)
(maphash (lambda (k v) (message "%s: %s" k v)) ht)
11.4 变量
变量定义
;; defvar - 只在变量未定义时设置(用于用户选项)
(defvar my-var 42
"我的自定义变量。")
;; defcustom - 可通过 M-x customize 配置的变量
(defcustom my-custom-var "default"
"一个可定制的变量。"
:type 'string
:group 'my-package)
;; setq - 总是设置值
(setq my-var 100)
;; let - 局部变量
(let ((x 10)
(y 20))
(+ x y)) ; → 30
;; let* - 顺序绑定(后面的可以用前面的)
(let* ((x 10)
(y (* x 2)))
y) ; → 20
变量作用域
;; Emacs Lisp 使用"动态作用域"(默认)和"词法作用域"
;; 推荐启用词法作用域
;; 在文件头部添加:
;;; -*- lexical-binding: t; -*-
;; 词法作用域示例
(let ((x 10))
(lambda () x)) ; 闭包,捕获 x
;; 动态作用域示例(注意区别)
(defvar dyn-var 100)
(defun use-dyn-var ()
dyn-var)
(let ((dyn-var 200))
(use-dyn-var)) ; → 200(动态查找)
11.5 函数
定义函数
;; defun - 定义命名函数
(defun greet (name)
"向 NAME 问候。"
(message "Hello, %s!" name))
(greet "Emacs") ; → "Hello, Emacs!"
;; interactive - 使函数可交互调用(M-x 或快捷键)
(defun greet-interactive ()
"交互式问候。"
(interactive)
(let ((name (read-string "请输入名字: ")))
(message "Hello, %s!" name)))
;; 可选参数和 rest 参数
(defun example (a &optional b &rest args)
"参数示例。"
(list a b args))
(example 1) ; → (1 nil nil)
(example 1 2) ; → (1 2 nil)
(example 1 2 3 4 5) ; → (1 2 (3 4 5))
;; 关键字参数
(defun make-person (&key name age city)
(list :name name :age age :city city))
(make-person :name "Alice" :age 30 :city "Beijing")
Lambda 函数
;; 匿名函数
(lambda (x) (* x x))
;; 调用
(funcall (lambda (x) (* x x)) 5) ; → 25
;; 作为参数传递
(mapcar (lambda (x) (* x x)) '(1 2 3 4)) ; → (1 4 9 16)
(seq-filter (lambda (x) (> x 3)) '(1 2 3 4 5)) ; → (4 5)
常用高阶函数
;; mapcar - 映射列表
(mapcar '1+ '(1 2 3)) ; → (2 3 4)
(mapcar 'upcase '("a" "b" "c")) ; → ("A" "B" "C")
;; mapc - 像 mapcar 但不收集结果
(mapc 'message '("a" "b" "c"))
;; apply - 将列表作为参数调用
(apply '+ '(1 2 3 4)) ; → 10
;; funcall - 调用函数
(funcall '+ 1 2 3) ; → 6
11.6 条件与循环
条件
;; if
(if (> 3 2)
"大于"
"不大于")
;; when - 只有 then 分支
(when (> 3 2)
(message "大于"))
;; unless - 只有 else 分支
(unless (< 3 2)
(message "不小余"))
;; cond - 多分支
(let ((x 3))
(cond ((= x 1) "一")
((= x 2) "二")
((= x 3) "三")
(t "其他")))
;; pcase - 模式匹配
(pcase 42
(0 "零")
(42 "答案")
(_ "其他"))
(pcase '(1 2 3)
(`(,a ,b ,c) (format "%d + %d = %d" a b (+ a b)))
(_ "不匹配"))
循环
;; dolist - 遍历列表
(dolist (item '(1 2 3 4 5))
(message "item: %d" item))
;; dotimes - 重复 N 次
(dotimes (i 5)
(message "i: %d" i))
;; while - while 循环
(let ((i 0))
(while (< i 5)
(message "i: %d" i)
(setq i (1+ i))))
;; seq-do - 序列遍历
(seq-do (lambda (x) (message "%s" x)) '(1 2 3))
11.7 Buffer 和文本操作
缓冲区操作
;; 获取缓冲区
(current-buffer) ; 当前缓冲区
(get-buffer "*scratch*") ; 按名称获取
(get-buffer-create "*my-buffer*") ; 创建或获取
;; 切换缓冲区
(with-current-buffer "*scratch*"
(message "在 scratch 缓冲区中"))
;; 创建临时缓冲区
(with-temp-buffer
(insert "Hello, World!")
(buffer-string)) ; → "Hello, World!"
文本插入和删除
;; 插入文本
(insert "Hello, World!")
(insert "Line 1\n" "Line 2\n")
(insert-char ?- 10) ; 插入 10 个 -
;; 删除
(delete-region (point-min) (point-max)) ; 删除全部
(kill-line) ; 删除到行尾
(delete-char 1) ; 删除 1 个字符
;; 获取文本
(buffer-string) ; 整个缓冲区内容
(buffer-substring (point-min) (point-max)) ; 同上
(thing-at-point 'word) ; 光标处的单词
(thing-at-point 'line) ; 当前行
(thing-at-point 'sexp) ; 当前 S-表达式
;; 替换
(replace-string "old" "new")
(replace-regexp "old[0-9]+" "new")
光标操作
;; 获取位置
(point) ; 当前光标位置
(point-min) ; 缓冲区最小位置
(point-max) ; 缓冲区最大位置
(line-number-at-pos) ; 当前行号
(current-column) ; 当前列号
;; 移动光标
(goto-char (point-min)) ; 跳到开头
(forward-char 5) ; 前进 5 个字符
(backward-char 3) ; 后退 3 个字符
(forward-line 10) ; 前进 10 行
(beginning-of-line) ; 行首
(end-of-line) ; 行尾
(search-forward "target") ; 向前搜索
(re-search-forward "regex") ; 正则搜索
11.8 宏(Macros)
定义宏
;; 宏是代码转换器,在编译时展开
(defmacro when-let* (bindings &rest body)
"当所有 BINDINGS 非 nil 时执行 BODY。"
(declare (indent 1))
(let ((result (reverse bindings)))
(dolist (binding result)
(setq body `((let ((,(car binding) ,(cadr binding)))
(when ,(car binding)
,@body)))))
(car body)))
;; 使用示例
(when-let* ((name (get-name))
(age (get-age)))
(message "%s is %d" name age))
;; 展开为:
;; (let ((name (get-name)))
;; (when name
;; (let ((age (get-age)))
;; (when age
;; (message "%s is %d" name age)))))
常用内置宏
;; when - 条件执行
(when condition body...)
;; unless - 条件不执行
(unless condition body...)
;; let / let* - 局部绑定
(let ((x 1)) body...)
;; progn - 顺序执行
(progn expr1 expr2 expr3)
;; lambda - 匿名函数
(lambda (args) body)
;; with-current-buffer - 在指定缓冲区执行
(with-current-buffer buf body...)
;; with-temp-buffer - 在临时缓冲区执行
(with-temp-buffer body...)
;; save-excursion - 保存和恢复光标位置
(save-excursion body...)
;; save-restriction - 保存和恢复窄化
(save-restriction body...)
;; dolist / dotimes - 循环
(dolist (var list) body)
(dotimes (var n) body)
11.9 Hooks 与 Advices
Hooks(钩子)
;; Hook 是一种事件机制:当某个事件发生时调用钩子列表中的函数
;; 添加钩子
(add-hook 'python-mode-hook
(lambda ()
(setq indent-offset 4)))
;; 定义自己的钩子
(defvar my-file-open-hook nil
"打开文件后运行的钩子。")
(add-hook 'find-file-hook
(lambda ()
(run-hooks 'my-file-open-hook)))
;; 常用钩子
;; prog-mode-hook - 编程模式
;; text-mode-hook - 文本模式
;; after-init-hook - Emacs 初始化后
;; kill-buffer-hook - 关闭缓冲区前
;; before-save-hook - 保存前
;; after-save-hook - 保存后
;; window-configuration-change-hook - 窗口变化
Advices(建议)
;; Advice 可以在不修改原函数的情况下增强函数行为
;; 添加 advice
(advice-add 'message :before
(lambda (fmt &rest args)
(when (string-match-p "^DEBUG:" fmt)
(apply 'message (concat "[DEBUG] " fmt) args))))
;; 移除 advice
(advice-remove 'message ...)
;; 定义带 advice 的函数
(define-advice message (:around (orig-fun fmt &rest args) my-prefix)
"给所有消息添加前缀。"
(apply orig-fun (concat "[LOG] " fmt) args))
;; advice 类型:
;; :before - 在原函数之前执行
;; :after - 在原函数之后执行
;; :around - 包裹原函数(最灵活)
;; :override - 替代原函数
;; :filter-args - 修改传入参数
;; :filter-return - 修改返回值
11.10 开发一个简单包
包结构
my-package/
├── my-package.el # 主文件
├── my-package-pkg.el # 包描述(可选)
├── README.md # 说明文档
└── LICENSE # 许可证
my-package.el 示例
;;; my-package.el --- 一个简单的示例包 -*- lexical-binding: t; -*-
;; Author: Your Name <your@email.com>
;; Version: 0.1.0
;; Package-Requires: ((emacs "28.1"))
;; URL: https://github.com/yourname/my-package
;; Keywords: tools
;;; Commentary:
;; 这是一个示例包,演示如何开发 Emacs 包。
;;; Code:
(defgroup my-package nil
"My Package 自定义组。"
:group 'tools
:prefix "my-package-")
(defcustom my-package-name "World"
"问候的目标名称。"
:type 'string
:group 'my-package)
(defun my-package-greet (&optional name)
"向 NAME 问候。"
(interactive)
(let ((target (or name my-package-name)))
(message "Hello, %s!" target)))
(defun my-package-count-words ()
"统计当前缓冲区的单词数。"
(interactive)
(let ((count (count-words (point-min) (point-max))))
(message "缓冲区中有 %d 个单词" count)
count))
;;;###autoload
(defun my-package-setup ()
"设置 My Package。"
(interactive)
(message "My Package 已设置!"))
(provide 'my-package)
;;; my-package.el ends here
autoload 注释
;;;###autoload 告诉 Emacs 在包被加载前就记录这个命令,
;; 这样 M-x 就能找到它,而无需加载整个包。
;;;###autoload (autoload 'my-function "my-package" nil t)
;;;###autoload (defun my-function () ...)
;;;###autoload (defvar my-variable nil)
;;;###autoload (add-to-list 'auto-mode-alist '("\\.xyz\\'" . my-mode))
11.11 本章小结
| 概念 | 说明 |
|---|
| 数据类型 | 数字、字符串、列表、符号、哈希表 |
| 变量 | defvar, defcustom, setq, let |
| 函数 | defun, lambda, interactive |
| 条件 | if, when, unless, cond, pcase |
| 循环 | dolist, dotimes, while |
| 宏 | defmacro,代码转换器 |
| Hook | add-hook,事件回调 |
| Advice | advice-add,函数增强 |
| 包开发 | provide, ;;;###autoload |
11.12 扩展阅读
← 上一章 第 10 章:编程环境 | 下一章 → 第 12 章:包管理