乐趣区

关于emacs:Elisp-07命令行程序界面

上一章:缓冲区变换

很多程序是有图形界面的,就是日常所见的那些有菜单和按钮的窗口以及对话框之类。在终端里运行的程序,通常也叫命令行程序,它们也有界面,即一组选项和参数。这两种程序,各有千秋,也各有所短。

我之所以学习 Elisp 语言,是因为感觉它的短处适宜编写文本处理程序,例如上一章所写的一个简略的文本处理程序,它能够将文本由 Markdown 格局翻译为 HTML 格局。像这样的文本处理程序,它们的运行通常并不需要图形界面,否则我为何不间接为 Emacs 写一个插件呢?

命令行选项和参数

如同函数能够有参数,命令行程序也能够有一些参数。但凡函数或程序无奈决断的一些因素,可形象为一组参数,交由函数或程序的使用者决断。命令行选项实质上也是命令行参数,只不过它相当于程序的一些性能开关,可用于开启或关闭程序的一些性能,也可用于润饰其余参数。

选项偏向于定性,而参数偏向于定量。当二者对立为程序的参数时,便可使得程序可能明确咱们要用它解决什么问题。有些问题只须要定性的角度去解决。有些问题只须要从量化的角度去解决,因而对二者作辨别,也是有意义的。

在 Linux 零碎里,命令行程序占据了半壁甚至更多的江山。这些命令行程序的选项,通常以 --- 作为前缀,参数则没有前缀,于是在模式上对于程序的使用者而言,二者有着显著的区别。

为一个命令行程序设计界面

在上一章里,我写了个可将文本由 Markdown 格局变换为 HTML 格局的程序。这个程序尽管在性能上远不健全,然而曾经到了要为它设计选项和参数的时候了。

假如这个程序名为 mdc.el,执行这个程序时,它反对 -i-o 两个选项。-i 选项用于指定输出文件名,-o 选项用于指定输入文件名,其中输出文件名和输出文件名都是与选项对应的参数。例如

$ emacs -Q --script mdc.el -i foo.md -o foo.html

假使不向 mdc.el 提供任何选项和参数,或者提供了它不意识的选项和参数,它也不示意任何不称心,仅仅是在终端输入:

 用法:emacs -Q --script mdc.el -i 输出文件 -o 输入文件 

命令行界面的实现

嵌入在 Emacs 外部的 Elisp 解释器,它可能从终端里取得所有的选项和参数,将后果保留为一个列表变量 argv,这是个全局变量。于是,在 mdc.el 程序里,只需拜访这个列表,便能够取得所需的选项和参数。当然,这须要对 argv 进行遍历,而后做一些文本匹配方面的工作。这些工作不再有任何难度,所须要的常识,在后面的章节里曾经使用得很纯熟了吧。

假如 mdc.el 的实现如下:

(defun princ\' (x)
  (princ x)
  (princ "\n"))

(while (not (null argv))
  (princ\' (car argv))
  (setq argv (cdr argv)))

留神,在判断列表是否为空,我始终是应用 (not (null 列表对象)) 的形式,因为我始终不想抵赖 Elisp 语言里非 nil 即为真的规矩。然而,当初感觉,入乡还是随俗吧,抵赖 (not (null 列表对象)) 等价于 列表对象

执行 `mdc.el,

$ emacs -Q --script mdc.el a b foo bar blab blab

后果在终端输入以下信息:

a
b
foo
bar
blab
blab

这意味着遍历 argv 的程序是正确的。假使在遍历过程中减少文本匹配和参数获取性能,便能够失去输出文件名和输入文件名了。例如,

(let (x input output)
  (while argv
    (setq x (car argv))
    (setq argv (cdr argv))
    (cond
     ((string= x "-i")
      (setq input (car argv)))
     ((string= x "-o")
      (setq output (car argv)))))
  (if (or (not input) (not output))
      (princ\'"emacs -Q --script mdc.el -i 输出文件 -o 输入文件")))

在上述代码中,遍历 argv 过程完结后,基于逻辑「或」运算 or,对 inputoutput 变量进行了根本的有效性检测。该检测仅能保障它们曾经失去了赋值,然而所赋之值是否正确,例如在命令行里输出了谬误的文件名,这种状况,程序无奈判断。

性能与界面的联合

上一节最初的那段代码里,检测变量 inputoutput 的有效性的条件表达式只含有逻辑表达式为真时对应的程序分支,另一个分支不存在,当初能够为将 mdc.el 的性能局部作为该分支。

mdc.el 的性能局部,即上一章所实现的缓冲区变换程序,其中可与界面代码进行联合的局部是

(let ((html-buffer (generate-new-buffer "html")))
  (translate-buffer (find-file "foo.md") html-buffer)
  (with-current-buffer html-buffer
    (write-region nil nil "foo.html")))

当初能够将这段代码中的 foo.mdfoo.html 替换为字符串变量 inputoutput,而后将这段代码嵌入到界面代码里,后果为

(let (x input output)
  (while argv
    (setq x (car argv))
    (setq argv (cdr argv))
    (cond
     ((string= x "-i")
      (setq input (car argv)))
     ((string= x "-o")
      (setq output (car argv)))))
  (if (or (not input) (not output))
      (princ\'"emacs -Q --script mdc.el -i 输出文件 -o 输入文件")
    (let ((html-buffer (generate-new-buffer "html")))
      (translate-buffer (find-file input) html-buffer)
      (with-current-buffer html-buffer
        (write-region nil nil output)))))

Hash 表

上一节最初给出的代码有些繁冗,无妨将命令行选项解析局部以及程序性能局部解决进去,封装为函数。

若将命令行解析局部封装为函数,那么该函数的求值后果应该蕴含着 inputoutput 的值。可能蕴含多个值的求值后果,在 Elisp 语言里,只有表。能够是列表,也能够是 Hash 表,后者更适宜存储命令行程序的选项和参数,因为它能够将选项以及它润饰的参数组成键值对构造。

应用 Elisp 函数 make-hash-table 可创立 Hash 表实例,例如

(make-hash-table :test 'equal)

其中,:test 'equal 用于指定应用 equal 函数判断用于从 Hash 表检索数据的键与 Hash 表的键是否相等。我不晓得为什么 string= 不能够。equal 能够比拟两个对象是否雷同,利用范畴要比 =string= 更为宽泛,例如它也能够判断两个列表是否相等。除 equal 外,Elisp 的 Hash 表还有两个可选的键相等测试函数,eqeql,假使不指定测试函数,make-hash-table 默认应用 eql,仅实用于创立以数字作为键的 Hash 表。

将一个符号与 Hash 表绑定,便有了一个 Hash 表变量:

(setq mdc-args (make-hash-table :test 'equal))

Elisp 函数 puthash 可向 Hash 表增加键值对,例如:

(puthash "-i" "foo.md" mdc-args)

Elisp 函数 gethash 可应用键,从 Hash 表里取得与键对应的值,例如

(gethash "-i" mdc-args)

把握了上述函数的用法,便可实现一个解析命令行,并将解析后果存储到 Hash 表的函数了,例如:

(defun mdc-get-args (mdc-args)
  (let (x)
    (while argv
      (setq x (car argv))
      (setq argv (cdr argv))
      (if (string-match "-i\\|-o" x)
          (progn
            (puthash x (car argv) mdc-args)
            (setq argv (cdr argv)))))))

mdc-get-args 函数的用法如下:

(let ((mdc-args (make-hash-table :test 'equal)))
  (mdc-get-args mdc-args)
  (let ((input (gethash "-i" mdc-args))
        (output (gethash "-o" mdc-args)))
    (if (or (not input) (not output))
        (princ\'"emacs -Q --script mdc.el -i 输出文件 -o 输入文件")
      (let ((html-buffer (generate-new-buffer "html")))
        (translate-buffer (find-file input) html-buffer)
        (with-current-buffer html-buffer
          (write-region nil nil output))))))

关联列表

Elisp 的关联列表也可用于存储选项和参数,用法与 Hash 表相似,只是数据拜访效率远低于后者。不过,对于存储命令行选项和参数这样的工作,关联列表足以胜任。

关联列表的每个元素是序对。cons 可结构序对,例如:

(cons "-i" "foo.md")

也能够用 . 语法结构序对,例如:

("-i" . "foo.md")

事实上,Elisp 的列表的实质就是一组级联的序对构造,例如 '(1 2 3 4),在 Elisp 解释器看来,它的真正构造是

(1 . (2 . (3 . (4 . ()))))

car 能够取序对的第一个元素。cdr 则用于取序对的第二个元素。

结构关联列表,能够像一般列表那样应用 cons 函数。例如:

(setq mdc-args '())
(setq mdc-args (cons ("-i" . "foo.md") mdc-args)))

Elisp 函数 assoc 可依据给定的键,可从关联列表里获取第一个同键的序对,例如:

(assoc "-i" mdc-args)

求值后果为 ("-i" . "foo.md")。为什么要强调「第一个」呢?因为关联列表里,容许多个序对有雷同的键。

要取得键对应的值,须要应用 cdr,例如

(cdr (assoc "-i" mdc-args))

至于如何应用关联列表保留命令行选项和参数,这个工作能够作为本章的练习题。

结语

这个教程,更确切地说,是我学习 Elisp 所作的笔记,第一局部可就此闭幕了。至于这个教程,会不会有第二局部,这取解决于我是否遇到了新的文本处理问题。

附录

残缺的 mdc.el 程序代码如下:

(defun princ\' (x)
  (princ x)
  (princ "\n"))

(defun section-n (level name)
  (let ((n (length level)))
    (format "<h%d>%s</h%d>" n name n)))

(defun empty-line (text) "")

(defun paragraph (text)
  (format "<p>%s</p>" text))

(defun translate (text)
  (cond
   ((string-match "^\\(#+\\)[[:blank:]]+\\(.+\\)$" text)
    (section-n (match-string 1 text) (match-string 2 text)))
   ((string-match "^$" text)
    (empty-line text))
   (t (paragraph text))))

(defun current-line ()
  (buffer-substring (line-beginning-position) (line-end-position)))

(defun translate-buffer (source target)
  (with-current-buffer source
    (let (text)
      (while (< (point) (point-max))
        (setq text (translate (current-line)))
        (with-current-buffer target
          (insert text)
          (insert "\n"))
        (forward-line 1)))))

(defun mdc-get-args (mdc-args)
  (let (x)
    (while argv
      (setq x (car argv))
      (setq argv (cdr argv))
      (if (string-match "-i\\|-o" x)
          (progn
            (puthash x (car argv) mdc-args)
            (setq argv (cdr argv)))))))

(let ((mdc-args (make-hash-table :test 'equal)))
  (mdc-get-args mdc-args)
  (let ((input (gethash "-i" mdc-args))
        (output (gethash "-o" mdc-args)))
    (if (or (not input) (not output))
        (princ\'"emacs -Q --script mdc.el -i 输出文件 -o 输入文件")
      (let ((html-buffer (generate-new-buffer "html")))
        (translate-buffer (find-file input) html-buffer)
        (with-current-buffer html-buffer
          (write-region nil nil output))))))
退出移动版