etags揭秘

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的神奇世界;
  • 使我能够持续改善工作流程;
  • 甚至“强迫”我阅读它的实现代码!

2 thoughts on “etags揭秘

  1. jin says:

    请问截图用的是什么字体

    我发现读emacs lisp的手册比较痛苦,有没有比较友好点的教程,让我更顺利的度过这个瓶颈

    • netcasper says:

      字体是Mac上的Monaco。如果是想学Lisp可以看一些Common Lisp的书,但是要写程序的话,看Emacs Lisp手册是不能避免的。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据