]> git.draconx.ca Git - dxcommon.git/blob - scripts/gen-strtab.awk
Explicitly test for empty strings in awk scripts.
[dxcommon.git] / scripts / gen-strtab.awk
1 #!/bin/awk -f
2 #
3 # Copyright © 2021, 2023 Nick Bowler
4 #
5 # Generate a C string table based on an input string specification file.
6 #
7 # A string table is a single large char single array containing all of
8 # the specified (0-terminated) strings, which is then offset to obtain
9 # the desired string.  By storing these offsets instead of string pointers
10 # into read-only data structures, this can reduce the need for relocation
11 # processing at startup when programs are built in PIC mode.
12 #
13 # The string specification file is processed line by line.  Comment
14 # lines may be included by beginning the line with a # character, which
15 # must be the very first character on the line.  If a comment is encountered,
16 # processing immediately moves on to the next line and the result is as if
17 # the comment line were omitted from the input.
18 #
19 # Options may be used to alter the normal behaviour.  An option is placed
20 # on a line by itself beginning with an @ character, and may appear anywhere
21 # in the input file.  The following options are defined:
22 #
23 #   @nozero
24 #     All strings will have a non-zero offset in the strtab.
25 #
26 #   @macro
27 #     Instead of a variable declaration, the generated header will define an
28 #     object-like macro that can be used as the initializer for a char array.
29 #
30 # A string is defined by beginning a line with one or two & characters, which
31 # must be immediately followed by a C identifier.  Two & characters indicates
32 # a string that should not be translated, as described below.  A nonempty
33 # sequence of whitespace (with at most one newline) separates the identifier
34 # from the beginning of the string itself.  This whitespace is never included
35 # in the output.
36 #
37 # The string is then interpreted as follows:
38 #
39 #   - Leading blanks on each line are ignored.
40 #   - The sequences \\, \a, \b, \t, \n, \v, \f and \r can be entered and
41 #     mean the same as they do in C string literals.  The "\\" sequence
42 #     prevents any special interpretation of the second backslash.
43 #   - Newlines in the input are included in the output, except for the
44 #     where the entire string (including its identifier) are on one line.
45 #   - If this is not desired, a newline which is immediately preceded by an
46 #     unescaped backslash will deleted, along with the backslash.
47 #   - All other backslashes are deleted.  This can be used to prevent special
48 #     handling of whitespace, # or & characters at the beginning of a line.
49 #
50 # Unless the @macro option is specified, the output defines a variable,
51 # strtab, which contains all of the strings, and each identifier in the input
52 # is declared as an emumeration constant whose value is the offset of the
53 # associated string within strtab.  Otherwise, if the @macro option is
54 # specified, no variables are defined and STRTAB_INITIALIZER object-like macro
55 # may be used to initialize a char array with static storage duration.
56 #
57 # Normally, the generated source code wraps strings using the identity macro
58 # N_(x), which has no effect on the resulting data structures but enables tools
59 # such as xgettext to extract translatable strings from the source code.  An
60 # identifier preceded by two ampersands (&&) suppresses this output to allow
61 # a single string table to also contain both translateable strings as well as
62 # ones that should not be translated.
63 #
64 # The object-like macro STRTAB_MAX_OFFSET is defined and expands to the
65 # greatest string offset, suitable for use in #if preprocessing directives.
66 #
67 # License WTFPL2: Do What The Fuck You Want To Public License, version 2.
68 # This is free software: you are free to do what the fuck you want to.
69 # There is NO WARRANTY, to the extent permitted by law.
70
71 END {
72   print "/*"
73   if (FILENAME) {
74     print " * Automatically generated by gen-strtab.awk from " FILENAME
75   } else {
76     print " * Automatically generated by gen-strtab.awk"
77   }
78   print " * Do not edit."
79   print " */"
80 }
81
82 BEGIN {
83   # Check if "\\\\" in substitutions gives just one backslash.
84   bs = "x"; sub(/x/, "\\\\", bs);
85   bs = (length(bs) == 1 ? "\\\\" : "\\");
86
87   opts["zero"] = 1
88   opts["macro"] = 0
89   collected = ident = ""
90   startline = endline = 0
91   num_vars = 0
92 }
93
94 # Comments
95 NF == 0 || $0 ~ /^[#]/ { next }
96
97 # Options
98 sub(/^@/, "", $0) {
99   if (NF == 1) {
100     orig=$1
101     gsub(/-/, "_", $1);
102     val = !sub(/^no_?/, "", $1);
103     if ($1 in opts) {
104       opts[$1] = val;
105     } else {
106       print "error: unrecognized option: @" orig | "cat 1>&2"
107       exit 1
108     }
109   }
110   next
111 }
112
113 sub(/^[&]/, "") {
114   if (ident != "") {
115     finish_string_input(strings, ident, collected);
116     vars[num_vars++] = ident;
117   }
118
119   current_l10n = !sub(/^[&]/, "", $1);
120   startline = NR
121   ident = $1
122
123   $1 = ""
124   collected = ""
125 }
126
127 ident != "" {
128   sub(/^[ \t]*/, "")
129   if (collected) {
130     collected = collected "\n" $0
131   } else {
132     collected = $0
133   }
134
135   endline = NR
136 }
137
138 END {
139   if (ident != "") {
140     finish_string_input(strings, ident, collected)
141     vars[num_vars++] = ident
142   }
143 }
144
145 END {
146   strtab = cont = ""
147   strtab_len = 0
148   count = bucketsort(sorted_strings, strings)
149   max = 0
150
151   print "\n#define STR_L10N_(x)"
152   print "#ifndef N_"
153   print "#  define N_(x) x"
154   print "#endif"
155   if (opts["macro"]) {
156     cont = " \\";
157     print "\n#define STRTAB_INITIALIZER" cont;
158   } else {
159     print "\nstatic const char strtab[] =";
160   }
161
162   for (i = 0; i < count; i++) {
163     s = sorted_strings[i]
164     gsub(/\\\\/, "\2", s)
165     if ((n = index(strtab "\1", s "\1")) > 0) {
166       offsets[sorted_strings[i]] = real_length(substr(strtab, 1, n-1));
167       if (!(sorted_strings[i] in nol10n))
168         print "\tSTR_L10N_(N_(\"" sorted_strings[i] "\"))" cont;
169     } else if (strtab) {
170       strtab = strtab "\1" s
171       offsets[sorted_strings[i]] = strtab_len + 1
172       strtab_len += real_length(s) + 1
173     } else {
174       strtab = s
175       offsets[sorted_strings[i]] = 0
176       strtab_len += real_length(s)
177     }
178   }
179
180   gsub("\2", bs bs, strtab);
181   n = split(strtab, split_strtab, "\1");
182   for (i = 1; i <= n; i++) {
183     printf("\t%4s ", i > !!opts["zero"] ? "\"\\0\"" : "");
184
185     if (split_strtab[i] in nol10n) {
186       print "\"" split_strtab[i] "\"" cont;
187     } else {
188       print "N_(\"" split_strtab[i] "\")" cont;
189     }
190   }
191   print "\t\"\"" substr(";", 1, !opts["macro"]);
192
193   print "enum {"
194   for (i = 0; i < num_vars; i++) {
195     sep = (i+1) != num_vars ? "," : ""
196     s = vars[i]
197     o = offsets[strings[s]] + (!opts["zero"])
198     print "\t" s " = " o sep
199     if (o > max) {
200       max = o
201     }
202   }
203   print "};"
204   print "\n#define STRTAB_MAX_OFFSET " max
205 }
206
207 # finish_string_input(strings, ident, val)
208 #
209 # Deal with backslash-escapes and special characters in val, then set
210 # strings[ident] = val.
211 function finish_string_input(strings, ident, val, n, tmpval)
212 {
213   gsub(/\\\\/, "\1", val);
214   if (endline > startline)
215     val = val "\n";
216   gsub(/\\\n/, "", val);
217
218   tmpval = ""
219   while ((n = match(val, /\\[^abtnvfr]/)) > 0) {
220     tmpval = tmpval substr(val, 1, n-1)
221     val = substr(val, n+1)
222   }
223   tmpval = tmpval val
224
225   # Escape special characters
226   gsub(/"/,  bs"\"", tmpval)
227   gsub(/\t/, bs"t", tmpval)
228   gsub(/\n/, bs"n", tmpval)
229   gsub("\1", bs bs, tmpval)
230
231   strings[ident] = tmpval
232   if (!current_l10n) {
233     nol10n[tmpval] = 1;
234   }
235 }
236
237 function real_length(s, t)
238 {
239   t = length(s)
240   return t - gsub(/\\./, "&", s)
241 }
242
243 # bucketsort(dst, src)
244 #
245 # Sort the elements of src by descending string length,
246 # placing them into dst[0] ... dst[n].
247 #
248 # Returns the number of elements.
249 function bucketsort(dst, src, max, count, i, t)
250 {
251   # Note: ULTRIX 4.5 nawk does not support local array parameters
252   split("", bucketsort_buckets);
253
254   for (t in src) {
255     i = length(src[t])
256     if (i > max) { max = i }
257     bucketsort_buckets[i]++
258   }
259
260   for (i = max; i > 0; i--) {
261     if (i in bucketsort_buckets) {
262       t = bucketsort_buckets[i]
263       bucketsort_buckets[i] = count
264       count += t
265     }
266   }
267
268   for (t in src) {
269     i = length(t = src[t])
270     dst[bucketsort_buckets[i]++] = t
271   }
272
273   return count
274 }