☰
15 分钟学会 Emacs Lisp:从 s-表达式到 buffer 文本处理实战(learnxinyminutes-docs 中文教程精讲)
2026/10/10 2:25:13 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载

本文以 learnxinyminutes-docs 仓库中的 zh-cn/elisp.md 中文教程为骨架展开。该文档以"注释即代码"的形式,用一段可逐行求值的 Elisp 源码完整演示了 Emacs Lisp 的核心语法与常用函数:从 s-表达式、变量、函数定义,到多窗口 buffer 操作、列表遍历、正则查找替换,直至给文本加粗的文字属性处理。读完本文,你将能够在 GNU Emacs 的*scratch*buffer 中亲手求值每一段代码,并在 15 分钟内搭建出自己的第一个可交互 Elisp 函数。

一、准备工作:安装 GNU Emacs 并进入*scratch*buffer

Emacs Lisp(简称 Elisp)是 GNU Emacs 的内置扩展语言,因此学习它的第一步就是安装 Emacs。原文档给出的安装指引如下(按发行版不同略有差异):

  • Debian / Ubuntu 系:apt-get install emacs;
  • macOS:使用 emacsformacosx.com 提供的官方构建包;
  • Windows:使用 GNU 官方 ftp 站点的 Windows 构建版。

安装完成后启动 Emacs,按下q键关闭欢迎界面,观察窗口底部的灰色状态栏。你会看到*scratch*这个名字——这就是当前编辑空间的名称,在 Emacs 中它被称为一个buffer(缓冲区)。

这里有两个贯穿全文的重要概念:

  1. 你从来不是在直接编辑文件,而是在编辑 buffer。每次启动 Emacs 都会默认打开*scratch*buffer,此时你尚未关联任何文件,之后可以随时把 buffer 保存为文件。
  2. 每个 buffer 都有一组内置命令,激活特定模式后会获得对应的命令子集。*scratch*默认处于lisp-interaction-mode(Lisp 交互模式),该模式提供了求值与在 Elisp 代码中导航的命令——这正是我们逐行实验的基础。

二、s-表达式:Elisp 的最小构成单元

注释与符号表达式

在 Elisp 中,分号(;)是注释的开始标志,从分号到行尾的内容都会被忽略。整个教程文档本质上就是一段"活注释"——边读边执行。

Elisp 程序由**符号表达式(symbolic expression,简称 s-表达式或 s 式)**构成。最简单的 s-表达式如下:

(+ 2 2)

它的含义是"对 2 进行加 2 操作"。s-表达式用括号包裹,并且可以嵌套:

(+ 2 (+ 1 1))

一个 s-表达式可以包含**原子(atom)**或其他 s-表达式:上面的例子中1、2是原子,(+ 2 (+ 1 1))与(+ 1 1)是 s-表达式。这种"一切皆表达式、表达式层层嵌套"的结构,正是 Lisp 家族语言的本质特征。

求值:C-j与C-x C-e

在lisp-interaction-mode中你可以直接求值 s-表达式。把光标移到闭括号之后,按住 Ctrl 再按j(记作C-j),即可得到结果:

(+ 3 (+ 1 2)) ;; ^ 光标放在这里 ;; 按下 C-j 就会输出 6

C-j会把求值结果插入到当前 buffer 中。而C-x C-e(先按 Ctrl+x,再按 Ctrl+e)则会把结果显示在 Emacs 最底部、被称为echo area的区域中。为了避免 buffer 被无用的输出填满,原文档建议后续统一使用C-x C-e。之后的每个 s-表达式,都需要你手动执行一次C-x C-e来观察效果。

三、变量、字符串与格式化输出:setq/insert/format

setq:给变量赋值

setq用于把一个值赋给一个变量,是 Elisp 中最常用的赋值形式:

(setq my-name "Bastien") ;; C-x C-e => "Bastien"(在 echo area 中显示)

insert:在光标处插入字符串

insert会在光标当前位置插入字符串,且支持一次传入多个参数:

(insert "Hello!") ;; C-x C-e => "Hello!" (insert "Hello" " world!") ;; C-x C-e => "Hello world!" ;; 也可以用变量名代替字符串字面量: (insert "Hello, I am " my-name) ;; C-x C-e => "Hello, I am Bastien"

注意insert的求值结果与可见输出略有区别:它把文本真正写入了 buffer,而不是仅仅打印到 echo area。

format:字符串格式化

format是 Elisp 的字符串格式化函数,%s是字符串占位符,\n是换行符:

(format "Hello %s!\n" "visitor") ;; => "Hello visitor!\n"

%s会被后面的参数替换。除%s(字符串)外,格式化指令体系还支持%d(整数)、%f(浮点数)等,具体格式约定可在 Emacs 内用C-h f format查阅文档。

四、定义与调用函数:defun

用defun可以把若干 s-表达式组合成一个函数。先看一个无参函数:

(defun hello () (insert "Hello, I am " my-name)) ;; C-x C-e => hello(函数定义本身返回函数名) (hello) ;; C-x C-e => Hello, I am Bastien

函数定义中空的圆括号表示该函数不接受任何参数。但把名字写死在函数体里显然不够灵活,于是我们让函数接受一个名为name的参数:

(defun hello (name) (insert "Hello " name)) ;; C-x C-e => hello (hello "you") ;; C-x C-e => "Hello you"

调用时把字符串"you"作为参数传给hello,name在函数体内被绑定为"you"。这是 Elisp 函数定义的通用模式:参数列表写在函数名后的括号中,函数体是一个或多个 s-表达式。

结合format,我们可以把hello打磨得更有输出层次:

(defun hello (name) (insert (format "Hello %s!\n" name))) (hello "you")

五、多窗口与 buffer 操作:progn/let/erase-buffer

在新窗口打开新 buffer

Emacs 支持同时显示多个窗口。下面的代码会在另一个窗口新建名为*test*的 buffer:

(switch-to-buffer-other-window "*test*") ;; C-x C-e => 屏幕分为两个窗口,光标位于 *test* buffer 内

之后既可以用鼠标点击上方窗口把光标移回,也可以按C-x o(Ctrl+x 后按 o)把光标交互式地切换到另一个窗口。

progn:把多个 s-表达式组合成序列

progn按顺序执行其后的所有 s-表达式,并返回最后一个表达式的值。它非常适合组合多步操作:

(progn (switch-to-buffer-other-window "*test*") (hello "you")) ;; C-x C-e => 屏幕分为两个窗口,并在 *test* buffer 中显示 "Hello you"

清空 buffer 是常用操作,erase-buffer负责删除当前 buffer 的全部内容:

(progn (switch-to-buffer-other-window "*test*") (erase-buffer) (hello "there")) ;; 或者执行完后把光标切回原窗口: (progn (switch-to-buffer-other-window "*test*") (erase-buffer) (hello "you") (other-window 1))

let:局部变量绑定

let用于把一个值与局部变量绑定,并且同样可以组合多个 s-表达式(因此这里不再需要progn):

(let ((local-name "you")) (switch-to-buffer-other-window "*test*") (erase-buffer) (hello local-name) (other-window 1))

let的语法是(let ((var1 value1) (var2 value2) ...) body...):变量只在let体内部有效,不会污染全局环境。这与setq(作用于全局/当前环境)形成鲜明对比。

下面的函数同时演示了let与format的配合,注意参数name与let绑定变量your-name的分工:

(defun greeting (name) (let ((your-name "Bastien")) (insert (format "Hello %s!\n\nI am %s." name ; 函数的参数 your-name ; let 绑定的局部变量 "Bastien" )))) (greeting "you")

六、与用户交互:read-from-minibuffer

有些 Elisp 函数具有交互性。read-from-minibuffer会在 Emacs 底部的 minibuffer(迷你缓冲区)弹出提示,并返回用户输入的内容:

(read-from-minibuffer "Enter your name: ")

把它与let结合,可以让greeting动态询问对方的名字:

(defun greeting (from-name) (let ((your-name (read-from-minibuffer "Enter your name: "))) (insert (format "Hello!\n\nI am %s and you are %s." from-name ; 函数的参数 your-name ; let 绑定的变量,来自用户输入 )))) (greeting "Bastien")

再进一步,把结果显示到另一个窗口中,形成完整的"输入 → 处理 → 展示"闭环:

(defun greeting (from-name) (let ((your-name (read-from-minibuffer "Enter your name: "))) (switch-to-buffer-other-window "*test*") (erase-buffer) (insert (format "Hello %s!\n\nI am %s." your-name from-name)) (other-window 1))) (greeting "Bastien")

此时你已经写出了第一个具备交互能力、跨 buffer 工作的 Elisp 函数——这正是许多 Emacs 小工具(扩展命令)的原型。

七、列表与高阶遍历:quote/car/cdr/push/mapcar

创建字面量列表:quote

列表是 Lisp 的核心数据结构。要创建字面量列表,需要用'(即quote)阻止列表被求值——不加引号的话,Elisp 会把("Sarah" ...)当作函数调用处理:

(setq list-of-names '("Sarah" "Chloe" "Mathilde"))

car/cdr/push:列表访问与修改

;; car 取第一个元素 (car list-of-names) ;; cdr 取除第一个元素外的其余部分(仍是一个列表) (cdr list-of-names) ;; push 把新元素添加到列表开头 (push "Stephanie" list-of-names)

这里有一个非常重要的区别:car和cdr只是读取列表,不会修改列表本身;而push会就地修改列表。这体现了 Elisp 中"有的函数没有副作用(如car),有的函数有副作用(如push)"的编程原则——理解副作用是写出可预期代码的关键。

mapcar:对列表逐元素映射调用函数

mapcar会把函数依次应用到列表的每个元素上,并返回结果组成的列表:

(mapcar 'hello list-of-names)

mapcar的第一个参数'hello是函数名(同样被quote保护,表示"函数对象"而非"调用它")。回忆我们之前定义的hello函数:它接受一个名字参数。于是mapcar依次把list-of-names中的每个元素作为参数传给hello,完成批量问候。

据此可以重构greeting,让它对名单上的所有人打招呼:

(defun greeting () (switch-to-buffer-other-window "*test*") (erase-buffer) (mapcar 'hello list-of-names) (other-window 1)) (greeting)

八、文本查找与替换:goto-char/search-forward/replace-match/while

现在进入真正的"文本处理"环节。下面这个函数会遍历*test*buffer,把所有出现的"Hello"替换为"Bonjour":

(defun replace-hello-by-bonjour () (switch-to-buffer-other-window "*test*") (goto-char (point-min)) (while (search-forward "Hello") (replace-match "Bonjour")) (other-window 1))

逐行拆解:

  • (goto-char (point-min)):把光标移动到 buffer 的开头(point-min是 buffer 起始位置,goto-char是移动光标的函数);
  • (search-forward "Hello"):从当前光标位置向后查找字符串"Hello";
  • (while x y):只要x返回非空值,就反复执行y;当x返回nil(空)时退出循环。这里search-forward每找到一处就返回非nil,同时把光标推进到匹配点之后,从而驱动循环继续查找下一处,直到查完整个 buffer。

执行(replace-hello-by-bonjour)后,*test*buffer 中所有"Hello"都会被替换为"Bonjour"。

避免 "Search failed" 错误:search-forward的nil与t参数

但上面这个函数执行到最后会抛出一个错误:Search failed: Hello——因为search-forward在找不到任何匹配时默认报错。要避免它,需要告诉search-forward两件事:

(search-forward "Hello" nil t)
  • 第二个参数nil:表示查找不限于某个位置范围(即从当前点一直搜到 buffer 末尾);
  • 第三个参数t:表示找不到时静默失败,不抛出错误。

注意t是 Elisp 中"真"的惯用表示,nil表示"假/空"。把这一写法融入完整函数:

(defun hello-to-bonjour () (switch-to-buffer-other-window "*test*") (erase-buffer) ;; 为 list-of-names 中的每个名字调用 hello (mapcar 'hello list-of-names) (goto-char (point-min)) ;; 将 "Hello" 替换为 "Bonjour" (while (search-forward "Hello" nil t) (replace-match "Bonjour")) (other-window 1)) (hello-to-bonjour)

九、正则表达式与文字属性:re-search-forward/add-text-properties

用正则表达式匹配模式

search-forward只能查找字面字符串;若想按模式查找,需要使用re-search-forward(前缀re-即 regular expression 正则表达式):

(defun boldify-names () (switch-to-buffer-other-window "*test*") (goto-char (point-min)) (while (re-search-forward "Bonjour \\(.+\\)!" nil t) (add-text-properties (match-beginning 1) (match-end 1) (list 'face 'bold))) (other-window 1)) (boldify-names)

这里的关键是正则表达式"Bonjour \\(.+\\)!",逐字解读如下:

片段含义
Bonjour字面字符串 "Bonjour "
\\( ... \\)捕获分组结构,把中间匹配的内容单独记为一组
.匹配任意一个字符
+前面的字符可能出现一次或多次(贪婪重复)
!字面字符串 "!"

组合起来,它匹配的是:以"Bonjour "开头、中间是任意长度的任意字符、并以!结尾的整段文本,例如"Bonjour Sarah!"。

用add-text-properties给文本加粗

add-text-properties用于给 buffer 中指定区间的文本添加文字属性(text properties),比如字体样式:

(add-text-properties (match-beginning 1) (match-end 1) (list 'face 'bold))
  • (match-beginning 1)/(match-end 1):分别返回第 1 个捕获分组在 buffer 中的起始与结束位置——也就是名字本身所在区间;
  • (list 'face 'bold):设置face(字体外观)属性为bold(粗体)。

执行(boldify-names)后,*test*buffer 中所有名字都会被渲染为粗体。至此,我们已经完成了一次从"查找"到"正则匹配"再到"富文本属性"的完整文本处理管线——这正是 Elisp 作为编辑器脚本语言最典型的应用形态。

十、内建帮助系统与进一步学习

Emacs 自带强大的自文档(self-documenting)能力。当你对某个变量或函数不确定时:

  • C-h v 变量名 RET:查看变量的文档与当前值;
  • C-h f 函数名 RET:查看函数的文档、参数列表与源码位置;
  • C-h i m elisp RET:在 Emacs 内打开官方 Emacs Lisp 手册(Info 格式)进行系统学习。

十一、回顾与总结

回看整个教程,我们实际上完成了一条完整的 Elisp 学习曲线:

  1. 语言基础:s-表达式、原子、嵌套求值(C-j/C-x C-e);
  2. 数据与输出:setq变量赋值、insert文本插入、format格式化;
  3. 抽象与组合:defun定义函数、progn序列组合、let局部变量;
  4. 交互与窗口:read-from-minibuffer用户输入、switch-to-buffer-other-window多窗口、erase-buffer清空;
  5. 数据结构:quote字面量列表、car/cdr读取、push修改、mapcar批量映射;
  6. 文本处理:goto-char/search-forward/replace-match查找替换、while循环与nil退出条件;
  7. 高级能力:re-search-forward正则匹配、add-text-properties文字属性加粗。

这套技能足以支撑你开始阅读和修改自己的 Emacs 配置(如.emacs/init.el),并为深入 Elisp 编程打下基础。

本教程在仓库中的完整代码即 zh-cn/elisp.md(中文译本),其英文原版位于 elisp.md,另有 fr/elisp.md、es/elisp.md、pt-br/elisp.md、ms/elisp.md 等多个语言版本可供对照学习;若想了解 Emacs 本身的扩展体系,可进一步阅读仓库中的 emacs.md。所有文档遵循同一"注释即代码"的格式约定(详见 README.md 与 CONTRIBUTING.md),你完全可以把本文中的代码块逐段复制到*scratch*buffer 中,用C-x C-e亲自验证每一个结果。

  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载
上一篇:GHelper终极指南:轻量化华硕笔记本控制工具,完美替代Armoury Crate
下一篇:华硕笔记本硬件控制指南:GHelper开源工具深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询