Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

A few functions to let me manage TAGS files more easily. Typically, my projects contain at least one sub-folder. I got sick of manually updating, so I wrote this to help me update a single TAGS file per project (always in the projects' root directory).

Please share your thoughts (aside from "Why are you using Emacs?", you're not likely to change my mind).

One thing I've noticed that could trip me up is that it only tags files of one language (and if I had a project that had both Haskell and JS code, for example, I could tag one or the other). I would particularly appreciate advice on better ways to write find-parent-tags than just building each path from available sub-strings.

(defun create-tag-table ()
  "This will recursively tag all files of a given type starting with the current buffers' directory. 
   It overwrites the old TAGS file, if one exists. 
   Haskell files assume you've installed `hasktags`."
  (let ((file-type (get-taggable-extension))
        (cur-dir default-directory)
        (tags-file (find-parent-tags default-directory)))
    (if (equalp file-type "hs")
        (shell-command "hasktags --ignore-close-implementation --etags `find . -type f -name \"*.*hs\"`")
          (shell-command (concat "find -name \"*." file-type "\" -print | etags -")))))

(defun find-parent-tags (dir)
  "Traverses the directory tree up to /home/[user]/ or / whichever comes first. 
   Returns either nil or the directory containing the first TAGS file it finds."
  (interactive (list default-directory))
  (find-parent-tags-rec (build-tag-paths dir)))

(defun find-parent-tags-rec (list-of-filepath)
  (cond ((null list-of-filepath) nil)
        ((file-exists-p (car list-of-filepath)) (car list-of-filepath))
        (t (find-parent-tags-rec (cdr list-of-filepath)))))

(defun build-tag-paths (dir-string)
  (build-tag-paths-rec (remove-if #'empty-string? (split-string dir-string "/")) (list "/")))

(defun build-tag-paths-rec (steps acc)
  (if (null steps) 
      (mapcar (lambda (p) (concat p "TAGS")) acc)
    (build-tag-paths-rec (cdr steps)
             (cons (concat (car acc) (car steps) "/") acc))))

(defun get-taggable-extension ()
  "Either returns the current file's extension (if it's appropriate) or asks the user to pick one with completion"
  (let ((b-name (buffer-file-name (current-buffer)))
        (valid-exts (list "lisp" "py" "c" "hs" "rb" "ss" "scm" "js" "erl" "el")))
    (string-match "\\.\\(.*?\\)$" b-name)
    (let ((current-filetype (match-string 1 b-name)))
      (if (member current-filetype valid-exts)
        (completing-read "File type: " valid-exts nil 'confirm)))))

(defun empty-string? (s) (equalp s ""))

(provide 'tagariffic)
share|improve this question
add comment

1 Answer

up vote 5 down vote accepted

General notes

  • Always prefix your functions with the name of your package or project, e.g. tagariffic-create-tag-table and so on. Emacs has no package scoping.
  • Whenever possible (i.e. in the absence of mutual recursion), define your functions before using them. It's a lot clearer for the reader. It's also necessary if you ever define macros.
  • You're using some functions from the cl package, so you should start with (require 'cl).
  • The usual style for predicates in Lisp is name-p, not name? (that's Scheme).
  • Emacs has functions to manipulate file and directory names. Use them rather than going around concatenating slashes; this way your code will work on exotic platforms such as Windows.

Finding the tags file

Emacs 23 has a function locate-dominating-file (in files.el).

Look up the directory hierarchy from FILE for a file named NAME.
Stop at the first parent directory containing a file NAME, and return the directory. Return nil if not found.

Your (find-parent-tags default-directory) is almost equivalent to (locate-dominating-file (buffer-file-name) "TAGS"). If you want to use the current directory rather than the directory of the current buffer's file (they're almost always the same), use (locate-dominating-file (concat (default-directory) ".") "TAGS"). locate-dominating-file stops at your home directory, too.

The code you posted does nothing with the tags-file variable. Did you mean to run the tag generation command in that directory, or failing that in the current directory? If so, this should do it:

(let ((default-directory (if tags-file (file-name-directory tags-file) default-directory)))
  (shell-command …))

From file type to shell commands

Rather than build in an exception for Haskell, the usual style would be to define an alist from extensions to commands.

(defvar tagariffic-command-alist
  '(("hs" . "… hasktags …")
    (t . "… etags …")))

However, there's a better approach than separate commands for each type; see below.

The shell commands

Your hasktags command will choke on file or directory names that contain shell special characters: \[?* and whitespace. This isn't very common in source trees, but it could happen. Generally speaking, never use command substitution to generate a list of file names. Instead, let find call the command.

find . -type f -name '*.hs' -exec hasktags --ignore-close-implementation --etags {} +

A second problem is that if there are too many files, the command you wrote will die of a command line too long error; the one I just showed will run hasktags several times with command lines of a permitted size, causing the tag file to be overwritten. As a workaround, start by creating an empty tags file and tell hasktags to append to the tags file.

find . -type f -name '*.hs' -exec hasktags --ignore-close-implementation -e -a {} +

An improvement to the find commands would be to ignore certain directories belonging to version control systems. Especially with svn, this could speed things up.

find . -type d \( -name .bzr -o -name .git -o -name .hg -o -name .svn -o -name CVS -o -name _darcs \) -prune -o \
       -type f …

Above I mentioned a different approach to choosing the tag generation command. Your approach assumes that a given project will be in a single language. It's easy enough to cater for mixed-language projects: build a single tags file containing the output generated by etags and hasktags.

find . -type d \( -name .bzr -o … \) -prune -o \
       -type f -name '*.hs' -exec hasktags -e -a {} + -o \
       -type f \( -name '*.[CScls]' -o -name '*.el' -o … \) -exec etags {} +
share|improve this answer
add comment

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.