在前面的一系列文章中我们系统性的介绍了hook与advice 机制,现在我们来进行一个回顾和对应的综合练习
回顾
首先我们了解到Emacs中hook实际上就是一个存储了一个或者多个函数的变量。我们可以调用以下函数传入这些变量,Emacs会自动执行其中保存的函数:
run-hooksrun-hook-with-argsrun-hook-with-args-until-successrun-hook-wrapped
对于这个hook变量我们可以通过下面的函数添加或者删除hook函数
add-hookremove-hook
emacs 自身为我们定义了一系列的内置hook,我们选取了几个典型的例子:
-
after-change-major-mode-hook -
kill-buffer-hook -
kill-buffer-query-functions -
find-file-hook -
before-save-hook -
after-save-hook -
window-configuration-change-hook
我们定义minor mode 和 major mode 时宏默认埋下了hook点。对于minor mode 来说:每次切换mode状态都会无条件的调用普通hook,但是如果我们关系开启和关闭状态,它还额外有调用off-hook 和 on-hook的机制;对于major mode,通过 delay-mode-hooks 保证其在执行初始化代码时没有hook运行,在初始化完毕之后,通过调用 run-mode-hooks 做到先执行之前被挂起的父类hook,最后执行子类hook
关于 advice 就比较简单了,它主要通过 advice-add 和 advice-remove 来管理。它可以通过一些关键字详细的控制行为。
练习
我们本次的练习尝试结合hook 和 advice 机制。实现一个无感的自动备份插件,它的主要功能如下:
- 通过 advice 在执行
write-region保存的时候,创建一个带有时间戳的备份 - 在备份完成之后,注册自定义hook,由hook负责对外输出保存的信息
我们先定义具体的配置变量
(defgroup my-safe-guard nil
"自动备份系统"
:group 'tools)
(defcustom my-safe-guard-backup-dir (expand-file-name "~/.emacs.d/safe-backups/")
"自动备份的存放路径"
:type 'directory
:group 'my-safe-guard)
(defcustom my-safe-guard-audit-hook nil
"当文件成功备份后触发该hook
函数接收三个参数:(FILE-PATH BACKUP-PATH TIMESTAME-STR)"
:type 'hook
:group 'my-safe-guard)
接着我们来编写这个核心函数,它主要是通过 :around 关键字接管函数 write-region,在执行函数之前先备份源文件。备份成功之后执行原函数并触发hook,传入三个函数,外部可以接收这个事件,以便进行记录
(defun my-safe-guard-backup-internal (orig-fun start end filename &rest args)
(if (and filename (stringp filename) (file-exists-p filename))
(let* ((backup-dir my-safe-guard-backup-dir)
(time-str (format-time-string "%Y%m%d-%H%M%S"))
(safe-backup-path (expand-file-name (concat (file-name-nondirectory filename) "." time-str ".bak") backup-dir)))
;; 确保备份目录存在
(unless (file-directory-p backup-dir)
(make-directory backup-dir t))
(copy-file filename safe-backup-path)
;;执行原函数
(let ((result (apply orig-fun start end filename args)))
;;触发hook
(run-hook-with-args 'my-safe-guard-audit-hook filename safe-backup-path time-str)
result))
;;路径不是有效路径,直接执行原函数
(apply orig-fun start end filename args)))
最后我们来注册advice 和 hook启用这些功能
;; 挂载 :around advice 到 write-region
(advice-add 'write-region :around #'my-safe-guard-backup-internal)
;; 监听hook: 这里简单的输出到message buffer
(defun my-audit-listener-notify (file backup time)
(message "保存成功, 文件 [:%s] 写入成功, 快照已归档" (file-name-nondirectory file)))
(add-hook 'my-safe-guard-audit-hook #'my-audit-listener-notify)
这里我们简单的演示了将信息写入到 *Messages* buffer 中,读者也可以进行扩展,弹框提示或者写入日志,但是一定要注意写入日志时要记得确保写日志的函数不能调用到 write-region,否则会造成死循环