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