Skip to content
Emacs-lisp 奇淫异技
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
README.org

README.org

Emacs-lisp 奇淫异技

开场白

经过深思熟虑的拍脑门后,二呆同学说:

50% 的决策都是拍脑门决定的。。。。

谁应该读这个文档

符合下面特征的同学值得读一下这个文档:

  1. 刚刚从编写 Emacs configure 转到编写 emacs library
  2. 希望将这个(些) library 打包成一个 package
  3. 愿意将这个 package 和其他同学共享。

读这个文档之前,同学们需要知道,下面所有的示例代码 都有两个前提:

  1. 当前 library 或者 package 的名称为:eltips
  2. Eltips 依赖的一个名字为 erdai 的 library :-)

Configure vs library

从 Emacs-lisp 语言角度来说,两者没有多大的区别,可能写 library 的时候, Emacs-lisp 语言应用的更加规范一点。

但编写 configure 和编写 library 需要遵循的理念有很大不同:

  1. Configure 是写给自己用的,如果其他同学使用你的 configure 出了问题, 那么他只能自认倒霉,你可以不负任何责任。
  2. Library 编写出来主要是给其他人使用的,如果出了问题,维护者是有直接 责任的:-)

Library vs package

Emacs-lisp 很早就有了 library 的概念, 但 package 的概念是随着 package.el 和 elpa 引入的,时间不是很长。

简单来说:package 是 library 的发布形式,一个 package 可以是一个 单独的 library 文件,也可以是一组功能联系紧密的 library 文件打包 生成的 zip 文件。

具体细节请阅读 elisp info 中的 “Preparing Lisp code for distribution” 章节。

https://www.gnu.org/software/emacs/manual/html_mono/elisp.html#Packaging-Basics

library 编写原则

  1. 成本效益原则:
    1. 编写的 library 不能含有恶意代码,不能窃取用户隐私。
    2. 编写的 library 值得用户花时间和精力去学习使用, 重复制造轮子的决定要慎重。
  2. 正确使用 Emacs-lisp
  3. 尊重社区惯例原则:主要是使用公认的代码缩进方式,让代码容易维护等。
  4. 尊重用户选择原则:
    1. 不能随意覆盖自己 library 的用户接口变量
    2. 不能随意覆盖其他 library 的任何全局变量, 包含用户接口变量和内部使用全局变量。
  5. 用户知情同意原则:当一个 library 必须 覆盖用户接口变量时, 这个 library 的维护者 尽自己最大的努力 让用户知道哪些用户 接口变量被覆盖了,这样做的话,即使出问题,用户也容易调试。

library 命名

给你的 library 命名这个工作非常非常重要,千万不能马虎,因为对 一个已经存在的 library 重命名是非常麻烦的事情,需要做许许多多 兼容性工作。

Emacs-lisp 和其他 lisp 不同,Emacs-lisp 的全局变量的作用范围非常大, 只要一个 library 加载了,那么在当前 Emacs session 的任何代码处,都 可以随意的访问和设置这个 library 的所有全局变量。

Emacs-lisp 这个特性很容易发生变量冲突,为了防止变量冲突,Emacs 社区 有这么一个惯例: 一个 library 的全局变量,函数定义以及宏定义等,名字 都应该使用统一的前缀,比如: company 的全局变量,命名时都使用 “company-” 前缀。

Emacs-lisp 语言本身对 library 的名称没有多少限制,但并不代表你可以 随意使用任何字符串做为一个 library 的名称,建议新手遵循以下惯例:

  1. library 的名字不能是其他 library 正在使用或者曾经使用过的名字。
  2. library 的名字 只能使用 小写字母,数字和下划线。
  3. library 的名字要好记,好写,好认。
    1. 最好是 一个 单词或者 一个 缩写,比如:company 或者 elpa 。
    2. 最好不要超过2个单词。
    3. 不要太长, 个人感觉 最好不超过15个字符。
  4. library 的名字中,最好不要包含太常见的单词,比如:emacs, chinese, good 之类的。
  5. library 的前缀最好使用 library 名称加连字符。

package 命名

最好的选择是:package 的名字就选 library 的名字。

选择合适的协议

这个需要早做决定,package 的维护者可以按照自己的喜好选择一个开源协议, 但最常用的是: “GNU General Public License” , 如果你想你的 package 加入 elpa, 那么你只能选择 GPL :-)

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

选择合适的发布方式

到目前为止,最常见的发布途径有两种:

  1. Melpa
  2. Elpa

library 的维护者应该早做决定,因为 elpa 要求 library 代码的所有供献者都签署 GNU 的纸质协议,如果这个事情在 library 编写的早期不作的话,后面的工作量就大了。

Melpa 的限制相对比较小了。

创建 library 框架文件

方法非常多,比较简单方便的一种方式是:

  1. 新建并打开一个文件 eltips.el
  2. 运行 auto-insert 命令(建议临时关闭 ivy-mode)。

    就会得到类似下面的一个 library 框架,然后开始 写代码就 OK 了。

    ;;; eltips.el --- Emacs-lisp tips                    -*- lexical-binding: t; -*-
    
    ;; Copyright (C) 2018  Feng Shu
    
    ;; Author: Feng Shu <tumashu@163.com>
    ;; Keywords: docs
    
    ;; This program is free software; you can redistribute it and/or modify
    ;; it under the terms of the GNU General Public License as published by
    ;; the Free Software Foundation, either version 3 of the License, or
    ;; (at your option) any later version.
    
    ;; This program is distributed in the hope that it will be useful,
    ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
    ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ;; GNU General Public License for more details.
    
    ;; You should have received a copy of the GNU General Public License
    ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
    
    ;;; Commentary:
    
    ;;
    
    ;;; Code:
    
    
    
    (provide 'eltips)
    ;;; eltips.el ends here
        

了解 Emacs-lisp Style

仔细阅读下面这个项目中的所有文档:

https://github.com/bbatsov/emacs-lisp-style-guide

定义变量的正确方式

Emacs-lisp 有许多定义变量的方法,但下面几种是最最常用的。

  1. 定义一个用户接口变量
    (defcustom eltips-name "eltips"
      "The name of elptips.")
        

    这是最正统的方式,但许多人嫌麻烦,最开始都使用下面的方式 定义一个用户接口变量,等到 library 相对稳定后,再改用上面的 方式:

    (defvar eltips-name "eltips"
      "The name of elptips.")
        
  2. 定义一个只读全局变量
    (defconst eltips-name "eltips"
      "Eltips's name")
        

    值得注意的是,defconst 并不能保证这个变量完全只读,而不被修改 它只是告诉同学们,library 的维护者可能不会在代码里面重新设置 这个变量,至于真的会不会,只有天知道,所以这个操作符的功能和 下面的这段代码类似:

    (defvar eltips-name "eltips"
      "The name of elptips.
    Please note: this variable is used as const variable.")
        
  3. 定义一个 library 内部使用 的全局变量
    (defvar eltips--name "eltips"
      "The name of elptips.")
        

    注:Lisp 有一个惯例:使用前缀加 -- 来表示这个全局变量是 library 内部使用的全局变量,用户不应该使用它,library 的维护者可以 随意添加,删除一个内部全局变量,可以对一个内部全局变量任意赋值, 更重要的是 library 维护者不需要维护内部全局变量的向后兼容性。

  4. 定义一个局部变量
    (let ((a 1)
          (b 2)
          c)
      (+ a b))
        
    (let* ((a 1)
           (b 2)
           (c (+ a b)))
      c)
        

变量赋值的正确方式

简单来说,变量必须先被定义,才能对其赋值。

可惜的是:这个规则非常简单,但新手往往不太注意。

在 Emacs-lisp 中,最常用的变量赋值操作符是:setq, 在一个 library 中,一般只能出现下面 两种 setq 赋值结构:

  1. 对一个 library 内部使用 的全局变量进行赋值:
    (defvar eltips--name "eltips"
      "The name of elptips.")
    (setq eltips--name "eltips2")
        
  2. 对一个局部变量进行赋值:
    (let ((a 1)
          (b 2)
          c)
      (setq c (+ a b)))
        

其他形式的 setq 赋值结构都是有问题的:

  1. 在 library 中对一个用户接口变量进行赋值
    (defcustom eltips-name "eltips"
      "The name of elptips.")
    (setq eltips-name "eltips2")
        

    这种做法是最应该避免的!!!

    无论这个用户接口变量属于自己 library 还是其他 library,都不应该 这么做,因为它直接违反了 “尊重用户选择” 这个原则,在一定条件下, 加载 library 会覆盖用户的设置,比如:

    (setq eltips-name "eltips3")
    (require 'eltips)
        
  2. 不能直接使用 setq 来定义变量

    setq 是变量赋值操作符,不是变量定义操作符,但 setq 有一个特性: 如果被赋值的变量不存在, setq 会首先定义这个 全局变量, 然后再赋值,下面两个例子是等价的:

    (setq eltips-name "eltips")
        
    (defvar eltips-name nil) ;这个全局变量会被用户当成用户接口变量
    (setq eltips-name "eltips")
        

    我个人感觉,Emacs-lisp 给 setq 添加这个特性是为了编写 configure 时省事, 但编写 library 的时候,这样做有覆盖用户设置的风险。

  3. 给一个没有定义的 局部变量 赋值
    (let ((a 1)
          (b 2))
      (setq c (+ a b)))
        

    这个例子本质是定义并赋值了一个 全局变量 c, 正确的写法应该是:

    (let ((a 1)
          (b 2)
          c) ; 这个 c 绝对不能遗漏
      (setq c (+ a b)))
        

    由于这种方式很容易出现遗漏,而且带来的问题不太容易调试( 因为容易覆盖 Emacs-lisp 核心使用的全局变量),所以建议使用 let* 来处理类似情况:

    (let* ((a 1)
           (b 2)
           (c (+ a b)))
      c)
        

对变量赋值的再思考

通过 “变量赋值的正确方式” 的讨论,我们可以发现,在编写 library 的 时候,setq 最合理的使用方式只有 一种 , 即:对 library 内部使用的 全局变量赋值:

(defvar eltips--name "eltips"
  "The name of elptips.")
(setq eltips--name "eltips2")

局部变量 赋值时要慎用 setq, 优先考虑使用 let* , 如果必须使用, 一定要确保这个局部变量已经在 let 结构中定义了。

在其他情况使用 setq 可能就是滥用了,当然我这里只是说 可能, 只要你的 使用方式遵循 library 编写原则,那也许就是合理的用法 :-)

如果必须设置用户接口变量,该怎么办?

虽然 library 维护者不应该随意覆盖用户接口变量,但现实情况是: 我们有时候必须这样做,理想很丰满,但现实却很骨感。。。

这时候,我们就要退而求其次,遵循 “用户知情同意原则”, 尽最大努力 减小影响范围。

常见的方式有四种,但一般只建议使用前两种方式,后面两种方式是 黑科技, 一定要谨慎使用,不合理的应用会让你遭到唾弃。

  1. 在 library 文档中指导用户自己设置

    这种方法是最稳妥可靠的,大多数情况下,我们只能使用这种方式。

  2. 使用 let 表达式来 局部覆盖 一个用户接口变量
    (let ((erdai-name "erdai2"))
      (erdai-return-name))
        

    在 let 定义的局部范围, erdai-name 会被强制绑定到另外一个值, 这个用法 非常的常用 ,当满足下面两个条件时,就可以这么用。

    1. library 所依赖的函数无法通过参数设置,只能通过全局变量来改变其行为。
    2. 对这个全局变量局部绑定,不会对所依赖的 library 造成影响。

    比如:

    (defun erdai-return-name ()
      (message erdai-name))
    
    (defun erdai-return-fakename ()
      (interactive)
      (let ((erdai-name "erdai2"))
        (erdai-return-name)))
        

    注:这种方式让熟悉词法作用域的同学很不习惯,确实是这样子的,在 Emacs-lisp 中全局变量无论什么时候,都是按照动态作用域的规则来处理。

  3. 使用激活函数来覆盖用户接口变量
    (defun elptip-erdai-enable ()
      (interactive)
      (setq erdai-name "erdai2")
      (message "eltips: `erdai-name' has been override."))
        

    这种方式要注意:

    1. 激活函数不能默认运行,只能通过文档告诉用户在它们的配置中添加。
    2. 如果无法做到完全无影响,就要提示用户哪个或者哪些 “用户接口变量” 被强制覆盖了。
    3. 最好告诉用户,如何简单的取消激活,如果可以,添加一个 disable 函数, 但令人遗憾的是,disable 函数看似容易编写,其实往往是不可行的。 像这种覆盖用户接口变量的激活函数,一般也只能让用户删除这行配置, 然后重启 emacs, 别无它法。

    比如下面这个例子,看似可行,实际是不合理的。。。。

    (defun elptip-erdai-disable ()
      (interactive)
      (setq pkgxx-name "erdai")))
        

    除非万般无奈,这种方式不建议使用。

  4. 使用激活函数来覆盖影响用户接口变量的函数

    假设 erdai 中有一个函数专门用来处理用户 接口 erdai-name :

    (defun erdai-return-name ()
      (message erdai-name))
        

    我们可以通过替换 `erdai-return-name’ 这个函数来改变 其行为,但我们不能直接在 eltips 包中添加一个新的 `erdai-return-name’ 函数,这种偷偷摸摸的覆盖让遇到 问题的用户很难调试,我们需要使用 emacs 内置的 nadvice 功能:

    (defun eltips-erdai-return-name ()
      (let ((erdai-name "erdai2"))
        (funcall orig-func)))
    
    (defun eltips-erdai-enable ()
      (interactive)
      (advice-add 'erdai-return-name
                  :around #'eltips-erdai-return-name))
        

    这样做的话,用户在阅读 `erdai-return-name’ 的文档 时,就可以发现这个函数被哪个函数 advice 了,算是 一种知情同意,这种方式的另外一种好处是可以写出一个 比较靠谱的 disble 函数。

    (defun eltips-erdai-disable ()
      (interactive)
      (advice-remove 'erdai-return-name
                     #'eltips-erdai-return-name))
        

    不过即便如此,emacs 官方社区也是不建议使用这种机制的

    这里还是那句话,除非万般无奈,不建议使用。

养成使用代码检查工具的习惯

我们有许多 Emacs-lisp 代码检查工具可以用来检查代码中 存在的问题:

  1. checkdoc
  2. elint
  3. package-lint
  4. byte-compile-file (用于检查 Emacs-lisp 编译错误)

我的建议是:代码提交之前,都应该用这些工具检查一遍, 去除所有的警告和错误后再提交,如果检查的频率太低, 可能你就没有动力做这个事情了。

如何把自己的 package 提交到 Melpa

这里以 github 为例:

  1. 注册一个 github 帐号,比如:tumashu
  2. 用注册的帐号登录 github
  3. 进入 Melpa 的 github 页面
  4. 点击 Fork 按钮, 在 tumashu 的帐号下得到一个 melpa 代码仓库的镜像。
  5. 抓取 Melpa 官方仓库
    cd ~/projects/
    git clone https://github.com/melpa/melpa.git
        
  6. 添加 melpa 镜像仓库(Fork 得到的仓库)的地址
    git remote add my-melpa https://github.com/tumashu/melpa.git
        
  7. 更新工作目录(很重要的工作)
    git reset --hard origin
    git pull
        
  8. 在 recipes 目录下添加 recipe 文件,并 commit 你的更改, recipe 文件的格式请参考: Melpa README
  9. 将更改推送到镜像仓库,这里一定要使用 强制推送
    git push -f my-melpa master
        
  10. 然后给 melpa 官方仓库提交一个 pull request
  11. 等待这个 pull request 合并,一般需要1周时间,在这个过程中, melpa 维护者会对 package 的代码做出相应的评价,你需要:
    1. 按照要求更改
    2. 说明理由,如果你的理由合理充分,Melpa 维护者会同意的。
  12. Pull request 合并后大约 4 个小时,你的 package 就会出现在 melpa 中。
  13. 更新工作目录(很重要的工作)
    git reset --hard origin
    git pull
        

如何把自己的 package 提交到 Elpa

Elpa 是 Emacs 官方的 package 仓库,所以把一个 package 提交到 elpa, 相对来说复杂罗嗦一点:

  1. package 代码的主要供献者,需要签署 GNU 纸质协议,一般需要自己下载 并打印 pdf 格式的协议文件,然后签上自己的名字,并用邮件的方式邮寄到 指定的地址,这个工作大概需要20天左右,什么是 “主要供献者”,GNU 有具体 规定,一般是按代码的行数来确定。
  2. package 代码的质量要求稍微高一点,package 的代码会在 emacs-devel 上 经过 emacs 大牛们的评价,不过有时候简单的 package, 大牛们也懒得评价 :-)
  3. Elpa 是直接把 package 的代码提交到 elpa.git, 而不是和 Melpa 一样, 只提交一个 package 描述,所以操作过程稍微罗嗦一点。

但将 package 提交到 elpa 也有不可比拟的优势:elpa 是 emacs 的官方 package 仓库, 属于亲儿子,你懂得。。。。

将 package 提交到 elpa 之前,你首先需要规划 package 未来的开发流程, 你需要做下面几个决定:

  1. 怎么维护 package
    1. 使用 elpa.git 做为唯一的代码仓库。
    2. 使用独立 package 仓库,定期和 elpa.git 同步。
  2. 如何提交 package
    1. 自己提交:这个需要用户获取 elpa.git 的推送权限。
    2. 别人代劳:这个不需要 elpa.git 的推送权限,只需要将代码 patch 发送到 emacs-devel 上,请 elpa.git 的管理员代为推送就可以了。

注意:“别人代劳” 模式只适用于 “以 elpa.git 做为 package 的唯一维护仓库” 这种开发模式,这种开发模式最简单,但用的人不是很多,大多数 package 维护者 都会向 elpa 管理员申请 elpa.git 推送权限,自己推送提交。

我在下面只介绍:“package 使用独立仓库,定期和 elpa.git 合并,并自己 提交代码” 这个流程。

  1. http://savannah.gnu.org/ 上注册一个帐户,比如: tumashu
  2. 登录后,上传 ssh 公钥,(必须的,不然以后无法推送代码)
    登录 savannah.gnu.org -> 点击 [ My Account Conf ] 链接
                          -> 点击 [ Authentication Setup ] 链接
                          -> 点击 [ Edit the 2 SSH Public Keys registered ] 链接
                          -> 按页面上的具体说明操作
        

    注:公钥生效需要1个小时,请耐心等待。。。。

  3. 给 emacs-devel 发一份邮件,主题为:[ELPA] New package: <pkg-name>, 邮件正文中介绍一下你的 package,并说明 package 代码的位置,具体细节请 参考 elpa README 中的 “Notify emacs-devel@gnu.org” 章节。
  4. 接受大牛们的 review, 这个过程一般需要2-7天时间。
  5. 按照大牛们提出的意见和建议更改代码,一直更改到大牛们认为你的 package 适合 加入 elpa. 这个过程难度不大,就是比较繁琐,不过也有直接被 KO 的可能。。。
  6. 如果 Emacs 的开发者或者 elpa.git 的维护者认为你的 package 已经 OK 了, 你就可以申请 elpa.git 的推送权限了:
    登录 savannah.gnu.org -> 点击 [ My Groups ] 链接
                          -> 点击 [ Request for Inclusion ] 链接
                          -> 搜索 emacs 并钩选
                          -> 写 comment, 要说明你是哪个 package 的维护者
                          -> 点击 [ request inclusion ] 按钮
                          -> 等待 elpa.git 管理员授权 (Request for Inclusion Waiting For Approval)
        
  7. 抓取 elpa.git

    确定当前帐号已经加入 emacs 组后,

    登录 savannah.gnu.org -> 点击 [ My Groups ] 链接
                          -> [ Groups I'm Contributor of ] 包含 emacs
        

    你就可以抓取 elpa.git 了。

    cd ~/projects/
    git clone tumashu@git.sv.gnu.org:/srv/git/emacs/elpa.git
        

    注:可以访问 https://savannah.gnu.org/git/?group=emacs , 来获取 member 抓取地址,千万不能搞错这个地址,不然以后就没法 git push 操作了,切记切记。。。。

  8. 仔细阅读 elpa README , 然后按照上面描述的流程提交代码就可以了, 这里只做一些说明:
    1. 你现在已经拥有 elpa.git 的推送权限了,做事千万别随意马虎。
    2. 禁用 git rebase -i 之类的更改 git 历史的命令,否则,全世界的 emacser 都可能会唾弃你。。。。
    3. 虽然 README 推荐 subtree, 但有些大牛觉得 externals 更靠谱, 估计智能看自己的喜好了,但值得注意的是: 如果已经使用 subtree 或者 externals 来同步,就千万别在使用 git format-patch 了,会有意想不到 的后果。
    4. 编辑 elpa/externals-list 文件,添加自己 package 的信息,按字母顺序排序。
    5. 如果遇到问题,勤问 emacs-devel, 别自以为是。。。。

未完待续。。。

尾注

You can’t perform that action at this time.