《elisp 每日一练》——hook 与 advice 综合练习

4 阅读3分钟

在前面的一系列文章中我们系统性的介绍了hook与advice 机制,现在我们来进行一个回顾和对应的综合练习

回顾

首先我们了解到Emacs中hook实际上就是一个存储了一个或者多个函数的变量。我们可以调用以下函数传入这些变量,Emacs会自动执行其中保存的函数:

  • run-hooks
  • run-hook-with-args
  • run-hook-with-args-until-success
  • run-hook-wrapped

对于这个hook变量我们可以通过下面的函数添加或者删除hook函数

  • add-hook
  • remove-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,否则会造成死循环