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