如何编写EmacsScript?

Posted Emacs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何编写EmacsScript?相关的知识,希望对你有一定的参考价值。

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

注意: EmacsScript的坑超级多,强烈推荐阅读这篇文章: (这里是它的中文版)。

--script选项

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

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

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

#!/usr/bin/emacs --script
(message "Hello world")

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

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

处理命令行参数

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

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

  • command-line-args-left

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

  • #!/bin/sh
    ":"; exec emacs --script "$0" "$@" # -*- mode: emacs-lisp; lexical-binding: t; -*-
    (princ (format "command-line-args-left=%s" command-line-args-left))

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

    /tmp/test1.el a b c
    
    command-line-args-left=(a b c)
  • command-line-args

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

  • #!/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)))

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

    /tmp/test2.el a b c
    
    command-line-args=(emacs -scriptload /tmp/test2.el a b c)
    $0=/tmp/test2.el

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

  • command-switch-alist

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

    该变量是元素为`(option . handler-function)’的alist。 这里若command line option后还带了其他参数,则在handler-function中可以通过变量`command-line-args-left’来获取剩余的命令行参数。

  • 例如有这么一个脚本:

    #!/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))

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

    /tmp/test3.el a -f filename b c
    
    command-line-args-left=(filename b c)
    value of -f is filename
    • option为command-line argument中的`-option’参数(带-),为字符串格式

    • handler-function为相应的处理函数名,它接收option为唯一参数

  • 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要打开的文件名称

  • 例如有这么一个脚本:

    #!/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)

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

    /tmp/test4.el a -p filename b
    
    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

    我们可以在脚本中同时使用 command-switch-alist 与 command-line-functions。它们的调用顺序是按照传递给EmacsScript的参数顺序来进行的。

    例如有这么一个脚本:

    #!/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))

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

    /tmp/test5.el a -f f -p p
    
    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

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,你需要使用  等这一系列的函数来输出内容.。然而这一类的函数本身并没有格式化输出的功能,因此你一般还需要用 format 函数预先将要输出的内容格式化成字符串。

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

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

获取外部命令的运行结果

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

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

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

/tmp/test6.el

捕获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

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

加速EmacsScript的启动过程

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


本期编辑:aborn & geekplux

欢迎投稿

投稿方式有以下两种(推荐第一种):

  1. 向该项目提交 Pull Request (投稿的文章请放在 tougao/这个目录下);

  2. 发邮件到投稿邮箱:860202632@qq.com; geekplux@gmail.com; aborn@aborn.me (建议以 org 文档附件形式投稿)。

投稿时请:

  • 注明你的昵称,或 ID,总之是你想显示的作者名;

  • 如投稿的文章已经再其他地方发表,可以附上原文链接。

以上是关于如何编写EmacsScript?的主要内容,如果未能解决你的问题,请参考以下文章

如何测量代码片段的调用次数和经过时间

我们可以在活动 xml 中编写 UI 以及在片段 xm 中编写 UI 吗?

如何在片段 kotlin 中编写 onBackPressed()

使用 Python 代码片段编写 LaTeX 文档

创建自己的代码片段(CodeSnippet)

编写代码片段的更简洁的方法