《elisp 每日一练》—— 同步进程调用

《elisp 每日一练》—— 同步进程调用

Masimaro
2026-10-22 / 0 评论 / 0 阅读 / 正在检测是否收录...

在前面我们了解了定时器的使用机制,但在日常使用中,通常是利用Emacs配合外部命名行工具的方式使用。所以我们今天讨论如何利用Emacs调用外部命令行工具

基础知识

我们今天主要讨论同步的方式调用,虽然它会阻塞Emacs的主线程,但是对于搜索、编译等工作来说,我们可能就是需要等待它们的执行结果。这种情况下使用同步的方式,反而简单一些。

对于同步方式调用,Emacs提供了三个不同的接口。

  • shell-command-to-string
  • process-lines
  • call-process

shell-command-to-string 的签名如下:

(shell-command-to-string COMMAND)

它通过系统的标准shell,执行 COMMAND 参数中输入的命令,并且等待结果,将结果以字符串的形式返回。当然我们也可以传入管道符号,对输出进行过滤,例如

(shell-command-to-string "gcc -c main.c -o main | grep \\"error\\"")

process-lines 与 shell-command-to-string 类似,但是它会自动将程序输出按行分隔为字符串列表返回

(process-lines PROGRAM &rest ARGS)

同时它也支持将程序的参数以多个字符串参数的形式输入。例如 (process-lines "git" "status" "--porcelain")

call-process 是比较底层的函数,它不经过shell,直接创建一个新的进程,调用指定的二进制可执行程序,并将参数以列表形式逐个传入。它的签名如下:

(call-process PROGRAM \&optional INFILE DESTINATION DISPLAY &rest ARGS)
  • PROGRAM 是被调用的程序名称,如果是相对路径,Emacs 会在变量 exec-path 列出的目录中查找,需要注意的是这个参数应该只包含程序名,不能包含命令行参数
  • INFILE 指定子进程的标准输入来源,如果为 nil 则从空设备(/dev/null) 中读取,否则应该是一个文件名,程序从指定文件中读取。
  • DESTINATION 决定程序的输出,可以指定标准输出或者错误输出。
  • DISPLAY 结果显示方式,如果是 nil,仅在函数完全结束后,结果才可见; 非 nil 会根据程序输出,即时刷新屏幕
  • ARGS 是真正需要传递给子进程的参数

DESTINATION 的取值可以有下面几个:

  • 缓冲区对象或名称,此时它会将输出打印到指定缓冲区光标所在位置的后面
  • t 将输出插入到当前缓冲区中
  • nil 丢弃所有输出
  • 0 丢弃输出并立即返回,不等待子进程结束。此时实际上是在异步执行,但是Emacs认为子进程已经执行完毕, Windows 上不支持
  • (:file 文件名) 此时会将输出放入到一个指定文件中

我们在上面说,它可以分别重定向标准输出和标准错误,它可以使用 (标准输出目标 标准错误目标) 这样的形式分别指定。

各位读者如果对操作系统、子进程、shell等概念比较熟悉的话,应该很容易区分这些函数的使用场景。这里就不再深入讨论了。

练习

我们来实现一个简单的 git 信息显示的功能,以此来演示一下这些函数的用法。我们实现以下功能:

  • 通过 shell-command-to-string 调用 git branch --show-current 显示当前branch
  • 通过 process-lines 调用 git status --porcelain 获取当前修改的文件列表, 并显示到临时buffer中
  • 在用户选择上一步中某个文件之后,通过 call-process 调用 git diff, 查看差异
(defun my/git-current-branch()
  (interactive)
  (let* ((output (shell-command-to-string "git branch --show-current"))
         (branch (string-trim output)))
    (if (string-empty-p branch)
        (message "当前目录可能不是一个git 仓库")
      (message "当前分支: %s" branch))))

(defun my/git-status()
  "在临时缓冲区中列出当前仓库修改的文件"
  (interactive)
  (let ((lines (process-lines "git" "status" "--porcelain"))
        (buf (get-buffer-create my/git-status-buffer-name)))
    (with-current-buffer buf
      (let ((inhibit-read-only t))
        (erase-buffer)
        ;;插入提示信息
        (insert (format "Branch: %s\n" (my/git-current-branch)))
        (insert (make-string 60 ?-) "\n")
        (if lines
            (dolist (line lines)
              (insert line "\n"))
          (goto-char (point-min)))))
    (pop-to-buffer buf)))

(defun my/git-status--path-at-point ()
  "从当前行解析出文件路径;若不是文件行则返回 nil。"

  (save-excursion
    (beginning-of-line)
    (let ((line (buffer-substring-no-properties
                 (line-beginning-position) (line-end-position))))
      ;; 跳过头部信息行
      (when (and (>= (length line) 4)
                 (not (string-prefix-p "Branch:" line))
                 (not (string-prefix-p "---" line)))
        (let ((path (substring line 3)))          ; 去掉 "XY " 前缀
          ;; 处理重命名 "OLD -> NEW"
          (when (string-match " -> " path)
            (setq path (substring path (match-end 0))))
          ;; 处理含空格的路径(porcelain 会用引号包裹)
          (when (string-prefix-p "\"" path)
            (setq path (string-trim path "\"" "\"")))
          path)))))

(defun my/git-show-diff-at-point()
  (interactive)
  (let ((path (my/git-status--path-at-point)))
    (if (not path)
        (message "当前行没有文件路径")
      (let ((diff-buff (get-buffer-create my/git-diff-buffer-name)))
        (with-current-buffer diff-buff
          (let ((inhibit-read-only t))
            (erase-buffer)
            (insert (format "diff for: %s" path))
            (insert (make-string 60 ?-) "\n")
            (call-process "git" nil t nil "diff" "HEAD" "--" path) ;; DESTINATION 设置为t,直接显示内容到当前buffer
            (goto-char (point-min))))
        (display-buffer diff-buff)))))

总结

本节我们了解了Emacs中同步方式调用进程的几种方式,它们各有千秋:

  • shell-command-to-string
  • process-lines
  • call-process
0

评论 (0)

取消