前言

Emacs中 使用 pass 编辑密码的时候碰到如下错误

1
/opt/homebrew/bin/pass: line 503: /opt/homebrew/Cellar/emacs-plus\@30/30.2/bin/emacsclient: No such file or directory

先看结论,只需要把如下代码加到 Emacs 的配置中即可

1
(setq with-editor-emacsclient-executable "emacsclient")

过程

根据错误提示,打开 /opt/homebrew/bin/pass 并定位到 503 行,内容如下

1
${EDITOR:-vi} "$tmp_file"

优先使用 Editor 变量,没有就回退到 vi

从提示中可以看出 Editor 的值是 /opt/homebrew/Cellar/emacs-plus\@30/30.2/bin/emacsclient 这个路径也确实是存在的,没看出来有什么问题。

那就从 pass 中入手,在使用 pass 按下 e 进行编辑的时候调用的是 pass-edit

1
2
3
4
5
6
7
(defun pass-edit ()
  "Edit the entry at point."
  (interactive)
  (pass--with-closest-entry entry
                            (when (or pass-suppress-confirmations
                                      (yes-or-no-p (format "Do you want edit the entry %s? " entry)))
                              (password-store-edit entry))))

这里实际调用的是 password-store-edit

1
2
3
4
(defun password-store-edit (entry)
  "Edit password for ENTRY."
  (interactive (list (password-store--completing-read t)))
  (password-store--run-edit entry))

password-store-edit 又调用了 password-store--run-edit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(defun password-store--run-edit (entry)
  (password-store--run-async "edit"
                             entry))

(defun password-store--run-async (&rest args)
  "Run pass asynchronously with ARGS.

Nil arguments are ignored.  Output is discarded."
  (let ((args (mapcar #'shell-quote-argument args)))
    (with-editor-async-shell-command
     (mapconcat 'identity
                (cons password-store-executable
                      (delq nil args)) " "))))

调了一堆函数发现调用了 with-editor-async-shell-command

1
2
3
4
5
6
(defun with-editor-async-shell-command
    (command &optional output-buffer error-buffer envvar)
  (interactive (with-editor-shell-command-read-args "Async shell command: " t))
  (let ((with-editor--envvar envvar))
    (with-editor
      (async-shell-command command output-buffer error-buffer))))

with-editor 是个宏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defmacro with-editor (&rest body)
  "Use the Emacsclient as $EDITOR while evaluating BODY.
Modify the `process-environment' for processes started in BODY,
instructing them to use the Emacsclient as $EDITOR.  If optional
ENVVAR is a literal string then bind that environment variable
instead.
\n(fn [ENVVAR] BODY...)"
  (declare (indent defun) (debug (body)))
  `(let ((with-editor--envvar ,(if (stringp (car body))
                                   (pop body)
                                 '(or with-editor--envvar "EDITOR")))
         (process-environment process-environment))
     (with-editor--setup)
     ,@body))

里面调用了 with-editor--setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
(defun with-editor--setup ()
  (if (or (not with-editor-emacsclient-executable)
          (file-remote-p default-directory))
      (push (concat with-editor--envvar "=" with-editor-sleeping-editor)
            process-environment)
    ;; Make sure server-use-tcp's value is valid.
    (unless (featurep 'make-network-process '(:family local))
      (setq server-use-tcp t))
    ;; Make sure the server is running.
    (unless (process-live-p server-process)
      (when (server-running-p server-name)
        (setq server-name (format "server%s" (emacs-pid)))
        (when (server-running-p server-name)
          (server-force-delete server-name)))
      (server-start))
    ;; Tell $EDITOR to use the Emacsclient.
    (push (concat with-editor--envvar "="
                  ;; Quoting is the right thing to do.  Applications that
                  ;; fail because of that, are the ones that need fixing,
                  ;; e.g., by using 'eval "$EDITOR" file'.  See #121.
                  (shell-quote-argument
                   ;; If users set the executable manually, they might
                   ;; begin the path with "~", which would get quoted.
                   (if (string-prefix-p "~" with-editor-emacsclient-executable)
                       (concat (expand-file-name "~")
                               (substring with-editor-emacsclient-executable 1))
                     with-editor-emacsclient-executable))
                  ;; Tell the process where the server file is.
                  (and (not server-use-tcp)
                       (concat " --socket-name="
                               (shell-quote-argument
                                (expand-file-name server-name
                                                  server-socket-dir)))))
          process-environment)
    (when server-use-tcp
      (push (concat "EMACS_SERVER_FILE="
                    (expand-file-name server-name server-auth-dir))
            process-environment))
    ;; As last resort fallback to the sleeping editor.
    (push (concat "ALTERNATE_EDITOR=" with-editor-sleeping-editor)
          process-environment)))

这里最重要的参数出现了 with-editor-emacsclient-executable 来看下它是怎么赋值的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
(defcustom with-editor-emacsclient-executable (with-editor-locate-emacsclient)
  "The Emacsclient executable used by the `with-editor' macro."
  :group 'with-editor
  :type '(choice (string :tag "Executable")
                 (const  :tag "Don't use Emacsclient" nil)))

(defun with-editor-locate-emacsclient ()
  "Search for a suitable Emacsclient executable."
  (or (with-editor-locate-emacsclient-1
       (with-editor-emacsclient-path)
       (length (split-string emacs-version "\\.")))
      (prog1 nil (display-warning 'with-editor "\
Cannot determine a suitable Emacsclient

Determining an Emacsclient executable suitable for the
current Emacs instance failed.  For more information
please see https://github.com/magit/magit/wiki/Emacsclient."))))

(defun with-editor-locate-emacsclient-1 (path depth)
  (let* ((version-lst (cl-subseq (split-string emacs-version "\\.") 0 depth))
         (version-reg (concat "^" (string-join version-lst "\\."))))
    (or (locate-file
         (cond ((equal (downcase invocation-name) "remacs")
                "remacsclient")
               ((bound-and-true-p emacsclient-program-name))
               ("emacsclient"))
         path
         (mapcan (lambda (v) (cl-mapcar (lambda (e) (concat v e)) exec-suffixes))
                 (nconc (and (boundp 'debian-emacs-flavor)
                             (list (format ".%s" debian-emacs-flavor)))
                        (cl-mapcon (lambda (v)
                                     (setq v (string-join (reverse v) "."))
                                     (list v
                                           (concat "-" v)
                                           (concat ".emacs" v)))
                                   (reverse version-lst))
                        (cons "" with-editor-emacsclient-program-suffixes)))
         (lambda (exec)
           (ignore-errors
             (string-match-p version-reg
                             (with-editor-emacsclient-version exec)))))
        (and (> depth 1)
             (with-editor-locate-emacsclient-1 path (1- depth))))))

到这里基本就知道 Editor 的值是怎么来的。

它是直接定位到 Emacs 的程序所在的路径,然后找到 /bin 目录,查找 emacsclient

一般情况下也不会有问题,但是在 Mac 下会有版本 emacs-plus@30 这个 @ 会被转义,这时候就会坏事,所以我们直接把 with-editor-emacsclient-executable 改为 emacsclient 就行了,它会从环境变量中获取。