上一章:命令行程序界面html
在上一章的结语里,我说这个教程是否会有第二部分,取决于我是否遇到了新的文本处理问题。结果很快如愿以偿。正则表达式
下面是 XML 文件 foo.xml 的内容:编程
<bib> <title>foo</title> </bib> <attachment> <resource src="files/foo.html"/> <title>foo</title> </attachment> <bib> <title>bar</title> </bib> <attachment> <resource src="files/bar.html"/> <title>bar</title> </attachment>
我须要从 <attachment>...<attachment>
块里提取如下条目:segmentfault
<resource src="files/foo.html"/> <title>foo</title> <resource src="files/bar.html"/> <title>bar</title>
如今假设已用 Elisp 函数 find-file
将 foo.xml 文件内容所有载入了缓冲区,即编程语言
(find-file "foo.xml")
而后发现,以前学过的 Elisp 知识几乎派不上用场了。以前学过的文本匹配和提取方法仅适用于单行文本,而如今面临的问题是多行文本的匹配和提取,即从当前缓冲区内提取函数
<attachment> <resource src="files/foo.html"/> <title>foo</title> </attachment> <attachment> <resource src="files/bar.html"/> <title>bar</title> </attachment>
莫说提取,仅仅是如何匹配 <attachment>...</attachment>
块就已经很差解决了。例如,如下程序命令行
(find-file "foo.xml") (let ((x (buffer-string))) (string-match "<attachment>\\(.+\\)</attachment>" x) (princ\' (match-string 1 x)))
输出 nil
,意味着 string-match
在当前缓冲区内容中匹配 <attachment>...</attachment>
块失败。致使失败的缘由也很简单,由于正则表达式 .
虽然能够匹配任意一个字符,但它不包括换行符。code
实现文本的跨行匹配,并不是不可行,可是须要比如今更多的 Elisp 的正则表达式知识 1。可是,我想说的是,对于上述问题,现有的 Elisp 知识其实也是足够用,只须要转换一下思路。xml
文本为何是多行的?是由于在输入文本的时候,每一行末尾由人或程序添加了换行符。假若能将这些换行符临时替换为一个很特殊的记号,那么多行文本就变成了单行文本。在文本匹配和处理结束后,再将这个特殊记号再替换为换行符,单行文本又复原为多行文本。此为瞒天过海之计。htm
将当前缓冲区内全部的换行符替换为一个特殊记号,可基于第 6 章所讲的缓冲区变换方法予以实现。本章给出一个更快捷的方法。Elisp 函数 replace-string
可在当前缓冲区内使用指定字串替换全部目标字串,例如
(let ((x "") (y "") (one-line (generate-new-buffer "one-line"))) (find-file "foo.xml") (setq x (buffer-string)) (with-current-buffer one-line (insert x) (goto-char (point-min)) (replace-string "\n" "<linebreak/>") (setq y (buffer-string))) (princ\' y))
执行上述程序后,在新建立的缓冲区 one-line 里存放的即是 foo.xml 缓冲区的单行化结果。假若将上述代码里的 (princ\' y)
语句替换为
(string-match "<attachment>\\(.+\\)</attachment>" y) (princ\' (match-string 1 y))
即可提取 <attachment>...</attachment>
块,尽管提取的结果是错的。
为了更方便观察错误,须要构造一个简单的例子:
(setq x "abcabcabc") (string-match "a\\(.+\\)a" x) (princ\' (match-string 1 x))
这个例子会输出什么呢?虽然我很指望它输出 bc
,但事实上它输出的是 bcabc
。这是由于 +
是很贪婪的,它老是但愿能匹配最长的结果,而不是最短的。*
也是如此。在 Elisp 的正则表达式里,在它们的后面加一个 ?
,即可以抑制它们的贪婪,例如
(setq x "abcabcabc") (string-match "a\\(.+?\\)a" x) (princ\' (match-string 1 x))
此时,程序的输出结果即是 bc
了。
Elisp 函数 re-search-forward
能够在缓冲区内搜索与正则表达式匹配的文本的同时,将插入点移动到缓冲区的匹配位置。基于该函数,再借助 Elisp 正则表达式的文本捕获功能,即可从上一节构造的 one-line 缓冲区内提取多个 <attachment>...</attaqchment>
块了。
为了演示 re-search-forward
的用法,我将上一节的那段示例代码改造为如下代码:
(let ((x "") (one-line (generate-new-buffer "one-line")) (output (generate-new-buffer "output"))) (find-file "foo.xml") (setq x (buffer-string)) (with-current-buffer one-line (insert x) (goto-char (point-min)) (replace-string "\n" "<linebreak/>") (goto-char (point-min)) (while t (if (re-search-forward "\\(<attachment>.+?</attachment>\\)" nil t 1) 程序分支 1 程序分支 2))))
re-search-forward
是迄今为止我用过的最为复杂的 Elisp 函数了,它有 4 个参数,但只有第 1 个参数是必须的,其余 3 个参数皆为可选——假若不设定它们的值,re-search-forward
会使用它们的默认值。这 4 个参数释义以下:
re-search-forward
是在当前缓冲区内进行文本匹配搜索,搜索的起始位置是插入点所在位置,终止位置可经过它的第二个参数设定,若该参数值为 nil
,则将当前缓冲区的尽头做为搜索范围的终止位置。nil
,在未搜索到匹配文本时,re-search-forward
便会报错。若该参数值为 t
,re-search-forward
会返回 nil
。若该参数值即不是 nil
,也不是 t
,则 re-search-forward
函数将插入点移动到搜索区域的尽头,而后返回 nil
。COUNT
,可令 re-search-forward
的搜索过程维持到第 COUNT
次匹配后结束,假若未设定这个参数,其值默认为 1。若充分理解了 re-search-forward
函数的用法,则上述代码虚设的程序分支 1 对应的代码即可写出来了,再也不须要新的 Elisp 知识,即
(let ((y (match-string 1))) (with-current-buffer output (insert (concat y "\n"))))
就是将 re-search-forward
捕获的文本用 match-string
函数取出后插入 output 缓冲区。在此须要注意,若正则表达式捕获的文本属于当前缓冲区,match-string
函数无需写第 2 个参数。
对于程序分支 2,即 re-search-forward
匹配失败状况的处理,现有的 Elisp 知识是真的不够用了。由于该程序分支属于一个无限迭代过程,要从后者跳出,须要像其余编程语言那样,须要有 return
或 break
语法,可提早终止迭代过程。
Elisp 语言没有 return
和 break
,可是它有 catch/throw
表达式。
下面的示例
(catch 'foo (princ\' "foo") (princ\' "bar"))
可输出
foo bar
如今,假若我将上述代码修改成
(catch 'foo (princ\' "foo") (throw 'foo nil) (princ\' "bar"))
那么位于 throw
表达式以后的代码便会被 Elisp 解释器忽略,于是如今的代码只能输出
foo
假若将上述代码修改成
(princ\' (catch 'foo (princ\' "foo") (throw 'foo nil) (princ\' "bar")))
输出结果则变为
foo nil
由于 throw
的第 2 个参数 nil
会被 Elisp 做为 catch
表达式的求值结果。
catch/throw 在 Elisp 语言里称为「非本地退出」,基于它们即可模拟其余编程语言里的 return
,break
以及异常机制。
基于 catch/throw,即可实现上一节所述的程序分支 2 了,例如
(throw 'break nil)
而后只需将 while
表达式放在 catch
块里,由后者捕捉 throw
抛出的 'break
,即
(catch 'break (while t (if (re-search-forward "\\(<attachment>.+?</attachment>\\)" nil t 1) 程序分支 1 (throw 'break nil))))
如今,如下代码
(let ((x "") (one-line (generate-new-buffer "one-line")) (output (generate-new-buffer "output"))) (find-file "foo.xml") (setq x (buffer-string)) (with-current-buffer one-line (insert x) (goto-char (point-min)) (replace-string "\n" "<linebreak/>") (goto-char (point-min)) (catch 'break (while t (if (re-search-forward "\\(<attachment>.+?</attachment>\\)" nil t 1) (let ((y (match-string 1))) (with-current-buffer output (insert (concat y "\n")))) (throw 'break nil))))))
已基本解决本章开始所提出的问题了,由于 output 缓冲区内存放着从 foo.xml 文件里提取的两个 <attachment>...</attachment>
块,接下来,我只需将其中的 <linebreak/>
替换为 \n
,问题便彻底解决了。可是,我以为这个任务能够留做本章习题。
在当前缓冲区内,insert
,replace-string
以及 re-search-forward
等函数,皆有反作用,它们会移动插入点。在文本处理时,要记住当前的插入点所在的位置,而后调用这些函数以后,须要再将插入点恢复原位。这是前面几节代码屡次出现
(goto-char (point-min))
的主要缘由。Elisp 提供了 save-excursion
语法,它能够自动将插入点的位置保存下来,而后执行一些可能会移动插入点的运算,最后再将插入点恢复原位。例如
(save-excursion (insert x))
与
(let ((p (point))) (insert x) (goto-char p))
等价。
所以,本章第二个习题是,基于 save-excursion
语法修改上一节习题的答案。
本章介绍了 Elisp 缓冲区里更多的运算以及非本地退出语法。掌握了这些知识,可从任何文本文档内提取符合模式的由多行文本构成的文本块。