基于我先前的一个包 psearch
实现。
适用于不便 advice 又不想改源码的场景,例如:一个巨长的函数,你只想改中间一两句。
改源代码当然是最稳妥的,但是改了至少得建个仓库来维护,不如 advice 和 patch 方便。
Melpa 上有一个看起来比较完善并且有不少人在用的包 el-patch,但是它很复杂,引入了很多概念,看得我头疼,而且写起来很累赘,几乎是把原函数抄一遍了。
例如把以下左边函数改成右边的形式:
(defun test-patch () ;; => (defun test-patch ()
(list '(1 2 3) ;; (list '(1 2 3)
(if nil '(4 5 6)) ;; (if t '(4 5 6))
'(7 8 9))) ;; '(7 8 9)))
-
psearch
的写法:(psearch-with-function-patch test-patch (psearch-replace '`(if nil ,body) '`(if t ,body)))
-
el-patch
的写法:(el-patch-defun test-patch () (list '(1 2 3) ;; 原样抄写 (if (el-patch-swap nil t) '(4 5 6)) ;; 修改的部分 '(7 8 9))) ;; 原样抄写
如果这个虚构的简单的例子让你感觉似乎 el-patch
更简单,那么你应该看看真实世界的例子:
在这个例子里,它 patch 的目标是 lisp-indent-function
函数,几乎是把原函数里面的语句全部「摘」出来,然后重新组合。
比如想改一个函数里的 (if cond then else)
结构,先用 el-patch-let
把各个部分代码「摘」出来,然后用 el-patch-swap
重新组合。「摘」的手法很原始粗暴:
(el-patch-defun a-function (some-args)
...
.... 虽然没改到这部分代码,请一字不落抄下来 ...
...
(el-patch-let (($cond (... 很长很长,原函数怎么写你就怎么写,一字不落 ...))
($then (... 很长很长,原函数怎么写你就怎么写,一字不落 ...))
($else (... 很长很长,原函数怎么写你就怎么写,一字不落 ...)))
(el-patch-swap
;; 原先的形式
(if $cond
$then
$else)
;; 新的形式
(if $cond
$else
$then)))
...
.... 虽然没改到这部分代码,也要一字不落抄下来 ...
...)
psearch
则使用 pcase 语法来定位代码,只要把关键部分代码特征交代清楚,不用面面俱到,也不用关心没改到的代码:
(psearch-with-function-patch some-function (some-args)
(psearch-replace
;; match ---------
'`(if ,(and cond (guard (equal cond
;; 假设 cond 细节如下:
'(or (string= char "\n")
(string= char "\t")))))
,then
,else)
;; replace -------
'`(if ,cond
,else
,then)))
如果某个 form 在函数里出现多次而且内容大同小异,你又不想把匹配规则写太细,还可以用位置来匹配,例如:
(with-eval-after-load 'corfu-doc-terminal
(psearch-with-function-patch corfu-doc-terminal--preprocess-docstring
;; 删除所有 `(let ...)' 但保留第一个
(let ((pattern '`(let . ,rest)))
(and (psearch-forward pattern)
(psearch-replace pattern nil)))))
确保每个 pattern 都写对了,并且执行成功。
已知问题:patch 很容易丢失,比如 edbug 或 eval 过原函数,就必需重新 patch。el-patch 也存在这个问题。
所以代码暂时放到 function-patch 分支,通过以下方法安装体验:
(quelpa '(psearch
:fetcher github
:repo "twlz0ne/psearch.el"
:branch "function-patch"
:files ("psearch.el")))
我在考虑是不是改为生成一个打过补丁的 advice 函数,然后用这个函数覆盖原函数,这样就不容易丢失了,然后 describe-function
也能看到这个函数被覆盖了。