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

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,代码转换器
Hookadd-hook,事件回调
Adviceadvice-add,函数增强
包开发provide, ;;;###autoload

11.12 扩展阅读


← 上一章 第 10 章:编程环境 | 下一章 → 第 12 章:包管理