强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 <[email protected]>
;; 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 章:包管理