Brian Silverman | 70325d6 | 2015-09-20 17:00:43 -0400 | [diff] [blame] | 1 | // Copyright (c) 2006, Google Inc. |
| 2 | // All rights reserved. |
| 3 | // |
| 4 | // Redistribution and use in source and binary forms, with or without |
| 5 | // modification, are permitted provided that the following conditions are |
| 6 | // met: |
| 7 | // |
| 8 | // * Redistributions of source code must retain the above copyright |
| 9 | // notice, this list of conditions and the following disclaimer. |
| 10 | // * Redistributions in binary form must reproduce the above |
| 11 | // copyright notice, this list of conditions and the following disclaimer |
| 12 | // in the documentation and/or other materials provided with the |
| 13 | // distribution. |
| 14 | // * Neither the name of Google Inc. nor the names of its |
| 15 | // contributors may be used to endorse or promote products derived from |
| 16 | // this software without specific prior written permission. |
| 17 | // |
| 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 19 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 20 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 21 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 22 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 24 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 28 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 | |
| 30 | // --- |
| 31 | // |
| 32 | // A utility for checking syntax and generating headers to |
| 33 | // use with Google Templates. |
| 34 | // |
| 35 | // For example: |
| 36 | // |
| 37 | // > <path_to>/make_tpl_varnames_h some_template_file.tpl |
| 38 | // |
| 39 | // This creates the header file some_template_file.tpl.varnames.h. If |
| 40 | // there are any syntax errors they are reported to stderr (in which |
| 41 | // case, no header file is created). |
| 42 | // |
| 43 | // |
| 44 | // Exit code is the number of templates we were unable to parse. |
| 45 | // |
| 46 | // Headers can be all written to one output file (via --outputfile) |
| 47 | // or written to one output file per template processed (via --header_dir). |
| 48 | // As such, we have a first stage where we load each template and generate |
| 49 | // its headers and a second stage where we write the headers to disk. |
| 50 | // |
| 51 | // TODO(jad): Prevent -f and -o from being used together. |
| 52 | // Previously -o would be silently ignored. |
| 53 | |
| 54 | // This is for windows. Even though we #include config.h, just like |
| 55 | // the files used to compile the dll, we are actually a *client* of |
| 56 | // the dll, so we don't get to decl anything. |
| 57 | #include <config.h> |
| 58 | #undef CTEMPLATE_DLL_DECL |
| 59 | #include <ctype.h> // for toupper(), isalnum() |
| 60 | #include <errno.h> |
| 61 | #ifdef HAVE_GETOPT_H |
| 62 | # include <getopt.h> |
| 63 | #endif |
| 64 | #include <stdarg.h> |
| 65 | #include <stdio.h> |
| 66 | #include <stdlib.h> |
| 67 | #include <string.h> |
| 68 | #ifdef HAVE_UNISTD_H |
| 69 | # include <unistd.h> |
| 70 | #endif |
| 71 | #include <string> |
| 72 | #include <set> |
| 73 | #include <vector> |
| 74 | |
| 75 | #include <ctemplate/template_pathops.h> |
| 76 | #include <ctemplate/template.h> |
| 77 | using std::set; |
| 78 | using std::string; |
| 79 | using std::vector; |
| 80 | using GOOGLE_NAMESPACE::Template; |
| 81 | |
| 82 | enum {LOG_INFO, LOG_WARNING, LOG_ERROR, LOG_FATAL}; |
| 83 | |
| 84 | // Holds information on each template we process. |
| 85 | struct TemplateRecord { |
| 86 | const string name; // filename given on cmd-line (may be relative |
| 87 | bool error; // true iff an error occurred during template loading |
| 88 | string header_entries; // output of tpl->WriteHeaderEntries() |
| 89 | |
| 90 | explicit TemplateRecord(const string& aname) |
| 91 | : name(aname), error(false) { |
| 92 | } |
| 93 | }; |
| 94 | |
| 95 | static void LogPrintf(int severity, int should_log_info, const char* pat, ...) { |
| 96 | if (severity == LOG_INFO && !should_log_info) |
| 97 | return; |
| 98 | if (severity == LOG_FATAL) |
| 99 | fprintf(stderr, "FATAL ERROR: "); |
| 100 | va_list ap; |
| 101 | va_start(ap, pat); |
| 102 | vfprintf(stderr, pat, ap); |
| 103 | va_end(ap); |
| 104 | fprintf(stderr, "\n"); |
| 105 | if (severity == LOG_FATAL) |
| 106 | exit(1); |
| 107 | } |
| 108 | |
| 109 | // prints to outfile -- usually stdout or stderr |
| 110 | static void Usage(const char* argv0, FILE* outfile) { |
| 111 | fprintf(outfile, "USAGE: %s [-t<dir>] [-o<dir>] [-s<suffix>] [-f<filename>]" |
| 112 | " [-n] [-d] [-q] <template_filename> ...\n", argv0); |
| 113 | fprintf(outfile, |
| 114 | " -t<dir> --template_dir=<dir> Root directory of templates\n" |
| 115 | " -o<dir> --header_dir=<dir> Where to place output files\n" |
| 116 | " -s<suffix> --outputfile_suffix=<suffix>\n" |
| 117 | " outname = inname + suffix\n" |
| 118 | " -f<filename> --outputfile=<filename>\n" |
| 119 | " outname = filename (when given, \n" |
| 120 | " --header_dir is ignored)\n" |
| 121 | " -n --noheader Just check syntax, no output\n" |
| 122 | " -d --dump_templates Cause templates dump contents\n" |
| 123 | " -q --nolog_info Only log on error\n" |
| 124 | " --v=-1 Obsolete, confusing synonym for -q\n" |
| 125 | " -h --help This help\n" |
| 126 | " -V --version Version information\n"); |
| 127 | fprintf(outfile, "\n" |
| 128 | "This program checks the syntax of one or more google templates.\n" |
| 129 | "By default (without -n) it also emits a header file to an output\n" |
| 130 | "directory that defines all valid template keys. This can be used\n" |
| 131 | "in programs to minimze the probability of typos in template code.\n"); |
| 132 | } |
| 133 | |
| 134 | static void Version(FILE* outfile) { |
| 135 | fprintf(outfile, |
| 136 | "make_tpl_varnames_h " |
| 137 | " (part of " PACKAGE_STRING ")" |
| 138 | "\n\n" |
| 139 | "Copyright 1998 Google Inc.\n" |
| 140 | "\n" |
| 141 | "This is BSD licensed software; see the source for copying conditions\n" |
| 142 | "and license information.\n" |
| 143 | "There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n" |
| 144 | "PARTICULAR PURPOSE.\n" |
| 145 | ); |
| 146 | } |
| 147 | |
| 148 | // Removes all non alphanumeric characters from a string to form a |
| 149 | // valid C identifier to use as a double-inclusion guard. |
| 150 | static void ConvertToIdentifier(string* s) { |
| 151 | for (string::size_type i = 0; i < s->size(); i++) { |
| 152 | if (!isalnum((*s)[i])) |
| 153 | (*s)[i] = '_'; |
| 154 | else |
| 155 | (*s)[i] = toupper((*s)[i]); |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | // Returns the given header entries wrapped with a compiler guard |
| 160 | // whose name is generated from the output_file name. |
| 161 | static string WrapWithGuard(const string& output_file, |
| 162 | const string& header_entries) { |
| 163 | string guard(string("TPL_") + output_file); |
| 164 | ConvertToIdentifier(&guard); |
| 165 | guard.append("_H_"); |
| 166 | |
| 167 | string out; |
| 168 | out.append(string("#ifndef ") + guard + "\n"); |
| 169 | out.append(string("#define ") + guard + "\n\n"); |
| 170 | |
| 171 | // Now append the header-entry info to the intro above |
| 172 | out.append(header_entries); |
| 173 | |
| 174 | out.append(string("\n#endif // ") + guard + "\n"); |
| 175 | return out; |
| 176 | } |
| 177 | |
| 178 | // Generates a multi-line comment that will go at the top of the output file. |
| 179 | // The comment includes the filename(s) that produced the output, one per line. |
| 180 | static string Boilerplate(const string& progname, |
| 181 | const vector<string>& filenames) { |
| 182 | string out(string("//\n")); |
| 183 | if (filenames.size() > 1) |
| 184 | out.append("// This header file auto-generated for the templates\n"); |
| 185 | else |
| 186 | out.append("// This header file auto-generated for the template\n"); |
| 187 | |
| 188 | for (vector<string>::size_type i = 0; i < filenames.size(); ++i) |
| 189 | out.append("// " + filenames[i] + "\n"); |
| 190 | |
| 191 | out.append("// by " + progname + "\n" + |
| 192 | "// DO NOT MODIFY THIS FILE DIRECTLY\n" + |
| 193 | "//\n"); |
| 194 | return out; |
| 195 | } |
| 196 | |
| 197 | // Returns true iff line is empty or only contains whitespace |
| 198 | // (space, horizontal tab, vertical tab, form feed, carriage return). |
| 199 | static bool LineIsAllWhitespace(const string& input) { |
| 200 | static const string kWhitespace(" \f\t\v\r"); |
| 201 | return input.find_first_not_of(kWhitespace) == string::npos; |
| 202 | } |
| 203 | |
| 204 | // Splits the input string into lines using the newline (\n) |
| 205 | // as delimiter. The newlines are discarded. |
| 206 | // An empty string input results in one empty line output. |
| 207 | // |
| 208 | // Examples: "Hello\nWorld\n" input results in two lines, |
| 209 | // "Hello" and "World". |
| 210 | // Same result for "Hello\nWorld" (not newline terminated). |
| 211 | // |
| 212 | static vector<string> SplitIntoLines(const string &input) { |
| 213 | vector<string> lines; |
| 214 | |
| 215 | string::size_type begin_index = 0; |
| 216 | string::size_type input_len = input.length(); |
| 217 | |
| 218 | while (1) { |
| 219 | string::size_type end_index = input.find_first_of('\n', begin_index); |
| 220 | if (end_index == string::npos) { |
| 221 | lines.push_back(input.substr(begin_index)); |
| 222 | break; |
| 223 | } |
| 224 | lines.push_back(input.substr(begin_index, (end_index - begin_index))); |
| 225 | begin_index = end_index + 1; |
| 226 | if (begin_index >= input_len) // To avoid adding a trailing empty line. |
| 227 | break; |
| 228 | } |
| 229 | return lines; |
| 230 | } |
| 231 | |
| 232 | // Receives header entries concatenated together from one or more |
| 233 | // templates and returns a string with the duplicate lines removed. |
| 234 | // |
| 235 | // Duplicate lines that contain only whitespace are not removed, |
| 236 | // all other duplicate lines (identical #include directives and |
| 237 | // identical variable definitions) are removed. If the last |
| 238 | // (or only) input line did not terminate with newline, we add one. |
| 239 | // |
| 240 | // Consider the following two templates: |
| 241 | // ex1.tpl: <p>{{USER}}</p> |
| 242 | // ex2.tpl: <a href="{{URL}}">{{USER}}</a> |
| 243 | // |
| 244 | // The header entries for ex1.tpl are: |
| 245 | // #include "template/template_string.h" |
| 246 | // static const ::GOOGLE_NAMESPACE::StaticTemplateString ke_USER = |
| 247 | // STS_INIT_WITH_HASH(ke_USER, "USER", 3254611514008215315LLU); |
| 248 | // |
| 249 | // The header entries for ex2.tpl are: |
| 250 | // #include "template/template_string.h" |
| 251 | // static const ::GOOGLE_NAMESPACE::StaticTemplateString ke_URL = |
| 252 | // STS_INIT_WITH_HASH(ke_URL, "URL", 1026025273225241985LLU); |
| 253 | // static const ::GOOGLE_NAMESPACE::StaticTemplateString ke_USER = |
| 254 | // STS_INIT_WITH_HASH(ke_USER, "USER", 3254611514008215315LLU); |
| 255 | // |
| 256 | // Simply concatenating both header entries will result in |
| 257 | // duplicate #include directives and duplicate definitions of |
| 258 | // the ke_USER variable. This function instead outputs: |
| 259 | // |
| 260 | // #include "template/template_string.h" |
| 261 | // static const ::GOOGLE_NAMESPACE::StaticTemplateString ke_USER = |
| 262 | // STS_INIT_WITH_HASH(ke_USER, "USER", 3254611514008215315LLU); |
| 263 | // static const ::GOOGLE_NAMESPACE::StaticTemplateString ke_URL = |
| 264 | // STS_INIT_WITH_HASH(ke_URL, "URL", 1026025273225241985LLU); |
| 265 | // |
| 266 | static string TextWithDuplicateLinesRemoved(const string& header_entries) { |
| 267 | string output; |
| 268 | set<string> lines_seen; |
| 269 | vector<string> lines = SplitIntoLines(header_entries); |
| 270 | const int lines_len = lines.size(); |
| 271 | for (int i = 0; i < lines_len; ++i) { |
| 272 | const string& line = lines[i]; |
| 273 | if (LineIsAllWhitespace(line) || // Blank lines always go in |
| 274 | !lines_seen.count(line)) { // So do new lines |
| 275 | output.append(line); |
| 276 | output.append("\n"); |
| 277 | lines_seen.insert(line); |
| 278 | } |
| 279 | } |
| 280 | return output; |
| 281 | } |
| 282 | |
| 283 | // Writes the given text to the filename header_file. |
| 284 | // Returns true if it succeeded, false otherwise. |
| 285 | static bool WriteToDisk(bool log_info, const string& output_file, |
| 286 | const string& text) { |
| 287 | FILE* outfile = fopen(output_file.c_str(), "wb"); |
| 288 | if (!outfile) { |
| 289 | LogPrintf(LOG_ERROR, log_info, "Can't open %s", output_file.c_str()); |
| 290 | return false; |
| 291 | } |
| 292 | LogPrintf(LOG_INFO, log_info, "Creating %s", output_file.c_str()); |
| 293 | if (fwrite(text.c_str(), 1, text.length(), outfile) != text.length()) { |
| 294 | LogPrintf(LOG_ERROR, log_info, "Can't write %s: %s", |
| 295 | output_file.c_str(), strerror(errno)); |
| 296 | } |
| 297 | fclose(outfile); |
| 298 | return true; |
| 299 | } |
| 300 | |
| 301 | int main(int argc, char **argv) { |
| 302 | string FLAG_template_dir(GOOGLE_NAMESPACE::kCWD); // "./" |
| 303 | string FLAG_header_dir(GOOGLE_NAMESPACE::kCWD); |
| 304 | GOOGLE_NAMESPACE::NormalizeDirectory(&FLAG_header_dir); // adds trailing slash |
| 305 | string FLAG_outputfile_suffix(".varnames.h"); |
| 306 | string FLAG_outputfile(""); |
| 307 | bool FLAG_header = true; |
| 308 | bool FLAG_dump_templates = false; |
| 309 | bool FLAG_log_info = true; |
| 310 | |
| 311 | #if defined(HAVE_GETOPT_LONG) |
| 312 | static struct option longopts[] = { |
| 313 | {"help", 0, NULL, 'h'}, |
| 314 | {"version", 0, NULL, 'V'}, |
| 315 | {"template_dir", 1, NULL, 't'}, |
| 316 | {"header_dir", 1, NULL, 'o'}, |
| 317 | {"outputfile_suffix", 1, NULL, 's'}, |
| 318 | {"outputfile", 1, NULL, 'f'}, |
| 319 | {"noheader", 0, NULL, 'n'}, |
| 320 | {"dump_templates", 0, NULL, 'd'}, |
| 321 | {"nolog_info", 0, NULL, 'q'}, |
| 322 | {"v", 1, NULL, 'q'}, |
| 323 | {0, 0, 0, 0} |
| 324 | }; |
| 325 | int option_index; |
| 326 | # define GETOPT(argc, argv) getopt_long(argc, argv, "t:o:s:f:ndqhV", \ |
| 327 | longopts, &option_index) |
| 328 | #elif defined(HAVE_GETOPT_H) |
| 329 | # define GETOPT(argc, argv) getopt(argc, argv, "t:o:s:f:ndqhV") |
| 330 | #else // TODO(csilvers): implement something reasonable for windows |
| 331 | # define GETOPT(argc, argv) -1 |
| 332 | int optind = 1; // first non-opt argument |
| 333 | const char* optarg = ""; // not used |
| 334 | #endif |
| 335 | |
| 336 | int r = 0; |
| 337 | while (r != -1) { // getopt()/getopt_long() return -1 upon no-more-input |
| 338 | r = GETOPT(argc, argv); |
| 339 | switch (r) { |
| 340 | case 't': FLAG_template_dir.assign(optarg); break; |
| 341 | case 'o': FLAG_header_dir.assign(optarg); break; |
| 342 | case 's': FLAG_outputfile_suffix.assign(optarg); break; |
| 343 | case 'f': FLAG_outputfile.assign(optarg); break; |
| 344 | case 'n': FLAG_header = false; break; |
| 345 | case 'd': FLAG_dump_templates = true; break; |
| 346 | case 'q': FLAG_log_info = false; break; |
| 347 | case 'V': Version(stdout); return 0; break; |
| 348 | case 'h': Usage(argv[0], stderr); return 0; break; |
| 349 | case -1: break; // means 'no more input' |
| 350 | default: Usage(argv[0], stderr); return 1; break; |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | if (optind >= argc) { |
| 355 | LogPrintf(LOG_FATAL, FLAG_log_info, |
| 356 | "Must specify at least one template file on the command line."); |
| 357 | } |
| 358 | |
| 359 | Template::SetTemplateRootDirectory(FLAG_template_dir); |
| 360 | |
| 361 | |
| 362 | // Initialize the TemplateRecord array. It holds one element per |
| 363 | // template given on the command-line. |
| 364 | vector<TemplateRecord*> template_records; |
| 365 | for (int i = optind; i < argc; ++i) { |
| 366 | TemplateRecord *template_rec = new TemplateRecord(argv[i]); |
| 367 | template_records.push_back(template_rec); |
| 368 | } |
| 369 | |
| 370 | // Iterate through each template and (unless -n is given), write |
| 371 | // its header entries into the headers array. |
| 372 | int num_errors = 0; |
| 373 | for (vector<TemplateRecord*>::iterator it = template_records.begin(); |
| 374 | it != template_records.end(); ++it) { |
| 375 | const char* tplname = (*it)->name.c_str(); |
| 376 | LogPrintf(LOG_INFO, FLAG_log_info, "\n------ Checking %s ------", tplname); |
| 377 | |
| 378 | // The last two arguments in the following call do not matter |
| 379 | // since they control how the template gets expanded and we never |
| 380 | // expand the template after loading it here |
| 381 | Template * tpl = Template::GetTemplate(tplname, GOOGLE_NAMESPACE::DO_NOT_STRIP); |
| 382 | |
| 383 | // The call to GetTemplate (above) loads the template from disk |
| 384 | // and attempts to parse it. If it cannot find the file or if it |
| 385 | // detects any template syntax errors, the parsing routines |
| 386 | // report the error and GetTemplate returns NULL. Syntax errors |
| 387 | // include such things as mismatched double-curly-bracket pairs, |
| 388 | // e.g. '{{VAR}', Invalid characters in template variables or |
| 389 | // section names, e.g. '{{BAD_VAR?}}' [the question mark is |
| 390 | // illegal], improperly nested section/end section markers, |
| 391 | // e.g. a section close marker with no section start marker or a |
| 392 | // section start of a different name. |
| 393 | // If that happens, since the parsing errors have already been reported |
| 394 | // we just continue on to the next one. |
| 395 | if (!tpl) { |
| 396 | LogPrintf(LOG_ERROR, FLAG_log_info, "Could not load file: %s", tplname); |
| 397 | num_errors++; |
| 398 | (*it)->error = true; |
| 399 | continue; |
| 400 | } else { |
| 401 | LogPrintf(LOG_INFO, FLAG_log_info, "No syntax errors detected in %s", |
| 402 | tplname); |
| 403 | if (FLAG_dump_templates) |
| 404 | tpl->Dump(tpl->template_file()); |
| 405 | } |
| 406 | |
| 407 | // The rest of the loop creates the header file |
| 408 | if (!FLAG_header) |
| 409 | continue; // They don't want header files |
| 410 | |
| 411 | tpl->WriteHeaderEntries(&((*it)->header_entries)); |
| 412 | } |
| 413 | |
| 414 | // We have headers to emit: |
| 415 | // . If --outputfile was given, we combine all the header entries and |
| 416 | // write them to the given output file. If any template had errors, |
| 417 | // we fail and do not generate an output file. |
| 418 | // . Otherwise, we write one output file per template we processed. |
| 419 | // . In both cases, we add proper boilerplate first. |
| 420 | if (FLAG_header) { |
| 421 | string progname = argv[0]; |
| 422 | |
| 423 | if (!FLAG_outputfile.empty()) { // All header entries written to one file. |
| 424 | // If any template had an error, we do not produce an output file. |
| 425 | if (num_errors == 0) { |
| 426 | vector<string> template_filenames; |
| 427 | string all_header_entries; |
| 428 | for (vector<TemplateRecord*>::const_iterator |
| 429 | it = template_records.begin(); it != template_records.end(); ++it) { |
| 430 | all_header_entries.append((*it)->header_entries); |
| 431 | template_filenames.push_back((*it)->name); |
| 432 | } |
| 433 | string output = Boilerplate(progname, template_filenames); |
| 434 | const string cleantext = |
| 435 | TextWithDuplicateLinesRemoved(all_header_entries); |
| 436 | output.append(WrapWithGuard(FLAG_outputfile, cleantext)); |
| 437 | if (!WriteToDisk(FLAG_log_info, FLAG_outputfile, output)) |
| 438 | num_errors++; |
| 439 | } |
| 440 | } else { |
| 441 | // Each template will have its own output file. Skip any that had errors. |
| 442 | for (vector<TemplateRecord*>::const_iterator |
| 443 | it = template_records.begin(); it != template_records.end(); ++it) { |
| 444 | if ((*it)->error) |
| 445 | continue; |
| 446 | string basename = GOOGLE_NAMESPACE::Basename((*it)->name); |
| 447 | string output_file = |
| 448 | GOOGLE_NAMESPACE::PathJoin(FLAG_header_dir, |
| 449 | basename + FLAG_outputfile_suffix); |
| 450 | vector<string> template_filenames; // Contains one template filename. |
| 451 | template_filenames.push_back((*it)->name); |
| 452 | string output = Boilerplate(progname, template_filenames); |
| 453 | output.append(WrapWithGuard(output_file, (*it)->header_entries)); |
| 454 | if (!WriteToDisk(FLAG_log_info, output_file, output)) |
| 455 | num_errors++; |
| 456 | } |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | // Free dynamic memory |
| 461 | for (vector<TemplateRecord*>::iterator it = template_records.begin(); |
| 462 | it != template_records.end(); ++it) { |
| 463 | delete *it; |
| 464 | } |
| 465 | |
| 466 | // Cap at 127 to avoid causing problems with return code |
| 467 | return num_errors > 127 ? 127 : num_errors; |
| 468 | } |