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