MTAGS(1) FreeBSD General Commands Manual MTAGS(1)

mtags
miscellaneous tags

mtags [-a] [-f tagsfile] file ...

The mtags utility makes a tags file for ex(1) from the specified make(1), mdoc(7) sh(1) sources.

The arguments are as follows:

Append to tags file.
tagsfile
Place the tag descriptions in a file called tagsfile. The default behaviour is to place them in a file called tags.

Files whose names are Makefile or end in .mk are assumed to be make(1) files. Files whose names end in .[1-9] are assumed to be mdoc(7) files. Files whose names are .profile, .shrc or end in .sh are assumed to be sh(1) files.

tags
default output tags file

ctags(1), ex(1), vi(1)
January 20, 2021 FreeBSD 12.2-RELEASE-p3

mtags.c in git

/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <assert.h>
#include <err.h>
#include <regex.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>

static void escape(FILE *file, const char *str, size_t len) {
	for (size_t i = 0; i < len; ++i) {
		if (str[i] == '\\' || str[i] == '/') {
			putc('\\', file);
		}
		putc(str[i], file);
	}
}

int main(int argc, char *argv[]) {
	bool append = false;
	const char *path = "tags";
	for (int opt; 0 < (opt = getopt(argc, argv, "af:"));) {
		switch (opt) {
			break; case 'a': append = true;
			break; case 'f': path = optarg;
			break; default:  return EX_USAGE;
		}
	}

	FILE *tags = fopen(path, (append ? "a" : "w"));
	if (!tags) err(EX_CANTCREAT, "%s", path);

	regex_t makeFile, makeLine;
	regex_t mdocFile, mdocLine;
	regex_t shFile, shLine;
	int error = 0
		|| regcomp(&makeFile, "(^|/)Makefile|[.]mk$", REG_EXTENDED | REG_NOSUB)
		|| regcomp(
			&makeLine,
			"^([.][^:$A-Z][^:$[:space:]]*|[^.:$][^:$[:space:]]*):",
			REG_EXTENDED
		)
		|| regcomp(&mdocFile, "[.][1-9]$", REG_EXTENDED | REG_NOSUB)
		|| regcomp(&mdocLine, "^[.]S[hs] ([^\t\n]+)", REG_EXTENDED)
		|| regcomp(
			&shFile, "(^|/)[.](profile|shrc)|[.]sh$", REG_EXTENDED | REG_NOSUB
		)
		|| regcomp(&shLine, "^([_[:alnum:]]+)[[:blank:]]*[(][)]", REG_EXTENDED);
	assert(!error);

	size_t cap = 0;
	char *buf = NULL;
	for (int i = optind; i < argc; ++i) {
		const regex_t *regex;
		if (!regexec(&makeFile, argv[i], 0, NULL, 0)) {
			regex = &makeLine;
		} else if (!regexec(&mdocFile, argv[i], 0, NULL, 0)) {
			regex = &mdocLine;
		} else if (!regexec(&shFile, argv[i], 0, NULL, 0)) {
			regex = &shLine;
		} else {
			warnx("skipping unknown file type %s", argv[i]);
			continue;
		}

		FILE *file = fopen(argv[i], "r");
		if (!file) err(EX_NOINPUT, "%s", argv[i]);

		while (0 < getline(&buf, &cap, file)) {
			regmatch_t match[2];
			if (regexec(regex, buf, 2, match, 0)) continue;
			fprintf(
				tags, "%.*s\t%s\t/^",
				(int)(match[1].rm_eo - match[1].rm_so), &buf[match[1].rm_so],
				argv[i]
			);
			escape(tags, buf, match[0].rm_eo);
			fprintf(tags, "/\n");
		}
		fclose(file);
	}
}