import os import re from stat import ST_MTIME from subprocess import Popen, PIPE, STDOUT class CtagsRunError(Exception): pass class CtagsParseError(Exception): pass # ctags --languages=... -R -excmd=number class TagManager(object): lang = None prune = set(['SCCS', 'RCS', 'CVS', '.svn', '.hg', '.git', '.bzr']) exts = set() def __init__(self, base='.'): self.db = None self.base = base self.path = os.path.join(self.base, 'tags') self.update() def has(self, name): return name in self.db.tags def get(self, name): return self.db.tags.get(name, []) def update(self): if self.is_outdated(): self.generate() self.db = Ctags(self.path) self.db.parse() def generate(self): args = ['ctags', '-f', self.path, '-L-', '--excmd=number'] if self.lang: args.append('--language-force=%s' % self.lang) pipe = Popen(args, stdin=PIPE, stdout=PIPE, stderr=STDOUT) indata = '\n'.join(self.get_paths()) + '\n' outdata = pipe.communicate(indata) if pipe.returncode != 0: raise CtagsRunError("%r: %r" % (args, outdata)) def get_paths(self): return list(self._walk()) def is_outdated(self): if not os.path.exists(self.path): return True mtime = os.stat(self.path)[ST_MTIME] itr = self._walk(mtime) try: next(itr) return True except StopIteration: return False def _walk(self, mtime=0): paths = [] for root, dirs, files in os.walk(self.base): for d in dirs: if d in self.prune: dirs.remove(d) for f in files: path = os.path.join(root, f) if not self._match(path): continue elif os.stat(path)[ST_MTIME] < mtime: continue else: yield os.path.join(root, f) return def _match(self, path): return os.path.splitext(path)[1] in self.exts class Ctags(object): line_re = re.compile(r'^(.+?)\t(.+?)\t(.+?);"(.+)$') def __init__(self, path): self.path = path self.records = [] self.tags = {} def get(self, tag): return self.tags.get(tag, []) def parse(self): for line in open(self.path, 'r'): if line.startswith('!'): continue m = self.line_re.match(line) if not m: raise CtagsParseError("%r" % line) name, path, ex, _ = m.groups() self.add(CtagRecord(name, path, ex)) def add(self, r): self.records.append(r) self.tags.setdefault(r.name, []) self.tags[r.name].append(r) class CtagRecord(object): def __init__(self, name, path, ex): if not ex.isdigit(): raise Exception("excmd %r unsupported; use --excmd=number" % ex) self.name = name self.path = path self.line = int(ex) def __repr__(self): return "CtagRecord(%r, %r, %d)" % (self.name, self.path, self.line) if __name__ == '__main__': import sys from pprint import pprint ctags = Ctags(sys.argv[1]) ctags.parse() if len(sys.argv) > 2: print("records for %r:" % sys.argv[2]) for r in ctags.get(sys.argv[2]): print(r) else: pprint(ctags.records)