Elisp 01:Hello world!

garfileo

上一章:不知多久能学会 Elisp

倘若将 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 便会打开该函数的文档。在文档里,函数的用途、参数以及返回结果皆有详细的说明。一开始,看不懂,也不大要紧,关键是要去看。

下一章:文本解析

阅读 2.2k

5.9k 声望
0 粉丝
0 条评论
5.9k 声望
1.9k 粉丝
宣传栏