etags是一个索引源代码的工具,经过一段时间的使用,发现某些行为与手册上的描述不一致,还有一些地方的描述比较模糊。通过阅读源代码,发现了一些不曾出现在手册上的规则,也明白了一些原来感觉很模糊的地方。
etags分为两个部分:可执行程序etags,由lib-src/etags.c
编译而来,用于扫描源代码,生成TAGS文件;etags.el是Emacs的一部分,它使用TAGS文件来完成定位、查找等各项工作。
生成TAGS文件
生成TAGS文件的方式很简单,比如给Emacs源代码生成TAGS文件
$ etags src/*.[hc] lib-src/*.[hc] lib/*.[hc] \ lisp/*.el lisp/*/*.el
这条命令在当前目录生成一个TAGS文件。
另外一种复杂的方式是生成多个TAGS文件,然后使用--include
生成一个主文件,如
$ for dir in src lib-src lib; do \ pushed $dir; \ etags *.[hc] -o TAGS.sub; \ INCLUDES="$INCLUDES --include $PWD/TAGS.sub"; \ popd \ done $ for dir in lisp lisp/*; do \ pushd $dir; \ etags *.[hc] -o TAGS.sub; \ INCLUDES="$INCLUDES --include $PWD/TAGS.sub"; \ popd \ done $ etags -o TAGS $INCLUDES
按照手册的说法,使用这两种方式生成的TAGS文件的效果应该是一样的。然而在实际使用中,我发现它们的行为有较大差别,至少对于部分命令如此,根据我的感受,推荐使用第一种方式生成TAGS文件,第二种方式的体验较差。
首先是find-tag
命令,使用第一种方式生成的TAGS文件能够更快定位,而第二种方式则需要更多次地用C-u M-.
查找。
其次是list-tags
命令,第一种方式可以正常工作,而第二种则完全不能工作,因为list-tags
只查找在主TAGS文件里引用的那些文件,然而这个TAGS里只有include指令,没有引用任何源文件,因此甚至没法完成输入(我个人认为这是个bug),如下图:
使用TAGS文件
在Emacs里运行M-x visit-tags-table RET
,然后输入TAGS文件路径即可。Emacs将这个文件放在一个buffer里面,并将major mode设置为tags-table-mode
。在使用过程中,我们并不会直接使用这个buffer,而是通过一系列命令(比如find-tag
)间接使用它。
如果我们希望切换到另一个项目的TAGS文件,比如从Emacs切换到gcc,那么只需要再次调用M-x visit-tags-table
并读入gcc相关的TAGS文件即可。需要注意的是,在第二次(以及之后)调用visit-tags-table
的时候,会被询问:
Keep current list of tags tables also? (y or n)
这时需要回答n
。之后Emacs会显示一条信息:
Starting a new list of tags tables
这时我们再使用任何etags命令,就只操作在gcc的TAGS文件上,Emacs的TAGS文件不会参与任何操作。如果想切换回Emacs的TAGS文件,只需调用select-tags-table,然后按照提示在相应TAGS文件上按t
键即可。
那我们如果回答y
会如何呢?在这种情况下,Emacs的TAGS文件和gcc的TAGS文件会同时参与etags命令。具体的影响可能有以下几种:
- tag数量增大,使得etags命令执行时间变长,响应迟钝;
- 如果两个TAGS文件里面有类似或相同的tag,则会严重影响使用体验;
因此,如果没有特殊的需求,最好不要合并不同项目的TAGS文件。
理解了TAGS文件、文件集合和集合的集合这三个概念,就可以根据自己需要,选择回答y
还是n
。
- TAGS文件(
tags-file-name
) - 初始值为用户创建的TAGS文件,如:
"/home/liang/src/gcc/4.6.1/TAGS"
。如果是用上述第二种方式创建的,在使用过程中tags-file-name
的值可能会发生改变,它指向当前正在访问的TAGS文件。 - 文件集合(
tags-table-list
) - 在回答 “Keep current list of tags tables also?” 时选择
y
而合并了不同项目的TAGS文件而形成的集合,如:("/home/liang/project/emacs/trunk/TAGS" "/home/liang/src/gcc/4.6.1/TAGS")
- 集合的集合(
tags-table-set-list
) - 在回答 “Keep current list of tags tables also?” 时选择
n
,而将之前的文件集合搁置,重新组建新的文件集合,这些文件的集合放在一起,就是集合的集合。从数据结构上来说,它就是tags-table-list可能取值的集合。如:(("/home/liang/project/emacs/trunk/TAGS" "/home/liang/src/gcc/4.6.1/TAGS")
("/home/liang/project/go/go/src/TAGS")
("/home/liang/src/gcc/4.6.1/TAGS"))
TAGS文件格式
TAGS文件不一定非要用etags程序生成,如果理解了它的文件格式,自己手写一个,照样能用,或者另外写个程序生成也可以。具体的格式描述参见源代码下的etc/ETAGS.EBNF文件。这里面有两个概念非常重要,即:explicit tag name和implicit tag name。etags程序会尽量使用implicit tag name,以减小TAGS文件的大小。所以我们在生成TAGS文件的时候,也应该尽量遵守这个规则,毕竟文件越小,响应越快。
implict tag name的例子如下:
find_base_value ^?998,33275
explicit tag name的例子如下:
static char *reg_seen;^?reg_seen^A1175,38518
其中^?
和^A
是分隔符,分别对应字符\177
和\001
。后面两个数字分别为行号和行首字符位置。
判断是否可以使用implicit tag name的规则如下(摘自etags.c):
* make_tag creates tags with "implicit tag names" (unnamed tags) * if the following are all true, assuming NONAM=" \f\t\n\r()=,;": * 1. NAME does not contain any of the characters in NONAM; * 2. LINESTART contains name as either a rightmost, or rightmost but * one character, substring; * 3. the character, if any, immediately before NAME in LINESTART must * be a character in NONAM; * 4. the character, if any, immediately after NAME in LINESTART must * also be a character in NONAM.
由于reg_seen左边的字符为*
,不属于NONAM
,不符合生成implicit tag name的规则,所以必须生成explicit tag name,即之后重复出现的reg_seen部分。
索引etags不认识的语言
etags默认支持多种编程语言或者文档结构,在这种情况下,etags能够根据上述规则尽量使用implicit tag name。对于etags不支持的语言,我们可以使用--regex
选项来指导etags生成TAGS文件,这时就要我们自己注意,在能够使用implicit tag name的情况下尽量使用,以减小文件大小。当然,生成更多的explicit tag name并不会产生任何正确性的问题。
下面的例子是用来索引go源程序的:
$ find . -name "*.go" | \ etags --language=none \ --regex='/type[ \t]+[^ \t]+[ \t]/' \ --regex='/func[ \t]+[^( \t]+[( \t]/' \ --regex='/func[ \t]+([ \t]*[^( \t]*[ \t]+\*?\([^) \t]+\)[ \t]*)[ \t]*\([^( \t]+\)[( \t]/\1.\2/' -
前面两个regex产生implicit tag name,最后一个regex生成explicit tag name,它生成的tag line如下:
func (url *URL) IsAbs(^?URL.IsAbs^A611,16231
也就是说,对于有receiver的函数,我们使用explicit tag name来进行更精确地定位,即通过M-x find-tag RET URL.IsAbs RET
定位该函数的定义位置。
改进与BUG
我希望find-tag
能够智能一点,比如在etags.c文件里运行M-x find-tag RET LOOKING_AT RET
,不要跳到ebrowse.c文件里,而是定位到etags.c里的定义,即优先选择本文件里出现的tag。有人已经提出了类似的需求,但是一直没有实现。
另外,list-tags
有个bug,不能正确处理explicit tag name,比如M-x list-tags RET src/buffer.c RET
,然后在*Tags List*
buffer的第三行按回车,会看到错误信息为
Rerun etags: `^current_buffer' not found in /User/wangliang/src/emacs/trunk/src/buffer.c
修改的方法很简单,patch如下:
diff --git a/lisp/progmodes/etags.el b/lisp/progmodes/etags.el index d321e9c..f38af05 100644 --- a/lisp/progmodes/etags.el +++ b/lisp/progmodes/etags.el @@ -1409,7 +1409,7 @@ hits the start of file." tag tag-info pt) (forward-line 1) (while (not (or (eobp) (looking-at "\f"))) - (setq tag-info (save-excursion (funcall snarf-tag-function t)) + (setq tag-info (save-excursion (funcall snarf-tag-function nil)) tag (car tag-info) pt (with-current-buffer standard-output (point))) (princ tag)
list-tags可以列出一个文件里的所有tag,所以我想是否可以把这个功能和Anything集成起来,实现类似anything+imenu的功能。通常来讲,anything+etags能够比anything+imenu获得更多的candidates。具体实现如下:
(defun wl-list-tags (file) (with-current-buffer (get-file-buffer tags-file-name) (goto-char (point-min)) (when (re-search-forward (concat "\f\n" (if (< (length default-directory) (length file)) (concat "\\(.*" file "\\|" (substring file (length default-directory)) "\\)") file) ",") nil t) (let (tags) (forward-line 1) (while (not (or (eobp) (looking-at "\f"))) (add-to-list 'tags (cons (car (save-excursion (funcall snarf-tag-function t))) (save-excursion (funcall snarf-tag-function nil)))) (forward-line 1)) tags)))) (defun wl-anything-c-etags-in-current-buffer () (when (buffer-file-name) (let ((file (buffer-file-name)) (first-time t) tags tmp) (while (visit-tags-table-buffer (not first-time)) (setq first-time nil) (setq tmp (wl-list-tags file)) (when tmp (setq tags (append tmp tags)))) tags))) (defvar wl-anything-c-source-etags-in-current-buffer '((name . "Tags") (candidates . (lambda () (when (or tags-file-name tags-table-list) (with-current-buffer anything-current-buffer (wl-anything-c-etags-in-current-buffer))))) (action . (("Find tag" . (lambda (tag-info) (etags-goto-tag-location tag-info))))) (persistent-action . (("Find tag" . (lambda (tag-info) (etags-goto-tag-location tag-info))))))) (defun wl-anything-etags-in-currect-buffer () (interactive) (anything 'wl-anything-c-source-etags-in-current-buffer nil nil nil nil "*anything etags for buffer*"))
调用命令wl-anything-etags-in-currect-buffer
,就可以列出当前buffer里面的所有tag,供选择定位。效果如图:
而wl-anything-c-source-etags-in-current-buffer
作为一个symbol可以和其它source symbol组合起来使用,如:
(setq wl-anything-sources (append anything-for-files-prefered-list '(anything-c-source-emacs-process anything-c-source-imenu wl-anything-c-source-etags-in-current-buffer))) (defun wl-anything () (interactive) (anything wl-anything-sources))
其实Anything默认已经集成了etags,只不过用法不一样,它显示TAGS文件里面的所有tag供选择,但是要求当前buffer对应的文件必须被TAGS文件索引,否则提示创建TAGS文件。
如果觉得传统的使用方式——M-.
加C-u M-.
——不爽,可以使用tags-apropos
列出所有匹配正则表达式的tag,如果想限定到完全匹配的symbol,可以使用如下wl-etags-apropos-symbol
命令:
(eval-after-load 'etags '(progn (defun wl-etags-apropos-symbol (tagname) (interactive (find-tag-interactive "Locate tag: ")) (tags-apropos (concat "\\_<" tagname "\\_>")))))
学习心得
etags.c和etags.el两个文件,断断续续看了近两个月。开始的时候,一头雾水,看哪都不懂,一天天坚持下来,突然有一天开窍了,然后每天一个小模块,看到最后,竟然也明白个大概。有时想想,Emacs真得给了我很多,它
- 让我养成了读手册的习惯;
- 带我进入了Lisp的神奇世界;
- 使我能够持续改善工作流程;
- 甚至“强迫”我阅读它的实现代码!
请问截图用的是什么字体
我发现读emacs lisp的手册比较痛苦,有没有比较友好点的教程,让我更顺利的度过这个瓶颈
字体是Mac上的Monaco。如果是想学Lisp可以看一些Common Lisp的书,但是要写程序的话,看Emacs Lisp手册是不能避免的。