EMACS-DOCUMENT

=============>随便,谢谢

无痛使用 Emacs 运行 shell 命令

drake-prf-shell-command.png

Emacs 作为仿真终端

Emacs可以成为一个强大的终端仿真器

它可以派生出一个交互式shell (shell-mode和term-mode)并执行单个shell命令。

这篇文章将关注在单个shell命令上。

关于交互式shell,请参阅下一篇文章

在这篇文章中,“command”这个词有两个意思:

  • shell命令,我们将它用 shell命令 来表示
  • 交互式Emacs函数,我们将用 命令 来表示

Emacs 单shell命令的API

主要命令有:

函数 执行方式 返回值 产生的 buffers
shell-command-to-string (command) 同步 stdout的内容  
shell-command (command & output-buffer error-buffer) 同步 命令返回值 stdout 与 stderr
async-shell-command (command & output-buffer error-buffer) 同步 包含输出buffer的window stdout 与 stderr

正如你所见,这三个函数接受的参数数量有限。

其行为更多的是通过各种变量来隐式设置的。

以下是最常用的一些变量:

var description
default-directory 启动shell的位置
explicit-shell-file-name / shell-file-name 执行的 shell 解释器 (例如. bash …)
shell-command-switch 使用shell来运行命令的参数

通过重新定义这些变量的值我们可以改变函数的行为。

具体地说:

  • 通过将 default-directory 设置为位于远程服务器上(通过TRAMP)的路径,我们可以在此远程服务器上打开shell
  • 通过改变其他变量,我们可以从非默认支持的解释器中衍生出一个shell (fish, zsh, ksh…)

为了避免总是重复定义这些值,我们可以在执行期间对这些变量用let进行帮顶(动态绑定)。

(defun my/uname-local ()
  (interactive)
  (let ((default-directory "~")
        (explicit-shell-file-name "fish"))
    (message "Launching \"uname -a\" locally")
    (message (shell-command-to-string "uname -a"))))

(defun my/uname-on-raspi ()
  (interactive)
  (let ((default-directory "/ssh:pi@raspi:/~")
        (explicit-shell-file-name "bash"))
    (message "Launching \"uname -a\" on Raspberry Pi")
    (message (shell-command-to-string "uname -a"))))

这很酷,但我不太喜欢API的隐藏部分。

将隐式参数显式化

值得庆幸的是,将隐式参数映射到显式参数并定义辅助函数非常简单。

因为我们希望这些参数是可选的,所以将它们定义为关键字更方便。

;; ------------------------------------------------------------------------
;; VARS

(defvar prf-default-remote-shell-interpreter "/bin/bash")
(defvar prf-default-remote-shell-interpreter-args '("-c" "export EMACS=; export TERM=dumb; stty echo; bash"))
(defvar prf-default-remote-shell-interpreter-command-swith "-c")


;; ------------------------------------------------------------------------
;; HELPER

(defun with-shell-interpreter--normalize-path (path)
  "Normalize path, converting \\ into /."
  (subst-char-in-string ?\\ ?/ path))


(defun with-shell-interpreter--get-interpreter-name (interpreter)
  (file-name-nondirectory interpreter))


;; ------------------------------------------------------------------------
;; MAIN

(cl-defun eval-with-shell-interpreter (&key form path
                                            interpreter interpreter-args command-switch)
  (unless path
    (setq path default-directory))
  (unless (file-exists-p path)
    (error "Path %s doesn't seem to exist" path))

  (let* ((func
          (if (functionp form) form
            ;; Try to use the "current" lexical/dynamic mode for `form'.
            (eval `(lambda () ,form) lexical-binding)))
         (is-remote (file-remote-p path))
         (interpreter (or interpreter
                          (if is-remote
                              prf-default-remote-shell-interpreter
                            shell-file-name)))
         (interpreter (with-shell-interpreter--normalize-path interpreter))
         (interpreter-name (with-shell-interpreter--get-interpreter-name interpreter))
         (explicit-interpreter-args-var (intern (concat "explicit-" interpreter-name "-args")))
         (interpreter-args (or interpreter-args (when is-remote prf-default-remote-shell-interpreter-args)))
         (command-switch (or command-switch
                             (if is-remote
                                 prf-default-remote-shell-interpreter-command-swith
                               shell-command-switch)))
         (default-directory path)
         (shell-file-name interpreter)
         (explicit-shell-file-name interpreter)
         (shell-command-switch command-switch))
    (cl-progv
        (list explicit-interpreter-args-var)
        (list (or interpreter-args
                  (when (boundp explicit-interpreter-args-var)
                    (symbol-value explicit-interpreter-args-var))))
      (funcall func))))

注意,我们定义了 prf-default-remote-shell-interpreter 变量,它有一个不同于本地 shell-file-name 的缺省解释器。

这允许我们重写 my/uname-local 例子为:

(defun my/uname-local ()
  (interactive)
  (eval-with-shell-interpreter
   :path "~"
   :interpreter "fish"
   :form
   '(progn
      (message "Launching \"uname -a\" locally")
      (message (shell-command-to-string "uname -a")))))

这很酷,但必须引用 :form 并将其封装在一个 progn 中是有点麻烦。

一个宏包装可以解决这个问题:

(defmacro with-shell-interpreter (&rest args)
  (declare (indent 1) (debug t))
  `(eval-with-shell-interpreter
    :form (lambda () ,(cons 'progn (with-shell-interpreter--plist-get args :form)))
    :path ,(plist-get args :path)
    :interpreter ,(plist-get args :interpreter)
    :interpreter-args ,(plist-get args :interpreter-args)
    :command-switch ,(plist-get args :command-switch)))

(defun with-shell-interpreter--plist-get (plist prop)
  "Like `plist-get' except allows value to be multiple elements."
  (unless (null plist)
    (cl-loop with passed = nil
             for e in plist
             until (and passed
                        (keywordp e)
                        (not (eq e prop)))
             if (and passed
                     (not (keywordp e)))
             collect e
             else if (not passed)
             do (setq passed 't))))

这让我们可以这样重写它:

(defun my/uname-local ()
  (interactive)
  (with-shell-interpreter
   :path "~"
   :interpreter "fish"
   :form
   (message "Launching "uname -a" locally")
   (message (shell-command-to-string "uname -a"))))

with-shell-interpreter 的代码可以在package with-shell-interpreter 中找到。

进一步优化

让我们派生出出自己的版本的 shell-command-to-string.

(cl-defun prf-shell-command-to-string (command &key path interpreter command-switch)
  "Call CMD w/ `shell-command-to-string' on host and location described by PATH"
  (with-shell-interpreter
   :form (shell-command-to-string command)
   :path path
   :interpreter interpreter
   :command-switch command-switch))

我们的示例命令可以变成:

(defun my/uname-local ()
  (interactive)
  (message "Launching \"uname -a\" locally")
  (prf/shell-command-to-string "uname -a"
                               :path "~"
                               :interpreter "fish"))

可以在prf-shell-command包中找到 prf-shell-command-string 的代码。