《elisp 每日一练》——interactive 交互式命令

《elisp 每日一练》——interactive 交互式命令

Masimaro
2026-08-23 / 0 评论 / 1 阅读 / 正在检测是否收录...

在前面我们写的所有能在命令行执行的函数,第一个语句都是 (interactive)。 过去我们仅仅机械性的加上这么一个语句,似乎没有对它进行深入的了解。本节我们将探讨有关 interactive 的内容

背景知识

在Emacs中,普通函数仅仅能被其他elisp代码调用,而交互式函数既能够被用户通过 M-x 的方式调用,也可以被其他lisp 代码调用。我们如果要判断一个函数是否是交互式命令可以使用 commandp

想要一个函数成为交互式函数, 可以在函数中包含 interactive 的声明。

interactive 的定义如下:

(interactive &optional ARG-DESCRIPTOR &rest MODES)

其中 ARG-DESCRIPTOR 参数定义了当用户交互式地调用该命令时,如何获取其参数。它主要有三种形式:

  • nil 或者省略时表示该函数不需要参数
  • 使用字符串:交互式读取参数
  • 使用 Lisp 表达式:完全自定义

第二种方式可能使用的频率最高。它包含了一组有特殊字符组成的代码字符和对应的提示信息。Emacs 会根据代码字符的类型,自动读取用户输入,进行类型的转化并传递给命令。常见的一些代码字符有:

  • r: 自动提取当前 Region 的起点与终点 Point(无需用户手动输入!)
  • s: 读取用户输入的字符串
  • n: 读取用户输入的数字
  • f: 交互式选择已有文件
  • F: 选择一个文件,文件可以不存在
  • p: 自动获取前缀参数(例如按下 C-uM-4)
  • B: 一个已经存在的buffer的名称
  • b: buffer的名称,buffer可以不存在

其他的参数还有很多,读者可以自行查阅对应的文档信息。

我们可以在每一个代码字符后面加上一个提示信息的字符串,多个代码字符的话使用\n作为分隔符,下面我们看一个具体的例子

我们可以利用上述内容实现一个简单的打开文件的功能,如果文件不存在则创建新文件。并且允许用户提供一个需要写入的字符串,如果我们带上对应的提示信息,就可以有下面一个简单的实现

(defun my/open-file(file input-str)
  (interactive "F输入文件名称:\ns输入文件中的字符串:")
  (let ((buffer (find-file file)))
    (goto-char (point-max))
    (newline)
    (insert input-str)))

因为我们需要接受用户输入的文件名和字符串,所以函数需要两个参数。而文件可以不存在所以我们采用 F 作为代码字符,后面接上提示信息,然后利用 \n 分隔符写上另一个参数的信息。

如果你觉得以上这些代码字符不能满足你的需求,我们可以采用第三种方式,也就是使用list来自定义表达式。list 中可以包含多个表达式,它会依次对表达式进行求值并将求值的结果按函数参数定义的顺序进行赋值。如果我们将上面的函数使用列表的方式重新组织可以得到如下的代码

(defun my/open-file(file input-str)
  (interactive (list 
             (read-file-name "输入文件名称:")
             (read-string "输入字符串:")))
  (let ((buffer (find-file file)))
    (goto-char (point-max))
    (newline)
    (insert input-str)))

练习

我们来实现一个C/C++ 语言版本的快速注释函数。可以有一下几种情况:

  1. 如果用户激活了一个 Region,则采用 /**/ 注释区间的代码
  2. 如果用户通过 C-u 输入了数字,那么就通过 // 注释当前行以及后续的n行
  3. 如果用户未进行上述操作,则通过 // 注释当前行
(defun my/comment-region (start end)
  "激活区间的情况下,通过 /**/注释区间的代码"
  (let ((text (buffer-substring-no-properties start end)))
    (delete-region start end)
    (insert (format "/* %s */" text))))

(defun my/comment-lines (arg-lines)
  "当前行开始,逐行添加 // 注释,总共添加 arg-lines 行"
  (dotimes (_ (or arg-lines 1))
    (save-excursion
      (beginning-of-line)
      (insert "// "))
    (forward-line 1)))

(defun my/quick-comment(start end arg-lines)
  "C/C++语言版本的快速注释函数"
  (interactive
   (if (use-region-p)
       (list (region-beginning) (1+ (region-end)) nil)
     (list nil nil (prefix-numeric-value current-prefix-arg))))
  (cond
   ;;优先处理选区的情况
   ((and start end)
    (my/comment-region start end))
   (t
    (my/comment-lines arg-lines))))

为了实现上述功能,我们提供三个函数,其中两个工具函数,一个用来做交互的主函数。我们一个个来分析:

my/comment-region,在激活 Region 的情况下调用该函数。它有两个参数分别是当前 region 的起始位置和结束位置。它主要是提取region 区域内的文本,并使用 /**/ 将文本包括,然后先delete区域内的文本然后在区域开始位置插入新文本实现注释。这里其实也可以在开始和结束位置执行两次insert 函数分别插入 /**/

my/comment-lines,该函数用来注释多行。它接受一个代表行数的整数参数。我们通过 dotimes 宏进行一个循环。因为它不需要迭代参数,所以迭代结果那块使用 _ 进行忽略。而迭代次数我这里使用了 oror 后面可以跟任意个表达式,它会依次往后求值,直到得到一个不为空的值。所以这里表示如果arg-lines 的值不为空则采用它的值,否则我只在当前行执行该操作。

插入注释时我们先通过 beginning-of-line 将光标移动到行首位置,然后在行首插入 //,接着移动到下一行准备进行插入操作

最后我们来看看主函数 my/quick-comment 的实现。在 interactive 参数部分,我们考虑了两种情况,如果当前region激活了,则采用region的范围给 start 和 end 进行赋值。但是因为 region 的范围是一个前开后闭的区间,所以想要包含区间的后一个字符,需要在此基础上加1

如果当前没有区间,我们通过 prefix-numeric-value 来获取输入的前置数字。然后通过list依次将每个表达式的求值结果赋值到函数参数。

最后我们进行判断,如果 startend 都不是空,则表明用户激活了一个选区,此时就需要调用 my/comment-region 对选区进行处理。否则调用 my/comment-lines arg-lines 进行行注释

总结

本篇我们了解了 interactive 的交互机制,并详细介绍了它接受的三种参数模式:nil 或者空参、提示的字符串以及自定义的list形式。

0

评论 (0)

取消