Similar to my previous post on Makefiles, where I
claim that Makefiles are the entry point for a project’s commands, here I claim
that tags are the entry point to navigating through a project’s code. For
example, if you use Vim or Emacs with evil-mode, with your cursor on a function
Ctrl-] will jump you to the function definition, while
jump you back to where you came from!
Yet, these simple, but essential, navigation commands won’t work out of the box. First, you need to generate a tags file. The benefit is that any editor that supports tags can navigate through your code automatically! In this post I focus on Vim and Emacs, but your editor of choice is just a Google search away.
If you have a tags file (named
tags) at the root of your project,
Emacs will find and load this file when running a tags related command (e.g.,
Ctrl-]). Vim probably has a similarly simple mechanism.
So, we need to generate a TAGS file. We start with a WTF.
So, how do you generate a tags file? I have discovered an incredibly simple approach that should work across the board for any git-based project. This approach is valuable to know, as there is a whole lot of muddy water in the area of tag generation, with conflicting tools, and overly complex tags commands. I believe that the reason more people aren’t utilizing tags is that the resulting Google searches are misleading and intimidating; who wants to have to write a command specifically tailored to their project, and what about projects that have multiple languages, as many do today?
Further complicating matters is an array of different tags generation tools,
that sometime have conflicting names! The
ctags tool that comes pre-installed
on macOS will not work, for example. Emacs installs it’s own tags tool
ctags that, confusingly, will also
not work. When I say “will also not work”, I mean they won’t work in a general
way that we would like, a command we can apply across the board, independent of
your OS and your editor (or as close to that as possible). There is another
ctags tool, which, confusingly, is referred to as “ExuberantCtags”, and this
tool does work, but it is no longer maintained; it is now maintained as
UniversalCtags. And don’t get me started on
confusingly referred to as simply GNU
Global, which has an Emacs mode that is
used by Emacs’s Projectile Mode (
projectile-regenerate-tags) but seems to only
result in endlessly hanging Emacs, and has two different “backends” - pygments
(which is actually a “generic syntax highlighter”!) and ctags (Which ctags
again? There are four different ones on the last count.), and on what basis
should someone choose between the Python “generic syntax highlighter” and ctags?
Navigating all of this makes it feel as if getting a tagging system in place is some sort of a dark art. At best, you find something that works sometimes for some projects, and otherwise crashes; good luck getting it to work on another machine, or helping your teammate get theirs working. (Uhhh, which ctags is installed again?).
So, we need a simple solution.
A Simple Solution
Luckily, you don’t need to navigate through all that mud to find a simple solution, because I’m going to tell you my solution.
Whatever system you are on, install ExuberantCtags (or UniversalCtags, they should be the same, I think!).
On macOS, this is what you get with
brew install ctags. This will replace any
other (wrong) ctags on your system. On other systems, just make sure you are
installing the ExuberantCtags version.
To check if you have a working version of ctags, run (in a project):
ctags -R .
If you don’t get an error, I think you have a good version.
ExuberantCtags knows a whole bunch of languages out of the box. It identifies
languages by the file extension. This means that you can run it on files of
different languages and it will generate tags for all of them. If you use a
language that it doesn’t know out of the box, such as Elixir, you can add it to
The command we just tested,
ctags -R ., recursively runs against all the
files in your project. This is simple, and is sometimes presented as a
solution. However, this isn’t a full solution, yet.
First, this command generates a vim compatible tags files, but if you want an
Emacs compatible file, you need to use the
vim: ctags -R . emacs: ctags -e -R .
This is pretty simple. However, if you are working in a project that imports dependencies into the project (such as projects which use node) or generates compiled files, ctags might choke on the sheer volume of files it has to work through.
Luckily, if you are using a git version-controlled project, it is very simple to filter out all ignored files (files which are not part of version control). It turns out that the only files you would need tags for are those which are version controlled.
This command lists all files that are version controlled by git. So, if we can operate ctags on only those files, we will save a lot of computation time. Furthermore, this is generic; it will work on any project controlled by git, so we don’t have to write a whole bunch of project specific includes or excludes. Simple.
Ctags has a
-L <file> A list of source file names are read from the specified file. If specified as "-", then standard input is read.
So, we could use the result of
git ls-files in a two-step process:
git ls-files > ls-files ctags -e -R -L ls-files
But if we specify the flag as
-L-, ctags will read this list from standard
input. This means we can pipe the list to ctags:
git ls-files | ctags -e -R -L-
And because this is generic, and recursive, I want to avoid problems that could
be caused by symbolic links. We can use
--links=no to avoid following these
links; but if your project uses symbolic links, you could always enable it.
git ls-files | ctags -e -R --links=no -L-
I have an alias in my
~/.bashrc for this:
alias tags=git ls-files | ctags -e -R --links=no -L-
This can really be used anywhere though. Using it in a Makefile and/or as a git hook is a great ideas. You can also add a function to your editor to call it with a keybinding. So far, I just call it directly in the command line.
- Install ExuberantCtags (
brew install ctags, if macOS)
git ls-files | ctags -e -R --links=no -L-(emacs)
git ls-files | ctags -R --links=no -L-(vim)