假若将 Elisp 的应用场景固定为文本处理,学习 Elisp,我认为无需像学习其余任何一门编程语言那样亦步亦趋,因此本章直接从文件读写开始入手,经过一些小程序,创建对 Elisp 语言的初步感觉。编程
虽然我已决定从文件读写开始学习 Elisp,可是我仍是但愿对初学者友好一点,毕竟我也是初学者。这种友好应该像学习任何一门编程语言那样,从写一个可以输出 Hello world!
的程序。小程序
用 Emacs 新建一份文本文件,名曰 hello-world.el。固然,也可使用其余文本编辑器完成此事,可是要保证系统已安装了 Emacs 且可用。hello-world.el 的内容只有一行:数组
(princ "Hello world!")
在终端(或命令行窗口)里,将工做目录(当前目录)切换至 hello-world.el 文件所在的目录,而后执行网络
$ emacs -Q --script hello-world.el
终端会随即显示编程语言
Hello world!
从这个 Hellow world 程序里,能学到哪些 Elisp 知识呢?编辑器
首先,princ
是一个函数,确切地说,是 Elisp 的内建函数。什么是函数?在数学里,y=f(x) 是函数,f 可将 x 映射为 y。princ
也是这样的函数,它将 "Hello world!
这个对象映射为显示于终端的对象。函数
其次,"Hello world!"
是 Elisp 的字符串类型,用于表示一段文本。文本是数据。数据未必是文本。若将 Elisp 做为用于处理文本的语言,字符串就是基本且核心的数据类型。学习
最后,这个做为示例的 Elisp 程序的最小单位是一个函数调用。我向 princ
函数提供一个字符串类型的值,即可令其工做,且足以构成一个程序。Emacs 里有 Elisp 解释器。Elisp 程序是由 Elisp 解释器解释运行的,相似于计算机程序是由计算机的 CPU 「解释」运行。换言之,Elisp 解释器可以读懂 Elisp 程序,并完成这个程序所描述的那些工做,例如,在终端里输出 Hello world!
。搜索引擎
hello-world.el 程序虽然能在终端里输出 Hello world!
,可是它的输出很容易令终端有所错乱,例如将个人终端弄成了下面这幅样子:spa
这是由于,princ
函数仅仅是将字符串类型的数据原样输出。若让终端保持有序,输出的文本末尾需附加一个换行符 \n
。虽然修改 princ
的定义完成此事彷佛甚为困难,可是站在它的肩膀上定义一个新的函数完成此事,则甚为简单:
(defun princ\' (x) (princ x) (princ "\n")) (princ\' "Hello world!")
princ\'
即是我定义的新函数。我本来想使用 princ'
这个名字,可是符号 '
已被 Elisp 语言做为一个有特殊语意的符号,所以我不得不使用 Elisp 语言的字符转义符 \
对 '
进行转义,代表后者无特殊含义,仅仅是一个符号。
定义一个函数,遵循的格式是
(defun 函数名 (参数) 函数体)
函数体由一个或一组表达式构成。在 princ\'
的定义中,函数体由两个表达式构成。
运行新的 hello-world.el 程序,结果以下图所示:
假设存在文本文件 foo.txt,其内容为
Hello world!
如何写一个 Elisp 程序,从 foo.txt 读取所有内容并输出到终端?
读取文件,这个操做意味着什么?意味着从计算机辅存(硬盘)中获取数据,放入主存(内存)。缘由在于,计算机 CPU 访问主存的速度远快于辅存。
为了简化文件的读写,Elisp 提供一种数据类型——缓冲区(Buffer)。缓冲区对象(也可称为缓冲区实例)本质上是计算机主存里的一段空间。文件的内容被读取后,存入缓冲区实例里,在后者中可进行文件内容的编辑工做。编辑完毕后,缓冲区实例包含的信息能够再存回文件。为了便于描述,在不至于引发误解的前提下,我会将缓冲区实例简称为缓冲区。相似的称谓也适用于 Elisp 的其余数据类型上,例如数字、字符串、列表、数组、哈希表等。
使用 Elisp 函数 generate-new-buffer
能够建立一个有名字的缓冲区。例如,建立一个名曰 foo
的缓冲区:
(generate-new-buffer "foo")
能建立一个,就能建立多个,可是不管建立了多少个,其中只可能有一个是激活的,亦即当前缓冲区。在读取文件时,从文件获取的数据老是存放在当前缓冲区内。Elisp 函数 buffer-name
能够得到当前缓冲区的名字。如下程序可查看当前缓冲区的名字:
(princ\' (buffer-name))
Elisp 解释器有一个默认的缓冲区,名字叫 *scratch*
。假若没有建立新的缓冲区并将其激活为当前缓冲区,那么上述程序的输出就是 *scratch*
。
Elisp 函数 set-buffer
可将指定的缓冲区设为当前缓冲区。例如,下面这个程序可将上文建立的 foo
缓冲区设为当前缓冲区,并经过输出当前缓冲区的名字它是否为当前缓冲区:
(set-buffer "foo") (princ\' (buffer-name))
set-buffer
的参数除了能够是缓冲区的名字,也能够是缓冲区自己。因为 generate-new-buffer
可以返回它建立的新缓冲区,所以它能够与 set-buffer
函数复合,用于建立一个缓冲区并将其设为当前的缓冲区,例如
(set-buffer (generate-new-buffer "foo"))
将上述代码综合一下,能够放在一个名字叫 foo.el 的文本文件里。foo.el 内容为
(defun princ\' (x) (princ x) (princ "\n")) (princ\' (buffer-name)) (set-buffer (generate-new-buffer "foo")) (princ\' (buffer-name))
在终端里,若以 foo.el 所在目录为工做目录,执行
$ emacs -Q --script ./foo.el
输出为
*scratch* foo
这是我写的第二个 Elisp 程序,感受还不错。别的编程语言里,可能没有缓冲区这种设施。没有对比,就没有伤害。没有伤害,就没有自豪。
对于上一节一开始所提出的问题,事实上并不须要我去为待读取的文件建立一个缓冲区,并将其设为当前缓冲区。Elisp 提供的 find-file
能够替我完成这项工做。例如,
(find-file "foo.txt") (princ\' (buffer-name))
所产生的输出为
foo.txt
这个名曰 foo.txt
的缓冲区,即是 find-file
函数为 foo.txt 文件而建立的。
如何确认 foo.txt 文件里的内容真的被读取后存放到 foo.txt
缓冲区呢?可经过 buffer-string
函数以字符串的形式得到当前缓冲区存储的数据,而后将所得结果显示于终端,例如
(princ\' (buffer-string))
所以,读取 foo.txt 文件里的内容,并将其显示于终端的程序至此便完成了。完整的程序以下:
(defun princ\' (x) (princ x) (princ "\n")) (find-file "foo.txt") (princ\' (buffer-string))
Elisp 代码,只要不破坏名字,它的风格是很随意的。例如 princ\'
函数的定义,写成
(defun princ\' (x) (princ x) (princ "\n"))
也是能够的。
写成
(defun princ\' (x) (princ x)(princ "\n"))
也不是不行。可是,最好不要写怪异的代码。毕竟,那层层括号的嵌套,人生已经很不容易了。
括号不管是内层的,仍是外层的,它们老是成对出现。Lisp 语言最大特色就是,不管是函数的定义,仍是函数的调用,仍是其余的一些表达式,在形式上是由括号构成的嵌套结构。这种结构,Lisp 语言称为列表。
若是使用 Emacs 编写 Elisp 代码或其余 Lisp 方言的代码,要记得安装 paredit 包。我不想浪费时间去讲如何安装和使用这个包。不彻底是由于没人给我发稿费,主要是每一个人都应该会用网络搜索引擎。
不管是用 find-file
函数自动建立的缓冲区,仍是基于 generate-new-buffer
建立的缓冲区,一旦它们被设定为当前缓冲区,即可以使用 Elisp 提供的一些函数,将数据写入其中。
insert
函数可将字符串类型的数据写入当前缓冲区,例如:
(defun princ\' (s) (princ (concat s "\n"))) (find-file "foo.txt") (insert "|||") (princ\' (buffer-string))
输出结果为
|||Hello world!
可见 insert
函数将 |||
插入到了当前缓冲区存储的文本数据的首部。这是由于,当前缓冲区内存在这一个不可见的光标,我将其称为插入点,它对应于 Emacs 图形窗口里不断闪动的那个光标,表示文本的插入点。在使用 find-file
打开一份文件时,插入点会自动定位在文件的开头,坐标为 1。为了理解插入点,就须要将缓冲区想像成一维数组,存放的元素为字符,这个一维数组就像一根很长的纸带那样,插入点的坐标就是插入点位于第几个字符以前。
point
函数能够得到插入点的坐标。例如
(find-file "foo.txt") (princ (point))
输出 1
。
goto-char
函数可将插入点移动到缓冲区内的任何位置。例如,假若将 |||
插入 Hello world!
的两个单词的中间,只需
(find-file "foo.txt") (goto-char 6) (insert "|||")
因为函数 point-min
和 point-max
能够得到缓冲区的起止位置,所以可基于它们将插入点移动到缓冲区的开头或结尾。例如,将 |||
插入到 Hello world!
的尾部:
(find-file "foo.txt") (goto-char (point-max)) (insert "|||")
在此,也许应该提出一个疑问,为什么须要用 point-min
得到缓冲区起始位置?难道这个位置不是 1 吗?由于在缓冲区内部能够建立更小的局部区域,而它也是 Elisp 的一种数据类型,它的名字叫 Narrowing。对于位于 Narrowing 区域的文本,也能够用 point-min
和 point-max
获取起止位置,故而 point-min
得到的结果未必是 1。关于 Narrowing,它在 Emacs 图形界面里较为有用,在使用 Elisp 编写文本处理程序方面,我暂时还没思考出它的应用场景。
Elisp 函数 delete-char
能够删除插入点以后的字符。例如,如下程序将 foo.txt 读入缓冲区后,插入点尚在缓冲区起始位置时,删除它后面 5 个字符,
(find-file "foo.txt") (delete-char 5)
Elisp 也提供了一些与插入点位置无关的缓冲区文本删除函数,其中 delete-region
能够删除落入指定区间的文本。例如,删除缓冲区内第 6 个字符到第 12 个字符之间的字符,被删除的字符包括前者,但不包括后者,
(find-file "foo.txt") (delete-region 6 12)
可使用 (princ\' (buffer-string))
查看缓冲区内容的变化。
如今,已经基本掌握了从文件读取内容到缓冲区,在缓冲区内写入一些内容,接下来,须要考虑的一个问题是,缓冲区的内容该如何保存到文件里。保存方式天然有两种,一种是保存到与当前缓冲区关联的文件,另外一种是保存到其余文件。
save-buffer
可将当前缓冲区保存到与之关联的文件里。例如
(find-file "foo.txt") (goto-char (point-max)) (insert "|||") (save-buffer)
运行上述程序后,可打开 foo.txt 文件查看其内容,是否在 Hello world!
以后多出了 |||
。
write-file
可将当前缓冲区保存到其余文件。例如
(find-file "foo.txt") (goto-char (point-max)) (insert "|||") (write-file "bar.txt")
本章的内容虽然较为简单,可是已经隐约触及了 Emacs 的一些本质。假若理解并熟悉了本文出现的全部 Elisp 已经提供的函数的用法,至关于掌握了 Emacs 最朴素的功能,即打开一份文件,添加一些内容,删除一些内容,而后保存,并不须要一个图形界面帮助咱们完成这些事。
文中所出现的函数,除 princ\'
以外,我将其余全部函数说成 Elisp 提供的,甚至一度想将它们称为 Elisp 标准库里的函数。但事实上,Elisp 只是一门语言,并且也不存在这个标准库。这些函数皆来自于 Emacs 的核心功能——数量庞大的函数集,分散于众多 Elisp 源程序文件。我将这些函数统称为 Elisp 函数。
在 Emacs 里默认的键绑定 C-h f
,而后输入其中的某个函数名,回车,Emacs 便会打开该函数的文档。在文档里,函数的用途、参数以及返回结果皆有详细的说明。一开始,看不懂,也不大要紧,关键是要去看。