Emacs Tips(持续更新)
Table of Contents
- 使用Emacs的一些tips
- 写 Elisp 时候遇到的一些问题
- Emacs Lisp 学习笔记
- Emacs Lisp 数据类型概览
- 运算 (Evaluation)
- 控制结构 (Control Structures)
- 变量 (Variables)
- 函数 (Functions)
- 什么是函数 (What is a Function)
- Lambda表达式 (Lambda Expressions)
- 函数名字 (Function Names)
- 定义函数 (Defining Functions)
- 调用函数 (Calling Functions)
- 匿名函数 (Anonymous Functions)
- 通用函数 (Generic Functions)
- 闭包 (Closures)
- Advising Functions
- 废弃函数 (Obsolete Functions)
- 内联函数 (Inline Functions)
- Declare Form
- 声明函数 (Declaring Functions)
- 函数安全 (Function Safety)
- 宏 (Macros)
- 个性化 (Customization)
- 加载 (Loading)
- 字节编码 (Byte Compilation)
- 调试 (Debugging)
- 读取和打印 (Read and Print)
- Minibuffers
- 入门Minibuffers (Intro to Minibuffers)
- 从Minibuffer中读取文本 (Text from MInibuffer)
- 从Minibuffer中读取对象 (Object from Minibuffer)
- Minibuffer历史 (Minibuffer History)
- 初始输入 (Initial Input)
- 补全 (Completion)
- Yes-or-No查询 (Yes-or-No Queries)
- 多重选择查询 (Multiple Queries)
- 读取密码 (Reading a Password)
- 在Minibuffer中使用的命令 (Minibuffer Commands)
- Minibuffer窗口 (Minibuffer Windows)
- Minibuffer内容 (Minibuffer Contents)
- 递归Minibuffer (Recursive Mini)
- Minibuffer杂项 (Minibuffer Misc)
- 命令循环 (Command Loop)
- 定义命令 (Defining Commands)
- 交互式调用 (Interactive Call)
- 区分交互式调用 (Distinguish Interactive)
- 命令循环的信息 (Command Loop Info)
- 命令后调整指针 (Adjusting Point)
- 输入事件 (Input Events)
- 读取输入 (Reading Input)
- 特殊事件 (Special Events)
- 等待结束时间或者输入 (Waiting)
- 中止 (Quitting)
- 前缀命令参数 (Prefix Command Arguments)
- 递归编辑 (Recursive Editing)
- 禁用命令 (Disabling Commands)
- 命令历史 (Command History)
- 键盘宏 (Keyboard Macros)
- 文本 (Text)
- 点附近的文本 (Near Point)
- 检视缓冲区的内容 (Buffer Contents)
- 比较文本 (Comparing Text)
- 插入文本 (Insertion)
- 插入文本的命令 (Commands for Insertion)
- 删除文本 (Deletion)
- 删除文本的命令 (User-Level Deletion)
- Kill Ring (The Kill Ring)
- 撤销 (Undo)
- 维护撤销列表 (Maintaining Undo)
- 填充 (Filling)
- 外边距 (Margins)
- 适应填充 (Adaptive Fill)
- 自动填充 (Auto Filling)
- 文本排序 (Sorting)
- 列 (Columns)
- 缩进 (Indentation)
- 大小写改变 (Case Changes)
- 文本属性 (Text Properties)
- 语法表 (Syntax Tables)
- 线程 (Threads)
- 进程 (Processes)
- 创建子进程 (Subprocess Creation)
- Shell参数 (Shell Arguments)
- 创建同步进程 (Synchronous Processes)
- 创建异步进程 (Asynchronous Processes)
- 删除进程 (Deleting Processes)
- 进程信息 (Process Information)
- 给进程发送输入 (Input to Processes)
- 给进程发送信号 (Signals to Processes)
- 接收进程的输出 (Output from Processes)
- 哨兵:检测进程状态的改变 (Sentinels)
- 在结束前询问 (Query Before Exit)
- 系统进程 (System Processes)
- 事务队列 (Transaction Queues)
- 网络链接 (Network)
- 网络服务器 (Network Servers)
- 数据报 (Datagrams)
- 底层网络访问 (Low-Level Network)
- 网络杂项 (Misc Network)
- 串口 (Serial Ports)
- 字节打包 (Byte Packing)
- 显示 (Display)
- 按键映射 (Keymaps)
- 按键序列 (Key Sequences)
- 按键映射的基础 (Keymap Basics)
- 按键映射的格式 (Format of Keymaps)
- 创建按键映射 (Creating Keymaps)
- 继承和按键映射 (Inheritance and Keymaps)
- 前缀键 (Prefix Keys)
- 激活的按键映射 (Active Keymaps)
- 查找激活的按键映射 (Searching Keymaps)
- 控制激活的按键映射 (Controlling Active Maps)
- 按键查找 (Key Lookup)
- 按键查找的函数 (Functions for Key Lookup)
- 改变按键绑定 (Changing Key Bindings)
- 重新映射命令 (Remapping Commands)
- 用于翻译事件序列的按键映射 (Translation Keymaps)
- 绑定按键的命令 (Key Binding Commands)
- 扫描按键映射 (Scanning Keymaps)
- 菜单按键映射 (Menu Keymaps)
- 模式 (Modes)
- Emacs 编码系统
- 文档 (Documentation)
- 打包 (Packaging)
- GNU Emacs Internals
- EIEIO
- Emacs 之深藏不漏
- 个人认为不错的的一些参考资源和博客
- 我与Emacs的一些事情
如果学一样东西不做知识管理的话,时间久了就会陷入Problem-Google-Solution-Forgotten 这样的循环中,记得读书时候有人说,看那个人做笔记那么
认真但还是考不好,真蠢.首先我个人不认为做笔记是一件蠢事,不过做笔记是需要技巧的,那个考不好的人可能是这里出了问题,或者只是他单纯的只是做笔
记而已.每当遇到问题去Google实际是一件很浪费时间的事情,为何不把内存里面的数据持久化下并且给它这个索引呢?记录的东西大部份都是很琐碎的,毕竟
是因为琐碎才要以这种形似记录下来.
P.S: 这文章做为我个人的知识管理方案之一会一直会更新.为什么不把每个话题分开写,那时因为记录的东西太琐碎了,不记录又不好,所以才写成一篇,不过分量大的话题会分开写,比如一些packages的用法.
使用Emacs的一些tips
如何快速了解Emacs内置库和内置功能的用法
很多人,包括我,都很好奇写Emacs Lisp的高人是如何知道那么多奇怪的功能,而且这些功能文档上又没提到.
现在有答案了,以下提示可以帮助你快速浏览这些奇怪的功能.
C-h p,根据分类浏览,缺点里面会混杂一些非内置的库并且貌似不全M-x find-library,根据名字搜索库,缺点没有一个系统的分类M-x apropos-library,根据名字搜索库,可以看到库里面全部重要的定义M-x find-function查看函数定义,C-h f或M-x describe-function查看函数描述以及用法M-x find-variable查看变量定义,C-h v或M-x describe-variable查看变量描述以及用法C-h b或M-x describe-bindings查看所有按键绑定信息,包含了全局按键绑定,以及使用该命令当前的缓冲区所启用的minor mode和major mode的按键绑定.可以配合
apropos-library查看mode定义来快速掌握mode的使用.C-h k则查询某个绑定按键所对应的信息,比如绑定的是哪个命令,定义在哪里等等.C-h a或M-x apropos-command输入想要搜索的命令名字,模糊搜索命令,比M-x自动提示的好M-x apropos-variable输入需要搜索的变量名字,模糊搜索变量
总之,
C-h是Emacs的help-command,可以通过它查看一些你不知道的Emacs的用法,是一个极其有用的内置文档,对于新手来说是一个不错的起点.比如C-h C-h就是一个非常有用的命令.还有
apropos库是一个辅助开发人员和用户的好东西,推荐使用.上面都是一些比较常用和有用的命令,可以了解一下.
P.S: 顺便提一下,代码也是很重要的文档.
如何学习库的API或者某一个API的用法呢
学习一个新的库/API也不是一件简单的事情,比如Emacs的文档上有很多东西都没给出,给出了但working examples太少,等等.对于这些问题我有一套学习策略.
阅读库/API的文本文档/维基
文档虽然不一定有想要的答案,但依然是一个不错的起点.但是使用文档也是有学问的(,我没多少就是了).
学习这个库/API之前了解所需要的概念和相关背景.
比如,
- 这个库/API是解决什么问题的?
- 它的使用流程是怎么样?(库的设计/架构)
- 需要注意版本问题吗?(版本是否导致跟其它库/API冲突)
- 它的changelog在哪?(一般文档里面有)
- 需要了解的术语有哪些?(一般可以找文档里面找到,遇到不懂的就弄明白)
这样自己大概就可以给库的APIs分个类,对库有个全貌的认识,并且能够保证自己不会对这个库/API产生什么误解,对以后使用同样类型的库也能快速上手.
- 了解文档.文档也是有分类的,每种文档的侧重点都不太一样.以Racket为例子,有
tutorial,guide和reference3类.tutorial就是给新手一个快速上手的例子,这种例子很丰富,但往往会忽略很多关于Racket东西.guide比tutorial更具备针对性,会利用例子对某一个点进行比较深入地说明,展示这个东西这么用,例子丰富.这类是给那种刷过一遍tutorial的新手使用的.reference比guide更加详细,大部份这类文档都仅仅给出一大段的说明,不会给出例子教你怎么使用,或者例子简陋难以掌握用法,这种文档是给熟悉了Racket一段时间的开发人员查阅用的.(而Emacs的内置文档就是这样.)
了解库的相关信息后,读懂文档应该就问题不大了.掌握了文档属性以后就可以知道该如何选择和使用文档了.
P.S: 身边有不少,包括我自己,这里怎么不对呢?跟文档说的不一样啊?这个库是不是有Bug啊?怎么这个库这么难用,就没有更好的解决方法吗?文档上面没有啊!.其实只要做足了功课,这些问题就很少听到了.不仅仅是读文档,读代码也是一样的.
阅读别人利用该库/API写的代码
有时候文档上面过程关于某个API的例子很简陋或者没有给出例子,而自己又实在头疼,那么只能到网上找例子,比如Github的repo,gist,Stack Exchange,别人的blog,等等.
阅读库/API的单元测试代码
如果找不到别人的代码可以去看一看这个库有没有单元测试的代码,如果有,那么它们就是你想要的API文档了.
阅读库/API的源代码
如果单元测试的代码也没有,那么可以自己去读想要了解的API的源代码,使用到它的地方就是你想要的例子.
请教别人
如果上面的方法全部失效,那去问别人吧.
快速做正则测试 (一次关于是否使用
\的思考,不仅仅限于Emacs Lisp)首先要清楚一点,
Emacs Lisp严格上来说没有正则表达式这个数据类型的, 它是直接用字符串来表示正则表达式.这么说可能不太好理解, 就拿
Python和JavaScript来说, 这两门语言都是专门为正则表达式定义了语法来和字符串进行区分的.Python用r"."来表示匹配换行符号以为的任意一个字符的正则表达式,JavaScript则使用/./来表示同样的正则表达式.但
Emacs Lisp它没有, 而是直接用"特殊"一点的字符串来表示:".".在表示文本的场合下它就是字符串, 在正则表达式场合下它就是正则表达式.
但是正则表达式是定义了一些有特殊含义的字符的, 比如句号(.), 它是用来表示换行符以外的任意一个字符的, 并非表示句号本身.
我们把表示字符本身叫做表达字符含义, 发挥正则定义叫做表达正则含义.
有一些特殊字符会同时存在两种含义, 这个时候就要求开发人员要对字符的含义进行选择.
具体做法就是使用
\字符放在需要进行选择的字符前面, 如果被标记的字符是有正则含义的特殊字符,那它就会反向表达为字符含义, 否则就直接表达为正则含义.
就比如
"\."就是让句号表达了它的字符含义, 用来匹配句号本身.实际上使用起来有两种情况, 在使用
Emacs的交互命令时, 比如M-x re-search-forward,假设想匹配当前缓冲区里的句号, 那么就得在
minibuffer里面输入\..但是如果你是在写代码的时候进行对句号本身的匹配, 那么就得这么写:
(re-search-forward "\\.")minibuffer里面的写法更符合人们心中正则的写法, 你可以理解为minibuffer里面的写法就类似于其它编程语言中正则对象的语法;而开发人员编码中的"正则"只是一个最终会转化成正则对象的字符串.
开发人员在编写这类字符串的时候需要知道这些字符串心算成正则对象, 方法很简单:
忽略掉所有转义字符(escaped characters)及相关处理, 移除掉所有其它字符的一个前置
\符号.所谓转义字符就是在字符串中有着由特殊含义的字符, 它们看起来像是一个字符串的字符, 比如
\n, 它表示的是换行符号.举些例子:
字符串 正则对象 备注 "abc\\.txt" abc\.txt 不存在 "\." 这样的转义字符, 所以把去掉 "." 前面的一个 "\" 符号 "abc\ndef" abc\ndef "\n" 是转义字符, 可以忽略 "abc\\ndef" abc\\ndef "\n" 是转义字符, "\\n" 则是对它进行禁止转义的操作, 可以忽略 "\\\\" \\\\ "\\" 是转义字符操作, 禁止 "\" 对后续的字符进行转义, 可以忽略
虽然说看上去对没有正则含义的字符使用
\标记好像什么问题, 但是万一恰好配出了一个转义字符就麻烦了, 比如对n使用就变成换行了;另外, 正则本身也是有定义一些有着特殊作用的字符(special characters),
比如
[...], 如果对它使用\则会禁用[...]在正则里面的作用, 换句话说就是\[...\]直接表示[和]符号本身,因此, 开发人员不能乱用
\对字符进行标记, 需要清楚自己在做什么.字符串 正则对象 备注 "\\[abc\\]" \[abc\] \[ 和 \] 在字符串未被定义, 所以在字符串中需要把 \[ 拆分为 \ 和 [ 两部分进行解析, 因此 \ 需要变成 "\\", \] 同理, 这样也满足了前面的心算方法.
此外,
Emacs Lisp的正则也像字符串那样定义了一些有着特殊含义的字符序列, 又被称为特殊构造(special constructs).比如
\(...\), 在正则表达式里面它是用来对匹配内容进行分组的, 要想发挥它们的正则作用, 字符串就得这么写:"\\(...\\)";再比如
\w是用来匹配单词组成(word-constituent)的字符, 需要这样"\\w"来发挥正则作用;这样得出来的字符串用在前面的心算方法上又可以得出特殊构造本身.
- 单词是由字母, 数字以及下划线组成, 这三种字符就叫做单词组成字符.
特殊构造和特殊字符也是一样分两种使用情况.
在
minibuffer进行输入的时候不需要多加一个\, 比如\w就直接输入\w;在编码的时候就得写成
"\\w".
Emacs提供了一些辅助编写正则表达式的工具, 本人还是极力推荐 不要 依赖这些工具.跟
Racket提供regexp-quote反输出用于匹配目标字符串的正则表达式一样,Emacs也提供一些辅助工具帮助你写"RegEx":对当前缓冲区即时比对正则表达式的
M-x re-builder(注意要写在它给你的双引号里面),以及简单强大的rx.早日熟悉
Emacs的正则表达式可以让你最大化享受到文本处理的便利.比如你想把缓冲区中
adj(M)的adj()去掉留下M,那么可以使用
replace-regexp命令:M-x replace-regexp RET adj(\(.*\)) RET \1,这是无法只用一次
replace-string命令来完成的.Linux上远程编辑和编辑需要sudo认证的文件
使用tramp库,它不仅可以远程编辑文件,也可以sudo修改本地文件
编辑远程文件
C-x C-f输入/ssh:user@host#port:/path/to/file, 就是说通过ssh以user身份编辑host:port上的/path/to/file文件.sudo修改文件
C-x C-f输入/sudo::/path/to/file, 这样打开/path/to/file的时候就会提示输入密码了
Emacs Lisp的交互式编程和很多直译型语言一样,
Emacs Lisp也支持REPL,不过这个功能藏得挺深得,M-x ielm.ielm全称Inferior Emacs Lisp Mode.Inferior Modes是个好东西.如何了解在使用
Emacs的过程中发生的事情有时候想知道自己操作了什么,使用按键输入对应什么命令,使用命令时发生了什么事情.
这种时候就会想起日志,在网上搜索后发现有三种打印日志:
使用 Emacs 自带的
command-history变量,或者 (command-history),但是这种方式有很多信息没有记录下来,具体可以看
(info "(elisp)Command Loop");使用第三方库 command-log-mode,这种能记录大部分命令,比第一种方案好不少,支持日志序列化,
不过还是有些输入是没法记录下来的,比如
Calc里的输入是没有被记录下来的;使用第三方库 interaction-log,相比
command-log-mode支持实时日志打印,并且能够记录下
command-log-mode的一些盲点,比如上面提到在Calc的输入,还会记录下一些文件按加载信息,内容十分详细.我个人是用的第三种方案.
把提示输入yes或no简化成y或n
(fset 'yes-or-no-p 'y-or-n-p)
C-x C-e默认eval的输出太丑,怎么美化(fset 'eval-last-sexp 'pp-eval-last-sexp)
Linux上无法在使用Fcitx输入中文?这是
Emacs的一个古老的 bug, 解决方法就是修改LC_CTYPE这个变量为zh_CN.UTF-8再运行Emacs,写脚本的时候需要这么写:
env LC_CTYPE=zh_CN.UTF-8 emacs如果只是在命令行里面运行, 可以偷懒点写成:
LC_CTYPE=zh_CN.UTF-8 emacs没有特殊情况就尽量用第一种形式.
写 Elisp 时候遇到的一些问题
如何解决写Elisp时候遇到的一些需要密码认证的命令行操作
以在个人版的Debian上面安装nodejs为例子.
先了解一下sudo,sudo的作用就是以另外一个用户身份执行命令,默认身份是superuser(这里是root),执行时候需要提供这个用户的密码. sudo有一个sudoers policy缓存凭证15分钟,除非重写了凭证,否则在这15分钟内以这个用户身份执行命令是不需要再次输入任何密码的.
利用tramp库,设定默认目录为"/sudo::"
(let ((default-directory "/sudo::")) (shell-command "apt-get install nodejs"))
缺点就是认证后不会生成凭证
对命令进行修改
(shell-command (string-join (list "echo" (shell-quote-argument (read-passwd "Password: ")) "|" "sudo" "-S" "apt-get" "install" "nodejs") " "))
缺点就是比较麻烦,也没凭证管理,优点就是你可以自己实现凭证管理(怎么安全管理是一个问题).
使用
eshell-command(eshell-command "sudo apt-get install nodejs")优点是简单,但还是没有凭证管理.
如何让调试器可以调试user-error?
写于 2018/10/21
Drew已经在这里面进行回答了,文档上只是做了暗示,之所以调试器不能调试 user-error,那么是因为 debug-ignored-errors 这个变量有 user-error 这个变量,
debug-ignored-errors 是告诉 Emacs Debugger 忽略哪些错误,所以只需要把 debug-ignored-errors 里面的 user-error 条目清空掉就可以了.也就是说 user-error 实际上还是可以唤醒 debugger.
(setq debug-ignored-errors (remove-if (lambda (item) (eq item 'user-error)) debug-ignored-errors))
Autoload函数引用未被require的变量,修改该变量后无法读取变量?
写于 2018/10/21
注意: 该问题虽然已经解决了,但是途中遇到一个奇怪现象我没有办法解释,以后还是会更新.
解决问题时候的 org-mode 版本为 org-plus-contrib-20181015.
实际情况就是: 写了一个函数 publish-all-posts 需要使用 org-publish 作为 subroutine, 主要是利用 Emacs Lisp 的动态作用域名来临时绑定全局变量,特别是 org-publish-project-alist 并且调用 org-publish.
目的是为了不污染全局变量和环境,然而有一个问题, org-publish 是 Autoload 函数,可是我并没有 require 它引用的变量 org-publish-project-alist,就在我用 let 进行绑定的时候发生了一个奇怪现象.
第一次执行函数 publish-all-posts 的时候报错了: "Unknown component static in project DarkSalt",引发错误的函数是 org-publish-expand-projects, 这还是可以理解,因为 org-publish-project-alist 并没有进行全局绑定默认是 nil 所以引发异常.
我不能理解的地方就在于接下来函数 publish-all-posts 的调用居然正常,没有发生报错.我读了一下 org-publish, org-publish-projects 和 org-publish-expand-projects 这3个函数的源代码并没发现在哪里给 org-publish-project-alist 进行赋值.
不过我还是带着疑惑把报错解决了, require ox-publish (也就是 org-publish-project-alist 的定义文件) 就可以解决问题,不过还是不明白这个奇怪现象的原因,有可能是我没有读透代码,所以这个问题不能算是完全解决.
(defun publish-all-posts (project &optional force async) "Now the project of blog is isolated from `org-publish-project-alist'. That is, when calling `org-publish-project' or `org-publish' would not see any project of blog, vice versa." (interactive (list (assoc (completing-read "Publish project: " blog-alist nil t) blog-alist) current-prefix-arg)) (create-project-directory-if-necessary) (write-posts-to-tag-inc) (rewrite-theindex-inc) (let ((org-publish-project-alist blog-alist) (org-html-home/up-format (ht-get home/up-formats 'blog)) (org-html-head (ht-get html-heads 'blog)) (org-html-preamble nil) (org-html-doctype "html5") (org-html-link-home "/") (org-html-link-up "/") (org-export-with-toc nil) (org-export-with-author t) (org-export-with-email nil) (org-export-with-creator nil) (org-export-with-date nil) (org-export-with-section-numbers nil)) (org-publish project)) (rename-theindex-to-index))
如何请求接口(JSON)
写于 2019/2/26
Emacs Lisp 没有 Python 那么直接的网络请求库,不过我们可以自己手动封装一下(这里只是提供一下思路)
(require 'json) (require 'url) (defstruct response headers body) (defun url-open (url) "Return the response by requesting the url." (with-temp-buffer (insert-buffer (url-retrieve-synchronously url)) ;; the message containing the headers and body (set-buffer-multibyte t) (decode-coding-region (point-min) (point-max) 'utf-8) ;; needed if text contains non-ascii character (goto-char (point-min)) (re-search-forward "^$" nil 'move) (make-response :headers (buffer-substring-no-properties (point-min) (point)) :body (buffer-substring-no-properties (point) (point-max))))) (defun response-to-json (response) (json-read-from-string (response-body response))) ;; example (response-to-json (url-open "https://api.jikan.moe/v3/anime/1/characters_staff"))
如果读取JSON文件
写于 2020/10/26
Emacs Lisp 读取 JSON 文件的方式比很多语言自带的灵活得多.
假设现有个JSON文件,
{
"name": "xxxxx",
"job": "xxxxxx",
"projects": [
{
"name": "xxxxx",
"date": "xxxxx",
"about": "xxxxxxxxxx"
},
{
"name": "xxxxx",
"date": "xxxxx",
"about": "xxxxxxxxxx"
},
{
"name": "xxxxx",
"date": "xxxxx",
"about": "xxxxxxxxxx"
}
]
}
现在需要读取它的 "projects" 值并且重新封装成一个 JSON,在 Emacs Lisp 里面可以这么做,
(require 'json) (let* ((json-object-type 'alist) ;; 指定 JSON 的对象解析为 alist 类型 (json-array-type 'list) ;; 指定 JSON 的数组解析为 list 类型 (json-key-type 'keyword) ;; 指定 JSON 的键解析为 keyword 类型 (json (json-read-file "~/test.json"))) (json-encode (alist-get :projects json)))
如果你不喜欢把 JSON 的对象解析成 alist, 可以把 json-object-type 设置为 hash-table, plist 这样的值,
自由设置解析的类型正是 Emacs Lisp 的 json.el 的灵活之处.
Emacs Lisp 学习笔记
这段话写于 2018/9/30
最后更新于 2020/10/31最后更新于 2021/12/18
我会对这段内容进行改版,理由如下:
- 距离刚开始写下这段内容已经过去快 3 年了,期间断断续续使用
Emacs完成工作,查阅文档的能力已经挺熟练了,基本上每次看文档都不需要翻阅这里的内容查阅这里的内容只有两种情况: 某个
API的文档描述没有提供例子以致难以理解其用法,以及某些概念不好理解.这也是我最初写这段内容就是为了通过补充理解和例子来解决这两个问题,但写着写着,不知不觉就想把文档翻译一边了.
由于太罗嗦了,在我遇到问题时不能马上解决问题.每当这时候我都会想为啥当初要翻译文档,我要的是错题集,又不百科全书.
不过即便如此,这些内容里面还是有不少值得保留的东西.
接下来我先要做的是对这段内容进行瘦身,然后针对于一些对于大部分人都不太好理解的概念,把我的理解整理下来,补充原本该有的例子.
我是以终身使用 Emacs 的目的去学 Emacs Lisp 的,而提用 Emacs Lisp 的使用水平的最好方法就是掌握 reference 文档的使用.
加上网上对于 Emacs 的相关内容整体看下来相对较少,有些问题还搜索不到解决办法,这有两种可能,其他人没有遇到,问题无解,或者问题的描述不对,
这里的所有可能都只能靠自己去读 reference 找解决办法.
Emacs Lisp 的 reference 有一个特点,很多 APIs 的说明文档都缺乏详细的使用例子,
如果掌握了相关概念的话这都不是问题,可碰巧就是这方面做得不好,很多概念说明都不够直观,
明明只要画个简单的图就可以解决的问题,它就是没有这么做,不过这也只限于 Emacs 相关的 APIs 了.
Emacs Lisp 和其它语言一样有着这操作进程,线程的等等能力,也就是说,开发人员要像熟悉使用这部分的接口,就必须去了解这类的"通用概念",
这对于开发人员来是说百利而无一害的,遇到这类概念又不懂的话把就老实的补上吧.
我写这部分的笔记有两个目的: 一是为了直观理解 Emacs 那些不太容易理解的概念;二是为了针对通用概念进行补盲.
因此这部分笔记的信息量会比较大(我会在保证内容直观的前提下尽量精简),更新期也会很长,计划一个星期更新一个概念的笔记.
最后重伸一遍,这并不是 APIs 的笔记(我没有抄一遍 reference 的理由,顶多补充一些使用例子),而是一些概念的笔记.
当然不同版本的 Emacs 所关联的概念也会不一样,这些我会尽可能进行说明.
Emacs Lisp 数据类型概览
Emacs Lisp 的数据类型分两大类: 语言内置的数据类型 以及 Emacs 编辑器内置的数据类型.
所谓的语言内置的数据类型就是像整数,浮点数,字符串等等这些基本是编程语言都会有的类型;
像线程,进程这种类型是为了操作特定平台功能的数据类型而不属于语言内置的数据类型, Emacs 编辑器内置的数据类型就是相当于这个,
可以理解为这是专门用于配置 Emacs 的数据类型.在文档里面,这两类型分别叫做 programming types 和 editing types.
Emacs Lisp 提供 type-of 来获取对象的类型.
每一种数据类型都有它们自己唯一 文本 输出格式,叫做打印表示(textual/printed representation),可以 prin1 函数来打印得到.
比如,
(prin1 (lambda (v) (+ v 1))) ;; (closure (t) (v) (+ v 1)) (prin1 (make-hash-table :size 30)) ;; #s(hash-table size 30 test eql rehash-size 1.5 rehash-threshold 0.8125 data ())
要向 Emacs Lisp 输入一个对象必须要符合某种的 文本 输入格式要求,叫做读取语法(read syntax),用 read 函数来读取.
有大部分对象的打印表示可以作为读取语法,看上面的例子你会发现 #s 开头的打印表示,这就是特殊对象的读取语法 M-: (info "(elisp)Special Read Syntax").
(apply '(closure (t) (v) (+ v 1)) '(1)) ;; 2 (hash-table-size #s(hash-table size 30 test eql rehash-size 1.5 rehash-threshold 0.8125 data ())) ;; 30
还有的对象是没有读取语法的,它们的打印表示都是 #< xxxxxx > 这种格式,这种对象要通过构造器(construct)定义.
此外,一个对象可能会有多种读取语法.
在别的语言里面一个表达式得出就是一段文本,但在 Lisp 里面一个表达式首先是一个 Lisp 对象,其次是对象的读取语法,
在 Lisp 里面表达式也会被成为 form 或者 S-expression(sexp),这个名词的关系要时刻记载心里,因为文档里面经常混用这几个词.
在交互运行一个表达式时,直译器 (Lisp interpreter) 会读取它的文本表示来生成一个 Lisp 对象,然后才运算对象,
读取和运算是两个不同的概念.
这个章节的剩下部分主要介绍一下在别的语言里不太常见的语言向数据类型,也就是 programming types,
editing types 会另行在别的章节介绍.
Records
Record 允许用户定义 Emacs Lisp 没有的数据类型,底层实际是用 cl-defstruct 和 defclass 定义的实例作为表示.
从内部实现来讲,一个 record 对象更像是一个向量,可以是用 aref 来访问它的槽位(slots),以及能够使用 copy-sequence 进行复制.
在当前的实现中, record 对象最多自由 4096 个槽位,而向量(vector)可以有更多, records 和数组(arrays)一样是用 0 做为第一个索引的 (zero-origin indexing).
record 的第一个槽位是用来存放类型的,叫做类型槽(type slot),不是叫做 slot one (索引为 1 的槽位才是),类型槽位的值可以通过 (type-of RECORD) 这种方式获取.
而类型槽存放的值有两种,类型名字的 symbol 以及类型描述符(type descriptor),类型描述符也是一个 record 对象,特别之处在于这个 record 对象的 slot one 存放的类型名字的 symbol,这个值同样也是可以通过 type-of 获取的.
(setq si (make-record 'salt 5 'x)) (type-of si) ; => 'salt (setq sii (make-record si 5 'z)) (setf (aref si 1) 'saltII) (type-of sii) ; => 'saltII
record 的打印是一个以 #s 开头的 list: #s(elm1 elm2 ... elmn). record 对象被认为是一个常
recordp 判断对象是否 record 对象; record 和 make-record 创建并且返回 record 对象.
老版本的代码都是用 cl-defstruct 而不是用 record,如果这些代码在新版本的 Emacs 上使用可能会引发一些问题.
Emacs 会在检测到老版本的 cl-defstruct 的调用时候启用一个模式,这个模式下 type-of 会像是处理 record 对象那样处理一个老版本的 struct 对象.
(cl-old-struct-compat-mode t) 就可以启用这个模式.
Finalizer Type
make-finalizer 接收一个函数作为参数,并且返回一个对象,在 Emacs 进行垃圾回收后,如果这个对象被 Emacs Lisp 认为不可到达(unreachable),
那么这个对象关联的函数就会执行,这种对象就叫做清理器对象(finalizer object),它关联的函数一般用来执行相关清理工作.
如果一个清理器对象只被别的清理器对象引用,这种情况=Emacs= 不 认为它可到达(reachable).
(setq obj (make-finalizer #'(lambda () (message "Do something")))) (setq obj 1) (garbage-collect) ;; 手动执行垃圾回收
运算 (Evaluation)
Lisp 解释器(Lisp interpreter),或者说求值程序(evaluator)是 Emacs 的一部分,负责计算表达式的值.
当调用一个 Lisp 函数时,求值程序会通过计算函数里表达式的值作为函数的值,因此运算 Lisp 程序就意味着运行 Lisp 解释器.
任何对象都可以被运算,实际常用的有 number, symbol list 和 string.
读取(reading)一个 form 然后运算这个 form,读取和运算是两个独立的活动.读取不会运算任何东西,只是把 Lisp 对象的打印表示(printed representation)转成对象本身.
运算则是一个递归过程,运算一个 form 通常都是每次只是运算 form 的一部分,比如 (car (cdr x)):
Emacs 首先检查第一个元素 car 是函数,宏还是 special form, 是函数,那么计算 (cdr x) 的值,在计算 (cdr x) 的时候同样先检查 cdr 是什么,是函数,那么先计算 x 的值,然后执行函数 cdr 得出结果,
这里我们给它个名字 res1;最后就运算 (car res1),按照前面的步骤得出最终结果,注意, list form 的第一个元素(这里是 car 和 cdr)是没有会被运算的.
运算发生在一个叫做环境(environment)的上下文中,由所有 Lisp 变量当前的值和绑定(bindings)组成.当 form 引用了一个变量,变量就会运算得到由当前环境为该变量提供的值,除非环境没有这个变量的绑定.
当然运算一个绑定变量的 form 可能会临时改变环境的.运算 form 的过程中可能会导致一些持久性的改变,这些改变叫做副作用(side effects),比如 (setq foo 1) 就是改变的内存上的地址.
Forms
Form 分类型,不同种类的 form 的运算方式不一样,分三种: list, symbol 和其它类型.
- 自运算form (Self-Evaluating Forms)
自运算
form就是list和symbol之外的form,它们的运算结果是它们自己,比如number,string和vector对象运算结果就是它们自己.这种
form可以直接写下来,这是很常见的事情,但是对于那些没有读取语法(read syntax)的类型就不怎么常见了,不过还是可以做得到的,比如,;; Build an expression containing a buffer object. (setq print-exp (list 'print (current-buffer))) ;; ⇒ (print #<buffer eval.texi>) ;; Evaluate it. (eval print-exp) ;; ⊣ #<buffer eval.texi> ;; ⇒ #<buffer eval.texi>
symbol中nil,t以及:开头的symbol都是比较特殊的,它们被Emacs Lisp看做常量不可改变,都属于自运算form. - List Forms分类 (Classifying Lists)
一个非空列表有可能是一个函数调用,或者是一个
special form,又或者是一个宏调用,Emacs是根据列表的第一个元素来判断的,其它元素则是组成参数.函数,宏以及
special form三者的运算方式是不一样的. - 函数的间接调用 (Function Indirection)
如果
list的第一个元素是symbol,运算的第一步会检查这个symbol的function cell并使用里面的内容,如果里面的内容是另外一个symbol,那么重复这个过程,直到获取到一个non-symbol.这个过程叫做
symbol函数的间接调用(symbol function indirection),这个过程有可能会是一个无限循环,一个symbol的function cell最终引用到它自己身上就会发生这种情况.正确的情况下,这个
non-symbol应该是函数,lambda表达式, 字节码函数(byte-code function), 原函数(primitive function),宏(macro),special form和autoload对象的其中一个.不正确的情况
Emacs会引发一个invalid-function错误.fset和symbol-function能够分别设置和获取symbol的function cell.比如这个文档的例子,
;; Build this function cell linkage: ;; ------------- ----- ------- ------- ;; | #<subr car> | <-- | car | <-- | first | <-- | erste | ;; ------------- ----- ------- ------- (symbol-function 'car) ;; ⇒ #<subr car> (fset 'first 'car) ;; ⇒ car (fset 'erste 'first) ;; ⇒ first (erste '(1 2 3)) ; Call the function referenced by ‘erste’. ;; ⇒ 1
如果第一个元素是
lambda表达式,那么就不会发生函数间接调用了.indirect-function能够获取symbol的真正含意,比如上面例子的后续,(indirect-function 'erste) ; ⇒ #<subr car>
- 函数Form (Function Form)
如果
list的第一个函数是function,byte-code function或者primitive function,那么这个list就是一个函数调用(function call).function call. 运算的第一步是计算出除了第一个元素之外所有元素的值,也就是参数的值,每个参数都有一个值.下一步就是根据这些值来调用函数:(apply 'FIRST-ELEMENT '(REST-ELEMENT-VALUES ...)).函数是用
Lisp写的,那么参数会被绑定到函数的参数变量上,然后按顺序运算函数体里面的form,最后一个form的值就是函数调用的结果. - 宏Forms (Macro Forms)
如果
list的第一个元素是macro对象,那么这个list就是一个宏调用 (macro call).宏调用的情况下,list剩余的元素是不会被马上运算的.这些元素会做为宏的参数,计算的时机由宏决定.宏的定义实际是计算出一个替代
form(replacement form),这叫做宏的展开式 (expansion of the macro): 一个新的form用来替代原来的form.调用宏叫做展开宏调用,或者叫展开宏.宏的展开式可能是以下其中一种
form: 一个自运算常量,一个symbol或者是一个list.如果展开式本身也是一个宏调用,展开处理会一直重复直到不能计算出展开式为止.一个宏调用是以运算完展开式为结束,然而,并非一得到宏的展开式就马上进行运算,因为其它
Lisp的程序也会展开宏调用,并且它们"可能"会运算展开式.通常,宏的参数不会在计算宏的展开式中进行计算,而是应该做为宏的展开式的一部分,在展开式被运算的时候一起被运算.
(defmacro mcadr (x) (list 'car (list 'cdr x))) ;; or using backquote (defmacro mcadr (x) `(car (cdr ,x))) (mcadr (cons 0 '(1 2 3))) ;; 展开式是 (car (cdr (cons 0 '(1 2 3))))
注意上面上面的展示式有
(cons 0 '(1 2 3)),与宏调用发生时候传入的参数是一样的,并不是被运算后的'(0 1 2 3). - Special Forms (Special Forms)
Special Form是原函数的一种,它们的参数是不会被全部运算的.大部份special forms是用来定义控制结构或者执行变量绑定,这些都是函数不能做的.每个
special form都有自己的运算规则,可以通过special-form-p来判断对象是不是special form, 比如(special-form-p 'and). - 自动加载 (Autoloading)
自动加载特性允许调用一个定义未被加载进
Emacs的函数或者宏,自动加载对象指定了包含了定义的文件,它做为一个函数定义对应的symbol出现,调用这个symbol会自动加载指定的文件,然后调用真正的函数定义.
引用 (Quoting)
special form quote 直接返回它未被经过运算的参数,这是 Lisp 给非自运算对象提供的一种免受运算的方法.
一般用于 symbols 和 lists 身上,对于 number, string, vector 这种类型的对象就没必要使用了.
因为 quote 很常用,所以 Lisp 为它提供了一种方便的读取语法(read syntax): ' 撇号后面跟着一个 Lisp 对象,
Lisp 会把这种形式展开成一个第一个元素为 quote 的 list,该 list 的第二个元素就是后面跟着的 Lisp 对象.
所以 'x 就是 (quote x) 的缩写.
其它类似的 quoting constructs 还有让 lambda 表达式被编译的 special form function,以及能够引用 list 任何一部分并且能够计算以及替换其他部分的 backquote (`).
反向引用 (Backquote)
反向引用允许你引用一个 list,但能够选择性地运算该 list 的元素.在最简单的情况下它的作用等同于 quote.
'(a (+ 1 2)) `(a (+ 1 2))
反向引用支持一些特别标记符号来告诉 Lisp 运算器如何处理标记符号后面的参数.有两种标记符号: , 和 ,@.
用上面的例子来说明它们的用法.
`(a ,(+ 1 2)) ; => '(a 3)
(+ 1 2) 被进行了运算, , 的作用就是告诉 Lisp 运算器跟在它后面的对象不是一个常量,这样 Lisp 计算器就会运算该对象.
`(a ,@'(1 2)) ; => '(a 1 2) `(a ,@(+ 1 2)) ; => '(a . 3)
这里有两个例子,第一个, '(1 2) 被去掉括号并且成为 a 的同级元素.
第二个可以看出 ,@ 后面的对象也是会被运算的,并非单纯的去掉括号,并且和 `(a ,(+ 1 2)) 的结果不一样,
这两个例子可以用 cons 进行改写,
(cons 'a '(1 2)) ; => '(a 1 2) (cons 'a (+ 1 2)) ; => '(a . 3)
运算 (Eval)
大部份情况下, form 会在 Lisp 程序运行中被使用的时候自动被 Lisp 运算器运算,在一些场景下可能需要写一些在运行时候运算的代码,
在运行的时候(比如读取文本时获得 form ,根据情况生成 form,从属性列表获取 form 等)从某处获得了一个 form,需要对它进行运算,在这些场景下使用 eval 进行运算.
通常 eval 并不是必须的,如果 form 是一个 symbol,那么 symbol-value 就更合适;除非 form 是一个 function call, 否则 funcall 或者 apply 更合适.
eval 的用法是 (eval FORM &optional LEXICAL), FORM 就是要在当前环境下被运算的 form, LEXICAL 指定作用域规则,默认是 nil,表示动态作用域, t 就是表示词法作用域.
LEXICAL 也可以是一个 lexical environment,调试器就用了这个来调试,比如
(eval '(+ a 1) '((a . 1))) ; => 2
eval 是一个函数, FORM 在 eval 调用前作为准备而运算一次,然后 eval 本身的调用运算一次,所以一个 eval 调用会运算 2 次.
比如上面的例子, '(+ a 1) 要先运算一次得到自己,然后 eval 的调用本身再运算一次. Emacs Lisp 有最大调用数,由 max-lisp-eval-depth 决定.
还有 eval-region 和 eval-buffer 这两个从流中读取 form 进行运算,还能够自定义读取用的函数,具体就不介绍了.
上面说到 max-lisp-eval-depth 会限制函数的最大调用层数,默认是 800, 如果超过了这个限制,那么就会引发错误,错误信息为 Lisp nesting exceeds max-lisp-eval-depth.
实际上,局部绑定(比如let)以及 unwind-protect 的嵌套层数也有限制,由 max-specpdl-size 限制,默认是1500,超过这个限制会引发错误,
错误信息为 Variable binding depth exceeds max-specpdl-size.
变量 values 是一个列表,记录了最近被读取,被运算,被 Emacs 命令打印到缓冲区(不包括 *ielm* 中运算和使用 C-j, C-x C-e 以及 lisp-interaction-mode 中类似命令的结果)上的表达式返回的结果.
最近的运算结果排列表第一位.
延迟运算 (Deferred Eval)
Emacs Lisp 有一个 thunk 库专门用来处理延迟运算.
thunk-delay 宏接收多个 forms 返回一个 thunk,一个 thunk 是一个继承了 thunk-delay 调用时候的环境的闭包,这个宏需要启用 lexical-binding.
thunk-force 强制 thunk 执行运算并且返回 thunk 里面最后一个 form 的结果, thunk 会记住自己有没有被强制执行过,如果有被强制执行过,
如果以后再次调用 thunk-force 会在没运算的情况下直接返回上一次的运算结果.
thunk-let 宏是 let 的惰性版本,每个绑定都是 (SYMBOL VALUE-FORM) 这种形式,只有 SYMBOL 第一次被使用的时候 VALUE-FORM 才会被运算,同样需要启用 lexical-binding.
thunk-let* 是 let* 的惰性版本,具体就不说了,和 thunk-let 差不多.
thunk-let 和 thunk-let* 会隐式使用 thunks: 它们的展开式会创建助手 symbols 并且绑定把这些 symbols 绑定到由 value-forms 转化成的 thunks 上,
先把这些 thunks 叫做 thunk-of-value-forms,单个 thunk 叫做 thunk-of-value-form,
所有出现在 body forms 的变量的引用(reference)都会在之后被 (thunk-force thunk-of-form) 这种形式的表达式替换.
控制结构 (Control Structures)
按序计算 (Sequencing)
按照顺序计算,基本所有 Lisp 方言差不多,和 Racket 对比的话,
progn 相当于 Racket 的 begin, prog1 相当于 begin0, prog2 是 prog1 的变种.
三者都是按照顺序计算表达式,差别在于返回值不一样, progn 返回最后一个表达式的值, prog1 返回第一个表达式的值, prog2 返回第二个表达式的值.
模式匹配条件 (Pattern-Matching Conditional)
Emacs Lisp 也像其它的 Lisp 方言那样具有模式匹配 form: pcase 宏,是 cond 以及 cl-case 的混合体.
pcase 克服了 cond 以及 cl-case 的限制并引入了模式匹配编程风格(pattern matching programming style).
所克服的限制有:
cond的主要限制在于cond的条件表达式里用let绑定的变量不能在从句体里面使用;另外一个就是当一系列的条件预测都是等性测试(equality tests),那么就会有一大堆重复代码,
(因为
cond的用法需要每一个分支都要写全,这个问题被cl-case的first-arg focus风格解决了.)cl-case宏运算第一个元素EXPR,并根据运算结果和特定的值集合KEYLIST做等性测试来决定是否运算BODY,它的用法
(cl-case EXPR (KEYLIST BODY...)...),它的限制有两个: 使用eql做等性测试以及需要提前知道值集合KEYLIST.因为这样, (
eql的原因)cl-case不适合判断字符和混合型数据结构.
因此, pcase 借用 cl-case 的 first-arg focus 的做法以及 cond 的从句处理流(clause-processing flow),
用模式匹配的变种等性测试来替换判断条件,并且添加了一些功能,这样才能简单明了地表达从句,并且能够在条件语句以及从句之间共享 let 绑定.
对于序列,还可以用 seq-let 进行解构.
生成器 (Generators)
迭代器的就是一个产生潜能无限(potentially-infinite)的数值流的函数,每次产生一个值然后挂起自己,等待调用者(caller)请求下一个值.
如果你接触过其它编程语言的迭代器,比如 Python, JavaScript, Racket 等等,那么 Emacs Lisp 的迭代器对于你而言会很熟悉.
要在 Emacs Lisp 使用迭代器就需要使用 generator 库并且开启 lexical-binding.
拿 Python 的迭代器来做类比,说真的我很惊讶它们的迭代器是如此相似,
#!/usr/bin/env python3 def gen(x): while x > 0: print("%s was passed\n" % (yield x)) x = x - 1 return -1 # 引发 StopIteration 异常时候的返回值 def client(n): g1 = gen(n) g1.send(None) # 等于 next(g1) while 1: try: res = g1.send(100) print("The return value %s from generator" % res) except StopIteration as e: return e.value client(5) g1 = gen(5) # g1.send(None) g1.close() for i in gen(5): print("value is %s" i) def subgen_wrapper(n): res = yield from gen(n) print("result is %s" % res) for i in subgen_wrapper(5): print(i)
(require 'generator) (setq lexical-binding t) (iter-defun gen (x) (while (> x 0) (message (format "return yes %s\n" (iter-yield x))) (setq x (1- x))) -1) (defun client (n) (let ((g1 (gen n))) (condition-case e (while t (message (format "the return value %s from generator\n" (iter-next g1 100)))) (iter-end-of-sequence (print (cdr e)))))) (client 5) (setq g1 (gen 5)) (iter-close g1) (setq res (iter-do (i (gen 5)) (message (format "value is %s\n" i)))) ;; res 为 -1 (iter-defun subgen-wrapper (n) (message (format "result is %s" (iter-yield-from (gen n))))) (setq res (iter-do (i (subgen-wrapper 5)) (message (format "value is %s\n" i))))
最后要注意 iter-yield, iter-yield-from 只能出现在 iter-defun 之中, unwind-protect 之外.
除了上面例子中的 forms,还有一个 iter-lambda 是 iter-defun 的匿名版.
非本地退出 (Nonlocal Exits)
写于 2018/9/4
一个 nonlocal exit 是一个把当前程序的点的控制(control)到另外一个点(remote point)的转移过程(transfer).
在 Emacs Lisp 中, Nonlocal exits 可以以一个错误结果(a result of errors)的形式出现,也可以通过显式控制(explicit control)的方式使用它们.
(这里的错误和异常是同样一个意思,虽然英文中的词是不一样,但的确指同一个东西).
下面我会用别的语言特性来做类比,主要是 C 语言和 Python
显式控制(Catch and Throw)
实现手段是利用
catch和throw两个 special forms.如何理解它们?如果对支持goto功能的语言有了解,那么这就很好理解了.用
C语言作为例子,catch就相当于设置跳转点的label语句,而throw相当于执行跳转的goto语句,而跳转的目的地就是catch设置点.最后,它们的
while循环都不会被执行,并且都返回0.#include <stdio.h> int main() { goto back; while(1){ //do something printf("%d", 1); } back: printf("You are going to exit now"); return 0; }
(defun catch-throw-example () (catch 'back (progn (print "You are going to exit now") (throw 'back 0)) (while t (print 1))))
在
Emacs Lisp中是没有return表达式的,函数的返回值只有函数最后一句执行的表达式的值,如何让函数在执行到一半的时候返回?现在可以通过catch和throw来实现,(throw tag value)相当于c语言的return value;.关于
catch和throw更多的示例可以在M-: (info "(elisp)Examples of Catch")找到,这里就不写了.利用错误/异常(Errors)
这个就是编程语言的异常处理机制.
这里用
Emacs Lisp和Python的异常处理机制对比一下,除了语法不一样以外真是十分一致.下面两个例子的变量的名字已经保持一样了.其中, 下面的
error不是平时的(error string &rest args),这里代表所有类型的错误的"祖先"/"root",所有类型的错误直接或间接派生自它.它与
Python的Exception一样可以用于捕捉使用错误/异常(事实上,Python的Exception有3个系统级别的异常捕捉不了).(defun err-handle-example () (condition-case err (+ 1 a) (error (message "Error occurs") err)))
def err_handle_example(): try: return 1 + a except Exception as err: print("Error occurs") return err
Emacs Lisp有3个引发异常的 special forms 和支持自定义异常.如何引发一个异常
M-: (info "(elisp) Signaling Errors")关于定义新异常和标准的异常
M-: (info "(elisp) Error Symbols")清理(Cleanups)
通过利用
Emacs Lisp的unwind-protect来确保在结束前执行动作,不论结束之前发生了什么,哪怕是发生报错.如果问这个跟
Python里面的哪样东西最像,那必然是异常捕捉的finally从语,都是不管发生前面什么事情,都会在结束前执行.这里只是为了作例子, Python 实际处理文本写入最好用 with 上下文管理器(其实一开始我就想用 with 做类比,不过对比发现 finally 更合适). 当然如果 f.open() 打开失败还是不会执行以后的语句.权限不足,文件所在的目录不存在,就会发生报错的情况. 使用 Emacs Lisp 的 find-file-noselect 是不会发生这种事情,现在假定 Python 不会发生这些情况.
两个程序都是打开一个文本名叫"text.txt"并且插入"Insert content"内容,最后关闭文本.
其中,
(kill-buffer buffer)跟f.close()一样都是关闭文本,前者是Emacs Lisp的unwindform, 后者是Python的finally从句.而
(insert "Insert content")和f.write("Insert content")都是处于异常捕捉的保护区域里面,这样两者的关系就很明了了.(let ((buffer (find-file-noselect "text.txt"))) (unwind-protect (with-current-buffer buffer (insert "Insert content")) (kill-buffer buffer)))
f = open('text.txt', 'w') try: f.write("Insert content") except Exception: pass finally: f.close()
变量 (Variables)
事实上变量并不简单,变量绑定这个概念决定了你是否够理解代码的上下文.
网络上好多前端的面试问题里面都有这类问题: 讲一下你对作用域链/闭包的理解,好多人都没有回到到点上,这是因为他们都不知道问题的本质.
这问题其实就是在问: 讲一下你对变量作用域的理解.
每一个变量绑定都有一个特定的作用域(scope)和绑定持续时间(extent),也就是在程序的哪个 范围内 能够访问到变量以及绑定在什么 时候 才失效.
假如下面是一个完整的 Lisp 程序:
(setq a 1) ;; `a' = 1, 这个绑定的作用域为整个程序 ;; 也就是说如果运行的时候没有对它进行修改,那么在整个程序的任意地方获取它都能得到 1 (defun set-a (v) ;; 函数 `set-a' 的作用域也是整个程序,和上面的 `a' 同级, ;; 但是 `v' 的作用域只有函数 `set-a' 的内部,这个作用域比 a = 1 的作用域低一级 (setq a v)) ;; `空白处' (let ((a 2)) ;; 这个 `a' 不是第一个 `a', 这个 `a' 的绑定是 a = 2,作用域只有该 let 表达式内部 a) ;; 所以访问到的这个 `a' 是 a = 2 的这个绑定, (let () a) ;; 同样这个 let 表达式内部也是一个作用域,但是在这个作用域里没有找到 `a' 的相关绑定, ;; 因此往上一级作用域找,找到 a = 1,所以该 let 表达式结果为 1, ;; a 这种在作用域内没有找到绑定的变量叫做 `自由变量' (free variables,因为没有绑定所以自由), (prin1 a) ;; 打印结果是 1,这里也属于 a = 1 这个绑定的作用域
然而这里有一个关键点没有提到: 函数 set-a 里面的自由变量 a 呢?它引用的是 \(a = 1\) 这个绑定吗?
这里的回答为是和否都可以,取决于接下来语言采用的作用域规则,分两种:
动态作用域(dynamic scope)和词法作用域(lexical scope),其中词法作用域又叫做静态作用域(static scope).
目前大部分编程语言采用的是词法作用域的设计, Emacs Lisp 采用的设计则是:默认动态作用域,可选开启词法作用域.
两者的差别体现在对自由变量的引用上,
在动态作用域下, set-a 的 a 引用的是最近一次创建的 a 绑定,也就是说这是由调用 set-a 的时机决定的,
如果在空白处后面加上代码 A:
(set-a 4)
那么程序最后的 (prin1 a) 打印结果毫无疑问是4,因为代码 A 访问到的是 \(a = 1\) 绑定的 a,
如果在空白处后面加上的是代码 B:
(let ((a 3))
(set-a 4))
那么程序最后的 (prin1 a) 打印结果是1,因为计算 (set-a 4) 时, set-a 里面的 a 是 \(a = 3\) 绑定的 a,
因此被改变绑定也是这个;
在词法作用域下, set-a 里面的 a 引用到的是 \(a = 1\), 就是引用到的绑定处于在 文本上 绑定创建的范围内,
通俗点就是, set-a 里面的自由变量 a 是引用 set-a 定义附近的绑定,所以就是 (setq a 1),
把 (setq a 1) 移动到 set-a 定义后一句的位置也是完全一样的结果.
关于如何在 Emacs 里面开启词法作用域可以看文档: M-: (info "(elisp)Using Lexical Binding").
大部分语言采取词法作用域的原因应该能够看出来了: 动态作用域容易得到意料之外的结果.
就好比上面动态作用域下的 set-a 在代码 B 的情况下修改到的居然是同名的局部变量 a,
这点对于开发模块而言是很糟糕的,没办法保证模块能够在任何上下文中都能够达到期待的结果.
Emacs 的变量除了作用域以外,还有一些地方和其它语言的变量不同.
在常量方面, Emacs Lisp 的常量分两种,分别含义是: 值不能发生改变的变量 以及 值不应该发生改变的变量.
前者只有 nil, t 以及关键字(keyword)类型(就是以 : 开头的 symbols)这三种,一旦试图给它们赋值就会引发 setting-constant 异常,这种是真正意义上的常量,
具体看 M-: (info "(elisp)Constant Variables").
后者则只是警告用户不应该改变的变量,具体看 M-: (info "(elisp)Defining Variables") 中对 defconst 的说明.
在变量方面, Emacs Lisp 除了全局变量以及局部变量以外还支持 buffer-local 变量,也就是针对特定缓冲区的变量,
用于创建 buffer-local 变量的基础上还有 file-local 变量, directory-local 变量以及 connection-local 变量,
file-local 变量是由文件(只能是 Emacs Lisp 代码文件)本身定义的,用来给文件关联的缓冲区创建 buffer-local 变量;
directory-local 变量由目录(其实是目录下特别的文件指定: M-: (info "(elisp)Directory Local Variables"))指定,
作为目录下所有文件的 file-local 变量,并且文件本身就有的 file-local 变量也会被覆盖掉,当然也可以通过设置来让文件本身的 file-local 变量优先于 directory-local 变量;
connection-local 变量则是针对和远程连接关联的缓冲区定义的,具体就看 M-: (info "(elisp)Connection Local Variables").
关于变量方面的细节就不展开说了,基本比较重要的内容(包括了 Emacs Lisp 变量不同的总结)都在上面了.
函数 (Functions)
什么是函数 (What is a Function)
广而言之(In a general sense),函数就是一套计算规则: 给出一些名为参数(arguments)的值作为输入,然后进行计算,计算最终结果叫做函数的值(value)或者返回值(return value),
计算可能会产生副作用(side effects),比如对变量或者数据结构的内容产生持久改变(lasting changes),参见的 print 语句也是一种;
有的函数在计算过程中不会产生任何副作用,并且不管处于何种外部因素下(比如机器类型,系统状态),每次输入同样参数总能产生一样的计算结果,这种函数叫做纯函数(pure function).
大部分的编程语言的每个函数都有自己的名字,不过 Lisp 的函数在严格意义上是没有名字(function name)的,它只是一个可以和 symbol 关联的对象(function object),
一旦关联上,我们就把关联的 symbol 称作(refer to … as)函数,话虽如此,我们还是需要把 函数名字 和 函数对象 的区别铭记于心.
Emacs Lisp 有类函数(function-like)的对象,和函数一样都能够执行计算,但并不被认为是函数,因此在使用 Emacs Lisp 进行编程的时候要分清楚以下几个概念:
Lambda Expression: 函数对象,常说的匿名函数;Primitive: 用C语言编写并且能够被Emacs Lisp调用的函数,也叫built-in function或者subrs,请参考(info "Writing Emacs Primitives");Special Form: 属于primitive,和函数一样可以执行计算,但是不像函数一样按照定义顺序运算所有参数,它可以只运行部分参数,也可以不按照定义顺序对参数进行运算,还可以控制运算参数的次数,在别的语言里面它被叫做结构控制语句.
Macro: 和函数一样可以调用,不同的是macro把Emacs Lisp表达式翻译成另外一个表达式,表达式再被执行,开发人员可以通过它来做到special form能够做到的事情;Command: 可以通过command-executeprimitive激活的对象叫做command,定义函数的时候可以通过添加interactive form把函数变成command,此外不是函数的键盘宏(
Keyboard macros,本质是字符串和向量)也是commands.Closure: 类似Lambda Expression,除了它还闭合了一个包含词法变量绑定的环境;Byte-code Function: 被字节码编译器编译过的函数;Autoload Object: 函数的占位符(place-holder).Emacs一旦调用autoload object就会加载包含函数定义的文件然后调用真正的函数.
Lambda表达式 (Lambda Expressions)
- Lambda的组件 (Lambda Components)
一个
lambda表达式就是以下形式的一个列表.(lambda (ARG-VARIABLES...) [DOCUMENTATION-STRING] [INTERACTIVE-DECLARATION] BODY-FORMS...)第一个元素
lambda是必定要有的,目的是为了与其他列表进行区分,以及告诉Emacs这个列表是一个函数.第二个元素
(ARG-VARIABLES...)就是一个symbol list,这些symbols都是参数(arguments)的名字.在调用函数的时候,参数的值就会根据该列表来进行匹配,形成
local bindings.第三个元素
DOCUMENTATION-STRING是可选的,是一个string对象,用来描述该函数.第四个元素
INTERACTIVE-DECLARATION也是可选的,是一个形式为(interactive CODE-STRING)的列表.这是用来声明在函数成为命令时候提供参数的方式,成为命令的函数可以通过
M-x来调用或者被绑定到一个按键上.剩下部分就是函数体,在
Lisp下我们叫做a list of Lisp forms to evaluate,函数的返回值就是BODY-FORMS里的最后一个form. - 参数列表 (Argument List)
参数列表的完整语法如下,
(REQUIRED-VARS... [&optional [OPTIONAL-VARS...]] [&rest [REST-VAR]])
REQUIRED-VARS...是必要参数列表,在其他语言可能叫做POSITION ARGUMENTS.定义函数的时候有多少个
REQUIRED-VAR,调用的时候就需要传入多少个实际参数(actual argument),否则会有wrong-number-of-arguments错误.&optional后面的OPTIONAL-VARS...是可选参数,调用时候的参数数量不能超过定义时候的数量,并且传入的参数序号一定要和定义的匹配.&rest后面跟着只有一个REST-VAR,调用时传入的实际参数数量为大于等于0.结合上面的描述你会发现
&optional以及&rest的参数在调用时是可以不传入的,不传入的情况下默认值是nil.函数没有办法区分显示传入
nil以及默认nil.比如有一个这样的参数列表:
(a b &optional c d &rest e)
调用时候的参数数量:
2个:a和b分别绑定,c,d,e都为nil;3个:a,b和c分别绑定,d,e为nil;大于等于5个:a,b,c,d分别绑定传入的前四个参数,剩下的参数都被打包成一个列表和e进行绑定.
和
Python这些语言不一样,Emacs Lisp不能在不传入c, d的情况下直接给e传入参数,必须给c,d传入nil.和
Common Lisp不同,Emacs Lisp的函数不支持用户给可选参数设定默认值,也不支持原生定义关键字参数(keyword arguments),其实
Emacs Lisp里面也是有使用了关键字参数的函数,比如make-hash-table,EmacsWiki上有如何自己实现Keyword Arguments的话题: 把&rest当作plist处理来实现类似与Common Lisp的关键字参数.比如这下面使用
Emacs的关键字(keyword)类型作为参数列表中的关键字,(defun keyword-support-func (&rest keywords) (let ((key1 (plist-get keywords :key1)) (key2 (plist-get keywords :key2))) (+ key1 key2))) (keyword-support-func :key2 2 :key1 1)
也不一定要用关键字类型的,能够正确访问就可以,比如下面用普通
symbol作为关键字,(defun keyword-support-func (&rest keywords) (let ((key1 (plist-get keywords 'key1)) (key2 (plist-get keywords 'key2))) (+ key1 key2))) (keyword-support-func 'key2 2 'key1 1)
不过这样不符合(
Common Lisp的)"规范",因此在实际中看到的都是第一种.除此以外可以通过
cl-lib的cl-defun宏来使用Common Lisp那样的函数定义方式. - 函数文档 (Function Documentation)
lambda表达式有一个可选的文档字符串,该字符串不影响函数执行,就是一个注释,它会被Emacs里面的帮助文档功能适用,比如apropos会显示文档字符串的第一行,所以第一行文档字符串应该用一两句话来总结函数的意图.写文档字符串的时候需要注意几点:
- 文档字符串的第一行总是会自动缩进,比如下面这样,
(defun fn-example (arg) "This is an examnple only to show the indetation of the documentation string." )
这样是正确的,但是有人会这么做,
(defun fn-example (arg) "This is an examnple only to show the indetation of the documentation string." )
这是错误的,虽然在源代码中看起来不错,但这在帮助功能的显示下会十分难看.
- 最后一行可以指定调用规范,比如
(defun fn-example (arg) "This is the example only to show how document looks like. \(fn-example ARG)" )
\的使用是为了避免和Emacs的动作命令(motion commands)搞混.
函数名字 (Function Names)
当 symbol 的 function cell 包含了一个函数对象,它就是函数的名字,它也就是一个可调用对象.
function cell 的内容被称为 symbol 的函数定义,如果 function cell 是另外一个 symbol,
那么就会用另外的这个 symbol 的函数定义来替代原来 symbol 的 function cell,这叫做函数间接调用.
实际中几乎所有函数都有名字,通常都是通过它们的名字来调用,你可以通过定义一个 lambda 表达式再把它放到 function cell 里面的方式来定义函数,
当然最常见的方法就是用 defun. 给函数名字是因为可以通过名字来引用,特别是对于递归函数来说函数名字是必须的.
还有就是原函数(primitive function)只能通过名字引用,因为原函数没有读取语法.
函数不需要有一个唯一的名字,通常一个函数对象只会出现在一个 symbol 的 function cell 里面,
实际上用 fset 能够把一个函数对象储存到几个 symbols 上,这样这几个 symbols 都是不同名字的同一个函数.
Emacs Lisp 的 symbol 可以同时作为变量以及函数,变量和函数有不同的命名空间,这叫做 Lisp-2,而 Scheme/Racket 这种就不是,叫做 Lisp-1.
在 Emacs Lisp 中,函数名字中有 -- 分隔号的函数是用于内部使用的,而 C 语言实现的函数的名字一般都是以 -internal 结尾来表示内部使用.
定义函数 (Defining Functions)
Emacs Lisp 定义函数有几种方式,
defun(重新)定义函数(甚至是原函数);defalias给函数一个别名,一般在内部它用了fset来设定函数定义,如果该别名有一个
defalias-fset-function属性,该属性关联的值酒会做为函数来使用,替代fset.defalias像defun记录函数定义的位置一样来记录赋予别名的时候的位置.define-inline宏定义内联函数,比起同样作用的defsubst,define-inline可以作为mapcar的参数,并且更加有效率,还能作为
place forms储存值.还有一些应该用在define-inline内部的宏,具体看文档.
调用函数 (Calling Functions)
调用(call/invocation)函数就是运行函数.调用函数分两种情况,硬编码调用以及运行时调用.
硬编码调用一般就是确定和限定程序要调用什么函数以及需要多少参数,这种情况可以用 function call 的 list form 来调用函数.
运行时调用就是不确定调用哪个函数以及传入多少参数,都是由运行时决定的,这种情况可以用 funcall 或者 apply 根据情况来进行调用.
funcall 和 apply 的区别在于 apply 的最后一个参数必须是个 list,如果想让 funcall 想使用命令那样调用一个命令,那么请使用 funcall-interactively.
有时候可能因为某些原因不想每次调用参数的时候传入相同的参数,需要固定函数的一部分参数的值,这种叫做偏函数 (partial application),结果是一个新的函数,
如下面这个例子,
(defun example (a b c) (+ a b c)) (defun partial-example (c) (+ 1 2 c))
偏函数和柯里化(Currying)相似也有关,但两者不一样,而且目的也不一样,在编程语义学中,柯里化是由于 lambda 表达式只能接收一个参数,但想要实现接收多个参数的一种技术.
这个是 Racket 代码,下面的例子就是一个如何利用柯里化实现一个计算两个参数的和的 lambda 表达式.
(((lambda (x) (lambda (y) (+ x y))) 1) 2)
Emacs Lisp 因为 Lisp-2 的原因要这么写
(funcall ((lambda (y) (lambda (x) (+ y x))) 2) 1)
在 Emacs Lisp 中有专门的 apply-partial 来实现偏函数.还有可以用 call-interactively 调用一些身为命令的函数.
匿名函数 (Anonymous Functions)
匿名函数就是没有名字的函数,有三种构建匿名函数, lambda 宏, function special form 以及 #' 读取语法.
lambda 都很熟悉,具体不说了, function 接收一个函数对象,在没有运算的情况下返回一个函数对象,读取语法 #' 就是它的简写(short-hand).
当 function 接收的 FUNCTION-OBJECT 是一个合法的 lambda 表达式,有两种效果:
- 当代码被编译的时候,
FUNCTION-OBJECT就会被编译成字节码函数(byte-code function); - 当启用了词法绑定,
FUNCTION-OBJECT会转换成一个闭包对象.
如果 FUNCTION-OBJECT 是一个 symbol 并且代码被编译,那么字节编译器(byte-compiler)会因为函数没有被定义,或者编译器不知道定义的情况下进行警告.
就个人目前使用 Emacs 的经历来看,后一种情况比较多.
通用函数 (Generic Functions)
一般函数都是硬编码(hard-coded)的,也就是假定了函数的类型以及预期的参数值(预期的参数类型),调用的时候只能传入特定的参数以及根据返回值类型来使用返回值.
相反,面向对象程序(object-oriented programs)能够使用多态函数(polymorphic functions)解决这个问题,
多态函数: 就是一个有相同名字的函数集合,每一个函数都有自己的参数类型集合,多态函数会根据运行时传入的参数类型决定调用集合里面的某个函数.
Emacs 提供了像其他 Lisp 方言的多态支持,特别像 Common Lisp 以及它的 Common Lisp Object System (CLOS), Emacs 的通用函数就类似多态函数,
Emacs 的多态支持就是模仿 CLOS.一个通用函数就是一个抽象,定义函数名字以及参数,通常是没有函数的实现.
实际的实现会根据特定参数类型来由对应的"方法"提供,每个方法实现一个与通用函数同名的函数,但要指定它接受的参数类型,这就是专化(specializing)参数,这些参数类型叫做参数专化器(argument specializers).
参数的专化程度可以或多或少,比如 string 比 sequence 更加特定/专有化.
不像基于消息传递的面向对象的编程语言(message-based OO languages),比如, C++ 和 Simula, Emacs Lisp 实现通用函数的方法不属于类,
它们属于它们实现的通用函数,所以才说通用函数就是一个抽象.当调用一个通用函数的时候,它会通过比对实际传入的参数以及方法的参数专化器来选择合适的方法(applicable methods).
为同一个通用函数提供实现的方法的参数数量必须要一样,通过参数类型来区分,会出现有多于1个合适方法的情况,这种情况下这些合适的方法会根据特定规则组合在一起.
来一个简单的例子,
(cl-defgeneric genericFunc (a) "Example for generaic" nil (message "Body as default method")) (genericFunc 0) ;; 如果没有提供方法,那么就调用提供的函数体,作为默认方法 ;; 提供方法,方法的参数可以和通用函数定义的不一样,因为目前而言定义通用函数时的参数不会被处理的 ;; 提供主要实现, primary implementation (cl-defmethod genericFunc ((a (eql 1))) ; (eql 1) 表示判断实际参数a是否等于1 1) (cl-defmethod genericFunc ((c (eql 2))) 2) (genericFunc 1) ; => 1 (genericFunc 2) ; => 2 ;; 提供辅助实现, auxiliary method (cl-defmethod genericFunc :before ((c (eql 3))) 3) ;; 使用辅助实现要求通用函数提供函数体,否则会有 No primary method 报错 ;; 辅助函数对通用函数有一个要求: 通用函数的参数数量要和辅助方法的参数数量一样,因为辅助方法的参数会传入到通用函数里面,这个时候需要参数数量一致.
闭包 (Closures)
在 Emacs 启用了词法绑定的情况下,任何有名字的函数以及用 lambda/function 或者 #' 读取语法构建的匿名函数,都会被自动转化成闭包对象(closure).
闭包就是一个包含了定义发生时的词法环境(lexical environment)记录的函数,在调用的时候,任何自由/词法变量都会从这个记录里面找值.
所谓自由变量就是不是在函数内部定义但却在内部被引用的的变量. Emacs 中的闭包是暴露的,是一个首个元素为 symbol closure 的列表,可以手动构建.
#'(closure (t) (x) (* x x)) ; => 相当于 (lambda (x) (* x x)) (funcall #'(closure (t) (x) (* x x)) 2) ; => 相当于 ((lambda (x) (* x x)) 2)
这里的第二个元素 (t) 表示词法环境, (x) 就是这个闭包函数接受的参数, (* x x) 毫无疑问就是函数体了.
Advising Functions
当需要修改定义在别的库的函数,或者需要修改一个 hook,一个进程过滤器,或者持有函数值的任意变量或者对象字段,你可以是用恰当的 setter 来修改定义,
对于命名函数可以用 fset 或者 defun, 对于 hook 可以用 setq, 对于进程过滤器可以用 set-process-filter,但是这些会完全覆盖旧的定义.
Emacs Lisp 提供 advice 特性让你在不覆盖原有定义的情况下对原有定义进行拓展. Emacs 的 advice 系统提供两类原操作,
针对变量和对象字段(variable and object fields)有 add-function 和 remove-function,针对命名函数有 advice-add 和 advice-remove.
advising 已经存在的函数,就是组合函数,想想钩子(hooks)
defadvice和advice-add比如,在display-buffer命令执行之后提示buffer的名字,用display-buffer做实验是因为一旦出错了
minibuffer都用不了,反馈快速.老风格
defadvice(defadvice display-buffer (after after-display-buffer (buffer-or-name &optional action frame) activate) (message "buffer is named %S" (if (bufferp buffer-or-name) (buffer-name buffer-or-name) buffer-or-name))) (ad-deactivate #'display-buffer)
可以以
:around来执行,不过写法稍微有点不太一样,around是直接把advised函数给包裹起来(defadvice display-buffer (around around-display-buffer (buffer-or-name &optional action frame) activate) (interactive (list (read-buffer "Display buffer: " (other-buffer)) (if current-prefix-arg t))) (if (called-interactively-p) (progn (message "buffer is named %S" (if (bufferp buffer-or-name) (buffer-name buffer-or-name) buffer-or-name)) (funcall-interactively (ad-get-orig-definition 'display-buffer) buffer-or-name action frame)) (progn (funcall-interactively (ad-get-orig-definition 'display-buffer) buffer-or-name action frame) (funcall (ad-get-orig-definition 'display-buffer) buffer-or-name action frame))))
新写法
advice-add和advice-remove(defun after-display-buffer (buffer-or-name &optional action frame) (message "buffer is named %S" (if (bufferp buffer-or-name) (buffer-name buffer-or-name) (buffer-or-name)))) (advice-add 'display-buffer :after #'after-display-buffer) (advice-remove 'display-buffer #'after-display-buffer)
对于
:around位置可以这么写(defun around-display-buffer (orig-fun buffer-or-name &optional action frame) (interactive (list (read-buffer "Display buffer: " (other-buffer)) (if current-prefix-arg t))) (if (called-interactively-p) (progn (message "buffer is named %S" (if (bufferp buffer-or-name) (buffer-name buffer-or-name) buffer-or-name)) (funcall-interactively orig-fun buffer-or-name action frame)) (progn (message "buffer is named %S" (if (bufferp buffer-or-name) (buffer-name buffer-or-name) buffer-or-name)) (funcall orig-fun buffer-or-name action frame)))) (advice-add 'display-buffer :around #'around-display-buffer)
注意到
around-display-buffer跟after-display-buffer相比多了一个orig-fun了吗?它表示advised函数,最后还要注意剩下的参数要与advised函数的参数兼容.
其它位置
:before,:after,advising函数的参数格式不能这么定义,要把表示advised函数的orig-fun去掉,否则参数会错位.上面的例子,特别是around-display-buffer,最好不要用,因为一旦Emacs的display-buffer发生了改变就很可能报错了,总的来说defadvice是挺危险的,不太推荐使用
- advising那些持有函数值(function value)的进程(process filters)/变量(variables)/对象(objects)
add-function和remove-function-比如定义一个赋值了函数的变量
my-func-var,现在用my-tracing-function包裹它(setq my-func-var (lambda (arg) (1+ arg))) (defun my-tracing-function (orig-variable arg) (message (format "Result is %S" (funcall orig-variable arg)))) (add-function :around my-func-var #'my-tracing-function) (funcall my-func-var 1) (remove-function my-func-var #'my-tracing-function)
其他位置也可以是一样的参数格式,
:around位置是必须这种参数格式,如果advised变量的持有函数需要一个参数,那么advising函数就要有两个参数,
第一个表示advised变量,剩下的表示advised变量的持有函数所需要的参数.
其它位置如
:before,:after可以不按照这种参数格式,区别就是把表示advised变量的参数去掉就好,advising函数的参数跟advised变量的持有函数要求的参数一样就可以.
废弃函数 (Obsolete Functions)
可以把一个有名字的函数标记为废弃(obsolete),表示该函数可能会在将来被移除.当编译的代码包含一个废弃的函数时候, Emacs 会警告说该函数是废弃的.
除此以外废弃函数与一般函数没有行为上的差别.
把一个函数标记为废弃的最简单做法就是在使用 defun 定义函数的时候使用 (declare obsolete ...) form,还可以使用 make-obsolete 函数来进行设定.
这里有一个 set-advertised-calling-convention 的函数,可以给函数指定 signature,相当于指定函数调用时候需要的参数类型,有点像 Racket 的 Contract.
内联函数 (Inline Functions)
内联函数的行为和普通函数一样,差别在于编译时候调用函数会不一样,内联函数会像宏一样被编译器张开,通俗点就是出现调用内联函数的地方就用内联函数的定义替换, defsubst 可以定义内联函数,
內联函数在编译时候被调用发生的事情可以这么理解,
(defun fake-inline-func () (message "hello")) ;; 这不是内联函数,只是用来演示,真正的内联函数请用 =defsubst= 或者 =define-inline= (defun caller () (fake-inline-func)) ;; 被编译后,上面的部分等于以下 (defun caller () (message "hello"))
既有优点,也有缺点,减少调用次数,提高程序性能,但是缺点也明显,由于展开是发生在编译的,所以一旦编译,之后每次修改程序后都需要重新编译,其次內联函数不能是用递归;
还有就是內联函数只适合小规模功能的实现,否则大量是用内联函数会大程度地编译文件在文件系统上和内存上所需要的空间,最后一个缺点就是不方便调试.
Declare Form
在函数/宏的定义中(defun/defsubst/defmacro)使用,给函数/宏设定元属性. declare 宏,如果用在函数和宏之外,那么就无视自己的函数并且返回 nil,不影响运行时.
只有用在函数/宏的定义中才能够设定元属性.
声明函数 (Declaring Functions)
编译一个文件经常会产生一些警报:编译器不知到函数的定义.有时候确实有这个问题,但通常不是问题,这个不是问题的警告是由于运行时才会加载的在别的文件中定义的函数.
比如编译依赖了 shell-mode 的 simple.el 时候, shell-mode 只能在执行 (require 'shell) 之后才能被调用,所以 shell-mode 会在运行时被正确定义,这明显不是一个问题.
我们可以让编译器不再警告这个问题,在第一次是用 shell-mode 之前执行以下 form,
(declare-function shell-mode "shell" ()) ;; or shell.,el for 2nd argument
这是告诉编译器 shell-mode 被定义在 shell.el 中.第三个参数是 shell-mode 的参数列表,如果提供了该参数,编译器就会根据参数列表检查 shell-mode 的调用;如果是 t,
那么就是指不提供参数列表,而不是 nil.
在声明后,可以用 check-declare-file 或者 check-declare-function 来检测特定文件和文件目录中所有的 declare-function 表达式,用此来判断函数是否真的被定义.
这些命令实际上都用了 locate-library 来做判断.
函数安全 (Function Safety)
有些函数可能会定义在一些不值得信任/来历不明的文件中,直接调用这些函数可能会有风险, unsafep 会简单分析 form 是否安全,
如果安全就返回 nil.
宏 (Macros)
在语法上来看宏和函数的调用是一样的, 函数的本质是一套计算规则,调用函数就会根据定义进行计算并且得到结果;
而宏不是, 宏是一种能够生成代码的功能, 调用宏会先根据定义生成代码,也就是 form,生成的代码 在之后 会被直译器执行得到结果.
宏计算并且生成代码这个过程叫做展开(expanding),生成的代码叫做展开式(expansion).
代码生成实际上就是编译,编译发生的过程叫做编译时(compile time),执行代码的过程叫做运行时(runtime),
大部分编程语言的实现都是编译时和运行时是严格分开的,编译型实现都是先编译完然后再根据编译结果执行,
不能编译到一半进入执行阶段或者运行到一半进入编译阶段,甚至有不支持编译的纯直译型实现.
有部分语言会支持宏,最常见的就是 C/C++,但是和 Lisp 的宏不一样,通常 C/C++ 都是编译型实现,它们的宏只能在编译时展开,
而 Lisp 的宏可以在运行时被展开,这是因为 Lisp 的宏这是语言层面上定义的, C/C++ 语言定义中就没有宏这东西,都是编译器提供的.
因此 Lisp 的编译时和运行时可以交错: 在运行时中编译代码,编译后回到运行时执行代码,
哪怕 Lisp 的实现不是编译型, Lisp 也能够支持编译,并且还能在编译时和运行时之间随意切换.
不过总的来说,不管是 Lisp 还是 C/C++ 的宏,它们都能够做到函数做不了的事情: 一定程度上定义新语法.
经常拿来和宏比较的概念非内联函数莫属了, Emacs Lisp 支持编译和直译两种模式,也支持内联函数,
在编译模式下内联函数和宏的作用非常相似的: 用于生成代码;不过它们的出发点是不一样的,因此它们看着相似实际差了不少.
宏可以根据情况生成代码,比如根据变量来判断生成怎么样的代码,并且不会对参数进行运算,这些都是内联函数做不到的,
内联函数设计出来目的是通过把函数的代码编译到需要调用它的代码中,免去了运行函数前先寻找函数地址的这个过程,
从而提高调用函数的效率,但是相应增加了编译工作量,为了不要对编译造成太大的影响,内联函数因此只适合用于简单的函数.
宏例子 (Defining Macros)
Lisp 是支持匿名宏的,但是 Emacs Lisp 并没有支持,只能用 defmacro 宏去定义宏: (defmacro NAME ARGS [DOC] [DECLARE] BODY ...).
Emacs Lisp 的宏对象是一个 list: (macro lambda ARGS . BODY), 这个 list 的 CDR 是一个 lambda 表达式,
也就是说宏对象储存在 NAME 的 function cell 里面,这意味着宏对象的 ARGS 是和函数的参数列表一样的.
(相比其他 Lisp 方言 Emacs Lisp 的宏可以说是非常简单了).
这里写一个自己的 if, 具体思路就是把 myif 的调用展开成 if 表达式,
(defmacro myif (cond-expr if-branch else-branch) (list 'if cond-expr if-branch else-branch))
如果你把 defmacro 换成 defun,那么不管是 if-branch 还是 else-branch 都会在传入参数的时候对参数进行运算,
不需要通过 cond-expr 的判断,这么一来就不是和 if 做一样的事情了.
还可以用 来写,相比上面用 list 构造 form, 用 backquote 构造 form 会更加简洁,
(defmacro myif (cond-expr if-branch else-branch) `(if ,cond-expr ,if-branch ,else-branch))
具体关于 backquote 的内容可以查看 M-: (info "(elisp) Backquote"),
最后再来定义一个接受任意参数的宏,这种宏看起来就像 progn 的用法一样,
(defmacro test-macro (&rest body) `(let ((a 1) (b 2)) ,@body)) (test-macro (let ((c (+ a b)) (d (- b a))) (+ c d)))) ;; => 4
宏的展开式 (Expansion)
宏的调用形式和函数的调用形式都是一个 (NAME ARGS ...) 形式的列表.
因为展开式是以正常方式的运算的,并且展开式 可能 会包含其它的宏调用,所以宏也是和函数一样支持递归的.
Emacs 在加载未编译的 Lisp 文件会先尝试展开里面的宏,如果成功,那么就可以提高后续的执行速度.
和大部份 Lisp 方言一样, Emacs Lisp 也提供检查宏展开式的函数: macroexpand, macroexpand-all 以及 macroexpand-1.
比如,
(defmacro prefixed-setq (name value) `(set (intern (format "prefixed-%s" (symbol-name (quote ,name)))) ,value)) (prefixed-setq var 1) ;; (macroexpand-1 '(prefixed-setq var 1)) => (set (intern (format "prefixed-%s" (symbol-name (quote var)))) 1) (+ prefixed-var 1)
宏和编译 (Compiling Macros)
当宏调用出现在编译时中, Lisp 编译器会像 Lisp 解析器那样调用宏,然后得到一个展开式.
由于宏调用是发生在编译阶段,展开式后并不会被执行,因为这是直译器的工作.
编译器把展开式编译到程序中,就好像是直接嵌入到程序里面一样.
宏展开的时候会进行一定程度的运算,这个过程可能会产生运算的值以及副作用,
然而这些产生的结果并不会出现在运行时中,只会在出现在编译阶段,因为这些东西没有被编译进去.
对于那些本以产生副作用为目的的宏来说并不是什么好事,比如
(defmacro myif (cond-expr if-branch else-branch) (message "Some imporant side effects just like me") (list 'if cond-expr if-branch else-branch))
编译后大概是这个样子,
(if cond-expr if-branch else-branch)
实际上通常副作用都是用于控制宏的展开行为的,比如下面这个在不同操作系统上进行不同的展开,
(defmacro myif-mod (cond-expr if-branch else-branch) (if (eq system-type 'windows-nt) (list 'or (list 'and cond-expr if-branch) else-branch) (list 'if cond-expr if-branch else-branch))) ;; 在 Windows 上编译得到 form 只会为: (or (and cond-expr if-branch) else-branch), ;; 在其它系统上编译得到的 form 只会为: (if cond-expr if-branch else-branch)
为了保证宏调用通过编译,编译器要求 macro 在调用前定义好.对于这个问题,编译器有一个特性做出这样的处理:
如果在编译文件时候发现了 defmacro form,那么宏就会被进行临时定义,在对文件的剩余部分进行编译的过程中都能够看到这个宏的定义.
编译文件时会在文件的 top-level 执行任何次数的 require 调用,也可以用这来确保所需的宏定义在编译时候可用.
要注意一个问题, require 调用也会被编译到生成的代码中,在运行生成的代码时会加载宏的定义,
然而对于编译得到的程序来说是没必要的, Emacs Lisp 提供 eval-when-compile 来解决这个问题,
(eval-when-compile (require 'macro-definition))
这样 require 调用就能既可以在编译阶段运行,也可以在直译阶段下运行,并且在编译阶段中不会被编译进生成代码中.
宏的问题 (Problems With Macros)
Lisp 的宏算是语言特色了,虽然说 C 也支持宏,但后者在能力上起 Lisp 的相差太远,同时 Lisp 的宏也更复杂.
所以一个不留神就容易出现一些问题.
- 时机错误 (Wrong Time)
这是最常见的问题之一: 在展开宏的时候处理想要完成事情,而不是在展开式本身进行处理.
假设有一个宏想要设置缓冲区为
multibyte缓冲区,(defmacro my-set-buffer-multibyte (arg) (if (fboundp 'set-buffer-multibyte) (set-buffer-multibyte arg)))
在直译模式下这个宏是能够正确完成任务的,然后在编译后是不行的,理由可以参考上面的 宏和编译.
正确的做法是,
(defmacro my-set-buffer-multibyte (arg) (if (fboundp 'set-buffer-multibyte) `(set-buffer-multibyte ,arg)))
- 重复运算宏的参数 (Argument Evaluation)
定义宏的时候需要注意运行展开式的时候参数的运算次数.
比如定义一个
for循环结构,(defmacro for (var from init to final do &rest body) "Execute a simple \"for\" loop. For example, (for i from 1 to 10 do (print i))." (list 'let (list (list var init)) (cons 'while (cons (list '<= var final) (append body (list (list 'setq var (list '1+ var)))))))))
它的
backquote版本如下,(defmacro for (var from init to final do &rest body) `(let ((,var ,init)) (while (<= ,var ,final) ,@body (setq ,var (1+ ,var)))))
下面是它的一个调用的展开式,
(for i from 1 to 3 do (setq square (* i i)) (princ (format "\n%d %d" i square))) ↦ (let ((i 1)) (while (<= i 3) (setq square (* i i)) (princ (format "\n%d %d" i square)) (setq var (1+ var)))) ⊣1 1 ⊣2 4 ⊣3 9 ⇒ nilfrom,to以及do就是语法糖(syntactic sugar),可以无视它们.这里有一个问题,在每一轮迭代中,参数
final都会进行运算,如果
final是常量的话没有问题;如果是一个复杂的
form, 比如一个耗时的函数调用,那么这会造成严重的性能问题,并且如果
final存在副作用,多次执行可能照成一些意想不到的结果.正确的做法应该是把
final的计算放到循环之外,(defmacro for (var from init to final do &rest body) `(let ((,var ,init) (max ,final)) (while (<= ,var max) ,@body (setq ,var (1+ ,var)))))
不过这样会引入另外一个问题,下个小节接着讨论它.
- 展开式的局部变量覆盖用户定义的变量 (Surprising Local Vars)
上一个章节说的引入的问题是引入了新的变量
max,如果使用该宏的时候用户也定义了一个max的变量,(let ((max 0)) (for x from 0 to 10 do (let ((this (frob x))) (if (< max this) (setq max this)))))
通过观察这个
form的展开式可以看到用户的max被for宏里面的max遮掩了,因此用户定义的max就没用了.解决这个问题的核心在于如何避免临时宏的变量
max不会和用户的变量发生冲突,正好Lisp有uninterned symbol,这种
symbol可以被绑定以及被引用,并且不会和任何其它的symbol发生冲突,哪怕是同名的symbol,如果不进行绑定的话,这种
symbol就没法引用了.可以利用这特性来解决上面的问题,
(defmacro for (var from init to final do &rest body) (let ((tempvar (make-symbol "max"))) `(let ((,var ,init) (,tempvar ,final)) (while (<= ,var ,tempvar) ,@body (setq ,var (1+ ,var))))))
uninterned symbolmax(make-symbol 生成的max) 不会和interned symbolmax(用户定义的max) 发生冲突. - 展开时运算参数 (Eval During Expansion)
PS: 标题是 "Eval During Expansion", During 说明了 Expansion
另外一个问题,就是不要在展开时通过调用
eval这样的方式去对参数(arguments)进行运算,因为一旦用户传入的变量名字和参数名字一样就会出现问题,
比如下面要引用 用户传入的变量 所指向的 变量,
(defmacro foo (a) (list 'setq (eval a) t)) (setq x 'b) (foo x) ;; -> (setq b t) (setq a 'c) (foo a) ;;-> (setq a t), 这是因为用户传入的参数和宏定义的参数发生冲突了
因此定义的宏最好 别 在展开过程中运算参数.
- 重复展开 (Repeated Expansion)
如果在直译函数里面执行宏调用,那么每次调用都会展开;对于编译函数,那么宏调用只会在编译时展开一次,之后编译函数只会调用编译好的展开式.
因为这个原因,如果宏的定义有副作用,那么宏调用的次数不同就有可能会产生不同结果,直译函数每次都会产生副作用,而编译函数只有编译的时候才会产生副作用.
因此尽量不要在宏展开的时候使用副作用(可以在展开式中使用),比如,
(defmacro my-message (msg) `(message msg)) ;; NOT THIS (defmacro wrong-message (msg) (message msg))
只有一种副作用不能避免: 构建
Lisp对象.这是大部份宏的重点,这是没问题的,只有一种情况需要注意:构建的对象是宏展开式的一个被引用常量(
quoted constant)的一部分.
缩进宏 (Indenting Macros)
在宏的定义中,可以使用 declare 指定宏使用多少个 <TAB>: (declare (indent INDENT-SPEC)).
更多的宏例子
这是我个人写的例子,都是心血来潮的时候写的,主要是为了验证自己的学习成果和备忘,并不保证宏的实用性.
比如说,这下面的一些例子是可以通过函数实现的.
- alist-get-rec.el (可以用函数实现)
个性化 (Customization)
Emacs 用户可以在不编写 Lisp 代码的情况下通过自定义界面来自定义变量和外观(face),详情阅读 M-: (info "(Emacs)Easy Customization")
可自定义的项包括可自定义变量(使用 defcustom 宏定义);可自定义外观(使用 defface 定义)和可自定义组(用 defgroup 定义得到一组相关的可自定义项).
加载 (Loading)
加载 Lisp 代码文件意味着把文件内容以 Lisp 对象的形式带入到 Lisp 环境中.
Emacs 查找并且打开文件,读取文本然后运算每个 form,最后关闭文件,这个文件也可以称作一个 Lisp 库.
加载文件的函数会运算文件中所有的表达式,就像 eval-buffer 运算 buffer 里面的所有表达式一样.
被加载的文件必须包含 Lisp 表达式,不管文件是源代码还是字节码形式.文件中每个 form 被叫作一个 top-level form.
已经加载文件的 forms 并没有什么特别的格式,任何 forms 都可以直接输入到 缓冲区 中并且运行它们.
Emacs 也可以加载已经编译好的动态模块:共享库(shared libraries),通常由 C/C++ 编写,在动态模块被加载时,
Emacs 会调用一个特别命名的初始化函数,这个函数需要模块自己实现,该函数的工作就是把函数以及变量暴露给 Emacs Lisp 程序.
关于编写动态模块可以阅读这里: (info "(elisp) Writing Dynamic Modules").
程序的加载过程 (How Programs Do Loading)
Emacs Lisp 有几个用于加载的接口,不过所有接口最后都是调用 load 函数来加载文件,因此,这里只了解 load 就可以.
假设现在执行一个表达式 (load "FILENAME"),那么就会按照以下程序执行:
先查找
FILENAME.elc,.elc文件格式是.el文件的字节码编译格式,如果找到就打开并且读取文件内容,然后运算,最后关闭文件,然后返回
t表示加载完毕,否则进行下一步;
- 如果编译文件不存在就尝试查找
FIELNAME.el,如果找到就加载,否者进行下一步; 如果编译的
Emacs支持加载动态模就尝试查找FILENAME.EXT,EXT表示动态模块的后缀,不同的系统后缀不一样.如果找到后就加载,如果没有找到或者
Emacs不支持加载动态模块,那么就进行下一步;其实前 3 步是由
load-suffixes变量决定的,load-suffixes记录了文件后缀,前面的查找顺序就根据这个执行的.一旦前 3 个步骤都没找到文件,那么就尝试查找
FILENAME,如果找到就加载,否者进行下一步;如果启用了
Auto Compression mode,就尝试查找压缩版本,默认是FILENAME.gz,Emacs是通过文件后缀判断文件是否被压缩,jka-compr-load-suffixes记录了若干个压缩文件后缀,默认值是(.gz).会按照
("FILENAME.elc.gz" "FILENAME.el.gz" "FILENAME.gz")顺序查找(这个说法可能有误,不过经过验证的确是会加载这三个其中一个,有时间的话用三个类似名字但代码不同的包做验证),找到后在其中尝试查找
FILENAME文件:(load "FILENAME"),找到后进行解压加载,这是一个递归过程.- 如果都没找到,默认引发
file-error错误,可以在参数missing-ok位置传入non-nil让它不报错并且返回nil.
加载后缀 (Load Suffixes)
事实上, load 函数的查找文件过程是由变量 load-suffixes 决定的,该变量的标准值是 (".elc" ".el"),
如果 Emacs 支持加载动态模块 .EXT,那么值是 (".elc" ".el" ".EXT"). load 函数有 nosuffix 参数,
如果该参数为 t 就无视 load-suffixes 的设定,把 load-suffixes 看做 nil.
load-file-rep-suffixes 指定了同一个文件的各个表示(representations),默认是 (""),这个也就是文件名的后缀,
该变量会把 jka-compr-load-suffixes 的值 (".gz") 添加到该变量中得到 ("" ".gz"),表示启用了 Auto Compression mode.
库的查找 (Library Search)
加载非 ASCII 字符 (Loading Non-ASCII)
自动加载 (Autoload)
自动加载功能允许先注册函数或者宏,但是没有加载定义了函数或者宏的文件本身,第一次调用这个函数或者宏的时候先自动加载对应的文件/库,
这是为了安装真正的定义以及其他相关的代码,然后运行真正的定义.自动加载也可以在查找函数和宏的文档时/补全变量名字以及函数名字时触发.
说的简单点,自动加载就是按需加载/懒加载,防止无用加载减少时间.
有两种方法使用自动加载: 调用 autoload 函数,以及在源文件里的函数/宏定义前面加上"魔法"注释("magic" comment).
后者实际上是基于前者实现的.
比如 (autoload 'fn-to-call "/path/to/source.el"), 这里的情况是函数 fn-to-call 定义在 /path/to/source.el 中,
这样就有一个名叫 fn-to-call 的占位符,通过 (symbol-function 'fn-to-call) 可以获得获取到一个 autoload 对象,
第一次调用 fn-to-call 的时候先判断它的定义是否存在,如果不存在就触发自动加载:通过 load 函数加载 /path/to/source.el 文件,
如果没有找到文件或者加载文件后没有 fn-to-call 这个定义就会报错.除了函数,也可以定义宏/按键映射(keymap)作为函数来自动加载.
至于"魔法"注释则是这么使用,
;; source.el ;;;###autoload (defun fn-to-call () (message "Hello")) (provide 'source)
其中 ;;;###autoload 就是"魔法"注释,单独一行,一个"魔法"注释通常被称为一个 autoload cookie.
实际上这个注释本身没什么动作,它是作为一个标记来服务 update-file-autoloads 或者 update-directory-autoloads 命令的,
当使用这两个命令其中一个的时候, Emacs 会把这个注释后面的定义写成对应的 autoload 对象到一个文件中,加载这个文件可以在以后的调用中触发自动加载,
典型的例子 lisp/loaddefs.el, 可以通过 (find-library "loaddefs") 查找到里面的内容.
字节编码 (Byte Compilation)
Emacs 拥有两个直译器和一个编译器,编译器可以把 Emacs Lisp 代码编译成字节码(byte-code),
然后由 Emacs 的字节直译器(byte-code interpreter)运行.字节码直译器和平常的 Emacs Lisp 直译器不是同一个直译器.
由于字节码不是由真正的硬件运算,所以不可能像真正的字节码一样快,正是因为这样,字节码可以在无需重新编译的情况下在转移于不同机器之间.
任何版本的 Emacs 可以运行旧版本 Emacs 产生的字节码,但是反过来不行.
可以通过设定文件变量(file-local variable)来让阻止 Lisp 文件编译.
;; -*-no-byte-compile: t; -*-
编译字节函数 (Compilation Functions)
可以针对函数(byte-compile-function),文件(byte-compile-file)和目录(byte-compile-file)3个等级进行编译.
文档字符串和编译 (Docs and Compilation)
加载编译后的文件是不会把函数和变量的文档加载进内存的,目的是为节省内存以及加快加载速度,只有在有需要的时候才会加载.
这叫做动态加载(dynamical loading)/惰性加载(lazy loading),不过有一个坏处,如果编译文件被删除/移动/修改(比如重新编译)了就不能访问之前加载函数/变量的文档了.
有两种方法可以解决,一是编译时候把 byte-compile-dynamic-docstrings 变量设置为 nil,二是重新编译文件.
动态加载个别函数 (Dynamic Loading)
其实函数也可以动态加载的,加载文件的时候会给函数的定义留下一个 place-holder,这个 place-holder 引用定义它的(编译)文件,只有在第一次调用的时候才读取函数的定义并且替换掉 place-holder.
它的优缺点和上面数的动态加载文档一样,解决方法也很相似,一是编译时候设定 byte-compile-dynamic 为 nil,而是重新编译.
在编译期间运行 (Eval During Compile)
要清楚编译时和运行时是不同的两个阶段, Emacs 提供了两个 forms 来允许代码在运行时候执行: eval-and-compile 和 eval-when-compile.
通俗点讲,如果你想让代码 body 在编译时以及在使用编译得到的代码时运行,那么用 eval-and-compile;
如果一段代码 body 只是针对编译时,对在执行编译得到的代码时没所谓,那么用 eval-when-compile.
最后要注意,这两个 forms 的 body 在直译时也是会被运行的.
编译器错误 (Compiler Errors)
编译时候产生的错误和警告信息会输出到 *Compile-Log* 缓冲区上面,这些信息包括文件名字和问题发生位置的行数.
当引发语法错误,字节编译器可能会不知道错误的实际位置,这个时候可以到 " *Compiler Input*" 缓冲区查看(注意有个空格).
这个缓冲区包含编译后的程序并且指出字节编译器能够读取到多远,问题可能就在附近.
一个常见的警告类型是使用的函数和变量没有定义,这些警告会报告文件最后的行号,不是使用的函数或者变量丢失的位置,只能手动搜索文档.
如果要消除这些警告,有以下手段:
- 通过
fboundp/boundp判断函数/变量确实定义后才使用; - 在定义面可以通过
declare-function声明函数/defvar定义没有初始值的变量告诉字节编译器它们已经定义; - 把不想提示错误和警告表达式放到
with-no-warnings里面; - 通过设置
byte-compile-warnings做更精确的控制.
字节码函数对象 (Byte-Code Objects)
编译器函数后会产生一个 byte-code function object,看起来就像一个以 #[ 开头的 vector,只要有4个元素,没有最大个数,
只有前面6个是有正常作用的:
ARGDESC
参数的描述符(descriptor),可以是一个参数列表(argument list)或者一个表示参数个数的整数.
后者的值的0到6位指定参数的最小个数,8到14位指定函数的最大个数,如果参数中有
&rest,那么就会设定第7位.如果
ARGDESC是一个列表,那么在执行字节码之前动态绑定参数;如果是整数,在执行直接码之前,参数就会被压到字节码直译器的stack中.BYTE-CODE
包含字节码指令(byte-code instructions)的字符串.
CONSTANTS
字节码引用的对象的
Vector,包含用于函数和变量对应的symbols.STACKSIZE
函数需要的最大
stack大小.DOCSTRING
函数的文档字符串(如果有的话),否则为空.如果有文档字符串,那么它可以是一个数字或者列表.
可以通过
documentation函数获取真正的字符串.INTERACTIVE
交互配置(interactive spec)(如果有的话).它可以是一个字符串或者一个
Lisp表达式.
;; backward-sexp 的字节码 #[256 ;; ARGDESC "\211\204^G^@\300\262^A\301^A[!\207" ;; BYTE-CODE [1 forward-sexp] ;; CONSTANTS 3 ;; STACKSIZE 1793299 ;; DOCSTRING "^p"] ;; INTERACTIVE
可以通过 make-byte-code 创建一个字节码对象,不过我们不应该手动编写字节码,因为很容易会不一致而导致程序崩溃.
不过总有人想走不同的路,这里有一篇很不错的文章教你手写字节码.
反汇编字节码(Disassembly)
(在不了解汇编知识的情况下,理解这章节的内容可能会有点难.本人后面写了一篇汇编的学习笔记,想要快速了解背景知识,可以阅读 机器码和字节码 的那一小节)
Emacs Lisp 支持把代码编译成字节码, Emacs Lisp 的虚拟机是采用堆栈机(stack machine)作为计算模型的.
此外,自己阅读字节码时必然要一份 opcode 的参考文档,这份文档就是 M-x find-library RET bytecomp.
需要注意的是,不同版本的 Emacs 的字节码定义是不一样的,因此必须阅读对应版本的 bytecomp.el.
通过自带的函数 disassemble 就能对别的函数反汇编,并且把结果显示出来,文档 M-: (info "(elisp) Disassembly") 上也有给出了几个具体例子.
我这里也补充一些关于显示结果的说明,出于演示目的,这里给出一个"啰嗦版本"的绝对值函数 my-abs.
(defun my-abs (num) (cond ((> num 0) num) ((= num 0) num) ((< num 0) (- num))))
(disassemble 'my-abs) 可以得到反汇编结果如下:
byte code for my-abs: doc: ... args: (arg1) 0 dup 1 constant 0 2 gtr 3 goto-if-nil 1 6 return 7:1 dup 8 constant 0 9 eqlsign 10 goto-if-nil 2 13 return 14:2 dup 15 constant 0 16 lss 17 goto-if-nil-else-pop 3 20 dup 21 negate 22:3 return
这个例子的前面3行给出了 my-abs 的一些信息: 文档和参数.关键是后面的内容,为了看懂它们需要了解相关概念.
它实际编译出来的字节码对象是这样的 ((byte-compile 'my-abs)):
#[257 "\211\300V\203^G^@\207\211\300U\203^N^@\207\211\300W\205^V^@\211[\207" [0] 3 "\n\n(fn NUM)"]
M-: (info "(elisp) Byte-Code Objects") 描述了一个字节对象的通用格式,
这里我们稍微结合例子来理解一下文档上的说明:
ARGDESC: 257该元素描述了字节码对象的参数(arguments)信息,在整数时,它描述了最少需要多少个参数,最多需要多少个参数,有没有使用
&rest.换成二进制,它的格式是这样的
xxxxxxx x xxxxxxx(这里用空格隔开是为了让读者能一目了然每一段是有不同作用的).0 到 6 位表示最少需要的参数个数, 8 到 14 位表示最多需要的参数个数,7 位就是表示是否用了
&rest了.257 的二进制为
0000001 0 0000001,可以看到 0 到 6 位(右手边的第一段)的十个进制值为 1,那么该字节码对象最少需要 1 个参数;
7 位是 0,表示没有使用
&rest;8 到 16 位的十进值同样为 1,最多需要 1 个参数.
BYTE-CODE: "\211\300V\203...\211[\207"编译
my-abs后得到的字节码,是一个unibyte字符串.CONSTANTS: [0]该元素记录了字节码所引用到的
Lisp对象,是一个向量(vector),包含了函数/变量名字所对应的symbol以及一些字面值.这个例子就只有 0 这 1 个字面值.
STACKSIZE: 3该字节码对象最多需要 3 个字节栈空间.
DOCSTRING: "\n\n(fn NUM)"函数的文档说明,如果没有些文档说明,那么默认为 "\n\n" 后面接着类似函数签名的字符串.
你可以通过
(documentation 'my-abs)来验证这一点.(本人在实验时,同时加了
doc string以及interactive form后得到的字节码对象的该元素还是为默认值,然而一些Emacs自带的函数没有出现这个问题.)有些已编译函数的
doc string会单独保存在一个文件中,比如(byte-compile 'disassemble)结果的DOCSTRING项类似如:("/PATH/TO/XXX/lisp/emacs-lisp/disass.elc" . 696),意思是说
disassemble的doc string保存在 "/PATH/TO/XXX/lisp/emacs-lisp/disass.elc" 上,从第696个字符开始的一些内容就是它的doc string.INTERACTIVE:由于
my-abs不是命令,因此该元素就是没有的;该元素是显示(interactive xxx)的里面的内容xxx.所以,如果
my-abs的interactive form是(interactive)这样定义,那么该元素就是nil(是显示出来的).因此需要注意 没有内容 和 nil 的区别,这和文档上的描述是不同的.
然后下面就是字节码信息,分两列:
第一列的数字叫做程序计数器(program counter,简称 PC),也叫指令指针,表示它对应指令在编译代码中的地址;
第二列就是字节对应的 opcode 以及参数.
这里还有一些小细节,这个例子上没有序号 4 和 5,根据 bytecomp.el 上定义的 byte-goto-if-nil 可以知道, goto-if-nil 需要使用它后面的两个字节用来储存参数,
而 goto-if-nil 正是 3 号字节对应的 opcode,这两个字节不属于代码上的字节码,所以没有这两个序号;
此外,有些字节序号后面跟了一些类似于 ":<label>" 这样的额外东西,这个和汇编码里面的标签是一个意思,比如序号 7 后面跟着的 ":1",
3 号字节的 goto-if-nil 的参数就是标签: goto-if-nil 1.
更多关于输出细节可以看 M-x find-function RET disassemble.
调试 (Debugging)
Emacs Lisp 有几种调试手段排查问题,不同手段针对不同情况下的问题:
- 如果在程序运行时发生错误,
Emacs Lisp内置的调试器会挂起执行中的程序, 可以在这个时候检查/修改运行时的状态. - 如果知道源代码的定义在哪里,可以使用
Edebug这个源码层级的调试器,这功能可以用来高效了解源码. - 如果是想要跟踪函数的执行,或者跟踪变量的变化,可以使用
trace.el. - 如果是语法问题,只需要使用
Emacs Lisp的编辑命令就好了. - 如果是编译时产生的错误或者警告,那么只要通过查看错误/警告内容就可以了.
- 如果是想做回归测试,可以使用
ERT包. - 如果是优化程序,那么可以查看文档这里:
M-: (info "(elisp)Profiling").
Debugger
Debugger 提供挂起 form 执行的能力,挂起(suspended)通常也被叫做中断(break),
这个状态实际上就是 Emacs 的一个递归编辑(recursive edit)
可以在这个状态下检查运行时栈(run time stack),检查局部或者全局变量,甚至还能改变这些变量.
当执行中的一个挂起(suspended,或者叫中断,break)实际是一个递归编辑(recursive edit),
在挂起的时候可以执行普通的编辑,比如检查运行时的栈,变量的值或者改变这些值等等.关于 Recursive Editing 可以查看 M-: (info "(elisp) Recursive Editing").
关于如何阅读 Emacs Lisp Debugger 的 backtrace,它是以栈的顺序动态显示的,也就是说,最底下底语句是第一句,顶层语句是目前执行的语句,也就是当前执行点.
- 错误调试 (Error Debugging)
设定调试的入口,也就是什么时候才唤醒调试器.
debug-on-error
设定该变量为
t,可以在debug-ignored-errors以外的错误发生时进入调试,debug-ignored-errors告诉调试器无视哪些错误.如果值是一个错误条件的列表,那么只有引发列表中的错误才会进入调试.
当这个变量的值为
non-nil的时候,Emacs是不会为进程过滤函数和哨兵 (process filter functions and sentinels)创建错误处理器,也就是它们一旦有错误就会进入调试.eval-expression-debug-on-error
如果该变量为
t, 那么在执行eval-expression命令,默认M-:,的时候动态绑定debug-on-error为t,其它时候debug-on-error的值还是原来的值.也就是针对执行
eval-expression发生的错误进行调试.debug-on-signal
正常来说,
condition-case捕捉到的错误是不会唤醒调试器的,因为condition-case在调试器之前处理了错误.假如
debug-on-signal这个变量设定为non-nil,那么调试器就可以无视condition-case在第一时间处理错误.这个变量可以在
Emacs的--eval选项进行设定,如果运行时候发生了错误,那么Emacs就会弹出一个backtrace.最好不要在编码中设定这个变量,因为会导致所有 condition-case 语句失去处理错误的机会,包括你计划外的部分.
如果需要调试
condition-case里面的代码,可以考虑使用condition-case-unless-debug.debug-on-event
如果给该变量设定一个特别事件(special event),那么
Emacs就会在接受到事件的第一时间绕过special-event-map进入调试器.目前值支持对应
SIGUSER1和SIGUSER2的值,当设定好inhibit-quit并且在Emacs没有响应时候这个变量十分有用.debug-on-message
给该变量设定用来匹配回显区域(echo area)的消息(message)的正则表达式(regular expression),如果匹配就会进入调试.用来查找造成该消息的原因就很有用.
比如,
(setq debug-on-message ".*\"q\".*") (defun test () (message "\"q\"")) (test)
- 无限循环 (Infinite Loops)
当程序死循环的时候,可以通过
C-g,也就是调用keyboard-quit来终止程序.这样直接停止的话是获取不了死循环的信息,可以设置debug-on-quit为non-nil来在C-g的时候进入调试. - 函数调试器 (Function Debugging)
如果想在调用特定函数的时候进入调试,可以通过使用
debug-on-entry函数添加想要的调试的函数,比如(debug-on-entry 'example).有一点要注意的是,
debug-on-entry不能直接添加C语言实现的原函数和Special Forms,间接是可以的,也就是Lisp函数用它们作为subroutine.如果要取消对某个参数的调试,可以通过
cancel-debug-on-entry来取消,比如(cancel-debug-on-entry 'example). - 直接调试 (Explicit Debug)
可以在源代码中想要调试的位置添加
(debug)来作为breakpoint,然后可以通过eval-defun等方式运行调试. - 使用调试器 (Using Debugger)
进入
Emacs调试器的时候会打开一个*Backtrace*缓冲区,它是一个使用了特别major mode的只读(read-only)缓冲区.这个
major mode是Debugger mode,把字母定义成调试命令.在该mode下依然可以做其它正常的Emacs操作,不过做这些操作之前最好用q命令退出调试.q命令可以*Backtrace*缓冲区 并且退出调试.默认情况下,退出只是隐藏*Backtrace*缓冲区,也就是该缓冲区没有被杀掉.要杀掉的话就设定
debugger-bury-or-kill为'kill.(and (require 'debug) (setq debugger-bury-or-kill 'kill))
在进入调试的时候会根据
eval-expression-debug-on-error临时设置debug-on-error变量,如果前者为non-nil,那么debug-on-error就会为t.这意味如果在调试的时候出现了更多错误,
Emacs将会触发另外的backtraces,如果不想这样的话可以在debugger-mode-hook里面把eval-expression-debug-on-error设置为nil或者把debug-on-error设置为nil.关于如何查看调试器
Backtrace缓冲区 展示运行中的函数以及它们的参数值,可以该缓冲区在上面通过移动Emacs指针到对应的行来选择一个栈帧(stack frame),栈帧是指Lisp直译器储存特定函数的调用信息(information about a particular invocation of a function)的位置.正在工作的栈帧被认为是当前帧(我也不太知道怎么翻译和理解,原文: The frame whose line point is on is considered the “current frame”.),只有一些调试命令可以操作当前栈帧.
Backtrace的栈帧是倒着的,也就是要从底往上读(read from bottom up)才是正确的执行顺序,也就是正序第一行就是当前栈帧.某些行的前面会有星号,一个星号表示一个函数调用的出口,该栈帧会在这个出口再次调用调试器,没错,调试器是一个递归编辑,每次进入一个栈帧就是进入一个子调试器,简单点就是调试器会在带星号的栈帧停下进行调试.
有一些函数名字会有下划线,这意味着调试器知道它们的源代码位置,可以通过鼠标点击或者指针加
<RET>来浏览源代码.其中是没有发生报错和因为报错而进入调试模式,两种的顶行显示是不一样的.
(setq debug-on-error t) (defun raise-error (a) (+ a nil)) (raise-error (raise-error 1))
Figure 1: 报错的时候
上面可以看到一些行的前面有些"单词"会画上下划线,那些都是
Lisp定义的函数或者宏,也是执行时候所调用的内容,从下往上看就是调用顺序.调试的时候可以根据这个"大纲"来快速定位问题发生的位置.
(defun no-error (a) (+ a 1)) (debug-on-entry 'no-error) (no-error (no-error 1))
Figure 2: 没有错误的时候
两者的提示是不一样的,除此以外,有一些命令在报错的时候是不可以执行的.比如
r命令就不可以,因为错误是不能返回的.调试器本身一定要经过编译运行,因为需要假设调试器自身需要使用多少个栈帧.如果是直译运行调试器,假设就会失败.
- 调试器命令 (Debugger Commands)
如果能理解 Emacs Lisp的调试器是一个递归编辑,每一个进入栈帧都是进入一个子调试器,文档上的说明就很好理解.尽管如此我还是要用自己的话总结一下(我没有了解以前可是一头雾水).
- c: 执行并且退出当前栈帧的调试,在下一个星号标记的栈帧处停止.
- d: 进入当前栈帧并且给该栈帧添加星号,
debug-on-entry实际上就是给指定的函数添加星号.进入后可以通过c命令跳出. - b: 给当前栈帧添加星号.
- u: 取消当前栈帧的星号.
- j: 给当前栈帧添加星号然后和c命令一样执行,不过会无视
debug-on-entry设定的星号(或者说临时禁止所有函数的break-on-entry).c和j的区别可以通过调试上面没有错误的例子来了解一下. - e: 在
minibuffer读取Lisp表达式并且(如果可以的划在当前词法环境)进行运算以及在回显区域(area echo)打印结果.调试器会在外部临时储存和恢复运行时变量值,所以可以随意检查和更改运行时的值. - R: 和e命令一样,不同的是R命令会储存计算结果到
*Debugger-record*. - q: 终止调试器(最开始的调试器),返回
Emacs的top-level. - r: 在带星号的栈帧返回时指定它的返回值,用于
mock. - l: 显示一个函数列表,这些函数都是会唤醒调试器的.
- v: 切换显示当前栈帧的本地变量.
- 唤醒调试器 (Invoking the Debugger)
关于
debug函数的细节.该函数的第一个参数可以用来改变*Backtrace*顶部的提示信息 "Debugger entered–XXX".具体看文档就好. - 调试器内部 (Internals of Debugger)
关于调试器内部使用的函数和变量.
debugger变量: 用来指定调用debug函数时候使用的参数,默认是debug,参考debug函数的细节,M-: (info "(elisp) Internals of Debugger").backtrace函数:debug函数使用该函数给*Backtrace*缓冲区填充,它是用C语言写的,因为必须要访问栈来判断函数调用是否active,返回值总是nil,该函数的输出默认到standard-output的.debug-on-next-call变量:non-nil表示在下一个eval, apply 或者 funcall之前调用调试器,进入调试器后把它设置为nil.调试器的d命令就是通过设置这个变量来工作.如果直接手动设置该变量会进入调试器,可以试试.backtrace-debug函数: 设置LEVEL级别的栈帧的debug-on-exit flag为FLAG,LEVEL和FLAG是函数的参数.FLAG为non-nil则是说在当前栈帧结束后进入调试器.command-debug-status变量: 记录当前交互命令的调试状态,每一次交互式调用命令就会把这个变量绑定为nil.调试器可以设置这个变量来给在调试时引发的新调试器调用(debugger invocation)留下信息.backtrace-frame函数: 返回第FRAME-NUMBER层级栈帧的信息.
使用 Edebug 调试
与 Debugger 相比, Edebug 更加适合用于阅读源码, Debugger 更多是输出回溯(backtrace)信息来进行调试,
这意味着如果要查看某处代码的运行细节,就得在那部分代码抛出异常进入调试模式,具体操作是在代码处添加 (debug),
然而修改别人的库/代码可是不是一件好事.
Edebug 作为一个源码级调试器(source-level debugger) 可以在不修改原有代码的前提下逐步执行,观察执行过程.
Edebug
Emacs Lisp 的代码级调试(source-level debugger),比 Debugger 强大好多.
- 使用Edebug (Using Edebug)
使用
Edebug调试需要先instrumentLisp代码,最简单的做法就是把指针移动到函数或者宏的定义然后执行C-u C-M-x,也就是带前缀参数的eval-defun命令.一旦完成,任何该函数/宏的调用就会激活
Edebug,定义的源代码的缓冲区就会临时变成read-only,当行的左边会有一个箭头表示当前执行的行,然后就可以在上面执行调试命令.和
Debugger一样,每一个list表达式的前面和后面都是点,和变量引用的后面也是点,Edebug可以在这些点上面停止执行,叫做停止点(stop points).可以通过
<SPC>来执行直到下一个停止点,当Edebug在一个表达式后面停止执行,就会在回显区域里面显示表达式的值.其它常用命令有:
- b: 在停止点设置
breakpoint - g: 执行直到到达一个
breakpoint - q: 退出
Edebug - ?:
Edebug的帮助命令
- b: 在停止点设置
- Instrumenting
Instrumenting代码其实就是给源代码插入额外的代码(当然并没有修改源代码),用来在合适的地方唤醒Edebug.如果要移除代码的
instrumentation,只要以不添加instrumentation的方式重新运行一遍就好了.(在instrumenting之后修改源代码会导致instrumentation失效).如果接移除单个定义的
instrumentation,可以在定义上面执行C-M-x,也就是eval-defun命令.可以通过
M-x edebug-all-defs来切换edebug-all-defs变量,该变量可以控制eval-defun是否使用前缀参数来instrument定义,nil表示需要.如果
edebug-all-defs为non-nil,那么直接移除单个定义的instrumentation就需要C-u C-M-x,为non-nil时候还可以配合eval-region, eval-current-buffer 和 eval-buffers命令来批量instrument,如果要批量移除
instrumentations先把edebug-all-defs切换回去再次运行命令就可以了(可以用eval-buffer体验一下).还有一个特定用来控制
eval-region是否instrument的edebug-all-forms.M-x edebug-eval-top-level-form会无视edebug-all-defs和edebug-all-forms的值来进行instrumenting.edebug-defun是它的别名.当
Edebug激活的时候,命令I (edebug-instrument-callee) 可以根据调用来instrument定义(当然只能是没有添加instrumentation的情况下).比如,
(defun fac (n) (if (< 0 n) (* n (fac (1- n))) (return-res 1))) (defun return-res (n) (+ n 0)) (fac 3)
如果
edebug激活了,上面instrument fac只会给fac添加instrumentation,调试的时候可以把指针移动到(return-res 1)的前面使用I命令,这样在
fac调用(return-res 1)的时候就会进入return-res.当然只能在Edebug知道定义源代码位置的时候才可以使用这命令.如果想直接跳进
return-res,可以直接使用i命令,它会先是instrument定义并且直接跳转.Edebug 知道如何
instrument所有标准special forms,但是不能靠它自己判断用户定义宏(user-defined macro)的参数信息.因此唯一通过使用
Edebug specifications来提供信息.当Edebug第一次instrument代码,它会运行edebug-setup-hook钩子然后把这个钩子设置为nil,可以使用这个钩子提供Edebug specification. - Edebug执行模式 (Edebug Execution Modes)
Edebug有很多个执行模式,比如可以手动逐步执行,可以自动逐步执行,可以手动逐个断点执行,当然也可以自动逐个断点执行,甚至可以无视断点执行.在两种情况下可以设定模式,分别是使用对应模式的命令来设定调试时的模式和设定调试开始时的模式.
文档没有明说,
Edebug和Debugger一样都是递归编辑,每运算一次表达式都是进入Edebug,自己要清楚这一点.下面是命令,
- S: Stop,不再执行调试,等待调试命令.
<SPC>: Step,在下一个停止点处停止.- n: Next,在下一个表达式的后面的停止点处停止.
- t: Trace,默认每一秒后运行到下一个停止点,就是
<SPC>的自动模式. - T: Rapid trace,
t的快速执行模式,中间没有停顿. - g: Go,运行到下一个断点.
- c: Continue,在每个断点处停留一秒,然后继续,就是
g的自动模式. - C: Rapid continue:
c的快速执行模式,中间没有停顿. - G: Go non-stop,无视断点执行,可以通过
S停止.
可以通过
edebug-set-initial-mode命令设置edebug-initial-mode来设定Edebug的初始模式.对于自动模式的停顿时间,可以通过设置
edebug-sit-for-seconds变量来修改. - 跳转 (Jumping)
跳转就是指执行到哪个停止点.命令如下,
- h: 执行到下一个断点.
- f: 执行完一个表达式.
- o: 执行完一个
containing sexp,也就是跳出(step out). - i: 跳进一个停字点后面函数或者宏(step in).
具体说明还是看文档吧.
- Edebug 的杂项命令 (Edebug Misc)
- ?: 帮助命令.
- C-]: 终止(abort)一个
level返回到上一个command level. - q: 返回到
top level,也就是停止edebug调试器.然而,被unwind-protect或者condition-case保护的instrumented code会恢复edebug. - Q: 像
q一样,不过无视被保护的代码. - r: 重新显示最近的表达式的结果.
- d: 显示
backtrace,这个backtrace不像标准的Debugger那样执行命令.继续执行的时候会自动关闭backtrace.
- 中断 (Breaks)
一旦
Edebug开始,除了step mode可以在下一个停止点停止执行,还有其它三种方法可以停止执行.- 断点 (Breakpoints)
可以在任何一个停止点设置断点.关于断点的命令如下:
- b: 在停止点设置断点.如果使用了前缀参数,断点就是临时的,停止调试后断点就失效了.
- u: 取消断点.
x CONDITION <RET>: 条件断点,只有CONDITION结果为non-nil才会停止.同样,如果使用了前缀参数的话就是临时的.- B: 移动到当前定义的下一个断点.
- 全局中断条件 (Global Break Condition)
全局中断条件不管在哪里,只要条件符合就停止调试的执行.
Edebug会在每个停止点运算全局中断条件的值,如果结果为non-nil就停止或者暂停执行,和断点一样.然而如果运行条件的时候报错是不会停止.条件表达式储存在
edebug-global-break-condition变量里面.可以在已经激活了Edebug的源代码缓冲区中使用X命令来添加条件表达式,也可以使用C-x X X按键(绑定edebug-set-global-break-condition)在任何缓冲区中添加条件表达式.全局中断条件很容易让调试变慢,如果不使用的话要把
edebug-global-break-condition设置为nil. - 源码断点 (Source Breakpoints)
上面通过b命令设置的断点会在
reinstrument定义后被遗忘(除了Emacs,人也可能会忘记),这个时候可以使用"源码断点"(和Debugger的(debug)的用法一样).在想要断点的地方插入
(edebug)表达式,如果定义没有被instrument,那么遇到(edebug)就会转而调用debug函数.可以使用g命令跳转到这种断点身上.
- 断点 (Breakpoints)
- 捕捉错误 (Trapping Errors)
设置
edebug-on-error或者edebug-on-quit可以快速定位没有被处理错误(unhandled errors),就拿edebug-on-error来说,它和debug-on-error的设置类似,用下面的例子来演示.(setq edebug-on-error t) ;; 当然要先 instrument fac 的定义,这是必须的 (defun fac (n) (if (< 0 n) (* n (fac (1- n))) (return-res nil))) ; 错误在这里,会在 (return-res nil)前面的停止点停下 (defun return-res (n) (+ n 0)) (fac 3)
在这个例子中可以使用
Rapid Trace模式来直接运行到错误发生的地方,会发生和注释一样的结果.如果把edebug-on-error设置为nil,是不会停止到错误发生的地方,而是直接在回显区域显示信息. - Edebug Views
一些用来浏览已经激活
Edebug的缓冲区和窗口状态的各个方面.外部窗口配置(outside window configuration) 集合了窗口(windows)和在
Edebug外部有效(in effect)的内容(个人认为像是发生调用的地方).- v: 切换到外部窗口配置中.
- p: 临时切换到等待
N秒(可以通过C-u N p设定暂停时间)后返回Edebug中. - w: 把点(point)返回到到源码缓冲区当前的停止点上.
- W: 切换是否保存和恢复外部窗口配置,有前缀参数的话就表明只是对被选择窗口切换是否保存和恢复.
- 运算 (Edebug Eval)
当
Edebug启用的时候,你可以像在没有运行Edebug的情况下运算表达式.e EXP <RET>,在Edebug外部的上下文运算表达式EXP.这样Edebug可以减少它和运算之间的冲突/干扰(interference).M-: EXP <RET>,在Edebug的上下文中运算表达式EXP.C-x C-e,在Edebug外面运算点之前表达式.
Edebug支持运算引用由cl.el里面lexical-let, macrolet和symbol-macrolet词法绑定的symbols的表达式. - 运算列表缓冲区 (Eval List)
可以使用运算列表缓冲区(evaluation list buffer),叫做
*edebug*的缓冲区,来交互运算表达式.也可以设置表达式的运算列表(evaluation list),这样它们在每次Edebug更新显示的时候都会更新.在
Edebug激活之后使用E命令切换到运算列表缓冲区 –*edebug*,然后在里面添加元素表达式组(evaluation list groups).一个运算表达式组包含一到多条表达式,用注释行(comment lines)分组,如下(point) ; whatever comment you like, but the ';' must be without any prefix. this-command ; undefined ;再使用
C-c C-u根据*edebug*缓冲区的内容建立新的运算列表(evaluation list),结果如下(point) 264 ;------------------------------------------------------------------------------------------------------ this-command eval-last-sexp ;------------------------------------------------------------------------------------------------------ undefined "Symbol's value as variable is void: undefined" ;------------------------------------------------------------------------------------------------------使用
C-c C-u运算的话,只有每个组的第一条表达式会执行,结果会显示在第二行,其它行会被删除.如果运算时候发生错误,那么错误信息就会作为结果.还可以把指针移动到组内然后使用
C-c C-d删除分组.除了
C-c C-u,还有其它运行模式,看文档就好. - Edebug 的打印 (Printing in Edebug)
如果尝试在
Edebug中打印一个包含循环列表结构的值,那么可能会发生错误.克服(cope with)循环结构的一个方法就是把
print-length或者print-level来分断显示.Edebug已经帮你做好了.它把这两个变量分别绑定到
edebug-print-length和edebug-print-level,默认值都是50.也可以通过设置
print-circle为non-nil来打印那种有着共享元素的循环结构体.比如
(setq a '(x y)) (setcar a a) ;; 显示为 #1=(#1# y), #1= 表示用1标记结构,#1#表示引用前一个被标记的结构.这个标记可以用在任何列表或者向量的共享元素上.
相应的
Edebug有edebug-print-circle,会把这个变量的值绑定给print-circle. - 运行步骤缓冲区 (Trace Buffer)
Edebug可以把执行步骤记录在叫做*edebug-trace*的缓冲区中,一个函数调用和返回的日志,显示函数的名字和它们的参数和值.只要把edebug-trace设置为non-nil就可以启用该功能.比如上面
fac的记录会是这样,{ fac args: (3) :{ fac args: (2) ::{ fac args: (1) :::{ fac args: (0) :::} fac result: 1 ::} fac result: 1 :} fac result: 2 } fac result: 6{和}分别表示函数的入口和出口,:表示递归深度,同一深度的{对应同样深度的}.可以通过重新定义edebug-print-trace-before和edebug-print-trace-after函数来自定义记录函数入口和出口显示的条目.edebug-tracing和edebug-trace函数在*edebug*中插入行,不管是否启用Edebug.插入行也会自动滚动窗口来显示最新行. - 覆盖测试 (Coverage Testing)
Edebug还提供不完全的覆盖参数和执行频次(execution frequency)的显示.覆盖参数的原理就是比较每个表达式的当前结果和上一次结果,如果返回的两个结果不一样,这个表达式被覆盖了.
覆盖参数就是需要在大量的各种不同条件下执行程序,并且观察程序是否符合预期,
Edebug会在足够的尝试后告诉开发人员是否每个form返回两个不同结果.覆盖测试会让执行变慢,
edebug-test-coverage为non-nil的时候测试所有被调试的表达式.不管是否启用了覆盖测试或者是否
Go-nonstop执行模式,instrumented function的所有执行都会伴随频次计数(frequency counting)的执行."C-x X =" (edebug-display-freq-count) 可以显示一个定义的覆盖信息和频次计数. 单纯 = (edebug-temp-display-freq-count) 会临时显示同样的信息,知道输入了另外一个按键.
还是用那个老例子说明,当然还有先
instrument fac.(setq edebug-test-coverage t) ;; 1. instrument ;; 3. move cursor on definition and execute edebug-display-freq-count command (defun fac (n) (if (< 0 n) (* n (fac (1- n))) (return-res 1))) (defun return-res (n) (+ n 0)) ;; 2. then execute, can use Rapid Trace mode for a quick travel (fac 5)
edebug-display-freq-count的用法: 先调试运行一遍(否则全部数据为0),然后把指针移动到instrumentd定义中,然后执行该命令显示覆盖信息和执行频次.结果如下,
(setq edebug-test-coverage t) ;; 1. instrument ;; 3. move cursor on definition and execute edebug-display-freq-count command (defun fac (n) (if (< 0 n) ;#6 (* n (fac (1- n))) ;# 5 (return-res 1))) ;# 1 = 6 (defun return-res (n) (+ n 0)) (fac 5)
执行频次会出现在表达式前面的
(,后面的)或者是变量最后一个字母的底下.为了简化显示,如果表达式的频次计数等于同一行中前一个表达式的频次,那么这个频次就不显示.跟在频次计数后面的 "=" 号表示表达式每次执行结果都是一样,也就是该表达式没有被覆盖.
再以文档上的例子来做说明,
(defun fac (n) ;; (edebug) 只是返回了一次,不算覆盖. ;; (if (= n 0) ... 执行了6次,每次结果都是 =nil=. ;; = 和频次计数是没有关系的,切记. (if (= n 0) (edebug)) ;#6 1 = =6 ;; (< 0 n) 和 (if (< 0 n) ... 的执行频次都是一样,所以 (< 0 n) 没有显示频次 ;; (< 0 n) 每次的结果都为 t, (if (< 0 n) ;#6 ;; (* n (fac ... 以及它的子表达式的执行频次都是一样的(并且都在同一行),所以简化显示只显示了第一个表达式的执行频次. (* n (fac (1- n))) ;# 5 1)) ;# 6 (fac 5)
还可以在
Edebug调试中的时候使用 = 命令临时显示覆盖信息和频次计数. - 外部上下文 (The Outside Context)
对于调试中的程序来说,
Edebug尝试变得透明,然而没完全成功.也尝试过在我们运行e命令或者使用运算列表缓冲区的时候通过临时恢复外部上下文来变得透明.这个章节主要介绍
Edebug储存什么上下文并且为什么完全透明.- 检查是否停止 (Checking Whether to Stop)
当进入
Edebug的时候,它在决定是否产生执行信息(trace information)或者停止程序之前就需要储存和恢复一定的数据.- 增加
max-lisp-eval-depth和max-specpdl-size都可以减少Edebug对栈的影响.不过这样很容易用完栈的空间. - 键盘宏的执行状态回被保存和恢复.当
Edebug激活的时候,executing-kbd-macro会设置为nil, 除非edebug-continue-kbd-macro为non-nil.
- 增加
- Edebug显示更新 (Edebug Display Update)
当
Edebug需要显示一些信息的时候,它会储存Edebug外部的当前窗口配置.当退出Edebug的时候它就会恢复之前的窗口配置.只有在
Edebug暂停的时候Emacs才会重新显示(redisplay).通常,当继续执行的时候,程序会在断点处或者单步执行(stepping)后重新进入(re-enter)Edebug,中间没有任何停顿或者输入读取.在这写例子中,
Emacs没有任何机会重新显示外部配置.因此,你所看见的就是和最后一次激活Edebug时的同一个窗口配置,没有任何中断(interruption).用于显示信息的
Edebug入口也会储存和恢复以下数据(尽管它们中的一些会因为error and quit signal的发生而有意不储存).- 当前的缓冲区,点的位置(point positions),marks和已经被储存的和恢复的数据.
如果
edebug-save-windows是non-nil,那么外部窗口配置就会被储存和恢复.不会在error或者quit的时候恢复,不过即使save-excursion激活时候出现了error或者quit,外部被选中的窗口还是会被重新选择.如果
edebug-save-windows是一个列表,只有被列出的窗口会被储存和恢复.窗口开始以源代码缓冲区的水平滚动位置是不会储存的.然而,它们仍然会被保留并且显示在Edebug中.- 如果
edebug-save-displayed-buffer-points为non-nil,那么每个显示的缓冲区的点都会被保留和恢复. overlay-arrow-position和overlay-arrow-string会被储存和恢复,因此可以安全地从在同一个缓冲区中任何地方的递归编辑(recursive edit)唤醒Edebug.cursor-in-echo-area局部绑定nil,这样指针(cursor)会显示到窗口上.
- Edebug Recursive Edit
当进入
Edebug并且读取命令时,会储存和之后恢复以下额外的数据.- 当前的匹配数据(Match Data)
last-command, this-command, last-command-event, last-input-event, last-event-frame, last-nonmenu-event and trace-mouse.Edebug的命令不会在Edebug之外影响它们.Edebug的执行命令可以改变this-command-keys返回key sequence,并且没有办法从Lisp中重置.不能储存和恢复unread-command-events的值.command-history记录着Edebug中执行的命令,在北少数情况下这个可以修改执行(execution).Edebug中的递归深度比外部的递归深度要深,当时对于自动更新的运算列表窗口来说是错的.standard-output和standard-input会被recursive-edit命令绑定为nil,不过Edebug会在运算中临时恢复它们.- 键盘宏的定义的状态会被保存和恢复.当激活
Edebug时,defining-kbd-macro会绑定到edebug-continue-kbd-macro.
- 检查是否停止 (Checking Whether to Stop)
- Edebug and Macros
- Instrumenting Macro Calls
当
Edebuginstrument一个调用Lisp宏的表达式,它需要额外的宏信息来保证正确工作.那是因为没有一个先验 (
a-priori)的方法来判断会运算宏调用的哪些子表达式(宏体可能会发生运算,或者拓展时候发生运算,又或者之后任何时刻).因此必须为每一个
Edebug会遇到的宏定义Edebug specification来解释宏的调用格式.做法是给宏定义添加一个debug声明.Edebug specification告诉Edebug宏的哪些部分需要运算.关于如何为宏定义Edebug specification,看这里M-: (info "(elisp) Defining Macros").当
instrument代码的时候要保证Edebug知道specification.如果instrument一个文件中的函数,并且这个函数引用了使用eval-when-compile导入另外一个文件的宏定义,那么就需要load一遍那个文件.除了上面的方法外,还可以使用
def-edebug-spec为宏定义Edebug specification.添加debug更受欢迎以及更方便.不过def-edebug-spec可以为C实现的special forms定义Edebug specifications.如果一个宏没有
Edebug specification,edebug-eval-macro-args就会参与进来,如果该变量为nil(默认),运算的时候不会为任何一个参数instrument;否则全员instrumented. - Specification List
自己看文档
- Backtracking
自己看文档
- Specification Examples
自己看文档
- Instrumenting Macro Calls
- Edebug Options
大部份上面都提过,自己看文档
语法错误 (Syntax Errors)
Lisp reader 会提示非法语法,不过不会提示问题发生的地方.对于 Lisp 来说,最常见的语法错误就是括号不匹配.
- 多余的开括号 (Excess Open)
多余开括号的错误提示是
End of file during parsing.- 移动指针到发生错误的文件的最后执行
C-u C-M-u,以此找到错误的函数. - 研究错误的函数,可以根据现有的缩进来判断.
- 保证函数的定义有足够的闭括号(一般都是先移动到函数的结尾插入一个闭括号,不要使用
C-M-e移动,括号不平衡时会报错),否则C-M-q会报错或者重新缩进到文件最后. - 移动到函数定义的开始处使用
C-M-q来重新缩进并且过程哪部分发生右动,通常发生右偏移的起点的前一个点的附近就是少了闭括号或者多了开括号的地方(当然这不一定是对的).一定要细读代码. - 一旦找到了就用
C-_撤销(undo)C-M-q,恢复到旧的缩进. - 再次移动到函数定义的起点执行
C-M-q来检查缩进是否正常,如果缩进没有发生改变就证明括号匹配了.
- 移动指针到发生错误的文件的最后执行
- 多余的闭括号 (Excess Close)
多余闭括号的错误提示是
Invalid read syntax: ")".- 移动指针到发生错误的文件的起点执行
C-u -1 C-M-u查找第一个括号不平衡的函数. - 在函数定义的起点使用
C-M-f来匹配闭括号,执行让指针移动到定义应该结束的地方.很有可能就找到多余的闭括号. - 如果上面还没有找到问题,那么就在函数的定义起点执行
C-M-q进行缩进并且观察哪部分移动,通常发生左偏移的起点的前面一个点的附近就是多了闭括号或者少了开括号的地方(当然这不一定是对的).一定要细读代码. - 一旦找到了就用
C-_撤销C-M-q,恢复到旧缩进. - 再次移动到函数定义的起点执行
C-M-q来检查缩进是否正常,如果缩进没有发生改变就证明括号匹配了.
- 移动指针到发生错误的文件的起点执行
覆盖参数 (Test Coverage)
除了 Edebug 可以做覆盖参数,还可以使用 testcover 库来做.
M-x testcover-start <RET> FILE <RET>对整个文件进行instrument.- 调用一到多次来测试代码.
M-x testcover-mark-all高亮覆盖率低的代码.M-x testcover-next-mark移动到下一个高亮点(next highlighted spot).
关于高亮的说明,
- 红色(red)高亮点是指
form完全没被执行过.如果forms不能完成运行就会跳过红色高亮,比如error. - 棕色(brown)高亮点是指
form总是运行得到相同结果.如果forms是本来就是预期得到相同的值就跳过棕色高亮,比如(setq x 14).
testcover 库还可以提供 1value 和 noreturn form 来在特定情况下使用.
性能测试 (Profiling)
针对不同情况有以下4种方法可以测试性能,
Emacs的内置支持- 步骤
M-x profiler-start开始测试,可以选择测试的指标(cpu, mem, cpu+mem).- 做想要测试的动作.
M-x profiler-report显示测试结果.- 结束后关闭测试
M-x profiler-stop.
如何读懂结果
Figure 3: cpu+mem usage
上面的图里分别是内存和cpu的使用率.
每一行的内容项分别为 调用的函数名字, 函数的资源使用 以及 函数执行时间占总测试时间的百分比.
如果行的左边有
+号,那么可以对着行输入<RET>进行展开,里面有这一行函数调用的subroutines.可以通过C-u <RET>一次展开,再次<RET>可以再次折叠.可以使用
j或者mouse-2跳转到函数的定义.使用
d显示函数的文档.使用
C-x C-w保存测试结果.使用 = 对比两份测试结果.
- 步骤
elp库可以做为
profile的替代方案.bechmark库使用
benchmark-run和benchmark-run-compiled单独测试Emacs Lisp forms.调试
C实现的功能需要在
Emacs编译的时候启用configure选项的--enable-profiling,完成后会生成一份
gmon.out文档,可以使用Linux的gprof命令来检测.该特性主要用来调试
Emacs的,并且会停止上面描述的Lisp-levelM-x profiler-...命令.
ERT: Emacs Lisp Regression Testing
文档位于 M-: (info "ert").
ERT 是 Emacs Lisp 的一个自动化测试库.主要功能有测试定义,运行测试,输出测试结果以及交互调试测试错误(debugging for test failures).
交互调试测试错误这个真的是好东西.
事实上 ERT 还适用于测试驱动开发(Test-driven development)模式和传统软件开发模式.
- 简介 (Introduction)
ERT允许组合函数,宏,变量以及其它Lisp的东西(construct)来定义测试(tests).测试只不过是调用其它的代码并且检查它们是否与期望的行为一样的Lisp代码.ERT会跟踪定义的测试和提供一些运行测试的命令,用于检测定义是否通过测试.比如
pp.el的文档中有这么一些测试用例,;; (pp-to-string '(quote quote)) ; expected: "'quote" ;; (pp-to-string '((quote a) (quote b))) ; expected: "('a 'b)\n" ;; (pp-to-string '('a 'b)) ; same as above
用
ERT写对应上面测试就是这样,(ert-deftest pp-test-quote () "Tests the rendering of `quote' symbols in `pp-to-string'." (should (equal (pp-to-string '(quote quote)) "'quote")) (should (equal (pp-to-string '((quote a) (quote b))) "('a 'b)\n")) (should (equal (pp-to-string '('a 'b)) "('a 'b)\n")))
ert-deftest和defun的用法比较像,定义一个名字叫pp-test-quote单元测试,加载后可以使用M-x ert <RET> t <RET>来运行测试.如果三个调用结果全员
non-nil的话,测试就通过.上面的should宏就是ert版本的断言语句(assertion).每个测试应该有一个用来描述测试功能的名字,比如上面的
pp-test-quote就是测试quote,测试的名字不会和函数和变量放在同一个命名空间,所以可以随意选择(还是要符合Emacs Lisp的规范,加上前缀表明属于哪一个包).当测试不通过,
ERT就会显示测试的名字,还有测试的时候可以根据名字选择测试.第一行的
()目前没有任何意义,以后可能会用它来做拓展,同时也是为了接近defun的写法.文档 (
docstring) 用来描述所测试的功能点(feature).在交互测试中,如果测试失败,文档的第一行会被显示出来,当然文档是可选的.测试体,也就是里面那三个表达式,可以是任何
Lisp代码,可能的话可以有副作用,如果有,不论测试是否通过,应该完成后进行清理,还原成测试之前的状态. - 如何运行测试 (How to Run Tests)
有两种运行方式,第一种是在启动
Emacs后交互运行,也就是上面章节提到的方法;第二种就是在命令行中运行,也就是batch mode.前者比较方便,后者可以与用户的自定义独立开;并且允许从
makefiles中读取测试并且运行,能够根据不同版本的Emacs编写测试.- 交互运行测试 (Running Tests Interactively)
使用
M-x ert <RET> t <RET>来运行所有已经加载了的测试.其中t(也可以是字符串)是测试选择器(test selectors),还有别的选择器可以选.假设现有测试如下,
(ert-deftest pp-test-quote () "Tests the rendering of `quote' symbols in `pp-to-string'." (should (equal (pp-to-string '(quote quote)) "'quote")) (should (equal (pp-to-string '((quote a) (quote b))) "('a 'b)\n")) (should (equal (pp-to-string '('a 'b)) "('a 'b)\n"))) (ert-deftest addition-test () "Addition" (should (equal (+ 1 2) 4)))
其中
addition-test是注定测试不通过的,Selector: t Passed: 1 Failed: 1 (1 unexpected) Skipped: 0 Total: 2/2 Started at: 2018-11-01 00:00:17+0800 Finished. Finished at: 2018-11-01 00:00:17+0800 F. F addition-test Addition (ert-test-failed ((should (equal (+ 1 2) 4)) :form (equal 3 4) :value nil :explanation (different-atoms (3 "#x3" "?") (4 "#x4" "?"))))上面运行了两个测试,其中
addition-test失败了,另外一个测试通过.F和.分别表示一个失败的测试和一个通过的测试.上面的
:form指的是(equal (+ 1 2) 4)化简(reduced to)的结果,为(equal 3 4),:value是(3 4)= 的结果.和上面显示的一样,失败的测试会显示出细节,其中
:explanation叫做解释 (Explanation).M-: (info "(ert) Understanding Explanations")有关于如何理解解释.在测试结果的缓冲区中,可以做以下命令,
TAB和S-TAB在按钮之间循环,函数和宏就是按钮.RET在按钮处跳转到按钮的定义.r重新运行指针附近的测试.d使用调试器重新运行..跳转到点附近函数或者宏的定义,和RET差不多.b显示失败测试的backtrace.l显示测试中的shouldforms.m假如测试中使用了message函数产生信息,可以使用该命令进行显示.L失败测试的显示的表达式会根据print-length和print-level进行简短化.该命令可以增加显示限制.
- 以 Batch Mode 运行测试 (Running Tests in Batch Mode)
可以从命令行或者脚本,又或者是
makefiles自动运行测试.有两个函数可以做这件事情,分别是ert-run-tests-batch和ert-run-tests-batch-and-exit.在命令行下面可以这么用:
emacs -batch -l ert -l /path/to/tests.el -f ert-run-tests-batch-and-exit
如果测试全员通过就返回
0状态码,否则就是非0状态码.还可以先把运行结果重定向到别的文件,比如 output.log,然后使用
ert-summarize-tests-batch-and-exit产生总结信息,emacs -batch -l ert -l tests.el -f ert-run-tests-batch-and-exit >& output.log emacs -batch -l ert -f ert-summarize-tests-batch-and-exit output.log
如果
Emacs没有和ERT一起分发,那么需要-L /path/to/ert来先加载ert库,可能还需要用-L /path/to/tests.el来确保测试文件被加载. - 测试选择器 (Test Selectors)
运行
ert的时候需要选择测试选择器,也就是运行符合条件的测试.nil,不选择任何测试.t,选择所有测试.:new,选择所有还没被运行过测试.:failed和:passed,分别选择最近的测试结果为failed和passed的测试.:expected和:unexpected,分别选择最近测试结果为expected和unexpected的测试.- 正则表达式字符串,匹配测试名字.
- 测试,也就是
ert-test数据类型. symbol,根据测试名字的symbol进行选择.(member TESTS...),根据列表里面的测试进行选择.(eql TEST),根据测试名字的symbol选择选择.(and SELECTORS... ),选择符合所有SELECTORS的测试.(or SELECTORS... ),选择符合所有SELECTORS其中之一的测试.(not SELECTORS... ),选择不符合所有SELECTORS的测试.(tag TAG),选择所有拥有TAG的测试.Tags可以在定义测试的时候定义,是可选的.(satisfies PREDICATE), 选择所有满足PREDICATE的测试,PREDICATE是一个接受测试做为参数并且返回布尔值的函数,如果结果为non-nil就选择该测试.
- 交互运行测试 (Running Tests Interactively)
- 如何编写测试 (How to Write Tests)
在缓冲区通过
ert-deftest定义测试,再用eval-defun或者compile-defun运行测试.或者保存到文件中并且加载,还可以先编译.加载后可以像
find-function查找函数那样查找测试定义.- should 宏 (The should Macro)
和断言语句差不多,就是多了参数分析和记录
ERT会显示的信息.上面稍微提到过
should的用法,就不再说了.除了should还有should-not,should-error两个宏.分别用来检查判断是否返回
nil和是否引发对应的异常.下面分别是关于减法和除法的测试的例子,都是可以通过的.(ert-deftest subtraction-test () (should-not (equal (+ 1 2) 4))) (ert-deftest divide-by-zero () (should-error (/ 1 0) :type 'arith-error))
其中
should-error的:type参数是可选的,如果不写就意味着接受所有类型的错误.should-error可以返回错误的描述,用来做额外的检查.错误描述的形式为(ERROR-SYMBOL . DATA). - 预期的错误 (Expected Failures)
有些
bugs难以修复或者是不太重要的,这些bugs会被留下来,称之为已知bugs(known bugs).如果有测试用例(test case)触发了
bugs发生报错,ERT就在你每次运行测试的时候进行警告.对于已知bugs则不会.用文档上面例子,如下
(ert-deftest future-bug () "Test `time-forward' with negative arguments. Since this functionality isn't implemented, the test is known to fail." :expected-result :failed (time-forward -1))
测试上面的例子的时候会显示
f表示future-bug是一个已知bug,仍然是一个失败的测试,不过不会显示它的错误细节.如果没有修复某个
bug的意愿,可以把它的测试删除,把这个bug做为一个accepted feature,这也是标记已bug的一种手段.对于意外通过(
pass unexpectedly) 的测试和意外错误(unexpected failures)来说, 它们的ERT警告都是一样.这样的话就算无意修复了bug,也会知道要移除:expected-result从句来关闭相应的错误提示.:expected-result是在加载测试之后运算它的参数的,所以可以在做判断是需要标记为已知bug. - 测试和环境 (Tests and Their Environment)
一些测试需要先决条件(preconditions)才可以运行.比如需要的
Emacs feature需要编译才有,参数函数需要一个额外的二进制包并且参数机器上没有这包,等等.这种情况下可以利用
skip-unless宏根据条件来跳过测试.(ert-deftest test-dbus () "A test that checks D-BUS functionality." (skip-unless (featurep 'dbusbind)) ;; do the test)
测试结果不应该取决于当前的环境状态,并且每个测试应该保持结束时的环境和开始测试时的环境一样,特别是不能取决于
=Emacs的自定义变量和钩子.如果必须要改变
Emacs的状态或者外部状态(比如文件),那么应该在测试结束之前撤销这些更改,不管是否通过.这么做的目的是防止因为环境变动导致测试失效,或者导致在特定条件下(circumstances)发生错误并且难以重现(reproduce).
当然不是说不能有副作用(side effect),最好使用
let绑定,这样副作用的范围就只能在测试阶段中了;也可以为每个测试设置不同的配置.正如上面说的,在测试之后撤销对环境的更改,可以这么做,
- 对缓冲区(buffer)或者窗口配置(window confiugration)产生副作用,测试的时候应该用
with-temp-buffer临时创建一个缓冲区,用save-window-excursion. - 对于其它方面的可以使用
unwind-protect保证测试之后清理环境. - 对于
*Message*缓冲区,message或者类似的函数会打乱该缓冲区的储存,这个也需要恢复到原来状态.
总的来说就是避免使用
find-file这种可以自定义的命令(当然除了你是真的想测试它),因为这种命令取决于很多其它自定义变量,也就是上面提到的环境.可以使用
with-temp-buffer,insert或者insert-file-contents-literally并且在通过直接运行函数来激活想要的mode(要先设定对应mode的钩子变量为 nil) 来避免file-file的问题. - 对缓冲区(buffer)或者窗口配置(window confiugration)产生副作用,测试的时候应该用
- 编写测试的技巧 (Useful Techniques)
对于没有副作用和环境依赖的函数,基本就是
(should (eql EXPECTED ACTUAL))可以完事,当然也可以(should (eql ACTUAL EXPECTED)),不过前者更受欢迎.对于复杂的测试,比如文档的例子,
(ert-deftest ert-test-record-backtrace () (let ((test (make-ert-test :body (lambda () (ert-fail "foo"))))) (let ((result (ert-run-test test))) (should (ert-test-failed-p result)) (with-temp-buffer (ert--print-backtrace (ert-test-failed-backtrace result)) (goto-char (point-min)) (end-of-line) (let ((first-line (buffer-substring-no-properties (point-min) (point)))) (should (equal first-line " signal(ert-test-failed (\"foo\"))")))))))
先介绍一下这个例子的几个
forms,ert-fail引发测试错误.make-ert-test接受一个函数,返回一个匿名测试.ert-run-test接受一个测试并且运行它,返回测试结果.ert-test-failed-p判断测试结果是否失败.ert--print-backtrace接受测试结果,显示失败测试的结果backtrace.
这个例子就是通过检查
backtrace的第一行来测试ert backtrace的记录功能,只检查第一行是因为是backtrace剩下部分都是依赖于ERT的内部.通过检查第一行就可以检查到
backtrace是否正确捕捉到signal的结果,而signal的结果就是ert-fail的结果.这个例子告诉我们,先在脑海中构建出测试结构,再根据测试结构编写代码,这样再为该代码写测试(tests)就会变得很容易.
假如我们可以重写,这里还有几个可以提一下,
- 如果
ert-run-test只是接受symbol来选择测试的话,可以使用make-symbol来生成临时用的symbol来避免对Emacs造成任何副作用. - 还有
ert--print-backtrace会把backtrace打印到另外一个有固定名字的缓冲区中,这样撤销副作用会比较困难.如果可以选择缓冲区名字就可以与环境或测试独立开,不用担心副作用问题.
以后会在
Emacs中遇到很多没有根据脑海中测试结构写出来的代码,有时候可以重构代码来提供一个方便测试的接口,而且还有一个好处就是,通常这种接口也更加容易使用.这一章节的目的就是 讲如何写出可测试代码 ,网上有不少好的文章,这篇写得十分好.
- should 宏 (The should Macro)
- 如何调试测试 (How to Debug Tests)
- 理解解释 (Understanding Explanations)
在上面交互运行测试中就提到过
:explanation,实际上ERT只会给已经注册了explanation函数的谓词(predicates)提供explanations.比如上面写到的
addition-test中的equal就是这一类谓词,如果把equal换成 "=" 号,那么结果就是这样了,F addition-test Addition (ert-test-failed ((should (equal (+ 1 2) 4)) :form (equal 3 4) :value nil))里面的
different-atoms是所谓的解释,当然还有很多其它类型的解释.还有就是可以自定义
explanation函数. - 交互式调试 (Interactive Debugging)
其中
r,.,l,b,m和d命令都在交互运行测试中有提过.这里再补充两个,D命令,该命令可以选择测试进行删除;- 通过
C-u C-M-xinstrument测试的定义,然后回到ERT缓冲区 通过r或者d调试运行(这里使用的调试器是Debugger).
- 理解解释 (Understanding Explanations)
- 拓展 ERT (Extending ERT)
- 定义 Explanation 函数 (Defining Explanation Functions)
Explanation就是一个接受和谓语一样多参数并且返回一个explanation的函数.返回的结果应该解释为什么会返回这个结果,可以是任何结果,不过一定要可以被详细打印的结果.对于不需要解释的输入则返回
nil.如何定义呢? 文档上大概就是,先找一个用来表示谓词的
symbol,然后为它定义一个函数,最后把该函数设置为symbol的属性.反过来想一下,可以获取
equal的ert-explainer属性观察一下,(get 'equal 'ert-explainer) ; => ert--explain-equal
参考
equal的代码如下,(defun ert--explain-equal (a b) "Explainer function for `equal'." ;; Do a quick comparison in C to avoid running our expensive ;; comparison when possible. (if (equal a b) nil (ert--explain-equal-rec a b))) (put 'equal 'ert-explainer 'ert--explain-equal)
- ERT 的底层代码 (Low-Level Functions for Working with Tests)
ert-run-tests-interactively和ert-run-tests-batch都是基于在ert.el中标记为“Facilities for running a whole set of tests”部分的lower-level代码实现的.如果想要使用
ERT的代码实现一些功能,应该看一下它的lower level代码.ert--开头是指ERT内部使用,ert-开头是指可以被其它代码使用.目前没有完善的API.
- 定义 Explanation 函数 (Defining Explanation Functions)
- 其它测试概念 (Other Testing Concepts)
- Mocks and Stubs
可以先了解一下什么是
mock/stub(叫法不一样而已),最后就是ERT官方没有支持mock/stub,不过el-mock提供了,还可以和ERT混合使用(不能混用才奇怪). - Fixtures and Test Suites
Fixtures主要用来为测试设置和清理测试环境的,包含set-up和tear-down两类函数.Test suites主要把相关测试分成一组,方便运行的时候可以一起运行.然而
ERT都没有这两个功能(其实也没有必要,Lisp的强大可以轻松地解决这两个问题).对于
fixtures,可以利用unwind-protect宏作为subroutine来定义函数或者宏,使用这个定义来实现fixture,文档上有函数版本的伪代码,
(defun my-fixture (body) (unwind-protect (progn [set up] (funcall body)) [tear down])) (ert-deftest my-test () (my-fixture (lambda () [test code])))
宏版本的就自己研究了,都差不多的.
对于
test suites,经常用来为特定模块运行测试或者根据测试的时间长度来少运行慢的测试,定义测试的时候所有相关模块的所有测试使用相同前缀,或者使用:tag,然后通过正则测试选择器或者tag测试选择器运行所有相关测试.
- Mocks and Stubs
读取和打印 (Read and Print)
打印是转换 Lisp 对象为文本形式的操作,而读取就是把文本转换为 Lisp 对象.它们会使用 M-: (info "(elisp) Lisp Data Types") 中描述的打印以及读取语法.
读取和打印的简介 (Streams Intro)
读取一个 Lisp 对象意味着解析文本形式的 Lisp 表达式并且产生对应的 Lisp 对象.这也是 Lisp 代码文件怎么被读取进 Lisp 上并且运行的方式,这种文本叫做对象的读取语法 (read syntax of the object).
打印一个 Lisp 对象意味着产生一个表示该对象的文本,就是把对象转换成它打印表示(printed representation).
读取和打印是一对反操作:通常打印读取文本所得得结果会是同一段文本,反过来,读取一个打印对象时所得的结果通常会得到一个类似的对象.
然而这两操作是不能精确地互逆的,有这三个情况:
- 打印可以得到一些不可被读取的对象,比如,
buffers, windows, frames. subprocesses, markers这些都会得到#开头的文本; 如果尝试读取这些文本会得到错误,没有方法读取这些数据. - 一个对象有多种文本表示.比如,
1和01表示同一个整数,(a b)和(a . (b))表示同样的列表.任何一个都可以被读取,但是打印只有一种可能. - 注释可以出现再一个对象读取序列中间的任何一个点上,不会影响读取的结果,但是会影响打印的结果.
输入流 (Input Streams)
大部份用于读取的 Lisp 函数都会接收一个 input stream,也就是叫做输入流的对象做为参数.输入流指定了在哪以及怎么读取文本的字符.输入流有以下几种:
BUFFER: 从缓冲区读取字符,从点(point)的后面,也就是指针的后面开始读取.MARKER: 在Marker所在的缓冲区读取字符,从Marker的位置后面开始读取,这并不会影响缓冲区上面点的位置.STRING: 从字符串读取字符,在第一个字符的前边开始读取.FUNCTION: 用于生成字符的函数,这种函数需要满足满足以下两种条件调用方式,- 不带参数调用,并且返回下一个字符;
- 带一个参数调用,这参数还一定是个字符,
FUNCTION应该保存参数并且在下一次调用的时候返回这个参数.这叫做"unreading" the character; 这种调用发生在Lisp reader读取一个字符太多次并且想把它放回它所来自的地方.这种情况下,FUNCTION返回的值没有区别.
t: 表示流是minibuffer,一旦从minibuffer读取就会马上唤醒并且等待用户输入的字符串.如果Emacs是以batch mode下运行,那么就用标准输入(standard input)作为输入流.nil: 表示用标准输入standard-input作为输入流.SYMBOL:SYMBOL的函数定义作为输入流(如果有函数定义的话).
这章节的文档还有针对上面的演示,比如如何定义一个符合要求的 FUNCTION.
输入函数 (Input Functions)
与读取相关的函数以及变量.
输出流 (Output Streams)
输出流指定了怎么处理打印的字符.输入流有可能是以下几种类型:
BUFFER: 输出的字符被插入到缓冲区里面的点后面.MARKER: 把输出的字符插入到Marker所在的缓冲区里面,在Marker的位置后面插入,这并不会影响缓冲区上面点的位置.FUNCTION: 输出的字符会被传给FUNCTION进行储存,这个函数需要接收一个字符作为参数,输出的字符有多少个就需要传入多少次,并且能够在你想要的时候负责储存字符.t: 把输出字符显示在回显区域中(echo area).nil: 把输出字符输出到标准输出(standard-out)中.SYMBOL:SYMBOL的函数定义作为输出流(如果有函数定义的话).
这章节的文档还有针对上面的演示,比如如何定义一个符合要求的 FUNCTION.
输出函数 (Output Functions)
一些打印函数会在必要时候给输出添加一些引用字符串(quoting character)来使这些输出能够被正常读取.引用字符有 " 和 \,可以区分 string 和 symbol 以及防止在读取 string 和 symbol 的时候有中断(punctuation)的发生.
String 用 " \ 大部份人都很熟悉,但是 symbol 用 \ 就不一定了,比如, 'What\ The\ Fuck\ ! 是一个合法的 symbol,是的,就叫 What The Fuck !. 实际上可以设定打印函数是否添加引用字符.
Lisp 对象可以引用它们自身,也就是所谓的环状结构,用正常的方法打印这种对象会陷入无限递归的, 当 Emacs 检测到递归的时会打印 #LEVEL 来表示对于当前打印操作而言一个在 LEVEL 级上的对象的引用,而不是递归打印一个已经打印过的对象.
比如,
(setq foo (list nil)) ;; => (nil) (setcar foo foo) ;; => (#0)
这章节的文档剩下的部分就是一些 API 说明了.
影响输出的变量 (Output Variables)
自行查阅.
Minibuffers
minibuffer 就是一种特别的缓冲区, Emacs 命令会用它来读取比数字前缀参数(numeric prefix argument)更加复杂的参数.
这些参数包括文件名字,缓冲区名字和命令名字. minibuffer 会显示在 frame 的底部,和回显区域(echo area)的位置一样,区别在于 minibuffer 用来读取, echo area 用于打印.
入门Minibuffers (Intro to Minibuffers)
大部份情况下, minibuffers 就是一个普通的 Emacs 缓冲区.在缓冲区里面,大部份操作,比如编辑命令都能够正常工作.
然而,许多管理缓冲区的操作都不会应用到 minibuffers 上. Minibuffer 的名字必定是 ' *Minibuf-NUMBER*' 这种形式并且不会被改变.
Minibuffers 只会显示在用于显示 minibuffers 的窗口中,这些窗口一定是显示在 frame 的底部.
有时候frame是没有 minibuffer 窗口的,还有种特别的frame是只包含一个 minibuffer 窗口 的.
Minibuffer 里面的文本必定是以提示字符串开头(prompt string)的, prompt string 可以被调用 minibuffer 的程序指定,用来告诉用户应该输入什么.
提示字符串是 read-only 的,所以不会有意外删除或者改变它的情况发生.它还被标识为一个 field,一些动作函数(motion functions)包括 beginning-of-line,
forward-word, forward-sentence 和 forward-paragraph,会在提示文本和实际文本之间的边界停止.
Minibuffer 的窗口一般就是单行,它会在内容需要更多空间的情况下自动增长.在 minibuffer 激活的同时,可以通过改变窗口大小的命令临时改变 minibuffer 的窗口大小,
在 minibuffer 退出的时候会还原回去.在 minibuffer 没有激活的时候可以在同一个frame里其他窗口使用更改大小的命令或者通过鼠标拖动 mode line 来永远改变它 minibuffer 的大小.
由于目前版本 27.0.50 的实现细节,只有把 resize-mini-window 设置为 nil 才能成功.
对于只有 minibuffer 的frame,只要改变frame的大小就可以了.
minibuffer 的用途就是用于读取输入事件以及修改类似 this-command 和 last-command 这样变量的值.
如果你的程序不想改变这些变量的值,那么应该在 minibuffer 的外层作用域绑定这些变量.
在一些情况下,一个命令可以在即使已经存在一个已激活的 minibuffer 的情况下再使用一个 minibuffer,这个新的 minibuffer 叫做递归 minibuffer (recursive minibuffer).
第一个 mimibuffer 被命名为 ' *Minibuf-1*' .递归 minibuffer 就是通过增加后面的数字来命名.
(名字前面是有个空格的,因为这样就不会显示在正常的缓冲区列表中).在所有的递归 minibuffer 里面,只有最内层的那个是激活的,通常这个才被叫做 minibuffer.
可以通过设置 enable-recursive-minibuffers 这个变量来允许或者禁止递归 minibuffer,又或者给命令对应的 symbol 设置 enable-recursive-minibuffers 属性来达到局部控制.
和其他缓冲区一样, minibuffer 也有 local keymap.激活 minibuffer 的同时也会根据被完成的工作来设置它的 local map.
有针对于无补全的 minibuffer (non-completion minibuffer) 的 local map,还有针对带有补全功能的 minibuffer 的 local map.
当 minibuffer 是未激活状态,它的主要模式就是 minibuffer-inactive-mode, 对应的 keymap 就是 minibuffer-inactive-mode-map.
当 Emacs 是以 batch mode 的形式运行,从 minibuffer 读取实际上就是从标准输入描述符 (standard input descriptor) 读取, batch mode 下是不支持包括 history, completion 等特色 minibuffer 特性.
从Minibuffer中读取文本 (Text from MInibuffer)
针对 minibuffer 的输入而言最原始的操作就是 read-from-minibuffer,可以读取一个 string 或者一个文本形式的 Lisp 对象.
read-regexp 用来读取正则表达式.还有各种各样用于其它用途的函数.大部份情况下不应该直接调用这些函数,如果只是用于读取输入,可以用 interactive.
本章节基本都是关于 minibuffer 读取文本的 api,其中个人比较关注的是它 minibuffer-local-map 以及 minibuffer-local-ns-map 两个变量,可以了解到 minibuffer 的按键绑定.
从Minibuffer中读取对象 (Object from Minibuffer)
就是关于读取对象的 API.
Minibuffer历史 (Minibuffer History)
Minibuffer history list 是一个列表,用于记录用户输入的上一个命令,这样用户就可以方便地重新调用命令.
其实有很多个不同的 minibuffer history list 用与不同种类的输入,可以通过设定 read-from-minibuffer 或者 completing-read 的 HISTORY 参数来指定 minibuffer 的历史列表:
有两种可能的值:
VARIABLE: 使用变量(symbol)作为历史列表;(VARIABLE . STARTPOS): 使用变量作为历史列表,STARTPOS是历史列表的初始位置,是一个非负整数,如果为0就是和只使用变量一样.
如果不指定的话就会使用默认历史列表 minibuffer-history. read-from-minibuffer 和 completing-read 都会自动给历史列表新增加元素,并且提供命令给用户重新使用列表上的元素.
历史列表是有长度限制的,首先由 history-length 变量决定,如果某个历史列表有 history-length 属性,那么该列表的长度就由该属性的值决定.
history-delete-duplicates 变量可以决定是否删除重复的历史记录.
初始输入 (Initial Input)
有几个关于 minibuffer 输入的函数都有一个参数叫做 INITIAL,它能够让 minibuffer 以特定文本开头,不过这个几乎是被舍弃了.
因为这个参数为 non-nil 的时候是一个扰乱性的接口(intrusive interface),所以不推荐使用,历史列表提供了更好的方法来完成这种事情.
补全 (Completion)
补全就是根据文本的开头填充文本的剩余部分,通过对比用户的输入和一个可用的完整文本的列表,我们叫它补全表(completion table),然后判断用户输入的文本最接近哪一个.
标准的 Emacs 命令提供了 symbol / 文件/ 缓冲区 /进程名字的补全,当然 Emacs 也提供了原函数 try-completion 以及高级函数 completing-read 来让用户实现对其它类型名字的补全.
- 基础补全 (Basic Completion)
这章节的
API都是和minibuffer本身无关的,因为这样,它们能够用在很多其它的地方上. - Minibuffer补全 (Minibuffer Completion)
用于补全
minibuffer的基本接口. - 补全命令 (Completion Commands)
描述用于补全
minibuffer的按键映射(keymap),命令以及用户选项. - 高级补全 (High-Level Completion)
针对特定种类(缓冲区/
symbol/ 命令 / 颜色,等等)名字的高级补全函数. - 读取文件名字 (Reading File Names)
描述用于补全文件名字,目录名字还有
shell命令名字的高级函数. - 补全相关变量 (Completion Variables)
补全相关的用户选项,修改默认的补全行为.
- 编程补全 (Programmed Completion)
有时候不可能或者不方便提前创建一个包含匹配值的
alist或者obarray,这种情况下你可以提供自己的函数来计算指定string的补全的可能值.这叫做编程补全,比如
Emacs在补全文件名字的时候就使用了编程补全.这章节就是描述相关的接口. - 在缓冲区中补全 (Completion in Buffers)
除了在
minibuffer中补全,Emacs还允许在缓冲区上进行补全.钩子变量
completion-at-point-functions是一个函数列表,里面的函数都是用于计算用于补全某个点上文本的补全表(completion table).Major mode可以用这个变量来提供补全表.当运行completion-at-point的时候,列表上的函数就会被依次调用,不需要传入参数.每个函数都应该返回
nil,除非它能够负责该点上的文本的补全,否则就应该返回如下形式的列表(START END COLLECTION . PROPS).START和END界定了被补全的文本,COLLECTION就是补全表,PROPS就是一个记录了额外信息的plist.文章余下部分就是讲解编写这些函数需要注意的地方.
Yes-or-No查询 (Yes-or-No Queries)
Emacs 提供了询问用户 yes-or-no 问题的函数: y-or-n-p 的问题可以用一个字符回答; yes-or-no-p 的问题则需要用三或者四个字符回答, yes-or-no-p 适合用于重要的问题(momentous questions).
严格来讲, yes-or-no-p 使用 minibuffer, 而 y-or-n-p 不,还有 y-or-n-p 有一个限时版本: y-or-n-p-with-timeout,超过多少秒就提供默认回答.
多重选择查询 (Multiple Queries)
如果有一系列类似的问题需要逐个询问,那么可以使用 map-y-or-n-p,如果一个问题的回答不止 yes 和 no,那么使用 read-answer.
读取密码 (Reading a Password)
如果要读取一个密码并且把密码传递给另外一个程序,可以使用 read-password.
在Minibuffer中使用的命令 (Minibuffer Commands)
一些在 minibuffer 里面使用的命令.
Minibuffer窗口 (Minibuffer Windows)
一些 APIs: 可以访问和选择 minibuffer 窗口,以及测试它们是否激活和控制它们如何改变大小.
Minibuffer内容 (Minibuffer Contents)
用于获取 minibuffer 的提示和内容的函数.
递归Minibuffer (Recursive Mini)
用于处理递归 minibuffer 的函数和变量.
Minibuffer杂项 (Minibuffer Misc)
就是杂项.
命令循环 (Command Loop)
在启动 Emacs 时,它会马上进入一个无限循环:
每一轮循环是为了读取用户的按键序列(key sequence),一个按键序列就是一个输入事件序列(a sequence input events);
一旦等到到用户的输入,就会根据按键序列找到对应的命令;执行命令并且显示结果.
然后进行下一次循环,这和 shell 解析器的 REPL (Read-Eval-Print Loop) 一样,用代码描述大概如下:
(while is-emacs-running ;; is-emacs-running is t (let* ((input (func-to-read)) ;; func-to-read 在读取到输入前会一直暂停循环 (command (command-lookup input)) (res (func-to-eval command))) (func-to-print res)))
这个无限循环叫做编辑器命令循环(editor command loop),用户可以通过一些特别的输入来中断这个循环.
实际上 Emacs 的处理远比上面的描述要复杂,先抛开复杂的情况不谈,举个简单的例子:
假设用户现在输入 C-n 来进行移动到下一行.
首先 Emacs 通过 read-key-sequence 来读取用户输入的,也就是上面的 func-to-read,得到的 input 是一个字符串或者向量(vector),
这里是字符串 "^N";
然后 Emacs 根据 input 查找对应命令的,上面的 (command-lookup input) 大概就是 (lookup-key (current-global-map) input),
得到的 command 是命令的定义,这里是 next-line 的定义;
最后就是执行命令然后显示结果了, Emacs 会用 command-execute 执行 command,也就是上面的 func-to-eval,
至于 func-to-print 就不太清楚了,本人也没有在文档上看到有写是哪个函数.
之所以说 Emacs 的实际处理要更复杂是因为输入有很多种类型,并且 Emacs 本身也要做一些别的处理,
具体内容就看这里: M-: (info "(elisp)Command Overview")
定义命令 (Defining Commands)
Emacs 的命令和函数是两个概念,不过两者关系十分接近,函数的名字不能被 execute-extended-command 读取到,不过可以使用 interactive 来把一个 Lisp 函数变成命令.
一般是 interactive 一定要是在函数体的第一个 form (top-level),这条规则适用于 lambda 表达式和 defun form.
interactive 和函数的实际执行是没有关系的,它就是一个 flag,告诉 Emacs 的命令循环这个函数能不能被读取/交互式使用.
interactive 的参数规定了交互调用的时候如何读取参数.
除了在函数体中使用 interactive,还可以通过指定给函数的 symbol 的 interactive-form 属性设定为 not-nil 来把函数变为一个命令,这个的优先级比直接使用 interactive 要高,这个特性很少被使用.
有时候只是想让这个函数只能用于交互使用,这种情况下可以直接或者通过 declare form 给函数的 symbol 的 interactive-only 属性设定为 non-nil,如果从 Lisp 调用这个命令的话字节码编译器就会产生警告. describe-function 的输出会包含类似的信息.
值得注意的是,通用函数(Generic Functions)是不能转化变成命令的,不过可以通过定义一个正常的函数然后让这个函数调用通用函数这种方式来间接使用.
- 使用 interactive (Using Interactive)
(interactive arg-descriptor),调用命令的时候会根据arg-descriptor读取命令的参数,arg-descriptor有三种可能:nil或者放着不管,就是指命令不需要任何参数.string,它的内容是一个由newlines分割做为分割符的元素序列,每一个元素代表一个参数,每个元素是一个Interactive Code后面跟着一个可选的提示字符串(prompt).Interactive Code可以在这里看M-: (info "(elisp)Interactive Codes").提示字符串可以使用
%来获取上一个参数的值.如果
arg-descriptor以*字符开头,并且如果在read-only缓冲区 调用该命令就会引发错误;如果以
@字符开头,并且如果按键序列包含任何鼠标事件,那么与第一个事件关联的窗口就会在命令执行前被选中;如果以
^字符开头,并且如果命令在shift-translation下唤醒,那么命令在执行前就会临时设定mark和激活区域(region),或者拓展一个早已激活的区域,如果命令在没有
shift-translation的情况下激活,那么区域尖或被临时激活,然后在运行前取消激活(deactivate).可以三个字符一起使用,并且顺序无关要紧.
比如,
(defun pesudo-rename (oldname newname) (interactive "sOldname to rename: \nsRename %s to: ") (message (format "oldname: %s to newname: %s" oldname newname)))
一个非
string的Lisp表达式,并且它的运算结果应该是一个参数列表(list of arguments). 比如,(defun pesudo-rename (oldname newname) (interactive (let* ((name1 (read-string "name1: ")) (name2 (read-string (format "reaname %s to name2: " name1)))) (list name1 name2))) (message (format "Oldname: %s to newname: %s" oldname newname)))
注意的是,参数值不应该包含任何不能被打印然后读取的数据类型,一些功能会保存
command-history到一个文档里面,可能会在后续会话中被读取, 如果一个命令的参数包含这些数据类型(会使用#<...>语法打印),那么这些功能就不会正常工作.
(interactive-form function)可以获取函数的interactive form. - 通用命令 (Generic Commands)
有些交互函数的实现可以让用户选择实现,这些就是通用命令.
交互式调用 (Interactive Call)
一些关于命令的 APIs.
区分交互式调用 (Distinguish Interactive)
判断命令是否被交互式使用.
命令循环的信息 (Command Loop Info)
一些关于命令记录的 APIs.
命令后调整指针 (Adjusting Point)
当指针处于拥有 display 或者 composition 属性或者不可见的文本中 Emacs 就不能显示指针.因此在命令执行后,如果指针在这种文本中,命令循环会把指正移动到文本的边界中,这样文本就不能被触碰到.
可以通过 disable-point-adjustment 以及 global-disable-point-adjustment 来关闭这个特性.
输入事件 (Input Events)
Emacs 的命令循环读取一个输入事件的序列,输入事件表示键盘或者鼠标的活动,又或者是发送给 Emacs 的系统事件.
表示键盘活动的事件是字符(characters)或者 symbols,其他类型的事件一定是列表(lists).可以通过 eventp 来判断一个对象是否输入事件.
- 键盘事件 (Keyboard Events)
从键盘中能够获取的输入类型有两种,普通键(ordinary keys)和功能键(function keys).一个普通键实际上就是对应一个字符;
字符有可能被修饰过,也就是配合修饰键(
meta,control,shift,hyper,super,alt)一起输入.输入普通键的时候会产生一个事件,该事件的值就是一个
Lisp字符对象,也就是一个整数,这种事件叫做字符事件(character event).一个字符事件的值是这计算的:
有524288(\(2^{19}\))个基础字符,从 0 开始对所有基础字符进行编码: 0 ~ 524287,基础字符的编码叫做基础码(basic code),也就是说一个基础码需要 20 位字节;
有6种修饰键,每种都有它们各自的值;
如果没有配合修饰键输入,那么字符事件的值就是基础码的其中一个(0 ~ 524287),比如输入
a键所产生的事件的值就是?a,也就是 97.如果配合修饰键一起输入,那么 大部分的 字符事件的值就等于所有修饰键事件的值加上基础码:
meta: 2**27 (the 28th bit);
control: 2**26 (the 27th bit),但是有一些特殊情况,
control + ASCII这种组合对Emacs Lisp是有特殊意义的,它们都有自己的值,比如Ctrl + a就是1,Ctrl + b是2,Ctrl + c是3,如此类推,而对于
control + non-ASCII这种组合才是control的值和字符的值加起来,比如Ctrl + %的值就是37(?\%)加上 2**26 得到 67108901;shift: 2**25 bit (the 26th bit), 要注意
Shift + a得到的值不等于?A,Shift + letter这种组合不代表切换字符的大小写;hyper: 2**24 bit (the 25th bit);
super: 2**23 bit (the 24th bit);
alt: 2** 22 bit (the 23th bit),不过注意的是,大部份键盘的
Alt键都会被当作 meta 来处理的.总得来说,一个字符事件需要 28 位字节来储存:
0000 0000 0000 0000 0000 0000 0000. - 功能键 (Function Keys)
功能键与普通键不一样,采用
symbols作为表示,symbols的名字就是功能键的标签,比如<F1>键就是用f1表示.文档上举了几个例子,自行查阅.
- 鼠标事件 (Mouse Events)
Emacs支持点击事件(click events),拖拽事件(drag events),按钮按下(button-down events)和运动事件(motion events)4个类型的鼠标事件.所有类型的事件都是采用
lists做为表示.list的car结果是事件类型,也就是告诉Emacs使用的鼠标按钮以及modifier keys.事件类型还可以区分双击以及三击,剩余的
list元素包含位置以及时间信息.对于按键查找(key lookup)来说只有事件类型是重要的:同类型的两个事件是一定执行同一个命令,命令可以通过
einteractive code来获取这些事件的全值.一个以鼠标事件开头的按键序列会通过当前鼠标所在的窗口的
keymaps来读取,不是当前缓冲区,这并非暗示在某个窗口点击就会选择那个窗口和或者它的缓冲区,这完全是由按键序列的绑定命令来控制的.
- 点击事件 (Click Events)
用户按下鼠标按钮并且在同一个地方释放它,这样会产生一个点击事件.所有鼠标事件都用同一个格式:
(EVENT-TYPE POSITION CLICK-COUNT)EVENT-TYPE
一个表示使用了哪个鼠标按钮的
symbol.是mouse-1,mouse-2… 其中一个,分别从左到右表示鼠标按钮.还可以像功能键那样使用
A-C-,H-,M-,S-和s-来分别表示alt,control,hyper,meta,shift和super键.该
symbol也表示事件的事件类型,按键绑定根据它们事件类型来描述事件,因此如果有个mouse-1的按键绑定,那么这个绑定将会应用于所有事件类型为mouse-1的事件上.POSITION
一个鼠标位置列表,记录发生鼠标点击的位置.
点击的位置分为两类:
- 文本区域(
text area),mode line,header line, 条纹(fringe)或者边缘区域(marginal areas); - 滚动条
- 文本区域(
M-: (info "elisp(Accessing Mouse)")有介绍如何获取鼠标事件的位置列表,这里就先不说了.如果在第一类位置发生点击事件,那么鼠标位置的
list就是如下形式:(WINDOW POS-OR-AREA (X . Y) TIMESTAMP OBJECT TEXT-POS (COL . ROW) IMAGE (DX . DY) (WIDTH . HEIGHT))WINDOW: 点击发生的窗口.POS-OR-AREA: 如果点击位置在文本区域内,就表示被点击的字符的缓冲区位置,或者如果在文本区域之外就表示窗口位置. 是mode-line, header-line, vertical-line, left-margin, right-margin, left-fringe 或 right-fringe的其中之一. 如果事件的前置键(imaginary prefix keys)已经被Emacs绑定了的话,那么POS-OR-AREA就是一个包含了一个上述symbol的list.X, Y: 点击的相对像素坐标.TIMESTAMP: 点击事件的发生时间,是一个整数,表示从系统初始时间到目前的微秒.OBJECT: 如果点击的位置没有文本就返回nil;否则就是(STRING . STRING-POS),STRING表示被点击的string以及它的所有属性,STRING-POS就是点击的位置.TEXT-POS: 如果在边缘区域或者条纹(fringe)上点击,那么这就是窗口上第一个可见字符的缓冲区位置;如果在mode line或者header line点击就是nil;对于其他事件就是距离点击位置的最近缓冲区位置.COL, ROW: 实际的列行坐标.IMAGE: 点击发生的图像对象,如果没有图片就是nil,否则就是和find-image返回值一样的图像对象.DX, DY: 相对于OBJECT左上角的相对像素坐标,OBJECT为(0 . 0);如果OBJECT为nil,那么就是相对于点击位置字符的坐上角的像素坐标.WIDTH, HEIGHT: 如果OBJECT不为nil那么就是它的宽高,否则就是这些被点击字符.
如果在第二类位置发生点击事件,那么鼠标位置的
list就是如下形式:(WINDOW AREA (PORTION . WHOLE) TIMESTAMP PART)
WINDOW: 滚动条被点击中的窗口.AREA: 值为vertical-scroll-bar的symbol.PORTION: 滚动条顶部到点击位置的像素数量,有些工具,比如GTK+,Emacs是不能获取这个值的,这个时候就为0.TIMESTAMP: 点击时间,同样对于GTK+这些工具无法获取.PART: 滚动条的点击位置,如果在滚动条滑块上就是handle,如果在滑块以上就是above-handle,以下就是below-handle,如果到了滚动条的两端就分别是up和down.
CLICK-COUNT
记录了同一个鼠标按钮快速重复点击的次数.
- 拖拽事件 (Drag Events)
拖拽事件就是用户按下鼠标并且在释放按钮前移动到别的字符的位置,它的形式是这样的:
(EVENT-TYPE (WINDOW1 START-POSITION) (WINDOW2 END-POSITION))
EVENT-TYPE:drag-开头的symbol,比如drag-mouse-2.(WINDOW1 START-POSITION)和(WINDOW2 END-POSITION)分别表示按下按钮时候的位置和释放按钮时候的位置,释放时候可能会在窗口的边界外,那么就是一个包含了窗口所处于的frame的list.
如果
read-key-sequence接收了一个没有按键绑定的拖拽事件,那么就把它转化成在拖拽开始位置的发生的点击事件. - 按钮按下事件 (Button-Down Events)
没有办法在按钮释放以前区分点击和拖拽事件,如果要在按钮按下的时候进行处理,那么就需要处理这个事件,它的形式类似点击事件,除了
EVENT-TYPE是一个以down-开头的symbol.read-key-sequence会无视没有命令绑定的button-down事件,所以命令循环会无视它们.一般使用button-down事件的原因是为了在按钮以前通过读取motion events来跟踪鼠标的动作(motion). - 重复事件 (Repeat Events)
在没有移动鼠标的情况下快速按下同一个按钮多于一次,
Emacs会对后续按下动作产生一种叫做repeat的鼠标事件.最常见的是双击事件(
double-click),双击事件的类型包含了double-前缀,如果双击左键的时候按着meta键,那么事件类型就是M-double-mouse-1.当用户双击的时候
Emacs会对第一下生成一个普通的点击事件,然后才对第二下生成双击事件.如果点击了一下按钮然后再按下并且在释放前拖动鼠标,那么就会产生一个
double-drag事件,如果double-drag没有绑定,那么Emacs就会查找普通的drag事件的绑定,如果drag也没有绑定,那么就会直接忽略.在
double-click或者double-drag之前,Emacs会在用户第二次按下鼠标的时候生成一个double-down事件,同样如果没有绑定的话就会使用普通的button-down的绑定,如果button-down也没有绑定,那么就会直接忽略.三次快速点击会产生三击(triple-click)事件,和双击对应,有
triple-drag以及triple-down事件,三次以上也是triple事件,可以通过事件列表来看到实际点击次数. - 动作事件 (Motion Events)
所谓动作事件就是描述鼠标在没有任何按钮活动的情况下的动作.事件形式如下,
(mouse-movement POSITION)
其中
POSITION是一个位置list,表示了鼠标指针的当前位置.track-mouseform 允许动作事件在它的体内产生,之外则不行. - 焦点事件 (Focus Events)
Emacs提供通用的方法让用户拥有选择窗口的权力,窗口的选择叫做focus.在用户切换frame的时候就会产生 "focus" 事件.事件形式如下:
(switch-frame NEW-FRAME)
一些窗口管理器(window managers, WM for short)被设置成只要移动鼠标到一个窗口就足以
focus到它身上.然而
Emacs不同,需要用户通过鼠标或者键盘在frame上输入才行,只是在frame之间移动是不会产生focus事件的.Emacs是不会在按键序列的中间生成一个focus事件,因为会打乱(garble)序列. - 杂项事件 (Misc Events)
一些别的系统事件,具体自己看.
- 分类事件 (Classifying Events)
一些可以帮助判别事件类型的
API,event-modifiers获取事件的modifiers,event-basic-type返回事件描述的按键或者鼠标按钮,移除了所有modifiers.mouse-movement-p判断对象是否一个动作事件等等. - 访问鼠标事件 (Accessing Mouse)
一些关于获取鼠标事件数据的
APIs - 访问滚动条 (Accessing Scroll)
关于滚动事件的
APIs. - 把事件变为字符串 (Strings of Events)
不建议把事件储存为字符串,之所以存在这种东西是因为历史兼容问题,新的
Lisp程序最好不要采用这种方式,用向量(vector)来替代字符串(string),具体自行阅读文档.
读取输入 (Reading Input)
命令循环使用 read-key-sequence 函数来读取按键序列, read-key-sequence 使用 read-event 读取事件.
Emacs 会根据 extra-keyboard-modifiers 来调整所读的每一个事件,如果可以的话,会在 read-event 返回之前通过 keyboard-translate-table 来把事件翻译成别东西.
关于如何使用 keyboard-translate-table,可以阅读 M-: (info "(elisp) Event Mod") 来查看 keyboard-translate 函数的例子.
关于翻译的具体机制就在 Keymaps 章节说.事件读取函数还能够激活当前输入法,具体以后说.
特殊事件 (Special Events)
read-event 会处理这些事件并且不返回它们,相反 read-event 会等待第一个非特殊事件并且返回它.
等待结束时间或者输入 (Waiting)
Emacs Lisp 中有两个类似于别的语言的 sleep 一样的函数, sit-for 和 sleep-for,这两者都会等待指定的秒数,如果在结束之前接收到用户的输入,那么等待会提前结束.
两者的差别在于是否更行 Emacs 的显示,可以理解为是否异步,前者是异步而后者为阻塞.
中止 (Quitting)
默认情况 C-g 就是绑定 keyboard-quit 命令,在一个 Lisp 函数运行的时候输入 C-g 会中止该函数.
当命令循环在等待按键输入的时候输入 C-g 不会造成任何影响,当然是没办法看出差别的.
在 minibuffer 中 C-g 的定义不大一样,它会先退出(exits) minibuffer 然后才中止(quits),原因是这样就可以在 minibuffer 里面重新定义 C-g 的意思.
在 minibuffer 里面,跟着前缀的 C-g 是不会被重新定义的,并且有取消前缀按键和前缀参数的正常效果.
当 C-g 直接中止的时候是直接通过设置变量 quit-flag 为 t 来实现的. Emacs 会在合适的时间检查这个变量是否为非 nil.
在 C 的层面上中止(quitting)不会在任何地方发生,只会在特定地方检查 quit-flag,理由是在别的地方中止会导致 Emacs 的内部状态不一致,也正是因为这样中止才不会导致 Emacs 崩溃.
像 read-key-sequence 或者 read-quoted-char 这种特定的函数接收到 C-g 是完全不会中止的, C-g 对于它们来说只是它们请求的输入.
你可以在函数的局部通过绑定 inhibit-quit 变量为非 nil 来阻止中止的发生,尽管 C-g 仍然会把 quit-flag 绑定为 t,但是中止不生效,最终 inhibit-quit 会再次变为 nil.
至于为什么 read-quoted-char 这种函数可也不受中止影响,原理是先绑定 inhibit-quit 为 t 然后在 inhibit-quit 变回 nil 之前绑定 quit-flag 为 nil.
(defun read-quoted-char (&optional prompt) "...DOCUMENTATION..." (let ((message-log-max nil) done (first t) (code 0) char) (while (not done) (let ((inhibit-quit first) ...) (and prompt (message "%s-" prompt)) (setq char (read-event)) (if inhibit-quit (setq quit-flag nil))) ...set the variable ‘code’...) code))
前缀命令参数 (Prefix Command Arguments)
前缀命令参数有两种表示方式: raw 和数值(numeric).
命令循环内部使用raw方式, Lisp 变量也是这样储存信息的,不过两种方式都可以使用.
raw 形式的前缀命令参数会是以下其中一个:
nil:意味着没有前缀参数,数字表示方式为1,不过对于很多命令来说nil和数字1是不同的;- 整数:表示参数自身,也就是说就是要输入整数作为参数;
- 只有一个整数元素的列表:这种形式是由于只输入一个或者多个
C-us并且不带数字而导致得,数值就是列表里面的值; symbol -:表示输入了M--或者C-u -并且没带数字,对应的数值为-1.
递归编辑 (Recursive Editing)
当 Emacs 启动的时候就会自动进入一个命令循环(command loop),这是 第一个命令循环,并且只有 Emacs 结束的时候这个循环才会结束.
Lisp 程序可以在 第一个命令循环 下创建新的命令循环,这里暂且把新的命令循环叫做 子命令循环, 这种命令循环中嵌套子命令循环的情况叫做 递归编辑.
我们把 第一个命令循环 叫做顶层(top-level)命令循环,把 子命令循环 叫做递归编辑层(recursive editing level)命令循环.
一旦一个命令创建了一个递归编辑层命令循环,那么这个命令就会被挂起,用户可以在继续命令的执行前做任意的编辑.
顶层和递归编辑层的命令循环下可用的命令(commands)以及按键映射(keymaps)是一样的,
不过有少部分命令是针对递归编辑层命令循环的,这些命令是一直都可用的,另外有些命令只会在递归编辑层循环没有结束时可用.
这一小部分命令在任何时候都是可用的,但是在递归编辑以外的情况下使用是没任何作用的.
所有的命令循环都设立了 (all-purpose error handlers) 通用错误处理,所以命令循环里面引发的异常不会导致命令循环结束.
禁用命令 (Disabling Commands)
禁用命令就是标记一个命令,当使用到这个命令的时候就要请求用户确认.底层机制就是给命令的 symbol 存放(put)一个为非 nil 的 disabled 属性.
(put 'command-to-be-executed 'disabled t)
当然,只有交互式使用才会受到影响,在 Lisp 程序中当作函数来是用是不会受到影响的.
如果 disabled 属性的值是一个字符串,那么这个字符串就会作为提示消息.
命令历史 (Command History)
Emacs 会记录任何执行过的 M-x 命令, M-: 命令以及从 minibuffer 读取参数的命令,这些被认为是复杂的命令,可以通过 command-history 来查看.
键盘宏 (Keyboard Macros)
一组输入事件就是一个键盘宏,可以看做一个命令或者作为一个按键的定义.键盘宏的 Lisp representation 是一个字符串或者包含事件的向量.
比如,
[?\C-e return ?\C-y return]
在 M-: (info "Keyboard Macro Registers") 这里可以了解如何录制以及保存键盘宏.
文本 (Text)
这章节主要是关于如何处理缓冲区中的文本,这个章节中,文本指的是缓冲区中的字符,包括它们的属性.
处理的操作包括:在缓冲区上的点的附近进行文本检视,插入,或者删除. 点(point)是处于两个字符之间的,而指针(cursor)是出现在点的后面.
点附近的文本 (Near Point)
检视点附近的字符.
检视缓冲区的内容 (Buffer Contents)
一些可以把缓冲区的部分文本转化成字符串的函数.
比较文本 (Comparing Text)
比较两个缓冲区上部分文本.
插入文本 (Insertion)
这个章节主要是关于 Emacs Lisp 的插入文本的 API.
插入文本就是往缓冲区插入新的文本,被插入的文本会位于点之中,也就是两个字符之间.有些插入文本的函数会把文本插入到点之后,而有些相反.
如果在点之前插入文本会导致位于点后面的标记(marker)发生移动.如果当前缓冲区或者文本是只读(read-only)的,那么在里面插入文本的函数会报错.
从 string 或者 buffer 复制文本字符的函数会把字符的属性也复制过来.相比之下,字符作为参数而不是作为 string 或者 buffer 一部分的时候,它们会继承邻近文本(neighboring text)的文本属性.
假设文本来自 string 或者 缓冲区},为了把文本插入 multibyte 缓冲区 中,函数会把文本从 unibyte 转化为 multibyte,反过也来一样.
然而它们不会把 128 到 255 编码范围的 unibyte 字符转化,即使插入文本的当前的缓冲区是一个 multibyte 缓冲区.
插入文本的命令 (Commands for Insertion)
和上面章节不一样,这是关于插入文本的命令.
删除文本 (Deletion)
删除文本也就是说移除缓冲区上的部分文本,并且不把被移除的文本保存在 kill ring 中,因此,被删除的文本是不能被 yank 的,
不过可以使用 undo 机制重新插入被删除的文本到原来的位置上.不过在一些特殊案例下,某些删除文本的函数的确会把删除的文本保存在 kill ring 中.
删除文本的命令 (User-Level Deletion)
和上个章节不一样,这是关于删除文本的命令.
Kill Ring (The Kill Ring)
在 Emacs 中杀文本(kill text)和删除文本(delete text)是两个概念,这里先把杀文本的函数叫做 kill function,命令叫做 kill command.
kill function 基本都 kill-xxx 的命名方式,和 delete-xxx 的删除文本的函数不一样.
和删除文本一样, kill function 会删除文本,但是它们会把删除的文本保存在一个列表中,这个列表叫做 kill ring,
目的是为了之后可以把删除的文本找回来重新利用,之所以叫 ring 是因为会把列表里面的元素用循环的顺序来对待,这个列表保存在 kill-ring 中.
可以对 kill-ring 进行常规的列表操作.而删除文本的函数并不会把删除的文本保存起来,这就是两者的差别.
- Kill Ring 的概念 (Kill Ring Concepts)
Kill ring会记录被杀掉的文本到一个列表中,最近的记录会排在第一位,有点类似栈(stack),文档上有个例子:'("some text" "a different piece of text" "even older text")
Kill ring有长度限制,kill-ring-max可以设置.在列表达到最大限制的时候,新增加一个元素会自动删除列表中最后一个的元素.当
kill command和其它非kill command的命令交错使用的时候,每个都会kill command都会添加一个新的元素到kill ring中.如果多个
kill commands连续地(in succession)使用,那么就会只产生一个元素来添加到kill ring中.所有后续的连续kill commands会把文本加载第一个kill command生成的文本中.对于
yanking,只有kill ring中开头的元素才会被找回.一些yank命令会转换kill ring来把别的元素放在开头.不过这个转换并不会改变
kill ring本身,最近的元素还是放在第一位. - Kill Function (Kill Functions)
kill-region就是killing text的subroutine,任何调用这个函数的命令都是kill command.它会把最近被杀掉的文本添加到
kill ring的头部或者添加到最近的文本中,它会根据last-command来判断上一个命令是不是一个kill command,再决定是否添加到上一个命令的文本中,还是添加到头部中.
kill command在保存文本之前会先调用filter-buffer-substring来对文本进行筛选,所以保存的文本可能会与缓冲区上的不一样. - Yanking (Yanking)
Yanking意味插入kill ring中的文本,不过并非盲目的插入文本.yank命令或者相关命令会先使用insert-for-yank来对插入的文本进行特殊处理.insert-for-yank类似insert,不过它会根据文本的yank-handler属性以及yank-handled-properties还有yank-excluded-properties变量来对插入的文本进行处理,然后把处理后结果插入到当前缓冲区中.
yank-handler的格式如:(FUNCTION [PARAM] [NOEXCLUDE] [UNDO]),第一个以外的元素可以不写FUNCTION: 非nil的时候,会被调用来插入文本,nil的时候是调用insert.PARAM: 如果该元素为非nil,它就代替被插入的文本,作为FUNCTION的参数.NOEXCLUDE: 如果该元素为非nil,那么就禁用yank-handled-properties和yank-excluded-properties对插入文本的处理.UNDO: 如果该元素为非nil,那么它应该是一个可以被yank-pop调用的函数,用来撤销当前文本的插入,该函数需要两个参数:当前区域的起点和结束点.FUNCTION可以设置yank-undo-function来重写UNDO的值.
- Yank命令 (Yank Commands)
高级的
yanking命令,自己阅读. - 低级 Kill Ring (Low-Level Kill Ring)
一些用来访问
kill ring的低级函数和变量,方便于关注窗口系统选择的Lisp程序员. - Kill Ring 的内部 (Internals of Kill Ring)
kill-ring变量以string列表的形式保存了kill ring的内容,最近的文本必定是放在列表第一位.kill-ring-yank-pointer变量指向kill ring列表上的连接(link),连接是一个列表,它的car是被找回的文本,kill-ring-yank-pointer可以用来确认kill ring的头部.移动kill-ring-yank-pointer到别的连接上被成为转换kill ring(rotating the kill ring).当然转换是虚拟的,并不会改变
kill-ring的值.=kill-ring= 和kill-ring-yank-pointer都是普通的列表,kill-ring-yank-pointer和kill ring列表上的连接是同一个Lisp对象,可以通过eq测试.kill-ring ---- kill-ring-yank-pointer | | | v | --- --- --- --- --- --- --> | | |------> | | |--> | | |--> nil --- --- --- --- --- --- | | | | | | | | -->"yet older text" | | | --> "a different piece of text" | --> "some text"
撤销 (Undo)
大部份缓冲区都有一个撤销列表(undo list),用来记录缓冲区上的文本改变,用于之后实现撤销操作, 缓冲区局部变量 buffer-undo-list 记录了缓冲区的改变记录.
只有一些特殊用途的缓冲区是不记录改变的,如果 buffer-undo-list 为 t 就表示该缓冲区不记录改变,因此这些缓冲区不能执行撤销.
维护撤销列表 (Maintaining Undo)
这个章节是关于如何启用或者禁用一个缓冲区的撤销信息以及解释了撤销列表如何自动截断.
一般一个新创建的缓冲区会自动启用撤销消息记录, 除非是一个名字以空格开头的缓冲区 (后面这个从句个人验证过,和文档描述不符合).
填充 (Filling)
填充的意思是通过移动行的断口(breaks)来调整行的长度来接近最大宽度(不能大于).另外,还可以通过插入空格来产生左或者右边距进行对齐.行的宽度由 fill-column 来控制,
为了方便阅读,行的长度或者和列最好不要超过 70. 这个章节介绍的命令都是做这方面的事情.
外边距 (Margins)
是上一章的继章.
适应填充 (Adaptive Fill)
Emacs 有 Adaptive Fill Mode,当启用的时候, Emacs 或只动根据当前被填充的段落的文本来决定填充前缀(fill prefix),而不是是用固定长度.
在填充发生的时候,填充前缀会被插入到当前段落的第二行的开头,后续行也会收到影响.
自动填充 (Auto Filling)
这里介绍了 Auto Fill mode,就不说了.
文本排序 (Sorting)
这里讲的函数全部都是和排序文本有关的,和 sort 类似但不一样,并非给列表里面的元素进行排序.
列 (Columns)
字符的位置是从缓冲区开始位置进行计算,而列的位置是从行的开始位置计算,列相关的函数就是在这两者之间转换.
缩进 (Indentation)
从左外边距之后开始计算出列以及缩进数量.具体就不写了,反正有关缩进操作的可以看这里.
大小写改变 (Case Changes)
这里文档描述的改变文本大小写的函数都是用于缓冲区上的文本的,如果是应用于字符串或者字符可以看这里 M-: (info "Case Conversion").
还可以通过 case table 来自定义如何切换大小写,具体看这里 (info "Case Tables").
文本属性 (Text Properties)
缓冲区上的每个字符或字符串都有一个文本属性列表,比较像 symbol 的属性列表.文本属性是属于在特定地方上的特定字符,也就是说,脸个同样的字符在不同地方上的属性是可能不一样的.
每个属性都有一个名字和值,它们可以使任意 Lisp 对象,但通常名字是一个 symbol,一般每个属性名字都是有它的意义的,比如 face 表示字符是如何显示的,
category 表示字符的分类.
在字符串和缓冲区之间复制文本会保留每个字符的文本属性,比如 insert, substring 以及 buffer-substring 这三个函数就是这样.
- 检查属性 (Examining Properties)
主要是关于一些获取某个位置上的文本属性,比如
get-text-property以及text-properties-at,后者返回一个属性列表,前者得到某个属性的值,具体阅读文档.
- 改变文本属性 (Changing Properties)
这里是关于用于改变缓冲区或者字符串指定范围内的文本属性的函数,
set-text-properties函数可以用来增删改指定范围内的文本的属性.因为文本属性被认为是缓冲区内容的一部分,所以改变文本属性会把缓冲区认为被修改过.缓冲区上的文本属性是可以撤销的(undoable).
还有,在字符串中,位置是从0开始的,而在缓冲区中是从1开始的.
- 文本属性搜索 (Property Search)
通常连续的文本字符的同一个属性会有相同的值,所以说不需要一个一个的检查字符的属性,直接处理一段有着相同属性的字符会更快.
这章节的函数不会移动点(point),相反会返回一个位置(position)或
nil. 一个位置是处于两个字符之间的,这些函数返回的位置是处于两个不同属性的字符之间的. - 特殊属性 (Special Properties)
一些有特殊用途的属性,比如上面提到的
face和category. - 格式化用的文本属性 (Format Properties)
这些属性影响填充命令的行为.
- 文本属性的粘性 (Sticky Properties)
当用户在缓冲区上面输入
self-inserting字符 (可以理解为输入的字符)时候,输入的字符会拥有与输入位置前面的字符一样的属性,这称为属性的继承.一个
Lisp程序可以做到有继承插入或无继承插入,取决于对插入原函数的选择,比如insert就是不带继承地插入文本,它会保留被插入字符串的属性,被插入的字符串的属性不会因为在别的缓冲区上改变.而
serf-inserting字符因为输入的时候使用了带继承插入的原函数,所以self-inserting字符会继承属性.在插入文本的时候,哪些属性被继承,以及是从哪里继承是取决于那些
sticky属性.在一个字符后面插入并且继承这个字符的属性,这种叫rear-sticky,在文字前面插入并且继承这个字符的属性叫做
front-sticky.如果两边都对同一个属性提供不同的sticky值(就是在文本中间进行文本插入的操作),那么前面的一个字符的值优先.可以通过
front-sticky以及rear-nonsticky两个属性以及text-property-default-nonsticky变量来控制不同文本属性的粘性.如果一个字符的
front-sticky属性的值为t,那么这个字符的所有属性都是有front-sticky粘性的,如果是指定特定属性,比如(face read-only),这样就是指定face属性为粘性,在该字符前面插入会导致被插入的文本都继承这个字符的
face属性.read-nonsticky是让在字符后面插入文本不继承该字符的属性,字符的大部份属性默认都是rear-sticky,接受的值和上面front-sticky描述一样. - 惰性计算文本属性 (Lazy Properties)
就是在需要这些属性的时候计算出结果.
- 定义可点击文本 (Clickable Text)
设置链接的官方教程,就不总结了,自行阅读.
- 定义以及使用域 (Fields)
所谓域就是缓冲区里面一段连续的字符,并且它们的属性
field的值都是一样(用eq比较). - 为什么文本属性不是区间 (Not Intervals)
具体不翻译了,自己读.
语法表 (Syntax Tables)
语法表负责为缓冲区上的每一个字符分配语法角色(syntactic role).语法表可以决定单词(word),符号(symbol)和其他语法成分(syntactic constructs)从哪里开始到哪里结束.
许多 Emacs 功能都是用这些信息,包括 Font Lock mode 和各种复杂的移动命令(movement commands).
当你想为一门语言编写一个 major mode 的时候,语法表就用得上了,如果没有编写这类 mode 的经验,可以参考这篇教程.
语法表基础 (Syntax Basics)
语法表是一种数据结构,可以用来查找每个字符的语法类(syntax class)和其他语法属性(syntactic properties). Lisp 程序会用它来扫瞄文本以及在文本间移动.
内部实现中,一个语法表就是一个字符表(char-table),在 index C 上的元素就描述了代号为 C 的字符,它的值是一个指定了字符的语法的 cons cell.
然后 Emacc 提供了高级函数 char-syntax 和 modify-synbtax-entry 来检查和修改语法表的内容,而不是像字符表那样使用 aset 和 aref.
每个缓冲区都有自己的主要模式,并且每个主要模式都有自己的语法表,每个语法表对同一个字符指定的作用可能不一样,比如,
在 Emacs Lisp mode 里面以 ; 开头的语句表示注释,在 C mode 里面 ; 则是表示语句(statement)的结尾.很明显,语法表对于每个缓冲区来说是局部的(local).
总的来说,每个 mode 都有自己的语法表,而使用了 mode 的缓冲区会安装对应 mode 的语法表.比如, Emacs Lisp mode 的语法表就是 emacs-lisp-mode-syntax-table 的值,
C mode,改变 mode 的语法表会影响所有是用了该 mode 的缓冲区.
语法表会用继承于别的语法表,被继承的语法表叫做父语法表(parent syntax table),继承得到的语法表可以只改变某些字符,其余的不变.
Emacs 提供一个标准语法表 (standard-syntax-table),也就是默认的父语法表,同时这张语法表也被 Fundamental mode 使用着,按照继承级别来说就是根语法表.
Emacs Lisp reader 不使用语法表,它有自己内置的语法规则,并且不能改变.一些 Lisp 系统会提供重新定义读取语法(read syntax)的能力,比如 Racket,而 Emacs Lisp 不提供这特性是为了简洁.
语法描述符 (Syntax Descriptors)
字符的语法类决定了它的语法角色,每个语法表都会为每个字符指定语法类,不同语法表上的同一个字符的语法类是没有必然联系的.
每个语法类都是用一个方便记忆的字符(mnemonic character)来命名,这个字符叫做指示字符(designator character),虽然说指示字符要被分配和语法类,
但是不管指示字符被分配到哪个语法类,示字符本身是有自己的语义,比如 \ 就是表示转义,不管它在语法表上面是哪个类, M-: (info "(elisp)Syntax Class Table") 有语法类和它们的指示字符.
语法描述器是一个 Lisp 字符串,用来描述一个字符的语法类和其他语法属性.当你要修改一个字符的语法,给 modify-syntax-entry 函数传入一个语法描述器作为参数就可以完成了.
语法描述器的第一个字符就是语法类的指示字符,如果有第二个字符,那么就是前者的匹配字符(matching character),比如在 Lisp 里面 ( 的匹配字符就是 ),空格没有匹配字符.然后剩下的字符就是额外的语法属性(syntax properties)了,可以看 M-: (info "(elisp)Syntax Flags").
"<DESIGNATOR-CHAR><MATCHING-CHAR><PROPERTIE-CHARS>"
最简单的语法描述符就是只有一个指示字符.再举一些例子,
C mode 里面的 * 的语法描述器是 . 23,第一个字符是 punctuation (可以查看上面的 Syntax Class Table 得知), 中间的 <MATCHING-CHAR> 没有被使用,第3和第4字符:2和3,具体意义可以通过上面的 Syntax Flags 查看.
语法表函数 (Syntax Table Functions)
一些关于创建,访问和修改语法表的函数.
语法属性 (Syntax Properties)
当语法表不足够灵活来指定一门语言的语法,可以为缓冲区上的特定字符而覆盖语法表,通过应用 syntax-table 文本属性来完成,该属性的合法值是以下其中之一:
SYNTAX-TABLE如果属性的值是个语法表,缓冲区就会使用该表,而不是当前缓冲区正在使用的
mode的语法表.(SYNTAX-CODE . MATCHING-CHAR)是 (
raw syntax descriptor)raw形式的语法描述器.nil是用缓冲区当前的语法表,不做任何改变.
当 parse-sexp-lookup-properties 为 non-nil 的时候,像 forward-sexp 这种扫瞄函数就会注意文本的 syntax-table 属性,然后根据属性的值来决定是否是用对应的语法表.
否则就是用当前语法表.
动作和语法 (Motion and Syntax)
能够在有着指定语法类的字符之间进行移动的函数. skip-syntax-forward 和 skip-syntax-backward 可以分别向前和向后跳过指定的语法类,在遇到不符合条件的地方停下,参数是一个表示了多个语法类的字符串 SYNTAXES 和一个位置限制 LIMIT,最后返回移动距离.
如果 SYNTAXES 是以 ^ 开头,那么就会跳过没有出现在 SYNTAXES 里面的情况.
backward-prefix-chars 把移动点经过任意数量带有前缀语法(prefix syntax)的表达式,前缀语法表达式可以通过 M-: (info "(elisp)Syntax Flags") 来了解.
解析表达式 (Parsing Expressions)
这章是关于如何解析(parsing)和(scanning)对称的表达式(balanced expressions),我们叫它们 sexps,这是 Lisp 的术语,实际上这些函数还能对 Lisp 以外的语言生效.
语法表控制的字符的直译(interpretation),这也就是为什么同一个字符在不同 mode 下有不同作用的原因.而一个字符的语法控制着它如何改变 parser 的状态,而不是描述它自身的状态.
比如一个字符串分隔符(string delimiter character)就让 parser 在处于字符串中(in-string)和处于代码中(in-code)两个状态之间切换,
但是字符的语法不能直接说明字符是不是字符串里面,比如: (put-text-property 1 9 'syntax-table '(15 . nil)),根据 M-: (info "(elisp)Syntax Table Internals") 可以看到 15 是字符串分隔符的代号,
这是告诉 Emacs 当前缓冲区的前8个字符都是字符串分隔符,而不是一个字符串,而结果就是 Emacs 会把这个 8 个字符看作4个连续的字符串常量(因为不同的变换状态,所以是4个).
- 根据解析移动 (Motion via Parsing)
根据解析结果移动的相关
API. - 位置解析 (Position Parse)
根据缓冲区位置来计算出语法状态,用于语法分析(
syntactic analysis),比如缩进信息这种有用的信息. - Parser的状态 (Parser State)
Parser状态是一个装着11个元素的列表,描述了语法parser的状态,像syntax-ppss这种函数就会返回一个parser状态作为返回值.Parser状态的元素分别有以下含意:- 第一个元素: 括号的深度,从0开始计算,如果闭合括号
)比 开括号(还要多,那么该元素就是一个负整数. - 第二个元素: 最内层的括号分组的开始字符的位置,如果没有最内层表达式就返回
nil,比如(a b c)),解析表达式内任意一个位置就是开括号(的位置. - 第三个元素: 上一个完全子表达式(
complete subexpression)的结束处开始的字符位置,否则为nil, 比如(a b c),如果点在a的后面,那么该元素就是a的位置,如果点在a上,那么就是nil. - 第四个元素: 如果位置在字符串上就返回该字符串的结尾字符,或者如果结尾的是通用分隔符,那么就返回
t.比如位置处于"abc"中,返回就是34("). - 第五个元素:
t表示位置处于不可嵌套的注释中,否则就是nil. - 第六个元素: 如果端点(
end point)跟在一个引用字符后面(我也不太清楚什么是引用字符),那么它就是t. - 第七个元素: 扫瞄时候遇到的最小括号深度(难道不应该都是0吗).
- 第八个元素: 当前位置的注释风格.
- 第九个元素: 但位置处于字符串或者注释之中,该值就是开始的位置.
- 第十个元素: 当前位置从里到外所有开括号的位置.
- 第十一个元素: 如果上一个被扫瞄到的缓冲区位置是双字符结构(比如注释分隔符或者转义和字符引用符号对)的第一个字符,那么就是该位置上的
syntax code,否则为nil.
- 第一个元素: 括号的深度,从0开始计算,如果闭合括号
- 底层解析 (Low-Level Parsing)
使用表达式解析器(parser)的最基本方法就是指定开始位置和结束位置之间的范围进行解析:
parse-partial-sexp. - 控制解析 (Control Parsing)
一些控制解析的参数.
语法表内部实现 (Syntax Table Internals)
语法表就是由字符表实现的,但是大部份 Lisp 程序不直接通过它的内部实现来工作,语法表并不是以语法描述符的形式储存语法数据(syntax data).
语法表的每个条目都是一个生语法描述符 (raw syntax descriptor): (SYNTAX-CODE . MATCHING-CHAR).
SYNTAX-CODE 是一个整数,编码了语法类和语法 flags, MATCHING-CHAR 就是匹配字符(如果有的话).
总体上来看跟语法描述符差不多.
使用 aref 获取一个字符的生语法描述符: (aref (syntax-table) CHAR). 分别在 Lisp Interaction mode 中和 C mode 中的 * ,
;; in Lisp Interaction mode (lisp-interaction-mode) (aref (syntax-table) ?\*) ;; => (3) ;; in C mode (c-mode) (aref (syntax-table) ?\*) ;; => (393217)
分别是 3 和 393217,如果想根据 SYNTAX CODE 获取出语法类,那么可以使用 syntax-class:
(syntax-class '(3)) ;= > 3 (syntax-class '(393217)) ; => 1, 393217 = 1 + (ash 17) + (ash 18), syntax descriptor: ". 23"
Syntax flag 被编码在高阶位上(higher order bits),从最小的关键位(永远是第0位)之后的16位(bits)或16位以上进行储存,比如上面 C mode 的 *,它的 SYNTAX-CODE 应该是这样的,
110 0000 0000 0000 0001
根据它的语法描述符 ". 23" ,可以推理到它的计算过程如下,其中 23 是 SYNTAX FLAGS:
- 指示器字符
.表示的语法类punctuation,对应的代码为1(文档有语法类对应的代码表). - 第一个
FLAG为2,根据prefix对应flag的表可以看出,要从第0位开始往左移动17位,(ash 1 17). - 第二个
FLAG为3,同上,左移动18位,(ash 1 18). - 综合所得
(+ 1 (ash 1 17) (ash 1 18)),等于393217.
其实 Emacs Lisp 已经提供了 string-to-syntax 帮助我们计算出语法描述符的 SYNTAX CODE.
分类 (Categories)
Emacs Lisp 提供了别的方法给字符分类: 根据需要来定义不同分类,然后独立地把字符分配到一到多个分类中.
每个缓冲区都有一个分类表(category table),记录了定义哪些分类以及哪些字符属于哪些分类.
每个分类表定义了自己的分类,实际上它们大部份都复制标准分类表(standard categories table)来的.
具体 API 看文档.
线程 (Threads)
以前的计算机的 CPU 都只有一个核心,也就是一时间只能处理一个任务,
为了让计算机 看上去 能够像忍者那样(通过高速移动)分身同时处理多个任务,假设有 A, B, C 三个任务,
人们想到了让 CPU 在一段时间内执行任务 A,这个时间非常短,时间结束后记录下任务 A 的进度,
然后 CPU 去执行任务 B,执行一段时间后同样记录下任务 B 的进度,
接着 CPU 去执行任务 C ,经过一轮切换又回到 A 这个未完成的任务上,
假如 A 在上一轮中已经完成了,那么从这一轮开始再也不会切换到 A 而是下一个未完成的任务的执行上了;
如果其中个任务的优先级比较高,可以在不需要等到所有任务都切换一轮地情况下让 CPU 拥有更加多的机会/时间执行优先度高的任务,
如此类推一直持续直到所有任务完成.
请务必理解这种 CPU逐个执行不同任务的"切片" 的方式, 这个概念以后会在协程(coroutine),进程(process)以及异步编程中出现.
任务的调度方式有两种: 协作式(cooperative or non-preemetive)以及抢占式(preemetive).
第一种就是在定义任务时制定好计划进度:执行到什么时候为一个阶段,什么时候为下一个阶段,在执行时按照计划进度来执行,
每当执行完任务的一个阶段时就暂停下来把 CPU 让出去执行下一个任务,如此类推,直到所有任务都完成, 任务之间的调度由任务自身决定,
因为没有规定任务的每个阶段需要多少时间,所以每次切换任务的时间都不一样,如果某个任务是 意外 永久占据了 CPU,
那么其它任务就一直处于等待 CPU 的状态;这种调度方式也是有优点的,在只有一个核心处理任务并且没有认为干涉的情况下,
这些任务每次都是以同样的顺序执行,在多核处理下就没有这个优势了;
第二种则是 由操作系统对任务进行调度,操作系统决定任务执行多久才让出 CPU,每个任务的切换时间是一样的,
一旦时间到了就会记录下当前执行中任务的进度,并强迫它把 CPU 让出去执行下一个任务,这样就没有第一种调度方式的弊端了,
但也是因为是操作系统进行控制调度,不管是一个核心还是多个核心处理这些任务,它们的执行顺序是不可预测的,
所以衍生出了如何确保多任务执行顺序的问题.
这两种调度方式中,抢占式是比较主流的.
一个线程的本质就是一个任务, CPU 的一个核心同一时间只能跑一个进程(process),而一个进程支持多个线程,
因此同一个进程下的线程们占用同一块内存区域,也就是进程占用的区域,线程之间可以通过访问/修改同一个内存地址实现 交流/通信,
通信必然需要考虑一个 先后 问题,就像两个人聊天一样,第一个人先说话,另外一个人听完后回应,第一个人听完回应后也作出自己的回应,
如此循环,直到两人聊天结束.
就拿一个假设的例子来看: 现有两个线程,其中一个线程 \(A\) 修改某个内存地址,另外一个线程 \(B\) 访问这个内存地址来获得修改后的值.
但因为是抢占式调度,容易出现这种情况: 线程 \(A\) 在执行修改到一半时被强迫让出 CPU,这样没法确定线程 \(A\) 是否在让出 CPU 前修改到了内存,
因此线程 \(B\) 获取到的值 可能 是未修改的,结果是不确定的.
为此必须要确保线程们的执行顺序能够正确完成任务,从而避免两个或者更多的线程同时访问/修改同一个内存地址的情况,这有一个叫法: 保证 线程同步.
Emacs Lisp 提供有限的并发机制-线程的支持,它的线程模型基本上是协作式(mostly cooperative, or non-preemetive)线程模型,
然而, Emacs 的线程支持已经被设计成方便以后支持更加细粒度(fine-grained)的并发,让程序不再依赖协作式线程.
在调用 thread-yield 发送请求时,或者在等待键盘输入/异步进程的输出时,又或者是与线程相关的阻塞操作时(比如 mutex locking 或者 thread-join),线程切换会出现.
Emacs Lisp 提供原操作来创建以及控制线程,同时也提供创建和控制互斥锁(mutexes)和条件变量(condition variables)用于线程同步.
全局变量被所有 Emacs Lisp 线程共享,而局部变量不是,一个动态的 let 绑定是局部的.
每个线程都有自己的当前缓冲区以及匹配数据(match data).
注意, let 绑定在 Emacs Lisp 的实现中是被特别对待的,没有任何方法可也复制 let 的 unwinding 以及 rewinding 行为.
所以,即使用 unwind-protect 手动 let 也不能让一个变量变成线程特定(thread-specific).
在词法绑定的情况中,闭包和其它 Lisp 对象一样,闭包中的绑定是被激活该闭包的线程共享的.
基础的线程函数 (Basic Thread Functions)
线程可以被创建和等待,不能被直接结束,但是当前线程可以被隐式结束,能够发送信号给其它的线程.
虽然该小节的剩下内容都是 API,但是唯读这一块的内容我很感兴趣.
(make-thread FUNCTION &optional NAME)创建一个调用
FUNCTION的线程并且返回该线程,在FUNCTION返回的时候线程就结束了.事实上(
in effect),被创建的线程没有任何局部绑定,新线程的当前缓冲区是从当前的线程中给继承的.NAME是给线程提供一个名字,目的用于调试以及告诉开发人员的线程目的,是一个字符串.(threadp OBJECT)判断对象是否一个
Emacs线程.(thread-join THREAD)阻塞(block)直到
THREAD结束或者当前线程接收到消息.返回THREAD执行函数的结果.如果THREAD早已经结束就会马上返回.(thread-signal THREAD ERROR-SYMBOL DATA)类似用于引发异常的
signal,区别信号是发送给线程,而大部份信号的动作都是停止执行.如果
THREAD是当前线程就会立刻调用signal,否则THREAD就会接收信号并且成为当前线程.如果
THREAD被mutex-lock, condition-wait 或者 thread-join阻塞,那么thread-signal就会取消它的阻塞 (unblock it).如果
THREAD是主线程,那么信号就不会被传播到那里,相反会作为主线程的消息(message)被显示.关于
Emacs是如何处理信号的,可以参考M-: (info "(elisp) Processing of Errors").(thread-yield)给下一个可运行线程让出执行(Yield execution to next runnable thread).
(thread-name THREAD)获取线程的名字,这个名字是在
make-thread指定的.(thread-live-p THREAD)判断线程是否存活.
(thread--blocker THREAD)返回
THREAD正在等待(waiting on)的对象.这个函数主要是用于调试的.如果线程因为
thread-join被阻塞,就会返回等待的线程;如果是因为mutex-lock就返回mutex;如果是因为condition-wait就返回条件变量.否则,返回
nil.(current-thread)返回当前的线程.
(all-threads)返回一个包含所有存活对象的列表.
main-thread储存了主线程的变量,如果
Emacs编译的时候没有线程支持就返回nil.(thread-last-error &optional CLEANUP)当线程执行的代码发送了一个错误信号并且没被处理,那么线程结束.其他线程可以通过这个函数来访问造成线程错误的原因.
只要有线程发生错误,结果就会被重写.
互斥锁 (Mutexes)
在任何时候有0到1个线程会拥有一把互斥锁(mutex).如果线程在别的线程早已经拥有的情况下试图获取(acquire)一个 mutex,
那么该线程会一直阻塞直到 mutex 被其它线程释放为止.
Emacs Lisp 的 mutexes 是可以递归的,就是说一个线程可以在自己早已拥有 mutex 的情况下再次获取一到多个 mutexes.
Mutex 会记录被获取多少次,然后获取次数一定要与释放次数匹配,最后一次的释放会把 mutex 还原成未被持有的状态 (unowned state),
允许其他线程获取它.
(mutexp OBJECT)判断是否
mutex.(make-mutex &optional name)创建一个
mutex并且返回它,类似线程可以指定名字.(mutex-name MUTEX)获取
MUTEX的名字,这个名字是被make-mutex指定.(mutex-lock MUTEX)一直阻塞直到有线程获取了
MUTEX,又或者直到该线程接收到了thread-signal发送的信号.如果该线程早已经拥有MUTEX,那么就直接返回.(mutex-unlock MUTEX)释放
MUTEX.如果该线程没有拥有MUTEX就会引发一个错误(signal an error).(with-mutex MUTEX BODY...)BODY执行的时候自动获取一个MUTEX,执行结束后自动释放,返回的结果是BODY的结果.
条件变量 (Condition Variables)
条件变量是线程同步的一种手段,在一些事件发生前一直阻塞,线程等到条件变量收到其它线程的通知,然后继续执行.条件变量需要结合 mutex 以及一些条件(conditions)来工作.正确的操作是,
;; for thread which is waiting (with-mutex mutex (while (not global-variable) (condition-wait cond-var))) ;; for thread which is notifying (with-mutex mutex (setq global-variable (some-computation)) (condition-notify cond-var))
(make-condition-variable MUTEX &optional NAME)创建一个关联
MUTEX的条件变量并且返回它,还可以给这个条件指定一个名字.(condition-variable-p OBJECT)判断对象是否条件变量.
(condition-wait COND)等待另外一个线程通知
COND,这个函数会一直阻塞直到条件COND被通知,又或者该线程接收到一个用thread-signal发出的信号.在不持有条件变量关联的
mutex下调用这个函数是错误的.这个函数会在等待的时候释放mutex,这样那些要发送通知的线程就可以获取mutex了.(condition-notify COND &optional ALL)通知
COND.一定要在持有mutex的情况下才能调用这个函数.一般是单个等待中的线程接收到通知,如果ALL是non-nil,那么所有等到COND的线程都会接收到通知.这个函数会在等待的时候释放
COND关联的mutex,这样那些等待COND的线程就可以获取mutex了.(condition-name COND)获取
COND的名字,这个名字是被make-condition-variable指定的.(condition-mutex COND)获取
COND关联的mutex,关联的mutex是不可改变的.
线程列表 (The Thread List)
list-threads 命令可以实时列出所有当前存活的线程,每个线程都是通过 make-thread 指定的名字或者是 Emacs Lisp 提供的标识展示.
thread-list-refresh-seconds列表刷新时间.
该列表的缓冲区支持一些命令:
b: 展示线程当前执行点上的backtrace,注意这个backtrace就是一个快照,实际时刻的状态可能与此刻显示的不一样.在
backtrace缓冲区中按下g可以进行刷新.s: 给线程在当前的执行点发送信号.在s后输入q发送结束(quit)的信号或者e发送错误(error)的信号.线程可以实现对信号的处理,否则默认行为是结束线程.使用这个命令的是后必须先要理解如何重启目标线程,因为
Emacs session会因为重要线程被杀而表现不正常.g: 更新线程列表.
进程 (Processes)
在操作系统的术语中,一个进程就是一个能够让程序运行的地方. Emacs 在进程内运行, Emacs Lisp 能够调用其它程序的进程,让它们在自己的进程内运行,
这些进程被称为 Emacs 进程的 subprocesses 或者 child processes,也就是子进程, Emacs 进程就是它们的 parent process,也就是父进程.
Emacs 的子进程可以是同步或者异步的,取决于创建时候的设定.同步的子进程会让 Lisp 程序等待子进程的结束才能继续执行;
而异步的进程则是可以与 Lisp 程序平行/同时运行.在 Emacs Lisp 中,子进程是 process 对象, Lisp 程序会通过这个对象来与子进程通信或者控制它,
比如,可以给子进程发送信号,从子进程获取状态信息/接收输出,或者给进程发送输入.
除了程序的进程外, Emacs Lisp 还可以打开几种不同的设备的链接,以及不同或者相同机器上的进程链接,支持的类型有:
TCP 以及 UDP 网络链接,序列端口链接(serial port connections)和管道链接(pipe connections),每个链接都是一个进程目标.
可以利用 processp 来判断一个对象是否一个进程.除了当前 Emacs session 的子进程外,还可以访问同一台机器上的其它进程.
创建子进程 (Subprocess Creation)
有三个原函数(primitives)可以用来创建一个新的子进程给程序运行: 创建并返回一个异步进程的 make-process, 创建但不返回同步进程的 call-process 和 call-process-region.
三者的参数形式十分相似,它们都是指定一个要运行的程序文件,然后再可选地提供参数.如果指定的文件不可执行或者没找到,那么就会发送一个错误信号.
如果文件名字是相对路径形式的,那么就会在变量 exec-path 包含的文件列表中查找, Emacs 在启动的时候就会根据环境变量 PATH 的值来初始化 exec-path.
标准的文件名字结构中, ~, . 和 .. 在 exec-path 是可以被正常解析的,但是环境变量替换(environment variable substitutions),比如 Emacs 是不认识 $HOME 的,但是可以使用 (substitute-in-file-name "$HOME") 执行替换.
exec-path 中的 nil 表示 default-directory.可以通过 exec-suffixes 设置执行文件的后缀来进行查找.
注意, call-process 和 call-process-region 的 PROGRAM 参数应该只包含程序的名字,不应该包含给程序提供的参数,要通过 ARGS 参数来传; make-process 只有一个 COMMAND 参数,可以包含程序名字以及参数.
每个子进程创建函数都有一个的参数用来指定程序的输出地方,一般就是缓冲区或者缓冲区的名字对应的形式参数是 BUFFER 或者 DESTINATION,可以是 nil,表示舍弃输出,除非有个自定一个筛选函数(filter function)处理输出.
如果指定的缓冲区不存在 Emacs 会自动创建.一般来说,不应该让多个进程把输出发送给同一个缓冲区,因为会导致输出随机混乱,而同步进程可以把输出发送给文件.在默认情况下,标准输出和标准错误的目的地是同一个,三个原函数允许决定是否同一个目的地.
比如要执行 ls 命令来显示 /home/saltb0rn 目录下的文件,并且把输出发送到缓冲区 - ls-buf 中.
;; async process (make-process :name "list-dir" :buffer "ls-buf" :command (list "ls" "/home/saltb0rn")) ;; sync process (call-process "ls" nil (list "ls-buf" "ls-buf") nil "/home/saltb0rn") ; 发送当前 buffer 的 (point-min) 到 (point-max) 的文本给 =ls=,可以新建一个 buffer 并且输入 /home/saltb0rn (call-process-region (point-min) (point-max) "ls" nil "ls-buf") ;; 对于一个以上的参数的命令 cp (make-process :name "cp-file" :buffer "cp-buf" :command (list "cp" "/home/saltb0rn/a" "/home/saltb0rn/b")) (call-process "cp" nil (list "cp-buf" "cp-buf") nil "/home/saltb0rn/a" "/home/saltb0rn/b") ;; call-process-region 就不演示了
这里要注意, wildcard 字符以及其他 Shell 的东西是没有用的,还要再次提醒 ~ 是解析不了的,要手动解析.
还有子进程会默认继承 Emacs 的环境,可以通过覆写 process-environment 来改变继承的环境.
Shell参数 (Shell Arguments)
有时候 Lisp 程序需要给一个 shell 一个由用户指定的命令.这些程序应该能够支持任何合法的文件名字,不过 shell 会特别对一些字符,所以这些字符出现在文件名字中会让 shell 很困扰,
shell-quote-argument 可以解决这个问题,构建一个 shell 命令.
(concat "diff -u " (shell-quote-argument oldfile) " " (shell-quote-argument newfile))
split-string-and-unquote 可以把命令变成一个字符串列表, combine-and-quote-strings 可以把一个字符串列表合并成一个命令.
这两个函数一般都是给 make-process, call-process 或者 start-process 是用的.注意, combine-and-quote-strings 并不是为了做和 shell-quote-argument 的事情,
所以一些 shell 会运算的特别字符还是需要用 shell-quote-argument 来处理,也就是说,当涉及到 shell 一定要用 shell-quote-argument.
创建同步进程 (Synchronous Processes)
在创建一个同步进程后, Emacs 需要等到进程结束后才能继续做其它事情.在等待的过程中,用户可以输入一次 C-g 给进程发送 SIGINT 信号来杀掉该进程,
但这只是告诉进程要结束,进程还需要等到一小段时间结束,在这个期间输入第二次 C-g 会发送 SIGKILL 给进程,立刻杀掉进程(在 MS-DOS 上是个例外).
同步的子进程会返回一个标识,也就是状态码,来说明程序是如何结束的:正常结束或者因为一些错误而结束.同步子进程的输出一般都是被编码系统编码过的,就像读取文件一样,输入同样经过编码.
在 创建子进程 的那篇笔记中有提到一下 call-process 的用法,这里就不详细笔记了,主要是关于 process-file 这个函数,它和 call-process 差不多,
不同之处在于 process-file 创建的子进程会根据 default-directory 的值来决定是否是用一个文件处理器(file handler), default-directory 的值就是子进程当前的工作目录,
process-file 可以在远程目录运行一个进程,当然本地也是 okay 的,是 call-process 的远程版本.
(如果你用 Emacs 远程编辑了文件,可能需要在远程服务器上执行一些命令,那么这个毫无疑问是你想要的),=process-file-shell-command= 是一个更加方便的版本.
其他就不写了,自己读文档.
创建异步进程 (Asynchronous Processes)
创建一个异步进程后,进程会和 Emacs 平行/同时运行, Emacs 还可以与它通信,但是注意, Emacs 与异步进程的通信是部分异步的:
Emacs 只有在调用特定函数下才会给进程发送数据, Emacs 只有在等待输入或者一段时间延迟后(waiting for input or for a time delay)才能接收到进程的数据.
异步进程是由 pty (pseudo-terminal) 或者 pipe 来控制的,选择 pty 或者 pipe 是在创建进程的时候就决定的了,默认是根据 process-connection-type 的值来决定.
一般 pty 对于用户来说是最好的选择,这个是对应于 Shell 模式,它支持进程和它的子进程之间的作业控制(job control)操作,比如 C-c, C-z 等等,
因为交互形的程序会把 pty 看做终端设备(terminal device). pipe 是不支持这些特性的,但如果进程是用于 Lisp 程序内部,那么 pipe 就是最好的选择,因为 pipe 更加高效,
并且避免/免疫(immune)像 pty 那样由于大量信息(500 byte 左右)而引入的全角字符注入(stray character injections)的问题,还有大部份系统都会对可运行 pty 的总数进行限制,这是一件好事.
make-process 就是创建异步进程的原函数, start-process 对于它来说是一个高级版本,和同步进程的 process-file 一样,异步进程的创建函数也有远程版本 start-file-process,
也就是说可以远程执行异步命令,如果 default-directory 是远程目录,那么执行的就是远程命令,否则是本地命令, start-file-process-shell-command 则更加好用.
其它自己看.
删除进程 (Deleting Processes)
"删除一个进程"会马上断开 Emacs 和这个(Emacs的)子进程的链接.其实在进程结束之后就会自动被 Emacs 删除(不一定是马上).可以在任何时候删除一个进程,删除一个已经停止但是又未被删除的进程是没有任何问题的.
删除一个进程就是给这个进程以及它的子进程发送一个信号,并且调用进程的哨兵(sentinel,同步进程没有哨兵这种东西).在进程被删除的时候,只要有其它 Lisp 程序指向它,那么它就会继续存在.
所有能够访问进程的原函数都接受已经被删除的进程.但是那些涉及 I/O 操作或者发送消息操作的原函数会引发错误.
delete-process 可以通过给进程发送一个 SIGKIL 信号来删除进程,它接收一个参数,可以是进程对象/进程名字,缓冲区对象/缓冲区名字,缓冲区对象以及缓冲区名字表示 get-buffer-process 返回的进程.
对一个运行中的进程调用 delete-process 会中止该进程,更新该进程的状态以及马上运行该进程的哨兵,对已经结束的进程没有效果.
如果正在运行的进程对象是一个网络(network),串口(serial)或者 pipe 连接,那么它的状态就变成 closed,否则是 singal.
进程信息 (Process Information)
一些能够访问进程信息的函数:
- 按照条件来查询符合条件的进程,可以用
list-processes以及process-list; - 获取进程名字有
process-name; - 根据名字获取进程;获取进程执行的命令有
process-command; - 获取链接进程(就是网络,串口或者管道链接其中一个)的信息;
- 和获取进程
pid有process-id; - 获取进程状态有
process-status; - 判断进程是否存活有
process-live-p; - 获取进程的结束状态码有
process-exit-status; - 获取进程正在使用的终端名字有
process-tty-name; - 获取进程的输出解码方式以及输入的编码方式有
process-coding-system; - 设置进程的输出解码方式以及输入的编码方式
set-process-coding-system; - 获取进程的属性列表(
property list, or plist)中的某个属性的值有process-get; - 设置进程的属性列表中的某个属性有
process-put; - 获取进程的属性列表有
process-plist; - 设置进程的属性列表有
set-process-plist;
给进程发送输入 (Input to Processes)
可以给异步子进程发送数据,如果子进程运行的是一个程序,那么数据就是该程序的标准输入;如果运行的是一个链接,那么就是给连接的设备或者程序发送的数据.
一些操作系统会限制 pty 的缓冲输入的大小,在这些系统上, Emacs 会定期发送 EOF 分开数据来强迫数据通过,对于大部份程序而言,这些 EOFs 是无害的.
在子进程接收到输入之前,会先根据 set-process-coding-system 的设定,或者为 non-nil 的 coding-system-for-write,又或者默认的编码方式来对这些输入进行编码.
有时候系统不能给进程接收输入,因为输入缓冲区(input buffer)满了,当这种情况发生了,发送输入的函数会等一下,接收子进程的输出然后再次尝试.
process-send-string 和 process-send-region 就是发送输入的函数, process-running-child-p 判断一个进程是否有自己的子进程的控制权.
这些函数的 PROCESS 参数可以是一个进程对象,进程的名字,缓冲区对象或者缓冲区名字(=get-buffer-process=可以根据缓冲区名字获得对应进程).
给进程发送信号 (Signals to Processes)
给子进程发送信号是阻止子进程活动的一种方法.有几种不同的信号,不同的信号有不同的效果,信号是由操作系统定义的.
比如 SIGINT 意味着 interrupting,也就是中断子进程,相当于输入 C-c 或者类似的操作.
大部份信号会杀掉进程,也有一些停止或者重启进程的执行,大部份信号可以被程序可选地处理,如果程序处理了信号,那么就不能知道信号的标准效果了.
(if the program handles the signal, then we can say nothing in general about its effects.)
在 Emacs 中,给子进程发送信号分显式和隐式, Emacs 会自动在特定时间里面发送信号,比如会在杀掉缓冲区的时候给所有关联的进程发送 SIGHUP 信号;
杀掉 Emacs 会给所有 Emacs 的子进程发送 SIGHUP 信号;这些都是隐式的方式.而显式的方式就是调用 Emacs Lisp 提供的 API 来手动杀死进程.
Emacs Lisp 的每一个信号发送功能都接收两个可选的参数: PROCESS 和 CURRENT-GROUP.
PROCESS 一定要是个进程对象,或者缓冲区对象,或者缓冲区名字,又或者 nil,当是一个缓冲区对象或者是缓冲区名字的时候,
会自动通过 get-buffer-process 获取进程. nil 表示当前缓冲区的进程.除了(except with) stop-process 和 continue-process 外,在进程不存活,或者进程是一个链接的情况下调用这些函数会引发错误.
CURRENT-GROUP 只有在运行一个作业控制(job-control)的 shell 作为 Emacs 子进程的情况下才会有区别. (类似线程组,建议先了解以下进程组的概念).
如果 CURRENT-GROUP 是 non-nil,那么信号就会被发送到 Emacs 与子进程通信的终端的当前进程组,当子进程是一个作业控制 shell 时候,该参数就表示 shell 当前 subjob.
如果 CURRENT-GROUP 是 nil,信号会被发送到 Emacs 的中间子进程的进程组,如果子进程是一个作业控制的 shell,那么这个参数表示 shell 本身.
如果 CURRENT-GROUP 是 lambda,那么信号会被发送到拥有终端的进程组上,但是该参数不是 shell 本身.
APIs 就自己阅读了,要注意的是,这些 APIs 都是只是针对本地的异步进程的,如果要用 interrupt-process 中断非本地进程,那么就需要接合 interrupt-process-functions 来实现自己的 interrupt-process.
interrupt-process-functions 是一个函数列表,而 interrupt-process 会按照这个列表的顺序调用列表里面每个的函数,直到其中一个函数返回 non-nil,这些函数接收的参数和 interrupt-process 的一样.
默认函数 internal-default-interrupt-process 需要放在列表的最后一位.现实中 Tramp 就是这么实现 interrupt-process 的方式.
接收进程的输出 (Output from Processes)
异步子进程的输出在写入标准输出流之前会先通过一个叫做过滤函数(filter function)的函数.每个异步子进程都有一个默认过滤函数,其行为就是简单地插入输出到缓冲区中,
该缓冲区就是进程关联的缓冲区.如果进程没有关联缓冲区,那么默认的过滤函数就会丢弃输出.
如果子进程写入标准错误流,默认的错误输出也会先通过进程的过滤函数.
如果 Emacs 使用 pty (pesudo-TTY) 与子进程进行通信,那么就不可能分开标准输出流和标准错误流,因为 pty 只有一个输出通道,如果不想占用这些输出流,那么就把输出重定向到文件中.
make-process 的 :stderr 参数可以通过设置为 non-nil 来把错误输出和标准错误分开,这种情况下, Emacs 会使用 pipes 和子进程通信.
在子进程结束的时候, Emacs 会读取所有等待中的输出(pending output),然后停止读取子进程的输出.因此,如果有子进程有还存活的子进程,那么 Emacs 不会接收它们的输出.
只有在 Emacs 等待的时候子进程的输出才能到达,等待的情况有以下几种:在读取终端输入时候;在调用 sit-for 或者 sleep-for 的时候以及在调用 accept-process-output 的时候.
这最小化了困扰(plague)并行编程的掌握错误时机的问题(problem of timing errors).
比如,你可以安全的创建一个进程并且指定缓冲区或者过滤函数;如果期间代码没有调用任何执行等待操作的原函数,没有任何输出在你操作结束前到达.
在一些操作系统上, Emacs 只能每次读取子进程输出很小的一部分数据,这会造成严重的性能问题,针对这个问题可以通过设置 process-adaptive-read-buffering 为 non-nil,
以此来自动延迟读取,这样就可以让进程产生更多输出后才让 Emacs 读取.
- 进程关联的缓冲区 (Processes Buffers)
一个进程可以有一个关联的缓冲区,是一个普通的缓冲区,有两个目的:储存进程输出和决定杀掉进程的时机.
可以根据缓冲区来识别它所关连的进程,因为在实际情况中,一般一个进程关联一个缓冲区,大部份进程也会用缓冲区来编辑发送给进程的输入.
默认情况下,程序输出会被插入到关联的缓冲区中,当然可以通过自定义的过滤函数来改变这一行为.输出插入的位置由
process-mark决定,在插入文本后,process-mark会更新为文本的最后一个点,正常情况下process-mark就是在缓冲区的结尾.杀掉程序关联的缓冲区也会杀掉进程本身,如果进程的
process-query-on-exit-flag属性是non-nil,Emacs会先征求用户的确认.征求确认的这个行为是通过
process-kill-buffer-query-function来完成的,而它又是从kill-buffer-query-functions中运行的.具体
API就自己看了. - 进程过滤函数 (Filter Functions)
一个进程过滤函数就是一个从关联进程接收标准输出的函数.进程的所有输出都会被传入到过滤函数中,默认的过滤函只是把进程输出直接插入到关联的缓冲区中.
默认情况下,进程的错误输出也会被传入到过滤函数中,除非在创建进程的时候分开了标准输出流和标准错误流.
过滤函数只有在
Emacs等待某些事情的时候被调用,因为进程的输出只会在这种情况下到达.具体上面的导读有写,就不再重复了.一个过滤函数一定要接收两个参数: 关联的进程和一个字符串,这个字符串表示进程的输出,这个函数可以对输出做任何事情.
一般情况下,会在过滤函数执行期间禁止中止(quitting)的,否则在命令层面上输入
C-g或者停止一个用户命令会发生意想不到的情况.如果想要允许这一行为,可以把
inhibit-quit设置为nil,大部份情况下正确做法应该是使用with-local-quit宏.如果在过滤函数执行的期间发生错误,错误会被自动捕捉,所以错误不会停止进程的执行,但是如果设置了
debug-on-error为non-nil,错误就不会被捕捉.这样可以方便使用
Lisp调试器进行调试.很多过滤函数有时候会模仿默认过滤器的行为那样插入输出到进程关联的缓冲区中.这种过滤函数需要确保在插入前保存了当前缓冲区,
以及选择正确的缓冲区,插入后回复原来的缓冲区,除了这些外,还应该检查缓冲区是否存活,更新
process-marker,在某下情况下还需要更新点的值.正确做法应该是这样:
(defun ordinary-insertion-filter (proc string) (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((moving (= (point) (process-mark proc)))) (save-excursion ;; Insert the text, advancing the process marker. (goto-char (process-mark proc)) (insert string) (set-marker (process-mark proc) (point))) (if moving (goto-char (process-mark proc)))))))
如果想在新文本到达的时候显示进程的缓冲区,可以在
with-current-buffer前插入(display-buffer (process-buffer proc)).还有最后一个表达式的判断并非必须的,可以去掉判断强迫点在缓冲区的尾部,默认的过滤函数使用
insert-before-markers移动所有markers,包括窗口的点,因为这可能会移动不相关的
markers,所以一般最好只移动窗口的点,有或者设置插入点的类型为t.当执行过滤函数的时候,
Emacs会自动保存以及恢复匹配数据(match data).过滤函数接收到的进程输出会有任何不同大小,比如一个程序可能一行内会两次产生相同的输出,然后一次性发送一批 (a batch)
200个字符的文本,接下来再发送5批40个字符的文本.如果过滤函数在子进程的输出查找特定的文本,那么需要注意文本可能会被分开到不同的批次的输出的情况,方法就是把接收的文本插入到一个临时的缓冲区中以供搜索.
set-process-filter和process-filter分别给进程设置过滤函数和获取进程的过滤函数. - 解码进程输出 (Decoding Output)
当
Emacs直接把输出写入多字节(multibyte)缓冲区中,它就会根据进程的输出编码系统(coding system)来解码.如果编码系统是
raw-text或者是no-conversion,Emacs会使用string-to-multibyte把统一字节(unibyte)输出转换成multibyte,并且插入处理得到后的multibyte文本.可以使用
set-process-coding-system来指定进程使用的编码系统,否则,如果coding-system-for-read是non-nil,那么就会使用
coding-system-for-read指定的编码系统;或者是默认的编码系统.如果进程输出的文本包含空字节(
null byte),Emacs默认会使用no-conversion做为编码系统.最好不要使用哪种类似于
undecided这种根据数据判断编码系统的编码系统,因为对于异步子进程的输出不完全可靠.因为
Emacs必须分批次处理异步子进程的输出,Emacs必须尝试检查出每个批次的正确编码系统,但是这并非百分百成功.所以,最好自己指定一个编码系统,比如
latin-1-unix,不要undecided或者latin-1.在
Emacs调用一个进程过滤函数时,Emacs会根据进程的过滤编码系统(filter coding system)给过滤函数提供一个多字节字符串或者统一字符串的进程输出.Emacs会根据进程输出的编码系统来解码输出,这个编码系统通常会产生一个多字节字符串,除了像binary和raw-text这样的编码系统. - 接收进程输出 (Accepting Output)
主要是关于
accept-process-output的用法. - 进程和线程 (Processes and Threads)
Emacs的线程是相对于进程后面才得到支持的,由于动态绑定有时候会和accept-process-output同时被使用,默认情况下,一个进程会被锁定到(be locked to)那个创建了该进程的线程上,进程的输出只能被该线程接收.
一个
Lisp程序可以指定进程被锁定到哪个线程上,或者让Emacs解锁一个进程,一旦被解锁,进程的输出就可以给任何线程处理.一个时间内只可以有一个线程等待指定进程的输出,一旦进程开始等待输出,
Emacs就临时锁定进程直到accept-process-output或者sit-for返回.如果线程结束,所有被锁定到该线程的进程都会被解锁.
process-thread返回锁定进程的线程,如果进程已经被解锁就返回nil;set-process-thread把进程锁定到线程上.
哨兵:检测进程状态的改变 (Sentinels)
进程哨兵就是一个普通的函数,在关联的进程发送状态改变的时候会调用这个函数.哨兵函数接收两个参数:发生状态变化的进程对象以及一个描述状态变化的原因的字符串.
描述有这几种可能:
"finished\n""deleted\n""exited abnormally with code EXITCODE (core dumped)\n", 其中(core dumped)只有进程dumped core的时候才会出现."failed with code FAIL-CODE\n""SIGNAL-DESCRIPTION (core dumped)\n",SIGNAL-DESCRIPTION是系统定义信号的描述文本,比如SIGKILL的就是killed,其中(core dumped)只有进程dumped core的时候才会出现."open from HOST-NAME\n""open\n""connection broken by remote peer\n"
哨兵只会在 Emacs 等待某些事情的时候才能运行,这避免了哨兵在运行时机的错误,运行时机的错误会导致哨兵在其他的 Lisp 程序中间运行.
程序可以通过调用 sit-for 或者 sleep-for 来进行等待,这样哨兵就会运行了. Emacs 还允许哨兵在命令循环读取输入的时候运行.
delete-process 终结运行中的进程会调用哨兵.
Emacs 不会为多个原因而维护一个队列(queue)来调用一个进程的哨兵,只是仅仅记住当前的状态以及状态发生了改变的事实.
因此如果状态连续(come in quick succession)发生两次改变,那么哨兵只会调用一次.然而进程的终结必定只会运行一次哨兵.
因为不可能在进程结束后再次改变进程的状态了.
Emacs 会在运行进程的哨兵前先检查以下进程的输出,一旦哨兵因为进程终结而运行,那么就不会有更多的进程输出了.
如果哨兵要把进程的输出写入到进程关联的缓冲区中,那么该哨兵应该先检查缓冲区是否还存活,插入输出到一个已经死亡了的缓冲区中会引发错误.
如果缓冲区死亡了, (buffer-name (process-buffer PROCESS)) 会返回 nil.
在哨兵执行期间,中止操作(quitting)是被禁用了的,否则 C-g 会有意想不到的结果.如果实在要允许中止操作,
那么可以把 inhibit-quit 绑定为 nil,大部份情况下正确的做法应该是使用 with-local-quit 宏.
哨兵执行的期间发生的错误会被自动捕获,这样就不会停止程序的执行了,如果想捕获错误,可以把 debug-on-error 设置为 nil,这样可以使用 Lisp 调试器来调试了.
在哨兵运行时可以临时设置进程哨兵为 nil,这样哨兵就不会递归运行了,由于这样是不可能为哨兵指定一个新的哨兵.
Emacs 会在执行哨兵的时候自动保存和恢复匹配数据(match data).
set-process-sentinel 可以关联一个进程和一个哨兵,如果关联的哨兵是 nil,那么进程关联的就是默认哨兵: 在进程状态发生改变的情况下插入信息到进程的缓冲区中.
改变进程的哨兵会马上生效,在还没有调用被选中要运行的哨兵时候,并且指定了一个新的哨兵,那么最终调用的是新的哨兵.
process-sentinel 可以获取进程的哨兵.
在结束前询问 (Query Before Exit)
Emacs 结束的时候会终结所有它的子进程,给所有正在运行的子进程发送 SIGHUP 信号,链接会直接关闭.
因为子进程可能会做一些有价值的事情, Emacs 一般会先征求用户的确认再终结子进程.
每个进程都有一个询问 flag,如果是 non-nil (默认是 t) 就要求 Emacs 在结束的前或者杀掉进程的前征求用户的确认.
process-query-on-exit-flag 获取进程的询问 flag; set-process-query-on-exit-flag 设置进程的询问 flag.
confirm-kill-processes 为 t 的时候进程在结束前需要征求用户确认,如果是 nil, Emacs 杀掉进程就不需要征求确认了,
同时所有进程的询问 flag 都会被忽略.
系统进程 (System Processes)
除了访问和操作当前 Emacs 会话的子进程外, Emacs Lisp 还能访问同一台机器上的其它进程,这些称为系统进程,目的是与 Emacs 的子进程进行区分.
Emacs 提供了一些访问系统进程的原函数,但并非所有操作系统都支持这些 APIs,在不支持的系统调用这些函数都会返回 nil.
list-system-processes 返回一个系统进程列表,每个进程都是通过它的 PID 来辨别, PID 就是一个由操作系统分配的数字进程 ID.
process-attributes 可以根据 PID 来获取进程的属性列表,这个属性列表是一个 alist,有些属性并非所有平台都支持的,所以,这些不支持的属性是不会出现在列表上的.
属性有这些:
euid激活进程的有效用户
ID(EFFECTIVE USER ID),是一个数字,如果用户与激活当前Emacs会话的用户是同一个人,那么
euid就是和user-uid返回的值一样.user对应
euid的用户名字,是一个字符串.egid有效用户的用户组
ID.group对应
egid的用户组名字,是一个字符串.comm运行线程的命令名字,是一个字符串,通常是进程的可执行文件的名字,不包含目录,但是一些特别的系统进程会显示一个不对应可执行文件名字的字符串.
state进程的状态码,是一个有特定含意的字符串,最常见的值有(所有值可以通过
ps命令手册查询):"D": 不可中断的休眠 (uninterruptible sleep) , 通常是I/O;"R": 运行;"S": 可中断的休眠 (interruptible sleep) ,一般是在等待某些活动;"T": 停止状态,比如被一个作业控制信号停止;"Z": 僵尸状态: 进程已经被终结,却没被它的父进程收割(reaped)/清理.
ppid进程的父进程
ID,是一个数字.pgrp进程的进程组
ID,是一个数字.sess进程的会话
ID,是进程的session leader的进程ID,是一个数字.ttname进程的控制终端(
controlling terminal)的名字,是一个字符串,在Unix以及GNU系统上,通常是一个对应的终端设备的名字,比如
/dev/pts65.tpgid正在使用进程终端的前台进程组的进程组
ID,是一个数字.minflt先简单介绍以下,内存访问机制,进程并不是直接访问物理内存的,而是先通过内存管理单元(
MMU)来映射物理内存到虚拟内存上,然后访问虚拟内存.在进程开始时由进程造成的
minor page faults的数字,minor page faults是指访问的内存地址不存在虚拟地址空间上,但存在物理内存上.majflt在进程开始候由进程造成的
major page faults的数字,major page faults是指访问的内存地址不存在虚拟地址空间上,也不存在物理内存上.cminflt,cmajflt类似
minflt和majflt,不过还包含了进程所有子进程的page faults.utime用户上下文中的进程运行应用代码所花费的总时间,是一个
Lisp时间戳.stime系统内核上下文中的进程处理系统调用所花费的总时间,是一个
Lisp时间戳.timeutime和stime的总和,是一个Lisp时间戳.cutime,cstime,ctime类似
utime,stime和time,不过还包含了进程所有子进程的时间.pri进程的优先级,是一个数字,越小优先度越高.
nice进程的
nice值,是一个数字,nice值会影响优先级pri(new) = pri(old) + nice,所以nice越小进程更容易被被执行.thcount进程的线程数量.
start进程开始的时间,是一个
Lisp时间戳.etime距离进程启动的时间,是一个
Lisp时间戳.vsize进程的虚拟内存大小,单位是
kb(kilobytes).rss进程驻留集(resident set)占用的物理内存的大小,单位是
kb.在内存管理中,一个作业会按照规定大小划分成若干个单位,这些单位叫做页,每次进程活动的时候会加载若干个页,这些页的集合叫做进程驻留集.
pcpu从进程启动开始到现在使用的
CPU时间百分比,是一个0到100之间的浮点数.pmen进程驻留集合所占用的物理内存百分比,是一个
0到100之间的浮点数.args激活进程的命令的参数部分,参数是被类似
shell-command的原函数处理过.
事务队列 (Transaction Queues)
Emacs 支持利用事务队列来和子进程通信,首先用 tq-create 为特定进程创建一个事务队列,然后调用 tq-enqueue 发送事务.
事务队列是通过过滤函数的方式实现的.
网络链接 (Network)
Emacs 可以打开连接同一台机器或者不是同一台机器上的其它进程的流(TCP)和数据报(datagram)(UDP)网络链接. Emacs 把网络链接当作一个子进程来处理,
并且在 Emacs 中网络链接就是进程对象.但是和其它真正的进程不一样,它不是 Emacs 的子进程,所以它没有进程 ID,不能杀掉它也不能给它发信号.
能够做的就只有发送和接收数据. delete-process 会关闭链接,但是不会杀掉通信端的另外一头的程序.
Lisp 程序可以创建网络服务器来监听链接.网络服务器也是一种进程对象,但是和网络链接不一样,它并不会自己传输数据,当接收到连接请求的时候,网络服务器会创建一个新的网络链接,
这个新的网络链接会从网络服务继承一些信息,包括进程的属性列表,然后网络服务器回去监听更多的请求.
make-network-process 可以创建网络链接和网络服务器,接收关键字参数,参数 :server 为 t 的时候表示创建网络服务器进程, :type 为 datagram 表示创建数据报链接.
进程是有类型之分的,可以通过 process-type 函数来获取进程类型,网络链接或者网络服务器的会返回 network 的 symbol,
串口(serial port)链接的就是 serial, 管道(pipe)链接的就是 pipe, 真正的子进程是 real.
对于网络链接, process-status 可以返回 open, closed, connect, stop or failed 的其中之一,对于网络服务器,必定是返回 listen.
除了 stop,上面的每一个状态对于真正的子进程是不可能的.
可以利用 stop-process 和 continue-process 来停止和恢复网络链接,对于一个网络服务器来说,停止意味着不接受新的链接,当恢复网络服务器的时候,
最多可能有 5 个网络请求被添加到队列中,这个数值可以被增加,除非操作系统不允许,具体看 make-network-process 的 :server 关键字参数.
对于一个流链接来说,停止意味着不处理输入,所有到达的输入都会等待直到恢复了链接;对于一个数据报链接来说,一些数据包会被添加到队列中,但是输入会被丢弃.
可以用 process-command 来判断网络链接或者网络服务器是否停止了, non-nil 意味着停止,比如以下例子.
(make-network-process :name "echo-server" :server t :buffer " *echo-server*" :service 10000) (stop-process (get-process "echo-server")) ;; (continue-process (get-process "echo-server")) (process-command (get-process "echo-server"))
还有 Emacs 能够创建加密的网络链接,可以使用内置或外部的支持.内置支持是使用 GnuTLS Transport Layer Security Library,
可以通过 gnutls-available-p 检查 Emacs 是否编译了 GnuTLS 支持;外部支持就是是用 starttls.el 库,这个要求系统安装了 gnutls-cli 这样的功能,而 open-network-stream 可以帮助你处理创建加密链接的细节.
网络服务器 (Network Servers)
主要是关于 make-network-process 的一些介绍,就不详细写了.
数据报 (Datagrams)
数据报链接是通过独立的数据包(packets)而不是数据流来通信的.调用一次 process-send 会发送一个数据包,并且每接收一个数据包都会调用一次过滤函数.
数据报链接不需要每时每刻都要连接上远程节点,它有一个远程节点地址来指定数据包发送到哪里去,每当接收一个数据包并且传入到过滤函数的时候,节点地址就会被设置为该发送该数据包的节点的地址,
所以如果过滤函数要发送一个数据包,那么就会回到那个地址上,可以在创建数据报链接的时候通过 :remote 关键字设置远程节点的地址.
process-datagram-address 获取链接的远程节点地址, set-process-datagram-address 可以设置链接的远程节点地址.
底层网络访问 (Low-Level Network)
make-network-process 就是 Emacs 用来创建网络链接的原函数,本章节的重点.
- 网络进程 (Network Processes)
主要是
make-network-process的文本,就不详细写了. - 网络选项 (Network Options)
主要是网络链接进程对象的选项,可以在创建网络链接后通过
set-network-process-option设置.就不详细说了. - 网络特性测试 (Network Feature Testing)
测试
Emacs是否支持某些网络特性,总体用法是这样,(featurep 'make-network-process '(KEYWORD VALUE))
比如要测试是否支持非阻塞连接,可以这样,
(featurep 'make-network-process '(:nowait t))
网络杂项 (Misc Network)
network-interface-list 获取机器上的网络接口列表,是一个 alist,每个元素都是 (NAME . ADDRESS) 的形式.
network-interface-info 根据网络接口名字获取网络接口的信息,格式如 (ADDR BCAST NETMASK HWADDR FLAGS),分别是 IPv4 地址,
广播地址,网络掩码,硬件地址和当前网络接口的 flags 列表.
format-network-address 把网络地址的 Lisp 表示转化成一个字符串,一个五个元素的向量 [A B C D P] 表示一个 IPv4 地址 A.B.C.D, P 是端口号.
一个九个元素的向量 [A B C D E F G H P] 表示一个端口号为 P 的 IPv6 A:B:C:D:E:F:G:H 地址.
这些函数并不是所有系统都适用的.
串口 (Serial Ports)
Emacs 可以和串口通信, serial-term 可以打开一个为使用串口的终端,如果是编写 Lisp 程序,可以使用 make-serial-process 创建一个串口进程对象.
串口的配置可以在运行时完成,不需要关闭以及重新打开, serial-process-configure 可以让你改变速度,字节大小,和其它参数.
和网络链接进程一样,串口进程也是没有进程 ID,也不能给它发送信号,以及状态信息也是和其它类型的进程不一样.
delete-process 和 kill-buffer 可以关闭链接,但是不会对连接到该串口上的设备产生影响.
字节打包 (Byte Packing)
这个章节主要是关于如何打包和解包字节数组,通常都是为了二进制传输协议才这么做. Emacs 提供了一些函数把字节数组转化成 alist 或者反过来.
字节数组可以是一个统一字节字符串(unibyte string)或者是一个整数向量,而 alist 则是 symbol 关联固定大小的对象或者是一个 alist.
这些函数都是来自于 bindat 库.字节数组到 alist 的转化也被叫做反序列化(deserializing)或者解包(unpacking),而反过来叫做序列化(serializing)或者打包(packing).
- Bindat 规范 (Bindat Spec)
在打包或者解包数据前需要制订一个数据布局规范(
data layout specification),就是一个嵌套列表,描述有名字以及类型的字段.该规范控制的字段被处理的长度以及打包和解包的方式.一般这个规范的变量的名字都是
-bindat-spec结尾,这种名字的变量会自动被Emacs识别为有风险.字段的类型描述了字段表示的对象的大小(byte为单位),还有在字段表示多字节的时候字节是如何排序的.只有两种排序,
big endian(大端,高位优先)和litte endian(小端,低位优先),big endian也叫网络字节顺序 (network byte ordering).比如#x23cd有#x23和#xcd两个字节,big endian排序就是#x23 #xcd,little endian就是#xcd #x23.类型的值参考就自己看文档了.
- Bindat Functions
Bindat库的APIs,自己阅读. - Bindat Examples
前两章节介绍了概念以及一些
APIs,这章节就有完整例子.打包:
(bindat-pack '((numberA u16) (numberB u16)) '((numberA . #x83aa) (numberB . #x7e80))) ; => "\203\252~\200"
解包:
struct header { unsigned long dest_ip; unsigned long src_ip; unsigned short dest_port; unsigned short src_port; }; struct data { unsigned char type; unsigned char opcode; unsigned short length; /* in network byte order */ unsigned char id[8]; /* null-terminated string */ unsigned char data[/* (length + 3) & ~3 */]; }; struct packet { struct header header; unsigned long counters[2]; /* in little endian order */ unsigned char items; unsigned char filler[3]; struct data item[/* items */]; };
对应
Bindat规范就是(setq header-spec '((dest-ip ip) (src-ip ip) (dest-port u16) (src-port u16))) (setq data-spec '((type u8) (opcode u8) (length u16) ; network byte order (id strz 8) (data vec (length)) (align 4))) (setq packet-spec '((header struct header-spec) (counters vec 2 u32r) ; little endian order (items u8) (fill 3) (item repeat (items) (struct data-spec))))
假设有一个如下的二进制数据,
(setq binary-data [ 192 168 1 100 192 168 1 101 01 28 21 32 160 134 1 0 5 1 0 0 2 0 0 0 2 3 0 5 ?A ?B ?C ?D ?E ?F 0 0 1 2 3 4 5 0 0 0 1 4 0 7 ?B ?C ?D ?E ?F ?G 0 0 6 7 8 9 10 11 12 0 ])解码成对应的结构是,
((item ((data . [1 2 3 4 5]) (id . "ABCDEF") (length . 5) (opcode . 3) (type . 2)) ((data . [6 7 8 9 10 11 12]) (id . "BCDEFG") (length . 7) (opcode . 4) (type . 1))) (items . 2) (counters . [100000 261]) (header (src-port . 5408) (dest-port . 284) (src-ip . [192 168 1 101]) (dest-ip . [192 168 1 100])))利用
bindat-get-filed获取字段的值,(bindat-get-field decoded 'item 1 'id); => "BCDEFG"
显示 (Display)
按键映射 (Keymaps)
储存输入事件的命令绑定所使用的数据结构就叫做按键映射.按键映射的每一个条目都关联/绑定一个独一无二的事件类型,或者绑定到另外一个按键映射,又或者是一个命令.
当一个事件类型绑定到一个按键映射,那么这个按键映射就被用来寻找下一个输入事件,一直持续到命令被找到为止,整个过程被称为按键查找(key lookup).
按键序列 (Key Sequences)
一个按键序列,又称按键,是一个由一到多个输入事件形成的序列,是一个单元.在 Emacs Lisp 中可以用 string 或者 vector 来表示按键序列.
在 string 的表示下,字母字符就是代表自身,比如 "a" 就是表示字母a, "2" 就是表示数字2.一些 modifiers, 比如 Control 字符是 "\C-", meta 字符是 "\M-".
"\C-x" 表示按键 C-x,另外, <TAB>, <RET>, <ESC> 和 <DEL> 分别是 "\t", "\r", "\e" 和 "\d",还有 "\C-xl" 表示按键 C-x l.
并不是所有按键,事件或者字符都能通过 string 表示,功能键,鼠标按钮事件,系统事件和 non-ASCII 字符 (比如, C-= 和 H-a)就不能用 string 表示,必须用 vector 表示.
在 vector 表示中, vector 的每个元素都是一个输入事件.比如 [?\C-x ?l] 表示 C-x l.
如果嫌直接用上面的表示麻烦,可以是用 kbd 和序列文本(比如 C-x l)来得出上面的表示.
按键映射的基础 (Keymap Basics)
按键映射是一种 Lisp 数据结构,为各种不同的按键序列指定按键绑定,所谓按键绑定的就是事件.
如果一个按键序列由一个事件组成,那么这个按键序列在按键映射上的绑定(binding in a keymap)就是那个事件的按键映射的定义(keymap's definition).
对于更长的按键序列:首先在按键映射上找到第一个事件的定义,然后重复这个过程直到按键序列里面所有的事件都被处理完.
如果一个按键序列的绑定是一个按键映射,那么这个按键序列就是一个 prefix key,否则就是一个 complete key (因为不能在它的基础上添加更多事件了).
如果绑定是 nil,那么就是 undefined.
查找一个按键序列的绑定会假设中间的绑定(最后一个之前的所有事件)都是按键映射,如果不符合假设,那么这个事件序列就形成不了一个真正的按键序列.
换句话讲,在任何合法的按键序列的尾部移除一个或者多个的事件都会产生一个 prefix key,比如 C-f C-n 不是一个按键序列,那么 C-f 就不是一个 prefix key,所有以 C-f 开头的序列都不是一个按键序列.
多事件的按键序列的动作 (the set of possible multi-event key sequences) 取决于 prefix keys 的绑定,因此不同的按键映射会不一样,并且会因为绑定的改变而发生改变.
然而单事件 (one-event) 按键序列必定是一个按键序列,因为它不依赖与任何 prefix keys.
在任何时候,总有几个基础的按键序列是激活的,用来查找按键绑定:
- 1个
global map: 被所有缓冲区共享; - 1个
local keymap: 与特定主模式(major mode)关联; - 0到多个
minor mode keymaps: 属于当前启用的次要模式(minor mode keymaps),但并不是所有次要模式都有按键映射.
local keymap 会遮掩 global map,而 minor mode keymap 会遮掩 local keymap 和 global map.
按键映射的格式 (Format of Keymaps)
每个按键映射都是一个列表,它的 CAR 是一个 symbol - keymap, CDR 定义了按键映射的按键绑定.
一个函数定义是一个按键映射的 symbol 是一个按键映射,可以利用 keymapp 函数来测试,比如,
(fset 'foo '(keymap)) (keymapp 'foo)
在 keymap symbol 的后面有这几种可能:
(TYPE . BINDING): 类型TYPE的事件的绑定,是一个字符或者symbol,BINDING是一个命令.(TYPE ITEM-NAME . BINDING): 和上面类似,只是这个绑定同是也是一个显示在菜单的项.(TYPE ITEM-NAME HELP-STRING . BINDING): 和上面类似,多了一个帮助说明.(TYPE menu-item . DETAILS): 该绑定同时也是一个拓展菜单项.(t . BINDING): 表示默认按键绑定,任何没有被按键映射的其他元素绑定的事件的绑定就是BINDING.默认绑定允许按键映射在不枚举所有可能的事件类型的情况下对它们进行绑定. 拥有默认绑定的按键映射会完全匹配所有低优先级(lower-precedence)的按键映射,除了那些绑定为nil的事件.CHAR-TABLE: 如果按键映射的元素是一个字符表,那么它就拥有所有非modifier bits的字符事件的绑定,比如C就是字符C.这种叫做full keymap,别的keymap叫做sparse keymaps.VECTOR: 类似与CHAR-TABLE,数据结构不一样而已.STRING: 并非和其他元素一样表示按键绑定,这是overall prompt string,用于做为菜单的按键映射.(keymap ...): 元素本身是按键映射,和上面的情况不一样,上面是针对只有一个事件的案件序列,这个是针对于长的按键序列的,就是C-c与C-c C-c的区别,是多层次的的表现.
当绑定为 nil 的时候,它不构成一个定义,但是它的确比默认绑定和 parent keymap 要高优先级,但是在另外一个方面, nil 的绑定不会覆盖低优先级的 keymap,因此如果 local map 是 nil 绑定,那么就会直接用 global map 的绑定.
按键映射不会直接记录 meta 字符的绑定,相反 meta 字符被用于 <ESC> (或者是 meta-prefix-char 的值) 开头的两字符的按键序列的按键查找上.因此 M-a 相当于 <ESC> a,这条规则只能用于字符按键上,比如 M-<end> 和 <ESC> <end> 是没有关系的.
想看现实的例子可以通过 c-h v 来找 XXX-map 看,比如 lisp-mode-map,对于如何看懂按键映射还要学会如何计算按键事件的值,具体阅读这里 M-: (info "(elisp) Keyboard Events").
(keymap (24 keymap ;; 24 是?\C-x (5 . lisp-eval-last-sexp)) ;; 5 是?\C-e,所以 ?\C-x?\C-e 运行 lisp-eval-last-sexp (menu-bar keymap (lisp #1="Lisp" keymap (ind-sexp menu-item "Indent sexp" indent-sexp :help "Indent each line of the list starting just after point") (ev-def menu-item "Eval defun" lisp-eval-defun :help "Send the current defun to the Lisp process made by M-x run-lisp") (run-lisp menu-item "Run inferior Lisp" run-lisp :help "Run an inferior Lisp process, input and output via buffer `*inferior-lisp*'") #1#)) (3 keymap ;; 3 是 ?\C-c (22 . lisp-show-variable-documentation) (6 . lisp-show-function-documentation) (4 . lisp-describe-sym) (1 . lisp-show-arglist) (11 . lisp-compile-file) (12 . lisp-load-file) (3 . lisp-compile-defun) ;; 3 是 ?\C-c,所以 ?\C-c?\C-c 就运行 list-compile-func (16 . lisp-eval-paragraph) (14 . lisp-eval-form-and-next) (18 . lisp-eval-region) (5 . lisp-eval-defun) (26 . switch-to-lisp)) (27 keymap (24 . lisp-eval-defun)) ;; 到这里为止的上面就是一个 keymap keymap ;; 这一部分是从别的 map 继承过来的,可以在 C-h v lisp-mode-map 的文档中看到. (127 . backward-delete-char-untabify) (27 keymap (17 . indent-sexp)) keymap (27 keymap (17 . prog-indent-sexp)))
创建按键映射 (Creating Keymaps)
按键映射有两种: full keymap 和 sparse keymap,前者是包含一张拥有非 modifier 字符的字符表,后者是一个没有任何条目的按键映射.
分别的函数是 make-keymap 和 make-sparse-keymap,而除了创建外,还可以通过 copy-keymap 复制别人 map,这个功能基本是用不上的,如果只是想要一个和别的差不多的按键映射,那么应该使用 set-keymap-parent 继承别的按键映射.
继承和按键映射 (Inheritance and Keymaps)
按键映射可以继承别的按键映射,被继承的一个叫做 parent keymap,这种按键映射结构是这样的:
(keymap ELEMENTS... . PARENT-KEYMAP)
一个比较熟悉的例子就是 lisp-mode-map,它的 parent keymap 就是 lisp-mode-shared-map.
lisp-mode-map 继承了所有 lisp-mode-shared-map 的绑定,在查找按键的时候, Emacs 会先看 lisp-mode-map 有没有查找的绑定,如果没有就去 lisp-mode-shared-map 上查找.
如果用 define-key 或者其它按键绑定的函数改变了 lisp-mode-shared-mode 的绑定, lisp-mode-map 也是能够看到这些改变.
但如果反过来,改变 lisp-mode-map 上面的绑定,是不会对 lisp-mode-shared-map 造成影响的.
构建一个有 parent keymap 的按键映射的正确做法是使用 set-keymap-parent,不应该直接通过"手动"构建.
可以通过 keymap-parent 来获取 parent keymap,比如:
(keymap-parent lisp-mode-map)
如果没有 parent keymap 就返回 nil.
假设现在要求构建一个和 lisp-mode-map 差不多的 keymap,利用继承可以这么做,
(let ((map (map-sparse-keymap)))
(set-keymap-parent map lisp-mode-map)
map)
non-spare 按键映射也可以有一个 parent keymap,不过意义不大,因为它们一定会为所有不带 modifier bits 的数字字符指定了绑定,
所以这些绑定是不可能从别的 parent keymap 继承到的.
有时候想继承多个映射,可以使用 make-composed-keymap,比如 Emacs 的 help-mode-map,
(let ((map (make-sparse-keymap))) (set-keymap-parent map (make-composed-keymap button-buffer-map special-mode-map)) ... map) ... )
前缀键 (Prefix Keys)
所谓前缀键就是一个绑定为一个 keymap 的按键序列.其它按键映射定义了那些拓展了前缀按键的按键序列的绑定,
比如 C-x 是一个前缀键,它使用的按键映射刚好也储存在 ctl-x-map 中,这个按键映射定义了以 C-x 开头的按键序列.
一些标准的 Emacs 前缀键也可以在 Lisp 变量中找到,具体就自己查阅了 M-: (info "(elisp) Prefix Keys").
前缀键的按键映射绑定就是用来查找跟在前缀键后面的事件的,绑定可能是一个 symbol,这个 symbol 的函数定义就是一个按键映射,
效果是一样的,不过 symbol 同时也是作为前缀键的名字.因此, C-x 的绑定就是一个叫做 Control-X-prefix 的 symbol,
它的 function cell 储存的是 C-x 的按键映射,也就是 ctl-x-map 的值.
前缀键可以定义在任何激活的按键映射上,如果一个前缀键出现在多个激活的按键映射上,那么各个的定义的作用就会被合并:
次要模式(minor mode)按键映射定义第一,然后是 local map 的,最后是 global map 的.
比如下面定义 C-p (原本是属于 global-map 的绑定) 等同于 C-x,
(use-local-map (make-sparse-keymap)
(local-set-key "\C-p" ctl-x-map)
(key-binding "\C-p\C-f")
可以通过 define-prefix-command 来定义一个 symbol 为前缀键的绑定,
(define-prefix-command SYMBOL &optional MAPVAR PROMPT)
大概的过程就是:创建一个 sparse keymap 并且把它储存为 SYMBOL 的函数定义, SYMBOL 随后的一个按键序列绑定会成为前缀键,返回值是一个 symbol.
如果 MAPVAR 是 non-nil,该函数会把 MAPVAR 变为变量而不是把 SYMBOL 定义为函数. PROMPT 用于菜单按键序列.
EmacsWiki上有一个不错的例子,可以看一下.
激活的按键映射 (Active Keymaps)
Emacs 有很多按键映射,但是只有少数是在任何时候是激活的.当接收到用户的输入, Emacs 根据 translation keymaps 翻译输入事件,然后在激活的按键映射查找按键绑定.
通常,激活的按键映射是这些,按照优先级高到低排序:
- 指定了
keymap属性的按键映射,通常是由某个point上的keymap文本或者overlay属性指定的. - 所启用的次要模式(
minor mode)的按键映射,如果次要模式有自己的按键映射, 一但启用就会由emulation-mode-map-alists,minor-mode-overriding-map-alist和minor-mode-map-alist指定为激活. - 当前缓冲区的本地映射(
local keymap),缓冲区特定的按键映射,由点上的local-map文本或者overlay属性指定, 正常来说是根据缓冲区的主要模式设定本地按键映射的,并且所有启用了相同主要模式的缓冲区共享同一个本地按键映射. 所以注意local-set-key的使用. - 全局按键映射(
global keymap), 绑定到global-map上,并且总是处于激活状态.
除了上面平时的 Emacs 还提供了其它激活按键映射的方法,首先, overriding-local-map 指定一个映射来替换上面的(除了 global map).
其次, terminal-local 变量 overriding-terminal-local-map 可以使一个映射的优先级高于任何其他映射,包括 overriding-local-map,
一般这是用于临时的(modal/transient)按键绑定,为此 set-transient-map 提供了一个方便的接口.
激活按键映射并非唯一使用射的方式,还可以用于 read-key-sequence 翻译事件.
current-active-maps 可以获取当前处于激活状态的按键映射,具体用法看文档.还有 key-binding 可以得到某个按键的绑定,具体用法是
查找激活的按键映射 (Searching Keymaps)
这是官方文档给出的查找过程的伪代码,其中 FIND-IN 和 FIND-IN-ANY 分别是在一个按键映射和一个按键映射 alist 进行查找的伪函数.
(or (if overriding-terminal-local-map (FIND-IN overriding-terminal-local-map)) (if overriding-local-map (FIND-IN overriding-local-map) (or (FIND-IN (get-char-property (point) 'keymap)) (FIND-IN-ANY emulation-mode-map-alists) (FIND-IN-ANY minor-mode-overriding-map-alist) (FIND-IN-ANY minor-mode-map-alist) (if (get-text-property (point) 'local-map) (FIND-IN (get-char-property (point) 'local-map)) (FIND-IN (current-local-map))))) (FIND-IN (current-global-map)))
值得注意的是,如果按键序列是以一个鼠标事件开始的,那么事件的位置就会替代上面代码中的 point.
控制激活的按键映射 (Controlling Active Maps)
API 文档,自己阅读.
按键查找 (Key Lookup)
按键查找就是一个从指定的按键映射中查找按键序列对应绑定的过程,绑定的指定或者使用不是按键查找的一部分.
按键查找只会使用按键序列中的每个事件的类型来查找,事件剩余的部分都会被忽视.
事实上用于按键查找的按键序列可能会通过它的事件类型来指定一个鼠标事件,这样的一个按键序列不满足 command-execute 的执行条件,
但满足查找或者重新绑定一个按键的条件.
按键查找逐个处理事件:第一个被找到的事件的绑定必须是个按键映射,然后第二个事件的绑定需要在从按键映射中查找,重复这个过程直到所有事件被处理完,
最后一个绑定可能不是一个按键映射.
我们用 keymap entry 这个术语来描述在按键映射中查找的结果:
nil: 按键未定义COMMAND: 按键绑定的命令ARRAY (string or vector): 按键绑定的键盘宏KEYMAP: 前缀键的绑定LIST: 如果CAR是symbolkeymap,那么这个LIST就是一个按键映射;如果是一个symbollambda,那么就是一个lambda表达式, 也就是一个函数,为了正常执行,它必须是一个命令.SYMBOL: 如果SYMBOL的定义是个函数就用函数定义来替代SYMBOL.如果SYMBOL的定义是另外一个SYMBOL,那么就重复这个处理过程,直到替代发生.最终结果应该是一个按键映射,或者命令,又或者一个键盘宏.
需要注意的是,按键映射以及键盘宏不是合法的函数,所以一个与作为函数定义的
keymap, string 或者 vector关联的symbol是不合法的函数.然而作为按键绑定是合法的.如果定义是一个键盘宏,那么
symbol作为command-execute的参数也是合法的.如果
SYMBOL是undefined,就是说,按键未定义,严格来说,按键是定义了,只不过它的绑定是命令undefined.而
undefined命令做得事情就是和未定义按键做的一样: 调用ding来响起提示音 (ring the bell) 但不引发错误.undefined用在本地按键映射上来覆盖一个全局按键绑定,使按键局部未定义.nil的本地绑定会导致undefined失败.ANYTHING ELSE: 所有其它类型的对象,表示绑定不能作为一个命令来执行.
按键查找的函数 (Functions for Key Lookup)
改变按键绑定 (Changing Key Bindings)
重新绑定按键的方式就是改变按键映射的条目.如果你改变全局按键映射里面的绑定,那么就会影响所有缓冲区(即使对于 global map 被 local map 遮掩的缓冲区不会有直接影响).
通常改变当前的缓冲区的 local map 会影响所有使用相同主模式(major mode)的缓冲区.
global-set-key 和 local-set-key 可以分别改变当前 global map 和 local map 的绑定.
通常都用 global-set-key 以及 local-set-key 来做这种事,除了这两个以外还有更通用的函数可以做这种事情:
define-key 给某个映射设定绑定;
substitute-key-definition 替换某个映射里面某个绑定,这个绑定出现的所有地方都会被替换;
suppress-keymap 通过把 self-insert-command 映射到 undefined 的手段来改变按键映射上所有字符,一旦这么就可能正常德输入文本了.
重新映射命令 (Remapping Commands)
有一种特别的绑定可以在不引用按键序列的情况下映射命令绑定的按键到另外一个命令上,比如我自己 mode 想提供一个叫 my-kill-line 函数,
以前执行 C-k 执行 kill-line,现在要在使用我自己的 mode 下输入 C-k 的时候执行 my-kill-line,通过 remap 事件可以轻松实现,
(define-key my-mode-map [remap kill-line] 'my-kill-line)
上面的表达式是这么描述: 把 kill-line 映射到 my-kill-line,这是一个重新映射关系,把这个映射关系放到 my-mode-map 中.
[remap COMMAND]
其中, remap 是一个事件, COMMAND 是要被重新映射的命令.其实可以这么理解,这个形式就是获取 COMMAND 的按键绑定,而实际上它做的事情也正是这样.
如果想取消重新映射,直接这么做就好,
(define-key my-mode-map [remap kill-line] nil)
要注意的是,重新映射只能发生在激活的映射中,比如在 ctrl-x-map 执行重新映射是无效的;还有一个重新映射只能在一级内的关系有作用,比如
(define-key my-mode-map [remap kill-line] 'my-kill-line) (define-key my-mode-map [remap my-kill-line 'my-other-kill-line)
这里 kill-line 没有被映射到 my-other-kill-line 上,输入 C-k 的时候会执行 my-kill-line 而不是 my-other-kill-line,当然, my-kill-line 是被映射到 my-ohter-kill-line 上了.
可以通过 (command-remapping 'kill-line nil my-mode-map) 来获得重新映射,这里返回的是 'my-kill-line.
用于翻译事件序列的按键映射 (Translation Keymaps)
当 read-key-sequence 函数读取一个按键序列,它会用翻译按键序列来翻译事件序列成别的事件序列.翻译用的按键映射就是 input-decode-map, local-function-key-map 和 key-translation-map 三个.
这三个是按照优先级排列的.在结构上,翻译用的按键映射和其他的按键映射是一样的,在用途上却不同:翻译用的按键映射(下面称为翻译映射)是用来指定要翻译的内容,而不是绑定映射.
每当读取一个按键序列,翻译映射就会检查每个翻译映射.如果其中一个翻译映射绑定了序列 k 到向量 v 上,而 k 又是按键序列的子序列,那么子序列出现的地方就会替换成 v.
翻译映射只有在 Emacs (通过由 keyboard-coding-system 指定的输入编码系统)解码(decode)键盘输入才工作.
比如, VT100 终端会在按下 <PF1> 的时候发送 <ESC> O P 序列, Emacs 一定把这个序列翻译成单个的事件 pf1,子所以能这么做是因为 input-decode-map 上绑定了 ESC O P 到 [pf1].
因此当输入 C-c <PF1> 的时候,终端会提交一个字符序列 C-c <ESC> O P,然后 read-key-sequence 会把它翻译成 C-c <PF1> 并且返回一个向量 [?\C-c pf1].
绑定按键的命令 (Key Binding Commands)
就是讲 API 的.
扫描按键映射 (Scanning Keymaps)
为了打印出帮助信息而扫描所有当前的按键映射(因为 keymap 结构本身不是给人阅读的)这章节就是说这些 API.
菜单按键映射 (Menu Keymaps)
通过为键盘按键和鼠标按钮定义绑定,按键映射可以像菜单那样操作.一般菜单需要鼠标操作,但同样也可以通过键盘来操作.
我个人对菜单这块不太感兴趣(我自己都不用菜单栏),所以暂时放下.等以后想读的时候再看.
模式 (Modes)
模式就是一个能够自定义 Emacs 行为的定义集合.有两种类型的模式: 在编译的时候可以选择关闭或者启用来决定是否提供特性的次要模式,
以及用来专门编辑某一种文本或者与特定种类文本进行交互的主要模式.每个缓冲区一个时间内只有一个主要模式,可以有0到多个次要模式.
钩子 (Hooks)
钩子其实就是变量,用来储存函数,这些函数会在特定程序执行结束后被调用. Emacs 提供了一些用来自定义的钩子,
通常都是在 Emacs 的初始文件 (init file) 进行设置,当然也能通过 Lisp 程序进行设置.
大部份钩子都是普通钩子(normal hooks),这种钩子是一个由不接受参数的函数组成的列表.根据约定,普通钩子的名字要求以 -hook 结尾.
每个模式命令(mode command)都应该运行一个叫做模式钩子(mode hook)的普通钩子作为初始化工作的最后一步.
用户可以很容易地通过修改模式定义的 buffer-local 赋值来自定义模式的行为.大部份次要模式也是在结尾执行模式钩子.除了模式之外,钩子也可以用在别的地方.
比如. Emacs 会在挂起自己之前运行 suspend-hook.给钩子添加函数的推荐方法是调用 add-hook.这种钩子函数只要是能够通过 funcall 调用就可以.
大部份普通钩子都是初始为 void 的, add-hook 知道如何处理, add-hook 可以全局或者缓冲区局部添加钩子函数.
如果一个钩子变量不是以 hook 结尾,根据约定,它就不是一个普通钩子,就是非普通钩子(abnormal hook),意味着钩子函数接受参数,或者它们的返回值会有特殊用途.
一般而言,钩子的文档会说明钩子函数如何调用, add-hook 也可以给非普通钩子添加函数,根据约束,非普通钩子的名字需要以 -functions 结尾.
如果一个变量是以 -function 结尾,并且它的值是一个函数,而不是一个函数列表,那么就不能用 add-hook 来调整这种函数钩子(function hook),而是用 add-function.
- 运行钩子 (Running Hooks)
run-hooks接受一到多个普通钩子,依次运行每个钩子,每个钩子都应该是一个函数列表,然后依次调用每个函数.有一个废弃的用法:钩子的值是一个函数,但是这种用法已经废弃了.如果钩子是缓冲区局部变量,那么就用缓冲区局部变量而不是全局变量,然而如果作为缓冲区局部变量的钩子包含元素
t,那么全局变量版本的钩子也会运行.run-hook-with-args接收一个非普通钩子,依次给钩子里面的钩子函数传递参数ARGS:(run-hook-with-args HOOK &rest ARGS),还有两个变种就不说了.普通钩子的用法,
(setq test-hook nil) (defun test () (run-hooks 'test-hook)) (defun func1 () (message "no")) (add-hook 'test-hook 'func1) (test) ;; log no
非普通钩子的用法,
(setq test-functions nil) (defun test () (run-hook-with-args 'test-functions "This is the prompt")) (defun func1 (prompt) (message (format "%s: %s" prompt "func1"))) (defun func2 (prompt) (message (format "%s: %s" prompt "func2"))) (add-hook 'test-functions 'func1) (add-hook 'test-functions 'func2) (test) ;; log: ;; This is the prompt: func2 ;; This is the prompt: func1
- 设置钩子 (Setting Hooks)
Emacs Lisp提供add-hook和remove-hook来添加和移除钩子上的函数,add-hook不会添加重复的函数,两者都是通过equal来比对要被添加或者删除的函数,所以这两个函数不论对有名字的函数还是对
lambda表达式都有效.
Emacs 编码系统
在计算机里面的所有文件都是二进制文件,文本文件(text files)就是一个例子,
在文本文件的日常使用中偶尔会出乱码问题,要想解决问题需要先了解显示文本的过程是怎么样的.
刚才也提到文本文件就是二进制文件,也就是说它储存的内容就是一些数值而已,那么这些数值怎么在人的眼里变成的字符呢?
这是因为计算机按照一套 标准 来把这些数值翻译成我们看到的文本,标准也就是所谓的 编码标准 (coding standard),或者说编码系统(coding system).
编码标准 就是一套给字符集编号的方案,下面就举个不恰当例子来 直观地 了解一下这个过程,
假设现在有一种语言 \(A\) ,它的字符有从 a 到 z 的 26 个字符,给它们每个字符编个号:
a 的编号为 0, b 的编号为 1, 如此类推, z 的编号就为 25,这些编号叫做字符码(character code / codepoint),
字符码的范围叫做字符码空间 (codespace),这里套编码标准的 codespace 是 0..26 (不包括 26).
如果我们在文件 \(a\) 看到一个单词: halo, 那么文件里面实际储存的数值应该是这样的 <7><4><11><11>,
计算机器储存/读取文件都是二进制,这里的 <n> 这种符号表示数字 n 的二进制.
因为这套字符里面只有 26 个字符,也就是说最少需要 5 位(bits)来表示 26 种可能中的一种,(实际上5位可以表示32种字符,多出来的的 6 个就不管了),
上面这串数值在计算机里面应该是这样排列的: 00111 00000 01011 01110,
要想正常看到字符串 halo,就必须使用遵从这套编码标准的程序来打开文件,程序根据标准解析:每 5 位翻译得到一个字符,
这个过程会根据编号找到对应的符号(symbol),最后才显示出我们看到的字符串.
假设现在有另外一门语言 \(B\),它的字符有从 0 到 9 的 10 个字符,按顺序进行从 0 开始进行编码,因为有 10 种可能的字符,那么至少需要 4 位来表示一个字符.
如果用遵从这套编码标准的程序来打开文件 \(a\),那么解析结果就为: 0011 1000 0001 0110 1110, 用户看到的内容大概就是这样 3816<14>,
可以发现最后 4 位没有被翻译出来; 甚至如果文本是 5 个字符的话,解析还会变成 0011 1000 0001 0110 1110 xxxx x 这样多出1位无法进行翻译.
这就是乱码以及乱码发生的根本原因,因此解决乱码的核心方法就是让 程序储存文件时使用的编码 和 程序读取文件时使用编码 保持一致.
程序使用某种编码标准储存文件/字符串叫做编码(encode), 而以某种编码标准读取文件/字符串叫做解码(decode).
不过这样一来,两门语言也没有办法在同一个文件中正常显示了,这个问题的解决办法是把两门语言的字符集合并在一起,
比如, 0 .. 9a .. z,然后从 0 开始依次给每个字符重新编号.
最早的时候计算机只支持 ASCII 编码,包括一些英文,数字和一些基础的标点符号,然后经过不断发展终于到了 1986 年,共支持了 128 个字符.
但它有两个缺点: 支持的字符基本都是以英语字符为主,非英语用户没法使用自己母语的字符;
其次,128 个字符是不够用的,因为键盘有一些比如 Ctrl, Enter 这样的特殊按键.
人们需要定义了一些足以解决这些问题的字符集, Unicode Standard 就是其中一个, Unicode Standard 最早也是只支持很小的一部分字符的,
从用 5 位表示一个字符,到用 6 位表示一个字符,后来再到的用 7 位表示一个字符,
这三个编码标准也就是我们今天所知道的 UTF-5, UTF-6 以及 UTF-7.
UTF-7 已经可以支持所有 ASCII 字符了,这个时候已经是 1990 年代中期了,
在过去,一个字节(byte)不像今天这样固定 8 位(bits),这给计算机的发展带来这很多问题(这里有这方面的讨论),
于是在 1993 年,国际标准化组织(ISO)规定8位为1个字节, Unicode Standard 当然也要紧跟时代的步伐.
到了 Unicode Standard 的第 4 个版本时总共支持 #x10FFFF 个字符, 字符码范围是 0..#x10FFFF,
每种语言的字符集都有自己的一个区间,详细内容可以看 官方文档 或者 这里(更加直观).
由于这个字符集实在是太大了,如果直接按照字符集元素个数来分配每个字符的空间,那么每个字符需要 3 个字节 (24 位) 的空间,
因此出于对空间节省的考虑,编码标准需要一些优化,比如经拓展后的 ASCII 字符集只有 256 个字符,并不需要 4 个字节,
那么这种字符就用一个字节空间储存足够了,中文字符需要更多,可能高达两个字节,同样也不需要 4 个字节,如此类推,其它字符也占用对应空间.
Unicode Standard 为此定义了三套编码标准来解决这个问题: UTF-8, UTF-16 以及 UTF-32, UTF-8 是最常见的一套编码标准,这里是官方页面.
UTF-8 表示 1 个字节作为基本编码单位,因为每个字符需要的空间不是一样的,因此需要一些位来标识是多少个字节为一个字符,
U+ 0000 ~ U+ 007F: 0xxxxxxx U+ 0080 ~ U+ 07FF: 110xxxxx 10xxxxxx U+ 0800 ~ U+ FFFF: 1110xxxx 10xxxxxx 10xxxxxx U+10000 ~ U+10FFFF: 11110xxx 10xxxxxx 10xxxxxx
左边的是 codespace,右边则是 codespace 内的 codepoint 经过 UTF-8 编码后位(bit)的排列模板.
这里每个开头的字节的开头有多少个 1 位就表示用多少个字节进行编码, 以 10 开头的字节是前面字节的后续.
这样最高就需要用到 4 个字节了,把 使用频率相对低的字符 以及 语言字符集比较大的字符 往后排,在在大部分情况下能节省不少空间.
那么 UTF-8 具体是如何对一个字进行编码的呢?
比如"盐"字的 codepoint 为 30416 (在 Emacs Lisp 可以直接通过 ?\盐 获得), 十六进制是 #x76D0, 二进制是 #b0111 0110 1101 0000,
按照上面的模板来看处于第三个区间,也就是要用到 3 个字节,根据对应模板的 x 的位置划分好位: 0111 011011 010000,
再把它们填入模板中,最后得到的 11100111 10011011 10010000 (E79B90) 就是"盐"字的 UTF-8 编码.
UTF-16 是指最少要用 2 个字节作为基本编码单位,也就是在这种编码下的字符需要 2n 个字节;
UTF-32 是指最少要用 4 个字节作为基本编码单位,也就是在这种编码下的字符需要 4n 个字节,n 是整数.
这里就不讨论 UTF-16 和 UTF-32 是怎么编码的了,它们都以多字节作为基本编码单位,这讨论一下多字节编码的排序问题,
对于每个需要 多字节编码 的字符,它们的字节排列顺序有两种: 大端(big endian)和小端(little endian), endian 是 end 的变形.
可能有点不太好理解,不妨从更抽象的角度出发.
一个字符就是一个 数据, 一个 数据 是由一到多个字节组成,通常在处理数据层面上来说,字节(byte)就是 基本单位 了,而不是位(bit).
一个 多字节编码 的字符就是一个由多个字节组成的 数据.
所以大/小端实际上就是指 数据 基本单位的顺序,特别是由多字节组成的 数据.
那么怎样才为之大端,又怎样才为之小端呢?
我们需要先了解一个概念,一个 数据 的基本单位的高低位,程序/硬件所处理的 数据 的第一个字节就是位于最低位,最后一个字节就位于最高位.
数据定义 上的第一位字节叫做最高有效字节(the most significant byte, MSB),最后一位就是最低有效字节(the least significant byte, LSB),
以 JPEG 图片为例,一个 JPEG 图片是由一个到多个 JPEG 像素组成的,每个像素由3个字节组成 (R,G,B),分别表示红,绿和蓝三个颜色的值.
其中 R 就是像素这个定义里的第一个字节,根据数据定义,它就是最高有效字节; G 没 R 那么有效,但比 B 有效, B 是最低有效字节.
最高有效字节存放在最低位,然后依次递减排列,这就是大端序;最低有效字节存放在最低位,然后依次递增排列,这就是小端序.
举个具体一点的例子,现有一张图片,它由两个像素点 (128, 0, 255)(252, 127, 254) 组成,它们的16进制表示字节排列为: 80 00 FF | FC 7F FE,这个是大端序,它的小端序排列是 FF 00 80 | FE 7F FC.
注意,这里并非对整个图片的字节序(order of the bits)进行逆转,而是单独地把每个像素的字节排列逆转,并保持两像素之间的顺序不变.
之所以有这两种排序是因为程序/硬件只能 固定 按照 \(低位 \longrightarrow 高位\) 方向读取 数据 的基本单位,
而在某些特殊情况下是想按照 \(高位 \longrightarrow 低位\) 方向读取数据,那么唯一解决办法只有改变 数据 基本单位的排列了.
有时候人们会把大端叫做网络字节序(network byte order),把小端叫做主机字节序(host byte order).
如果觉得这里总结得不够清楚的话,可以看 understanding big and little endian byte order 或者 Assembly Language Step by Step Programming with Linux, Third Edition 的第五章关于 Endianess 的内容.
UTF-16 和 UTF-32 可能会在文件的开头采用 BOM(Byte Order Mark) 来声明自己采用哪种顺序,有没有还是要看具体程序如何编码,
而 UTF-8 是不分大小端的,它只有大端,所以 BOM 对于 UTF-8 编码的文件是没有意义的,
然而很多 Windows 程序会在 UTF-8 文件前面加上无意义的 BOM,这是微软开的坏头,
这些文件并不是标准的 UTF-8 编码,在其它系统上打开这些文件可能会因为编码引起解析错误的问题.
后面不再继续对这 UTF-16 和 UTF-32 这两种编码进行探讨了,因为这和后面的内容关系不太大.
Emacs 作为文本编辑器,编码问题自然是不可能避而不谈的,而如果要编写 Emacs Lisp 处理文本的话,
那么理解 Emacs 内部文本机制是必要基础了.
Emacs 没有直接使用 Unicode Standard,而是在 Unicode 的基础上进行拓展,
在 #x110000..#x3FFFFF 的范围给一些 无法用 Unicode 和 raw 8-bit bytes 标准解析的字符 进行编码,
所以理论上 Emacs 的一个字符需要占用用 22 位(bit)空间,这就是 Emacs 的文本的内部表示(text representation).
同样为了节省内存, Emacs 也没有使用固定 22 位空间储存一个字符,而是用一个长度可变(variable-length)字节序列(a squence of 8-bit bytes)储存一个字符,
序列的长度范围在 1 到 5 个字节内(包括5).
并不是所有文件都是用的 Unicode Standard 储存的,而 Emacs 作为一款灵活的文本编辑器可不能只支持一种编码标准的文件,
每次 Emacs 打开文件都需要先把文件内容写入到缓冲区中, 文件内容叫做 encoded text, 虽然名字带了一个 text,
但是不是平常意义上人类所读的文本,而是一个计算机能读的字节序列,准确来说是一个由多个 单个/独立字节 排列成的序列,
然后 Emacs 会根据文件的编码标准把 encoded text 转换为 Emacs 编码标准能够解析的结构(另外一个种形式的字节序列),
这种编码之间的转换叫做字符码转换(character code conversion),最后把解析后的内容翻译(解码)成给人类看的字符,
这个时候缓冲区上面的内容不再是 encoded text (,虽然文档没有说,应该叫做 decoded text,这种才是人类所读的文本).
关于字符码转换也可以看看这里, 关于 Emacs 是显示字符的细节可以看这里 M-: (info "(elisp)Character Display").
保存文件则是文件读取的逆过程: 根据 Emacs 编码标准把缓冲区的内容转换为文件的编码标准所能够解析的结构,
也就是把缓冲区的内容转回 encoded text, 最后写入到文件中.
一般情况是这样的,然而 Emacs 作为一款灵活的编辑器,它支持用户在保存时选择别的编码标准,
这么一来文件就完成了字符码转码了,就像 Linux 的 iconv 命令一样,
在日常使用 Emacs 编辑文本文件时,可以通过 buffer-file-coding-system 变量来查看文件所使用的编码标准,
可以通过 set-buffer-file-coding-system 命令给当前正在编辑的文件选择别的编码标准,
这只影响文件保存时使用的编码,并不影响缓冲区的内容,虽然有点啰嗦,但还是举一个例子吧,
比如你打开了一个 utf-8-dos 编码的文件,发现上面有很多 ^M 符号,这些符号一般是不应该看见的,
因为 Emacs 默认使用 utf-8 编码打开文件,编码不对所以没法正常显示,你想最快地把这些符号隐藏起来,
最快的办法就是使用正确的编码打开文件了,但 set-buffer-file-coding-system 只会改变文件保存时候的编码,
要让缓冲区上显示的内容生效,就应该重新对 encoded text 进行转码: M-: (decode-coding-region (point-min) (point-max) 'utf-8-dos).
Emacs 也可以不对 encoded text 进行转码显示,用户可以在这种状态下对文件进行编辑,
如果要分析文件字节流,那么这种方式是最好的.
在 Emacs 中,这种储存 encoded text 的缓冲区和字符串叫做 unibyte 缓冲区和字符串,
unibyte 缓冲区和字符串在 Emacs 里面是以八进制的形式显示的,比如 \237,
用户平时看到的每个字符都是由1到多个字节解析得到的,储存这种字节序列的缓冲区和字符串叫做 multibyte 缓冲区和字符串.
总的来说, unibyte 缓冲区和字符串不是给人看的, multibyte 缓冲区和字符串才是.
Emacs Lisp 的字符和字符串数据类型支持直接使用字符码来表示对应字符,具体可以看 M-: (info "(elisp)Character Codes").
这下面有个例子: 请求一个中文页面,请求得到的内容会写入到缓冲区里面,然后把把请求内容解析成 DOM 树.
(url-retrieve "https://gaokao.chsi.com.cn/sch/search--ss-on,option-qg,searchType-1,start-0.dhtml" (lambda (status) (let ((buf (current-buffer))) (with-current-buffer buf (set-buffer-multibyte t) ;; (prin1 (decode-coding-string (buffer-substring-no-properties (point-min) (point-max)) 'utf-8-dos)) (decode-coding-region (point-min) (point-max) 'utf-8-dos) (prin1 (libxml-parse-html-region (point-min) (point-max))) ))))
请求得到的内容是字节流,默认情况下, Emacs 的编码标准是 utf-8, 所以 ASCII 字符是正常显示的,但是由于字节流是 unibyte 字符串,
其中中文字符不能被正确解析,只能显示为八进制.
这里可以稍微做一个小实验: 通过浏览器把上面请求的页面保存下来,然后通过 hexl-mode 使用二进制模式编辑页面,
用 上面的请求内容 和 二进制模式下显示的内容 进行对比后,你会发现前者显示八进制的位置和后者显示 . 的位置是对应的,
这些位置上的都是 NON-ASCII 字符,这就是前面说的不对文件进行转码显示.
nhexl-mode 作为 hexl-mode 的拓展版可以正常显示出这些字符.
在这种情况下调用 libxml-parse-html-region 是不会解析出正确结果(因为 libxml-parse-html-region 会把 unibyte 缓冲区 的 (buffer string) 作为文本进行解析),
因此要调用 set-buffer-multibyte 把 缓冲区 变成 multibyte 缓冲区 正常显示出文本.
解析前还可以调用 decode-coding-region 对当前缓冲区进行重新解码,这个页面是最早应该是写在 Windows 上的,
因为使用 utf-8 对该页面的字节流解码的话会显示 Windows utf-8 特有的行尾符号(^M),
所以这里使用正确的编码标准对缓冲区重新解码让行尾符号隐藏起来.
当然,这个行尾符号不影响 libxml-parse-html-region 的解析结果.
最后这里有一个完整的爬虫例子,有兴趣可以参考一下.
文档 (Documentation)
打包 (Packaging)
GNU Emacs Internals
如果你有以下想法:
- 想了解编译
Emacs的过程 - 想了解
Emacs对象的实现 - 想了解
Emacs整数类型的实现 - 想了解
Emacs的分配/储存/垃圾回收机制 - 想了解
Emacs的内存使用情况 - 想了解如何编写自己的
primitive - 想了解如何使用
C语言编写动态加载模块
但是又不知道从何入手,可以读一下官方的文档 M-: (info "(elisp)GNU Emacs Internals").
通常大部分用户都有自己写动态加载模块的想法,恰好文档关于这部分的内容十分详细,
不过这要求用户有一定的 C 语言基础,如果你有其它语言的经验,可以这样入手:
如果之前没有使用过编译型语言的经验,那么要先理解直译型语言何编译型语言的区别,
要知道什么是预处理器,什么是编译器,什么是链接器,它们是怎么一起工作的产生编译结果的;
C 语言不像 C++ 或者 Python 那样有一个官方组织发展,导致了它的语法没有一套实际的标准,
也就是有很多套方言,作为开发者需要知道自己用的是哪一种标准以及是什么版本,
常见的标准有 GNU C (gnu90, gnu89) 以及 ANSI C (c89, c99, c11),
这里推荐一个网站来度过上手期,编译器方面推荐和 Emacs 一个老家的 gcc 编译器;
不同编译器之间的差别不小,配套的预处理器就是其中一个差别,不同预处理器支持的指令不一样,
选择 gcc 的话可以看一下文档支持哪一些指令以及它们是干什么的;
C 语言和大部分语言的区别在于 C 语言采用了显示引用型设计,指针这种数据类型就是该设计的体现,
程序其中的一部分内存(堆上的内存)的分配和释放是由开发人员决定的,不提供没有垃圾回收机制,
也就是说要真正掌握指针,是必定要去掌握 C 的程序的内存布局,也叫程序的内存映像(program's image),
后面也稍微总结了一下;
在调试方面选择 gdb,关于它的使用可以看这个地址,
此外还要学会看转储(dump,这里是名词),转储(dumping,动名词)就是把运行时的信息保存到文件上,
这个文件就叫做 dump,比如 Linux 下有名的 core dump,它会在程序意外结束的时候生成,
可以配合 gdb 进行调试,具体操作参考这里,但这不是重点,重点是要学会如何读懂 core dump,
core dump 记录了信号(signal),比如常见的 segment fault 就是 SIGSEGV, Linux 的 Singal文档 或者 man 7 signal 就很详细了,
最好再去读一下 APUE 3rd 关于 Signal 的章节了解一下信号的工作方式.
C 程序的内存映像
主要参考资料 APUE 3rd 的第7章的第6小节,以及部分参考 C专家编程(Expert C Programming) 的第6,7章,
C专家编程 讲的十分详细,推荐认真读一下.
一个 C 程序文件本身划分了很多个区域,其中有 6 个区域会在程序运行的时候加载进内存,
也就是说内存映像由 6 个组成部分组成,比如老面孔 a.out,
Figure 4: C程序的内存映像
这是一张典型的内存排列图,从高位到低位依次看,
- 储存了命令行参数以及环境变量的区域/段;
Stack,在函数执行的时候,函数里所定义的变量(一般就是局部变量)以及相应的信息都会被储存到这里,当函数返回后这些变量以及信息就丢弃了.
并且每次调用函数时,该区域都会保存返回的地址以及调用者(caller)的环境信息.所以每次调用一个函数的时候都会在这个区域为函数的自动以及临时变量分配空间.
所以递归函数里面,每次调用一次就会使用一个栈帧,不同栈帧上的同名变量是不一样的.这个区域和叫做
stack的数据结构一样有着同样的特性,而栈底位于高位,内存从栈底开始增长.这个区域由编译器自动管理,与
stack数据结构一样,栈帧是连续的,也就是说,Stack上的内存分配是连续的.(在某个函数的定义中,)在变量
A之后定义的变量B必定是内存地址相邻的.Heap,这个区域不像
Stack一样有着同名数据结构的特性,动态内存分配就是发生在这个区域的,所谓动态分配就是分配的内存大小不定,甚至会在分配后发生改变,所以这个区域上的内存分配不能够是连续的.比如
char *s = malloc(s),这个区域上面数据不会像Stack那样自动释放,所以不在使用
s的时候一定要手动释放:free(s). Heap顶是位于高位的,与Stack相反,由于Heap底位于低位,所以Heap的内存是从低位往高位增长.(
Stack也可以像Heap那样动态分配并且同时拥有Stack的自动管理特点,alloca.h提供一个alloca的函数完成这样的事情).未初始数据 (Uninitialized Data Segment),又叫做 (bss, block started by symbol)区域/段,
包含了在函数外声明却未初始的变量,也就是未初始化的全局变量;
比如在函数外声明
long sum[1000];,sum就是属于这个区域,内核(kernel)会在程序执行前给这些数据给一个初始值, 0 或者
null pointer.已初始数据 (Initialized Data Segment)区域/段,包含了在函数外声明而且初始化了的变量,也就是已被初始化的变量;
比如在函数外声明
int maxcount = 99;maxcount就是属于该区域;Text 区域/段,包含了
CPU能够执行的机器指令(machine instructions),也就是编译后的函数,这块区域是可以共享的(sharable),所以这块才需要被加载进内存来方便频繁执行.
并且这块区域基本上是只读的,防止执行过程中被意外地修改.
a.out 文件其它类型的区域有符号表(symbol table),调试信息(debugging information),动态共享库的连接表(linkage tables)等等,
但是只有上面6个区域才会被加载到内存中,所以这些额外区域不能算入内存映像中.
这里面的链接库值得一提,有两种类型的链接库,一种是静态的,还有一种动态的,动态的又叫共享库(shared libraries).
在编译时,编译器会把静态库链接到可执行文件上,这样可执行文件就会偏大,发布的时候只需要发布可执行文件.
而共享库则是在运行时被加载,发布程序的时候,共享库需要和可执行文件一起配套发布,可执行文件大小相对于使用静态时候小,
由于共享库不需要链接到可执行文件上,所以单独更新共享库只需要替换一下.
gcc -static source.c # 静态链接库 gcc source.c # 使用共享库(默认)
EIEIO
Emacs 之深藏不漏
写于 2019/12/12
这部分专门记录本人对一些 Emacs 自带 mode 的学习记录,这些 mode 大部分都藏得很"深",
毕竟要一个一个试用需要很长时间,而且一些的学习成本很高,也并非每个人都有耐心学习,
但这些 mode 都能很方便的解决大部分问题,还是有学习的价值的,所以就专门有了这一个部分.
Calc
Calc 是一个功能复杂而全面的 mode,你可以用它来完成时区时间转换,绘图(需要gnuplot),各种代数运算(比如解决等式问题)等等各种数学任务.
就是因为 Calc 是太复杂了,时刻查阅文档是必须的,可能在过完官方的上手指南 M-: (info "(Calc)Tutorial") 时会觉得简单,
但实际上一用,或者说过一段时间再用的时候,都会有这种感觉: 做什么要用什么命令,要用哪个按键?
这就分情况了:
如果知道要做什么计算的话, 这文档就能解决这个问题了: 计算器命令索引 M-: (info "(Calc)Command Index").
大部分情况都是知道做什么计算,但是不知道命令以及其对应的按键序列索引,
得益于 Calc 的开发者遵守 语义一致 以及 定义的名字符合其含义 的原则,
可以在 计算器命令索引 中通过 \C-s 搜索关键字来查找出命令,再反查出命令对应的按键.
比如想要计算两个向量的叉积,那么通过 \C-s 搜索叉积(cross product)的英文中的关键字 cross 就可以找到 calc-cross,
回车进去后可以看到它的描述是否符合要找的叉积(答案是肯定的),并且描述中还有命令对应按键.
如果按键,想反查命令, 那么这份文档可以很好的解决这个的问题: (info "(Calc) Key Index").
个人认为不错的的一些参考资源和博客
这篇文章给想进坑Emacs的人做思想工作
https://github.com/redguardtoo/mastering-emacs-in-one-year-guide/blob/master/guide-en.org
挺不错的新手生存指南
https://github.com/emacs-tw/emacs-101-beginner-survival-guide
ElispCookbook,不过比PythonCookbook轻量,也就是不包括内置库的例子
EmacsWiki,虽然页面是比较乱,但是资源还是很赞的
Xah Emacs,一个十分友好而全面的教程,作者是一个多年的Emacs用户,有很多不错的学习建议
一个挺有名的博客
M-x Chris-An-Emacser,有不少有用的小技巧,比如摩斯密码
我与Emacs的一些事情
写于 2018/8/31
最初
我是在17年的4到6月中断断续续的接触Emacs,在这之前先是用VSCODE,本想长期使用VSCODE,一次意外改变了我的想法: 操作系统的桌面崩了.
于是找了一个可以在CLI环境下面使用的编辑器,便有了一段很短的VIM经历,没记错的话就是4月份的时候,等到适应了VIM后就没想到过要用别的
编辑器了.然而一次偶然看到了一个贴提到了两个"神话"编辑器,其中一个就是VIM,另外一个就是Emacs.抱着好奇的想法去了解一下Emacs,不过
先入为主的想法让我并没有觉得Emacs有多好,特别是操作比起VIM的繁琐多了.由于当时手头上还有工作,所以Emacs就放一边了,继续利用VIM红
作.后面无聊的时候在一个周末里面找了各种关于Emacs配置Python开发环境的文章,配置好了用它来工作了,但还是不习惯,至于是什么时候习惯
的,那应该是我不再配置VIM的时候,当时已经把VIM负责的全部工作都交给Emacs处理了,整个过程花了一个月左右.其实VIM挺不错的,刚开始用
Emacs的时候我还用了
Evil模拟VIM的按键,不过那个时候经常配置出错导致使用不了,而我又太依赖于VIM的按键导致了我一直没有熟悉Emacs自带的按键,每次报错我都得使用别得编辑器修正配置.于是我下定决定不再依赖VIM了,正式进入人生中Emacs时代.
现在
现在开始学习如何写Package,其实之前也有尝试写过,写了几个"没用"的东西出来,现在看来就是在浪费时间,不过这让我自己明白了还有很多东西不懂和不足.
记录这些不足的目的是为了不断地提醒自己,不让自己偏离目标.
不足一: 学习态度不够端正,不够虚心.
总是认为在几天内熟练使用一门编程语言,在有其它语言基础的情况下,入门别语言的确是可以很快.
但是熟练使用就是另外一个个概念了:深入细节地学习语言的特性以及经过大量实践学习其中的细节.
还有就是不要老想着造轮子,造轮子并不具备创造性.而我也在这上面浪费了很多时间,抗拒使用别人的package,浪费大量时间花在所谓的"自己写"上面.
确实"自己写"的确可以学习到很多东西,但是效率太低了,而Emacs本身就是想给用户提供一个高效的工具,而不是让你去舍弃效率.可以选择在空余时间里深入学习.
在空余时间学习要注意做好知识管理,因为大部份人的空余时间都不会太多,因为学到的东西可能都很碎片,越是碎片就越容易忘掉,所以知识管理就很有必要了.
在这知识管理这点上我是做的不够好,导致很多知识忘了,以前付出时间和努力都白费了.还有不要忽视这一些碎片,时间久了回过头会发现收获很大.(因为我自己忘掉的
东西实在是太多了,我自己回过头来看都吓了一跳).
不足二: 怕麻烦,行动力低下,总想一次做好
很多人都有这种心理: "这个很简单,做了只是浪费时间." 或者 "这个网上有解决方案,先休息一下稍后动手" 又或者 "这个工作量太大了,一两天做不完".
实践可能很简单,但是不真正操作一遍你是发现不了一些潜在的问题,比如系统环境的影响,一些软件依赖和版本问题.
还有尽早解决自己的拖延症,如果事情一点一点地拖下去,回过头你会发现明明是一件很简单地事情却拖了不少时间,更糟糕地是事情可能一直都不会完成甚至开始.拖延症/行动力低下是很多人在一件事情上面失败的主要原因.
至于工作量大的工作,要承认事实:"的确一两天是做不完的".别人的大地开源项目是怎么写出来的呢?通过
commits可以发现别人也不是一两天做好的,他们也是一点点地写出来地.如果事情/工作的确不能马上完成,那么请做好任务管理,记录要什么时候做什么,不这么做的话很有可能就回把这件事情给忘了.
不足三: 害怕失败
曾经给
fic-mode提交过两个pull request, 第一个合并了,第二个被无视了.第二个是添加新功能的,第一次写地挺认真的,所以有点伤心,GitHubissue和pull request产生了恐惧.现在想起来自己还是有点玻璃心,因为这是很平常的事情,很多人都有同样的经历,我只不过是其中一员.既然其他人能够挺过来,那么我也能.所以没必要因为一次失败而气馁,现在觉得早点遇到失败也是好事.
以后
以后也会一直使用Emacs,因为这个开源项目已经改变我了:
- 开始给别的项目提交
pull request - 跟别人交流
- 能够静下心阅读代码和文档
- 能够正视自己的缺点和不足
- 开始虚心向别人学习
有太多方面我想不起来,总的来说它对于我来说影响实在太大了,因此我也愿意把闲余时间投入到它的身上.
写这些的时候我才理解为什么有人说Emacs是一种生活态度了.
- 开始给别的项目提交