tags.c revision 1.14
1/*
2 * Copyright (C) 1984-2012  Mark Nudelman
3 * Modified for use with illumos by Garrett D'Amore.
4 * Copyright 2014 Garrett D'Amore <garrett@damore.org>
5 *
6 * You may distribute under the terms of either the GNU General Public
7 * License or the Less License, as specified in the README file.
8 *
9 * For more information, see the README file.
10 */
11
12#include "less.h"
13
14#define	WHITESP(c)	((c) == ' ' || (c) == '\t')
15
16char *tags = "tags";
17
18static int total;
19static int curseq;
20
21extern int linenums;
22extern volatile sig_atomic_t sigs;
23
24enum tag_result {
25	TAG_FOUND,
26	TAG_NOFILE,
27	TAG_NOTAG,
28	TAG_NOTYPE,
29	TAG_INTR
30};
31
32/*
33 * Tag type
34 */
35enum {
36	T_CTAGS,	/* 'tags': standard and extended format (ctags) */
37	T_CTAGS_X,	/* stdin: cross reference format (ctags) */
38	T_GTAGS,	/* 'GTAGS': function defenition (global) */
39	T_GRTAGS,	/* 'GRTAGS': function reference (global) */
40	T_GSYMS,	/* 'GSYMS': other symbols (global) */
41	T_GPATH		/* 'GPATH': path name (global) */
42};
43
44static enum tag_result findctag();
45static enum tag_result findgtag();
46static char *nextgtag();
47static char *prevgtag();
48static off_t ctagsearch();
49static off_t gtagsearch();
50static int getentry();
51
52/*
53 * The list of tags generated by the last findgtag() call.
54 *
55 * Use either pattern or line number.
56 * findgtag() always uses line number, so pattern is always NULL.
57 * findctag() uses either pattern (in which case line number is 0),
58 * or line number (in which case pattern is NULL).
59 */
60struct taglist {
61	struct tag *tl_first;
62	struct tag *tl_last;
63};
64#define	TAG_END  ((struct tag *)&taglist)
65static struct taglist taglist = { TAG_END, TAG_END };
66struct tag {
67	struct tag *next, *prev; /* List links */
68	char *tag_file;		/* Source file containing the tag */
69	LINENUM tag_linenum;	/* Appropriate line number in source file */
70	char *tag_pattern;	/* Pattern used to find the tag */
71	int tag_endline;	/* True if the pattern includes '$' */
72};
73static struct tag *curtag;
74
75#define	TAG_INS(tp) \
76	(tp)->next = TAG_END; \
77	(tp)->prev = taglist.tl_last; \
78	taglist.tl_last->next = (tp); \
79	taglist.tl_last = (tp);
80
81#define	TAG_RM(tp) \
82	(tp)->next->prev = (tp)->prev; \
83	(tp)->prev->next = (tp)->next;
84
85/*
86 * Delete tag structures.
87 */
88void
89cleantags(void)
90{
91	struct tag *tp;
92
93	/*
94	 * Delete any existing tag list.
95	 * {{ Ideally, we wouldn't do this until after we know that we
96	 *    can load some other tag information. }}
97	 */
98	while ((tp = taglist.tl_first) != TAG_END) {
99		TAG_RM(tp);
100		free(tp);
101	}
102	curtag = NULL;
103	total = curseq = 0;
104}
105
106/*
107 * Create a new tag entry.
108 */
109static struct tag *
110maketagent(char *file, LINENUM linenum, char *pattern, int endline)
111{
112	struct tag *tp;
113
114	tp = ecalloc(sizeof (struct tag), 1);
115	tp->tag_file = estrdup(file);
116	tp->tag_linenum = linenum;
117	tp->tag_endline = endline;
118	if (pattern == NULL)
119		tp->tag_pattern = NULL;
120	else
121		tp->tag_pattern = estrdup(pattern);
122	return (tp);
123}
124
125/*
126 * Get tag mode.
127 */
128static int
129gettagtype(void)
130{
131	int f;
132
133	if (strcmp(tags, "GTAGS") == 0)
134		return (T_GTAGS);
135	if (strcmp(tags, "GRTAGS") == 0)
136		return (T_GRTAGS);
137	if (strcmp(tags, "GSYMS") == 0)
138		return (T_GSYMS);
139	if (strcmp(tags, "GPATH") == 0)
140		return (T_GPATH);
141	if (strcmp(tags, "-") == 0)
142		return (T_CTAGS_X);
143	f = open(tags, OPEN_READ);
144	if (f >= 0) {
145		(void) close(f);
146		return (T_CTAGS);
147	}
148	return (T_GTAGS);
149}
150
151/*
152 * Find tags in tag file.
153 * Find a tag in the "tags" file.
154 * Sets "tag_file" to the name of the file containing the tag,
155 * and "tagpattern" to the search pattern which should be used
156 * to find the tag.
157 */
158void
159findtag(char *tag)
160{
161	int type = gettagtype();
162	enum tag_result result;
163
164	if (type == T_CTAGS)
165		result = findctag(tag);
166	else
167		result = findgtag(tag, type);
168	switch (result) {
169	case TAG_FOUND:
170	case TAG_INTR:
171		break;
172	case TAG_NOFILE:
173		error("No tags file", NULL_PARG);
174		break;
175	case TAG_NOTAG:
176		error("No such tag in tags file", NULL_PARG);
177		break;
178	case TAG_NOTYPE:
179		error("unknown tag type", NULL_PARG);
180		break;
181	}
182}
183
184/*
185 * Search for a tag.
186 */
187off_t
188tagsearch(void)
189{
190	if (curtag == NULL)
191		return (-1);  /* No gtags loaded! */
192	if (curtag->tag_linenum != 0)
193		return (gtagsearch());
194	else
195		return (ctagsearch());
196}
197
198/*
199 * Go to the next tag.
200 */
201char *
202nexttag(int n)
203{
204	char *tagfile = NULL;
205
206	while (n-- > 0)
207		tagfile = nextgtag();
208	return (tagfile);
209}
210
211/*
212 * Go to the previous tag.
213 */
214char *
215prevtag(int n)
216{
217	char *tagfile = NULL;
218
219	while (n-- > 0)
220		tagfile = prevgtag();
221	return (tagfile);
222}
223
224/*
225 * Return the total number of tags.
226 */
227int
228ntags(void)
229{
230	return (total);
231}
232
233/*
234 * Return the sequence number of current tag.
235 */
236int
237curr_tag(void)
238{
239	return (curseq);
240}
241
242/*
243 * ctags
244 */
245
246/*
247 * Find tags in the "tags" file.
248 * Sets curtag to the first tag entry.
249 */
250static enum tag_result
251findctag(char *tag)
252{
253	char *p;
254	FILE *f;
255	int taglen;
256	LINENUM taglinenum;
257	char *tagfile;
258	char *tagpattern;
259	int tagendline;
260	int search_char;
261	int err;
262	char tline[TAGLINE_SIZE];
263	struct tag *tp;
264
265	p = shell_unquote(tags);
266	f = fopen(p, "r");
267	free(p);
268	if (f == NULL)
269		return (TAG_NOFILE);
270
271	cleantags();
272	total = 0;
273	taglen = strlen(tag);
274
275	/*
276	 * Search the tags file for the desired tag.
277	 */
278	while (fgets(tline, sizeof (tline), f) != NULL) {
279		if (tline[0] == '!')
280			/* Skip header of extended format. */
281			continue;
282		if (strncmp(tag, tline, taglen) != 0 || !WHITESP(tline[taglen]))
283			continue;
284
285		/*
286		 * Found it.
287		 * The line contains the tag, the filename and the
288		 * location in the file, separated by white space.
289		 * The location is either a decimal line number,
290		 * or a search pattern surrounded by a pair of delimiters.
291		 * Parse the line and extract these parts.
292		 */
293		tagpattern = NULL;
294
295		/*
296		 * Skip over the whitespace after the tag name.
297		 */
298		p = skipsp(tline+taglen);
299		if (*p == '\0')
300			/* File name is missing! */
301			continue;
302
303		/*
304		 * Save the file name.
305		 * Skip over the whitespace after the file name.
306		 */
307		tagfile = p;
308		while (!WHITESP(*p) && *p != '\0')
309			p++;
310		*p++ = '\0';
311		p = skipsp(p);
312		if (*p == '\0')
313			/* Pattern is missing! */
314			continue;
315
316		/*
317		 * First see if it is a line number.
318		 */
319		tagendline = 0;
320		taglinenum = getnum(&p, 0, &err);
321		if (err) {
322			/*
323			 * No, it must be a pattern.
324			 * Delete the initial "^" (if present) and
325			 * the final "$" from the pattern.
326			 * Delete any backslash in the pattern.
327			 */
328			taglinenum = 0;
329			search_char = *p++;
330			if (*p == '^')
331				p++;
332			tagpattern = p;
333			while (*p != search_char && *p != '\0') {
334				if (*p == '\\')
335					p++;
336				p++;
337			}
338			tagendline = (p[-1] == '$');
339			if (tagendline)
340				p--;
341			*p = '\0';
342		}
343		tp = maketagent(tagfile, taglinenum, tagpattern, tagendline);
344		TAG_INS(tp);
345		total++;
346	}
347	fclose(f);
348	if (total == 0)
349		return (TAG_NOTAG);
350	curtag = taglist.tl_first;
351	curseq = 1;
352	return (TAG_FOUND);
353}
354
355/*
356 * Edit current tagged file.
357 */
358int
359edit_tagfile(void)
360{
361	if (curtag == NULL)
362		return (1);
363	return (edit(curtag->tag_file));
364}
365
366/*
367 * Search for a tag.
368 * This is a stripped-down version of search().
369 * We don't use search() for several reasons:
370 *   -	We don't want to blow away any search string we may have saved.
371 *   -	The various regular-expression functions (from different systems:
372 *	regcmp vs. re_comp) behave differently in the presence of
373 *	parentheses (which are almost always found in a tag).
374 */
375static off_t
376ctagsearch(void)
377{
378	off_t pos, linepos;
379	LINENUM linenum;
380	int len;
381	char *line;
382
383	pos = ch_zero();
384	linenum = find_linenum(pos);
385
386	for (;;) {
387		/*
388		 * Get lines until we find a matching one or
389		 * until we hit end-of-file.
390		 */
391		if (ABORT_SIGS())
392			return (-1);
393
394		/*
395		 * Read the next line, and save the
396		 * starting position of that line in linepos.
397		 */
398		linepos = pos;
399		pos = forw_raw_line(pos, &line, (int *)NULL);
400		if (linenum != 0)
401			linenum++;
402
403		if (pos == -1) {
404			/*
405			 * We hit EOF without a match.
406			 */
407			error("Tag not found", NULL_PARG);
408			return (-1);
409		}
410
411		/*
412		 * If we're using line numbers, we might as well
413		 * remember the information we have now (the position
414		 * and line number of the current line).
415		 */
416		if (linenums)
417			add_lnum(linenum, pos);
418
419		/*
420		 * Test the line to see if we have a match.
421		 * Use strncmp because the pattern may be
422		 * truncated (in the tags file) if it is too long.
423		 * If tagendline is set, make sure we match all
424		 * the way to end of line (no extra chars after the match).
425		 */
426		len = strlen(curtag->tag_pattern);
427		if (strncmp(curtag->tag_pattern, line, len) == 0 &&
428		    (!curtag->tag_endline || line[len] == '\0' ||
429		    line[len] == '\r')) {
430			curtag->tag_linenum = find_linenum(linepos);
431			break;
432		}
433	}
434
435	return (linepos);
436}
437
438/*
439 * gtags
440 */
441
442/*
443 * Find tags in the GLOBAL's tag file.
444 * The findgtag() will try and load information about the requested tag.
445 * It does this by calling "global -x tag" and storing the parsed output
446 * for future use by gtagsearch().
447 * Sets curtag to the first tag entry.
448 */
449static enum tag_result
450findgtag(char *tag, int type)
451{
452	char buf[256];
453	FILE *fp;
454	struct tag *tp;
455
456	if (type != T_CTAGS_X && tag == NULL)
457		return (TAG_NOFILE);
458
459	cleantags();
460	total = 0;
461
462	/*
463	 * If type == T_CTAGS_X then read ctags's -x format from stdin
464	 * else execute global(1) and read from it.
465	 */
466	if (type == T_CTAGS_X) {
467		fp = stdin;
468		/* Set tag default because we cannot read stdin again. */
469		tags = "tags";
470	} else {
471		char *command;
472		char *flag;
473		char *qtag;
474		char *cmd = lgetenv("LESSGLOBALTAGS");
475
476		if (cmd == NULL || *cmd == '\0')
477			return (TAG_NOFILE);
478		/* Get suitable flag value for global(1). */
479		switch (type) {
480		case T_GTAGS:
481			flag = "";
482			break;
483		case T_GRTAGS:
484			flag = "r";
485			break;
486		case T_GSYMS:
487			flag = "s";
488			break;
489		case T_GPATH:
490			flag = "P";
491			break;
492		default:
493			return (TAG_NOTYPE);
494		}
495
496		/* Get our data from global(1). */
497		qtag = shell_quote(tag);
498		if (qtag == NULL)
499			qtag = tag;
500		command = easprintf("%s -x%s %s", cmd, flag, qtag);
501		if (qtag != tag)
502			free(qtag);
503		fp = popen(command, "r");
504		free(command);
505	}
506	if (fp != NULL) {
507		while (fgets(buf, sizeof (buf), fp)) {
508			char *name, *file, *line;
509			int len;
510
511			if (sigs) {
512				if (fp != stdin)
513					pclose(fp);
514				return (TAG_INTR);
515			}
516			len = strlen(buf);
517			if (len > 0 && buf[len-1] == '\n') {
518				buf[len-1] = '\0';
519			} else {
520				int c;
521				do {
522					c = fgetc(fp);
523				} while (c != '\n' && c != EOF);
524			}
525
526			if (getentry(buf, &name, &file, &line)) {
527				/*
528				 * Couldn't parse this line for some reason.
529				 * We'll just pretend it never happened.
530				 */
531				break;
532			}
533
534			/* Make new entry and add to list. */
535			tp = maketagent(file, (LINENUM) atoi(line), NULL, 0);
536			TAG_INS(tp);
537			total++;
538		}
539		if (fp != stdin) {
540			if (pclose(fp)) {
541				curtag = NULL;
542				total = curseq = 0;
543				return (TAG_NOFILE);
544			}
545		}
546	}
547
548	/* Check to see if we found anything. */
549	tp = taglist.tl_first;
550	if (tp == TAG_END)
551		return (TAG_NOTAG);
552	curtag = tp;
553	curseq = 1;
554	return (TAG_FOUND);
555}
556
557static int circular = 0;	/* 1: circular tag structure */
558
559/*
560 * Return the filename required for the next gtag in the queue that was setup
561 * by findgtag().  The next call to gtagsearch() will try to position at the
562 * appropriate tag.
563 */
564static char *
565nextgtag(void)
566{
567	struct tag *tp;
568
569	if (curtag == NULL)
570		/* No tag loaded */
571		return (NULL);
572
573	tp = curtag->next;
574	if (tp == TAG_END) {
575		if (!circular)
576			return (NULL);
577		/* Wrapped around to the head of the queue */
578		curtag = taglist.tl_first;
579		curseq = 1;
580	} else {
581		curtag = tp;
582		curseq++;
583	}
584	return (curtag->tag_file);
585}
586
587/*
588 * Return the filename required for the previous gtag in the queue that was
589 * setup by findgtat().  The next call to gtagsearch() will try to position
590 * at the appropriate tag.
591 */
592static char *
593prevgtag(void)
594{
595	struct tag *tp;
596
597	if (curtag == NULL)
598		/* No tag loaded */
599		return (NULL);
600
601	tp = curtag->prev;
602	if (tp == TAG_END) {
603		if (!circular)
604			return (NULL);
605		/* Wrapped around to the tail of the queue */
606		curtag = taglist.tl_last;
607		curseq = total;
608	} else {
609		curtag = tp;
610		curseq--;
611	}
612	return (curtag->tag_file);
613}
614
615/*
616 * Position the current file at at what is hopefully the tag that was chosen
617 * using either findtag() or one of nextgtag() and prevgtag().  Returns -1
618 * if it was unable to position at the tag, 0 if successful.
619 */
620static off_t
621gtagsearch(void)
622{
623	if (curtag == NULL)
624		return (-1);  /* No gtags loaded! */
625	return (find_pos(curtag->tag_linenum));
626}
627
628/*
629 * The getentry() parses both standard and extended ctags -x format.
630 *
631 * [standard format]
632 * <tag>   <lineno>  <file>         <image>
633 * +------------------------------------------------
634 * |main     30      main.c         main(argc, argv)
635 * |func     21      subr.c         func(arg)
636 *
637 * The following commands write this format.
638 *	o Traditinal Ctags with -x option
639 *	o Global with -x option
640 *		See <http://www.gnu.org/software/global/global.html>
641 *
642 * [extended format]
643 * <tag>   <type>  <lineno>   <file>        <image>
644 * +----------------------------------------------------------
645 * |main     function 30      main.c         main(argc, argv)
646 * |func     function 21      subr.c         func(arg)
647 *
648 * The following commands write this format.
649 *	o Exuberant Ctags with -x option
650 *		See <http://ctags.sourceforge.net>
651 *
652 * Returns 0 on success, -1 on error.
653 * The tag, file, and line will each be NUL-terminated pointers
654 * into buf.
655 */
656static int
657getentry(char *buf, char **tag, char **file, char **line)
658{
659	char *p = buf;
660
661	for (*tag = p; *p && !isspace(*p); p++)		/* tag name */
662		;
663	if (*p == 0)
664		return (-1);
665	*p++ = 0;
666	for (; *p && isspace(*p); p++)			/* (skip blanks) */
667		;
668	if (*p == 0)
669		return (-1);
670	/*
671	 * If the second part begin with other than digit,
672	 * it is assumed tag type. Skip it.
673	 */
674	if (!isdigit(*p)) {
675		for (; *p && !isspace(*p); p++)		/* (skip tag type) */
676			;
677		for (; *p && isspace(*p); p++)		/* (skip blanks) */
678			;
679	}
680	if (!isdigit(*p))
681		return (-1);
682	*line = p;					/* line number */
683	for (*line = p;  *p && !isspace(*p);  p++)
684		;
685	if (*p == 0)
686		return (-1);
687	*p++ = 0;
688	for (; *p && isspace(*p); p++)		/* (skip blanks) */
689		;
690	if (*p == 0)
691		return (-1);
692	*file = p;					/* file name */
693	for (*file = p;  *p && !isspace(*p);  p++)
694		;
695	if (*p == 0)
696		return (-1);
697	*p = 0;
698
699	/* value check */
700	if (strlen(*tag) && strlen(*line) && strlen(*file) && atoi(*line) > 0)
701		return (0);
702	return (-1);
703}
704