走在 Elisp 的歧路上 · 01 缓冲区和文件

假若将 Elisp 的应用场景固定为文本处理,学习 Elisp,我认为无需像学习其余任何一门编程语言那样亦步亦趋,因此本章直接从文件读写开始入手,经过一些小程序,创建对 Elisp 语言的初步感觉。编程

Hello world!

虽然我已决定从文件读写开始学习 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

Hello world!

这是由于,princ 函数仅仅是将字符串类型的数据原样输出。若让终端保持有序,输出的文本末尾需附加一个换行符 \n。虽然修改 princ 的定义完成此事彷佛甚为困难,可是站在它的肩膀上定义一个新的函数完成此事,则甚为简单:

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

(princ\' "Hello world!")

princ\' 即是我定义的新函数。我本来想使用 princ' 这个名字,可是符号 ' 已被 Elisp 语言做为一个有特殊语意的符号,所以我不得不使用 Elisp 语言的字符转义符 \' 进行转义,代表后者无特殊含义,仅仅是一个符号。

定义一个函数,遵循的格式是

(defun 函数名 (参数)
  函数体)

函数体由一个或一组表达式构成。在 princ\' 的定义中,函数体由两个表达式构成。

运行新的 hello-world.el 程序,结果以下图所示:

princ'

缓冲区

假设存在文本文件 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-minpoint-max 能够得到缓冲区的起止位置,所以可基于它们将插入点移动到缓冲区的开头或结尾。例如,将 ||| 插入到 Hello world! 的尾部:

(find-file "foo.txt")
(goto-char (point-max))
(insert "|||")

在此,也许应该提出一个疑问,为什么须要用 point-min 得到缓冲区起始位置?难道这个位置不是 1 吗?由于在缓冲区内部能够建立更小的局部区域,而它也是 Elisp 的一种数据类型,它的名字叫 Narrowing。对于位于 Narrowing 区域的文本,也能够用 point-minpoint-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 便会打开该函数的文档。在文档里,函数的用途、参数以及返回结果皆有详细的说明。一开始,看不懂,也不大要紧,关键是要去看。

相关文章
相关标签/搜索