summaryrefslogtreecommitdiff
path: root/mk/help/help.awk
blob: c279fb0fce542e5e651bac94d63f100569a8e41e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# $NetBSD: help.awk,v 1.43 2022/06/11 15:58:41 rillig Exp $
#

# This program extracts the inline documentation from *.mk files.
#
# usage: env TOPIC="topic" awk help.awk file...
#

BEGIN {
	no = 0; yes = 1;

	debug = ENVIRON["HELP_DEBUG"] != "";
	topic = ENVIRON["TOPIC"];
	uctopic = toupper(topic);
	lctopic = tolower(topic);

	found_anything = no;	# has some help text been found at all?
	last_fname = "";
	eval_this_line = yes;
	print_this_line = yes;
	eval_next_empty_line = yes;
	print_this_section = yes;

	delete lines;		# the collected lines
	nlines = 0;		# the number of lines collected so far
	delete keywords;	# the keywords for this paragraph
	delete all_keywords;	# all keywords that occur anywhere
	comment_lines = 0;	# the number of comment lines so far
	print_noncomment_lines = yes; # for make targets, this isn't useful
	print_index = (topic == ":index");
				# whether to print only the list of keywords
	delete all_occurrences;	# all files where the topic occurs as text
}

# Help topics are separated by either completely empty lines or by the
# end of a file or by the end of all files. When there have been enough
# comment lines, the topic is considered worth printing.
#
function end_of_topic(   skip_reason, k, relevant, i) {

	skip_reason = \
		array_is_empty(keywords) \
		? "Ignoring section because of missing keywords." \
		: comment_lines <= 2 \
		? "Ignoring section because of too small comment." \
		: !print_this_section \
		? "Ignoring section because of a previous decision." \
		: "";

	if (skip_reason != "") {
		dprint(skip_reason);
		cleanup();
		return;
	}

	for (k in keywords)
		all_keywords[k]++;

	relevant = topic in keywords || lctopic in keywords ||
		uctopic in keywords || topic == ":all";

	if (relevant && !print_index) {

		if (found_anything)
			print "";
		found_anything = yes;

		print "===> " last_fname " (keywords:" sorted_keys(keywords, " ") "):";

		for (i = 0; i < nlines; i++) {
			if (print_noncomment_lines || (lines[i] ~ /^#/))
				print lines[i];
		}
	}
	cleanup();
}

# Returns the sorted keys of the array, each prefixed by the prefix.
function sorted_keys(array, prefix,   elem, list, listlen, i, j, tmp, result) {
	listlen = 0;
	for (elem in array)
		list[listlen++] = elem;

	for (i = 0; i < listlen; i++) {
		for (j = i + 1; j < listlen; j++) {
			if (list[j] < list[i]) {
				tmp = list[i];
				list[i] = list[j];
				list[j] = tmp;
			}
		}
	}

	result = "";
	for (i = 0; i < listlen; i++) {
		result = result prefix list[i];
	}
	return result;
}

function cleanup() {
	eval_next_empty_line = no;
	delete lines;
	nlines = 0;
	delete keywords;
	comment_lines = 0;
	print_noncomment_lines = yes;
	print_this_section = yes;
}

function dprint(msg) {
	if (!debug) return;
	print(FILENAME ":" FNR ": " msg);
}

function dprint_skip(word, reason) {
	if (!debug) return;
	print(FILENAME ":" FNR ": \"" word "\" is no keyword because " reason);
}

function array_is_empty(arr,   i) {
	for (i in arr)
		return no;
	return yes;
}

{
	print_this_line = $0 != "" && !(!eval_next_empty_line && $0 == "#");
	eval_next_empty_line = yes;
}

# There is no need to print the RCS Id, since the full pathname
# is prefixed to the file contents.
/^#.*\$.*\$$/ {
	print_this_line = no;
	eval_next_empty_line = no;
}

# The lines containing the keywords should also not occur in
# the output for now. This decision is not final since it may
# be helpful for the user to know by which keywords a topic
# can be reached.
$1 == "#" && $2 == "Keywords:" {
	for (i = 3; i <= NF; i++) {
		w = ($i == toupper($i)) ? tolower($i) : $i;
		sub(/,$/, "", w);
		keywords[w] = yes;
		dprint("Adding keyword \"" w "\"");
	}
	print_this_line = no;
	eval_next_empty_line = no;
}

$0 == "#" {
	eval_next_empty_line = yes;
}

$1 == "#" && $2 == "Copyright" {
	dprint("Ignoring the section because it contains \"Copyright\".");
	print_this_section = no;
}

# Don't show the user the definition of make targets, since they are
# usually not interesting enough. This allows the comments to reach
# until the line directly above the target definition.
#
$1 ~ /:$/ && $2 == ".PHONY" {
	end_of_topic();
}

print_this_line {
	lines[nlines++] = $0;
}

# Check whether the current line contains a keyword. Such a keyword must
# be all-lowercase (make targets) or all-uppercase (variable names).
# Everything else is assumed to belong to the explaining text.
#
eval_this_line && NF >= 1 && !/^[\t.]/ && !/^#*$/ && !/^#\t\t/ {
	w = ($1 ~ /^#[A-Z]/) ? substr($1, 2) : ($1 == "#") ? $2 : $1;

	# Reduce VAR.<param>, VAR.${param} and VAR.* to VAR.
	sub(/\.[<$].*[>}]$/, "", w);
	sub(/\.\*$/, "", w);

	if (w ~ /\+=$/) {
		# Appending to a variable is usually not a definition.
		dprint_skip(w, "it is appended to a variable");

	} else if (w != toupper(w) && w != tolower(w)) {
		# Words in mixed case are not taken as keywords. If you
		# want them anyway, list them in a "Keywords:" line.
		dprint_skip(w, "it is mixed case");

	} else if (w !~ /^[A-Za-z][-0-9A-Z_a-z]*[0-9A-Za-z]([,:]|\?=|=)?$/) {
		# Keywords must consist only of letters, digits, hyphens
		# and underscores; except for some trailing type specifier.
		dprint_skip(w, "it contains special characters");

	} else if (NF > 2 && w == tolower(w)) {
		dprint_skip(w, "it is lowercase and followed by other words");

	} else if (/^#[ \t][ \t]/ && w == tolower(w)) {
		dprint_skip(w, "it is indented by several spaces");

	} else if (w == tolower(w) && w !~ /:$/ && $0 != "# " w) {
		# Lower-case words (often make targets) must be followed
		# by a colon to be recognized as keywords.
		dprint_skip(w, "it is lowercase and not followed by a colon");

	} else if (w == toupper(w) && w ~ /:$/) {
		# Upper-case words ending with a colon are probably not
		# make targets, so ignore them. Common cases are tags
		# like FIXME and TODO.
		dprint_skip(w, "it is uppercase and followed by a colon");

	} else {
		sub(/^#[ \t]*/, "", w);
		sub(/(:|\?=|=)$/, "", w);
		sub(/[,:]$/, "", w);
		if (w != "") {
			if (debug) dprint("Adding keyword \"" w "\"");
			keywords[w] = yes;
		}
	}
}

# Don't print the implementation of make targets.
$1 ~ /:$/ {
	print_noncomment_lines = no;
}

eval_this_line && $1 == "#" {
	comment_lines++;
}

/^$/ || last_fname != FILENAME {
	end_of_topic();
}

index(tolower($0), topic) != 0 {
	all_occurrences[FILENAME] = yes;
}

{
	eval_this_line = substr($0, length($0)) != "\\";
	last_fname = FILENAME;
}

END {
	end_of_topic();
	if (print_index) {
		print "Available help topics:";
		print sorted_keys(all_keywords, "\n");
	} else if (!found_anything) {
		occurrences = sorted_keys(all_occurrences, "\n");
		if (occurrences != "") {
			print "No help found for " topic ", but it occurs in:\n" occurrences;
		} else {
			print "No help found for " topic ".";
		}
	}
}