缓冲区和文件

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

Hello world!

虽然我已决定从文件读写开始学习 Elisp,可是我仍是但愿像学习任何一门编程语言那样,从写一个可以输出 Hello world! 的程序开始。小程序

用 Emacs 新建一份文本文件,名曰 hello-world.el。固然,也可使用其余文本编辑器完成此事,可是要保证系统已安装了 Emacs 且可用。数组

hello-world.el 的内容只有一行:网络

(princ "Hello world!\n")

在终端(或命令行窗口)里,将工做目录(当前目录)切换至 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!\n 这个对象映射为显示于终端的对象,姑且这样认为。学习

其次,"Hello world!\n" 是 Elisp 的字符串类型,用于表示一段文本。文本是数据。数据未必是文本。若将 Elisp 做为用于处理文本的语言,字符串就是基本且核心的数据类型。搜索引擎

最后,这个做为示例的 Elisp 程序的最小单位是一个函数调用。我向 princ 函数提供一个字符串类型的值,即可令其工做,且足以构成一个程序。Emacs 里有 Elisp 解释器。Elisp 程序是由 Elisp 解释器解释运行的,相似于计算机程序是由计算机的 CPU 「解释」运行。换言之,Elisp 解释器可以读懂 Elisp 程序,并完成这个程序所描述的工做,例如在终端里输出 Hello world!命令行

习题:在 Hello world 程序中,将字符串 "Hello world!\n" 里的 \n 删除,而后从新运行程序,观察终端的输出有何变化。

定义一个新的函数

"Helo world!\n" 里的 \n 是换行符。计算机键盘上的 Enter 键,在大多数状况下,所起的做用即是 \n。从 Enter 键的角度看待

(princ "Hello world!\n")

就像咱们在与他人在网络上聊天同样,输入 Hello world,而后单击 Enter 键发送。下面,我定义了一个新的函数 princ\',它能够接受我要发送的信息,而后帮我发送:

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

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

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

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

一旦函数有了定义,即可以调用,就像调用 princ 函数那样。下面是基于 princ\' 重写的 Hello world 程序,

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

(princ\' "Hello world!")

缓冲区

假设存在文本文件 foo.txt,其内容为

Hello world!

如何写一个 Elisp 程序,从 foo.txt 读取所有内容并输出到终端?

读取文件,这个操做意味着什么?意味着从计算机辅存(硬盘)中获取数据,放入主存(内存)。缘由在于,计算机 CPU 访问主存的速度远快于辅存。

为了简化文件的读写,Elisp 提供一种数据类型——缓冲区(Buffer)。任何一种编程语言,都有数据类型,例如整型数字,浮点型数字,字符串,数组,列表等,这些类型在 Elisp 语言里也是有的。缓冲区也是一种数据类型。缓冲区对象(也可称为缓冲区实例)本质上是计算机主存里的一段空间。文件的内容被读取后,存入缓冲区实例里,在后者中可进行文件内容的编辑工做。编辑完毕后,缓冲区实例包含的信息能够再存回文件。为了便于描述,在不至于引发误解的前提下,我会将缓冲区实例简称为缓冲区。相似的称谓也适用于 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 便会打开该函数的文档。在文档里,函数的用途、参数以及返回结果皆有详细的说明。一开始,看不懂,也不大要紧,关键是要去看。

相关文章
相关标签/搜索