- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
本文以 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(缓冲区)。
这里有两个贯穿全文的重要概念:
- 你从来不是在直接编辑文件,而是在编辑 buffer。每次启动 Emacs 都会默认打开
*scratch*buffer,此时你尚未关联任何文件,之后可以随时把 buffer 保存为文件。 - 每个 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 就会输出 6C-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 学习曲线:
- 语言基础:s-表达式、原子、嵌套求值(
C-j/C-x C-e); - 数据与输出:
setq变量赋值、insert文本插入、format格式化; - 抽象与组合:
defun定义函数、progn序列组合、let局部变量; - 交互与窗口:
read-from-minibuffer用户输入、switch-to-buffer-other-window多窗口、erase-buffer清空; - 数据结构:
quote字面量列表、car/cdr读取、push修改、mapcar批量映射; - 文本处理:
goto-char/search-forward/replace-match查找替换、while循环与nil退出条件; - 高级能力:
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!
相关推荐
learnxinyminutes-docs 实战:15 分钟上手 Emacs Lisp——从 sexp 求值到 buffer 文本处理
learnxinyminutes docs 实战:15 分钟上手 Emacs Lisp——从 sexp 求值到 buffer 文本处理 本篇基于 learnxi
文档教程基于 learnxinyminutes-docs 的 Emacs Lisp 十五分钟速成:从 S-表达式求值到缓冲区文本自动化
基于 learnxinyminutes docs 的 Emacs Lisp 十五分钟速成:从 S 表达式求值到缓冲区文本自动化 本篇以 learnxinymin
文档教程Emacs Lisp 15 分钟入门:基于 learnxinyminutes-docs 从符号表达式到可交互函数
Emacs Lisp 15 分钟入门:基于 learnxinyminutes docs 从符号表达式到可交互函数 本指南以仓库中的 elisp.md https
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考