在前面我们了解了定时器的使用机制,但在日常使用中,通常是利用Emacs配合外部命名行工具的方式使用。所以我们今天讨论如何利用Emacs调用外部命令行工具
基础知识
我们今天主要讨论同步的方式调用,虽然它会阻塞Emacs的主线程,但是对于搜索、编译等工作来说,我们可能就是需要等待它们的执行结果。这种情况下使用同步的方式,反而简单一些。
对于同步方式调用,Emacs提供了三个不同的接口。
shell-command-to-stringprocess-linescall-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-stringprocess-linescall-process
评论 (0)