Home | History | Annotate | Line # | Download | only in dist
      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  */
     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  */
    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  */
    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  */
    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  */
    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  */
    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  */
    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  */
    234 public int ntags(void)
    235 {
    236 	return total;
    237 }
    238 
    239 /*
    240  * Return the sequence number of current tag.
    241  */
    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  */
    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  */
    373 public int edit_tagfile(void)
    374 {
    375 	if (curtag == NULL)
    376 		return (1);
    377 	return (edit(curtag->tag_file));
    378 }
    379 
    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  */
    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  */
    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  */
    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  */
    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  */
    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  */
    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