Elisp 07:命令行程序界面

上一章:缓冲区变换html

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

我之因此学习 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))))))
相关文章
相关标签/搜索