如何编写 EmacsScript

发布于 2023-07-04 14:13:43 字数 9374 浏览 44 评论 0

Emacs 作为一款文本编辑器已经为大家所熟知,但是可能比较少人会想到它还能用来像 python,ruby 一样作为一门脚本语言来用。

--script 选项

Emacs 提供了一个 --script 选项可以让 Emacs 运行在 batch 模式下,并运行指定文件中的 elisp 代码。

在 batch 模式下 emacs 完全作为一个 elisp 语言解释器来运行,并在执行完所有的 elisp 代码后直接退出。 在 elisp 代码执行期间,那些输出到 echo area 中的内容会输出到 stdout 或 stderr 中,那些从 minibuffer 读取内容的函数会变成从 stdin 读取内容。

Emacs 为了遵循 shell script 的 shebang 标准,特意将第一行内容中的的 #! 当成注释符号来处理,因此你的 EmacsScript 一般会是这样的:

#+BEGIN_SRC sh
  #!/usr/bin/emacs --script
  (message "Hello world")
#+END_SRC

当然,这种写法并不具有可移植性的,毕竟不是所有的 emacs 路径都是 /usr/bin/emacs. 真正具有可移植性的写法应该是:

#+BEGIN_SRC emacs-lisp
  #!/bin/sh
  ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
  (message "Hello world")
#+END_SRC

处理命令行参数

注意:对于 EmacsScript 来说,参数与选项是截然不同的两个东西。 而且选项不能放在参数最后,否则 Emacs 会提示 emacs: Option '-f' requires an argument

在 EmacsLisp 中,与处理命令行参数有关的常用变量有这么几个:

command-line-args-left

尚未处理的 command-line argument 列表。假设有这么一个 /tmp/test1.el 的脚本:

#+BEGIN_SRC emacs-lisp :tangle "/tmp/test1.el" :tangle-mode 755
  #!/bin/sh
  ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
  (princ (format "command-line-args-left=%s" command-line-args-left))
#+END_SRC

那么执行该脚本的结果是:

#+BEGIN_SRC sh :exports both :results org
  /tmp/test1.el a b c
#+END_SRC

#+RESULTS:
#+BEGIN_SRC org
command-line-args-left=(a b c)
#+END_SRC

command-line-args

传递给 Emacs 的完整 command-line argument 列表,但是这个变量一般很少用,但它可以用于获取 script 脚本本身的名字。假设有这么一个 /tmp/test2.el 的脚本:

#+BEGIN_SRC emacs-lisp :tangle "/tmp/test2.el" :tangle-mode 755
  #!/bin/sh
  ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
  (princ (format "command-line-args=%s\n" command-line-args))
  (princ (format "$0=%s" (nth 2 command-line-args)))
#+END_SRC

那么执行该脚本的结果是:

#+BEGIN_SRC sh :exports both :results org
  /tmp/test2.el a b c
#+END_SRC

#+RESULTS:
#+BEGIN_SRC org
command-line-args=(emacs -scriptload /tmp/test2.el a b c)
$0=/tmp/test2.el
#+END_SRC

可以用 (nth 2 command-line-args) 来获取脚本名称。

command-switch-alist

Emacs 在执行完 EmacsScript 中的语句之后,会检查 command-line-args-left= 中是否包含有以 =-= 开头的选项,并在该变量中查找并运行对应的 handler-function,每处理完一个选项之后,就将该参数从 command-line-args-left 中删除掉。

该变量是元素为 (option . handler-function) 的 alist,这里

  • option 为 command-line argument 中的 -option 参数带-,为字符串格式
  • handler-function 为相应的处理函数名,它接收 option 为唯一参数

若 command line option 后还带了其他参数,则在 handler-function 中可以通过变量command-line-args-left 来获取剩余的命令行参数。例如有这么一个脚本:

#+BEGIN_SRC emacs-lisp :tangle "/tmp/test3.el" :tangle-mode 755
  #!/bin/sh
  ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
  (defun print-option-value (option)
    (princ (format "command-line-args-left=%s\n" command-line-args-left))
    (princ (format "value of %s is %s\n" option (car command-line-args-left)))
    (pop command-line-args-left))

  (add-to-list 'command-switch-alist '("-f" . print-option-value))

#+END_SRC

那么执行该脚本的结果是:

#+BEGIN_SRC sh :exports both :results org
  /tmp/test3.el a -f filename b c
#+END_SRC

#+RESULTS:
#+BEGIN_SRC org
command-line-args-left=(filename b c)
value of -f is filename
#+END_SRC

command-line-functions

该变量是一系列函数的列表,这些函数用来处理无法识别的 command-line 参数。

每次处理一个没有特殊意义的 command line argument 时,该变量中的函数都会被依次调用, 直到有一个函数返回非 nil 的值

  • 这些函数被调用时并不传递参数,但在这些函数内可以通过变量 argi 获取当前待处理的 command-line argument,可以通过变量 command-line-args-left 获取尚未被处理的 command line arguments
  • 若某函数除了当前待处理的函数,同时也把后面的参数給处理过了,则需要把后面那些被处理过的参数从 command-line-args-left 中删除
  • 若某函数已经处理了当前代处理的参数,则一定记得返回非 nil 值,若所有的函数都返回 nil,该参数会被认为是 Emacs 要打开的文件名称

例如有这么一个脚本:

#+BEGIN_SRC emacs-lisp :tangle "/tmp/test4.el" :tangle-mode 755
  #!/bin/sh
  ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
  (defun print-option ()
    (princ (format "command-line-args-left=%s\n" command-line-args-left))
    (princ (format "option is %s\n" argi)))

  (add-to-list 'command-line-functions  #'print-option)
#+END_SRC

那么执行该脚本的结果是:

#+BEGIN_SRC sh :exports both :results org
  /tmp/test4.el a -p filename b
#+END_SRC

#+RESULTS:
#+BEGIN_SRC org
command-line-args-left=(-p filename b)
option is a
command-line-args-left=(filename b)
option is -p
command-line-args-left=(b)
option is filename
command-line-args-left=nil
option is b
#+END_SRC

我们可以在脚本中同时使用 command-switch-alistcommand-line-functions,它们的调用顺序是按照传递给EmacsScript的参数顺序来进行的。例如有这么一个脚本:

#+BEGIN_SRC emacs-lisp :tangle "/tmp/test5.el" :tangle-mode 755
  #!/bin/sh
  ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
  (defun print-option ()
    (princ (format "option is %s\n" argi)))
  (add-to-list 'command-line-functions  #'print-option)

  (defun print-option-value (option)
    (princ (format "value of option %s is %s\n" option (pop command-line-args-left))))
  (add-to-list 'command-switch-alist '("-f" . print-option-value))
#+END_SRC

那么执行该脚本的结果会是:

#+BEGIN_SRC sh :exports both :results org
  /tmp/test5.el a -f f -p p
#+END_SRC

#+RESULTS:
#+BEGIN_SRC org
command-line-args-left=(-f f -p p)
option is a
value of option -f is f
command-line-args-left=(p)
option is -p
command-line-args-left=nil
option is p
#+END_SRC

EmacsScript 的执行顺序

从上面命令行参数的说明中,大致可以推断出 EmacsScript 的执行顺序为:

  1. Emacs 读取并执行 EmacsScript 中的内容
  2. Emacs遍历 command-line-args-left 中的参数,对于 command-switch-alist 中的参数调用对应的函数,对于不在 command-switch-alist 中的参数依次调用 command-line-functions 中的函数
  3. 倘若 command-line-functiions 中没有定义函数,或者某参数在依次调用 command-line-functions 中的函数后所有函数都返回nil的话,那么该参数交由 emacs本身处理。

标准输出,标准错误与标准输入

在 interactive 模式下编写 EmacsLisp 函数时,我们习惯于用 message 函数来输出内容,然而在 batch 模式下,我们就不能再用 message 来输出内容了,因为 message 实际上会把内容输出到 stderr 上。

作为替代,若是要想将内容输出到 stdout,你需要使用 printprin1princ 等这一系列的函数来输出内容。 然而这一类的函数本身并没有格式化输出的功能,因此你一般还需要用 format 函数预先将要输出的内容格式化成字符串。

那么如何从标准输入读取内容呢? 只需要跟 interactive 模式下一样使用 read-xxx 系列函数就行了。 在batch模式下,原先从 minbuffer 读取内容的函数会改成从 stdin 中读取内容。

唯一需要注意的是:Emacs24 及其之前的版本的 Emacs 在 batch 模式下用 read-passwd 从标准输出读取密码时,会在终端上显示出密码的内容。 Emacs25 版本的 read-passwd 则解决了这个问题。

获取外部命令的运行结果

在 shell 编程中,可以使用 $() 来捕获命令的运行结果, EmacsScript 不支持这种语法,但可以通过函数 shell-command-to-string 来代替。 比如假设有这么一个脚本:

#+BEGIN_SRC emacs-lisp :tangle "/tmp/test6.el" :tangle-mode 755
  #!/bin/sh
  ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
  (princ "捕获ls的内容:\n")
  (princ (shell-command-to-string "ls -l"))
#+END_SRC

那么执行该脚本的结果是:

#+BEGIN_SRC sh :exports both :results org
    /tmp/test6.el
#+END_SRC

#+RESULTS:
#+BEGIN_SRC org

捕获 ls 的内容:

总用量 60
-rw-rw-r-- 1 lujun9972 lujun9972  9213 11月 22 22:29 Emacs查看日志常用命令.org
-rw-rw-r-- 1 lujun9972 lujun9972 10881 11月 22 22:29 Emacs中那些不常用的行操作命令.org
-rw-rw-r-- 1 lujun9972 lujun9972  5507 11月 22 22:29 Emacs作为图片浏览器.org
-rw-rw-r-- 1 lujun9972 lujun9972  3226 11月 22 22:29 tramp的一般用法.org
-rw-rw-r-- 1 lujun9972 lujun9972  2522 11月 22 22:29 判断Emacs是否在图形环境中的正确方法.org
-rw-rw-r-- 1 lujun9972 lujun9972  9725 11月 28 20:41 如何编写EmacsScript.org
-rw-rw-r-- 1 lujun9972 lujun9972  1524 11月 22 22:29 使用Emacs ediff作为git diff工具.org
-rw-rw-r-- 1 lujun9972 lujun9972  1791 11月 22 22:29 使用Emacs ediff作为git merge工具.org
#+END_SRC

当然,如果你愿意,完全可以使用底层的 call-processstart-process,这两个函数能让你更细致地控制子进程。

加速 EmacsScript 的启动过程

--script 选项会阻止 Emacs 启动时加载用户的初始化文件,但是依然会加载 global site 初始化文件。若因此而拖慢了 EmacsScript 的启动速度,那么可以考虑添加 --quick 选项来明确禁止 global site 的初始化。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

如日中天

暂无简介

文章
评论
641 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文