完美解决windows下emacs的中文输出乱码问题

之前也发过帖子, 但是没有发现问题的本质, 最近研究了下,发现问题的关键了,
process-coding-system-alist 这个变量大家肯定熟悉,比如可以这样设置某个进程输入输出的编码

(add-to-list 'process-coding-system-alist '("grep" . (utf-8-unix . utf-8-unix)))

但是在windows下会发现没效果

在windows下执行shell类的命令(shell / shell-command / async-shell-command)
他实际上是启动的cmdproxy去执行指定的命令,
而问题的关键就在于 process-coding-system-alist 只会作用于emacs的直接子进程
也就是说除非是用start-process直接启动grep, 否则设置了process-coding-system-alist也没有效果.

矛盾的地方就出现了
1 执行shell的时候默认是调用了cmdproxy -i , 他启动了一个cmd.exe, 是gbk编码
2 通过shell执行其他程序的时候, 特别是现代工具, 输出的都是utf-8编码的文本
3 执行任何shell命令,子进程都是cmdproxy, 用process-coding-system-alist只能设置一个编码
4 如果把cmdproxy设置成gbk,那2里面的程序输出就乱码, 如果设置成utf-8,那1中启动的cmd就乱码

其实linux下也是一样的,只是因为系统统一都是utf-8,所以对这个问题没感知

虽然可以在启动cmd的时候执行chcp,或者直接改系统的编码,但我觉得都不太完美.
最后想了下, 这样配置比较好
1 先统一所有编码为utf-8
2 针对涉及到gbk编码输出的地方做定制处理

(prefer-coding-system 'utf-8-unix)

(defun my-windows-builtin-command-p (cmd)
  "Return non-nil if CMD is a native Windows CMD built-in command."
  (let* ((builtin-commands
          '("assoc" "break" "call" "cd" "chcp" "cls" "color" "copy" "date"
            "del" "dir" "echo" "erase" "md" "mkdir" "move" "path" "pause"
            "ren" "rename" "rd" "rmdir" "set" "start" "time" "type"
            "ver" "vol"))
          ;; 提取命令的第一个单词作为真正的 cmd
          (first (car (split-string cmd "[ \t]+" t))))
    (member (downcase first) builtin-commands)))

(defun my-process-smart-encoding (orig-fun &rest args)
  "Smartly choose GBK or UTF-8 for process based on the command name."
  (let* ((cmd (car args))
         (use-gbk (and (eq system-type 'windows-nt)
                       (my-windows-builtin-command-p cmd)))
         (default-process-coding-system (if use-gbk '(gbk . gbk) '(utf-8 . utf-8)))
         (locale-coding-system (if use-gbk 'gbk 'utf-8)))
    (apply orig-fun args)))

(advice-add 'shell-command :around #'my-process-smart-encoding)
(advice-add 'async-shell-command :around #'my-process-smart-encoding)

这样配置后, 在以下场景都能正确显示中文
1 shell
2 shell-command
3 shell-command-to-string
4 async-shell-command
5 compile里面执行命令的输出
6 eshell


没解决的地方在于如果在shell里面执行输出utf-8的程序还是会有问题(有办法解决吗? 大召唤术 @include-yy ), 虽然用eshell能代替


更新:
看最终解决方案的代码,以下场景都正常了
1 shell(不管是执行内置的dir还是自己写的输出utf-8的程序)
2 shell-command
3 shell-command-to-string
4 async-shell-command
5 compile里面执行命令的输出
6 eshell

4 个赞

最近在研究用 ledger-mode 记账的时候碰到了类似的问题。下面是我的解决方法:

(advice-add 'ledger-do-report :around
            (defalias 'yy/ledger-report-utf8
              (lambda (it cmd)
                (dlet ((coding-system-for-read 'utf-8)
                       (coding-system-for-write 'utf-8))
                  (funcall it cmd)))))

ledger 这个记账工具强制要求 UTF-8,我的 Windows 默认还是用 936 代码页,所以会出问题。ledger-mode 没有处理非 UTF-8 的报告生成,只能加个 advice。、

应该是的,所以要用 default-process-coding-system 或者 coding-system-for-read/write

如果指定了系统级别的 UTF-8 应该就没问题,但 GBK 暂时想不到怎么解决(或者说没有很简单的办法,一种 hack 可能是 comint 发送之前在前面加个 chcp 后面又改过来的命令,大概是由 cmd 变成 chcp XXX; cmd; chcp origin,但这有点太蛋疼了)。

直接去系统的语言设置里把语言改成 utf8

系统级别UTF-8在旧软件上会有兼容性问题,文字直接乱码

编辑:UCRT版本

(prefer-coding-system 'utf-8)
(when (eq system-type 'windows-nt)
  (setq file-name-coding-system 'gbk))
(unless (version< emacs-version "30")
  (setq locale-coding-system 'utf-8) )
(set-default 'process-coding-system-alist
             '(("[pP][lL][iI][nN][kK]" utf-8 . gbk-dos)
               ("[cC][mM][dD][pP][rR][oO][xX][yY]" utf-8 . gbk-dos)))

然后执行 (insert (shell-command-to-string "fastfetch")) ,中文正常

参考上面advice的代码,给comint-send-input也这样设置呢? 感觉可行, 但是我试了下没效果

1 你是指先配置系统用utf-8再用你上面的配置? 我没配置系统utf-8,直接用, shell里面乱码了
2 process-coding-system-alist 里面 配置的两个编码应该一样才对吧,分别是给进程的输入输出

如果进程输出的是gbk, 那给这个进程的输入应该也编码成gbk, 不存在说用不同的编码处理同一个进程的输入输出吧

找到个comint-input-filter-functions, 可以操纵在shell模式下每次执行命令产生的子进程输出的内容,
这下所有场景的中文输出都正常了
自己有特殊需求的话直接改里面的命令 ,针对某个命令修改编码即可


(prefer-coding-system 'utf-8-unix)

(defun my-windows-builtin-command-p (cmd)
  "Return non-nil if CMD is a native Windows CMD built-in command."
  (let* ((builtin-commands
          '("assoc" "break" "call" "cd" "chcp" "cls" "color" "copy" "date"
            "del" "dir" "echo" "erase" "md" "mkdir" "move" "path" "pause"
            "ren" "rename" "rd" "rmdir" "set" "start" "time" "type"
            "ver" "vol"))
          ;; 提取命令的第一个单词作为真正的 cmd
          (first (car (split-string cmd "[ \t]+" t))))
    (member (downcase first) builtin-commands)))

(defun my-process-smart-encoding (orig-fun &rest args)
  "Smartly choose GBK or UTF-8 for process based on the command name."
  (let* ((cmd (car args))
         (use-gbk (and (eq system-type 'windows-nt)
                       (my-windows-builtin-command-p cmd)))
         (default-process-coding-system (if use-gbk '(gbk . gbk) '(utf-8 . utf-8)))
         (locale-coding-system (if use-gbk 'gbk 'utf-8)))
    (apply orig-fun args)))

(advice-add 'shell-command :around #'my-process-smart-encoding)
(advice-add 'async-shell-command :around #'my-process-smart-encoding)

(defvar-local my/comint-coding-utf8 '(utf-8-unix . utf-8-unix))
(defvar-local my/comint-coding-gbk  '(chinese-gbk-dos . chinese-gbk-dos))

(defconst my/comint-cmd-builtins
  '("assoc" "break" "call" "cd" "chcp" "cls" "color" "copy" "date"
    "del" "dir" "echo" "erase" "md" "mkdir" "move" "path" "pause"
    "ren" "rename" "rd" "rmdir" "set" "start" "time" "type"
    "ver" "vol")
  "cmd.exe 内置命令列表,小写匹配。")

(defun my/comint-builtins-gbk-p (input)
  (let* ((cmd (downcase (car (split-string input))))
         (prefix (string-match "\\`\\s-*\\([[:alnum:]]+\\)" cmd)))
    (and prefix (member (match-string 1 cmd) my/comint-cmd-builtins))))

(defun my/comint-set-coding-per-command (input)
  "根据命令行内容切换进程编码."
  (when-let* ((proc (get-buffer-process (current-buffer))))
    (pcase-let ((`(,dec . ,enc)
                 (if (my/comint-builtins-gbk-p input)
                     my/comint-coding-gbk
                   my/comint-coding-utf8)))
      (set-process-coding-system proc dec enc)))
  nil) ; 让 comint 继续正常处理输入

(add-hook 'comint-input-filter-functions #'my/comint-set-coding-per-command)

4 个赞

我也没配置系统UTF-8

编辑:想起来了,我用的UCRT版本,可能是这方面处理的问题

在UCRT版本中,按照你的配置重启emacs之后,星期输出会乱码:

(format-time-string "%m月%d日 %A %H:%M")

11月28日 鏄熸湡浜\224 10:53

再编辑:是这个的问题,与本楼代码无关

.emacs.d/lisp/init-winnt.el at main · kkkykin/.emacs.d

.emacs.d/lisp/init-winnt.el at main · kkkykin/.emacs.d

之前为了编码问题我也糊了一堆

我是通过修改 comint-input-sender 变量,在执行命令前用 set-process-coding-system 改 shell 的编码,就没用 chcp 命令

3 个赞