From: Francis Dupont Date: Wed, 25 Feb 2015 17:54:10 +0000 (+0100) Subject: [trac3516] C++ version of system_messages.py X-Git-Tag: trac3874_base~8^2~8 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=638666c9be658ae4b352e4c51f6f448c435db275;p=thirdparty%2Fkea.git [trac3516] C++ version of system_messages.py --- diff --git a/tools/kt_system_messages.cc b/tools/kt_system_messages.cc new file mode 100644 index 0000000000..e875b1e3e5 --- /dev/null +++ b/tools/kt_system_messages.cc @@ -0,0 +1,579 @@ +// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +// Produce System Messages Manual +// +// This tool reads all the .mes files in the directory tree whose root is given +// on the command line and interprets them as message files. It pulls +// all the messages and description out, sorts them by message ID, and writes +// them out as a single (formatted) file. +// +// Invocation: +// The code is invoked using the command line: +// +// kt_system_messages.py [-o ] [|] +// +// If no output file is specified, output is written to stdout. + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +typedef std::vector lines_type; + +/// dictionary values +struct Details { + std::string text; + lines_type description; + std::string filename; +}; + +/// Main dictionary holding all the messages. The messages are +/// accumulated here before being printed in alphabetical order. +typedef std::map dictionary_type; +dictionary_type dictionary; + +// The structure of the output page is: +// +// header +// message +// separator +// message +// separator +// : +// separator +// message +// trailer +// +// (Indentation is not relevant - it has only been added to the above +// illustration to make the structure clearer.) The text of these section is: + +// Header - this is output before anything else. +const std::string SEC_HEADER = +"\n\ +\n\ +\n\ +%version;\n\ +]>\n\ +\n\ +\n\ + \n\ +\n\ + \n\ + Kea Messages Manual\n\ +\n\ + \n\ + 2011-2014Internet Systems Consortium, Inc.\n\ + \n\ +\n\ + \n\ + \n\ + This is the messages manual for Kea version &__VERSION__;.\n\ +\t The most up-to-date version of this document, along with\n\ +\t other documents for Kea, can be found at\n\ + .\n\ + \n\ + \n\ +\n\ + This is the messages manual for Kea version\n\ + &__VERSION__;.\n\ + \n\ +\n\ + \n\ + Introduction\n\ + \n\ + This document lists each message that can be logged by the\n\ + programs in the Kea package. Each entry in this manual\n\ + is of the form:\n\ + IDENTIFICATION message-text\n\ + ... where \"IDENTIFICATION\" is the message identification included\n\ + in each message logged and \"message-text\" is the accompanying\n\ + message text. The \"message-text\" may include placeholders of the\n\ + form \"%1\", \"%2\" etc.; these parameters are replaced by relevant\n\ + values when the message is logged.\n\ + \n\ + \n\ + Each entry is also accompanied by a description giving more\n\ + information about the circumstances that result in the message\n\ + being logged.\n\ + \n\ + \n\ + For information on configuring and using Kea logging,\n\ + refer to the Kea Guide.\n\ + \n\ + \n\ +\n\ + \n\ + Kea Log Messages\n\ + \n\ + \n"; + +// This is output once for each message. The string contains +// substitution tokens: $I is replaced by the message identification, +// $T by the message text, and $D by the message description. +const std::string SEC_MESSAGE = +"\n\ +$I $T\n\ +\n\ +$D\n\ +"; + +// A description may contain blank lines intended to separate +// paragraphs. If so, each blank line is replaced by the following. +const std::string SEC_BLANK = ""; + +// The separator is copied to the output verbatim after each message except +// the last. +const std::string SEC_SEPARATOR = ""; + +// The trailier is copied to the output verbatim after the last message. +const std::string SEC_TRAILER = +" \n\ + \n\ + \n\ +"; + +/// Report an error and exit +void reportError(const std::string& filename, const std::string& what) +{ + std::cerr << "*** ERROR in " << filename << "\n"; + std::cerr << "*** REASON: " << what << "\n"; + std::cerr << "*** System message generator terminating" << "\n"; + exit(1); +} + + +/// Replaces the '<' and '>' in text about to be inserted into the template +/// sections above with < and > to avoid problems with message text +/// being interpreted as XML text. +std::string replaceTag(const std::string& src) +{ + std::string result; + for (std::string::const_iterator it = src.begin(); it != src.end(); ++it) { + if (*it == '<') { + result.append("<"); + } else if (*it == '>') { + result.append(">"); + } else { + result.push_back(*it); + } + } + return result; +} + +/// Replace $c in a string +std::string replaceShell(const std::string& src, char c, + const std::string& val) +{ + std::string result; + bool shell = false; + for (std::string::const_iterator it = src.begin(); it != src.end(); ++it) { + if (shell) { + if (*it == c) { + result.append(val); + } else { + result.push_back('$'); + result.push_back(*it); + } + shell = false; + } else if (*it == '$') { + shell = true; + } else { + result.push_back(*it); + } + } + return result; +} + +/// Replaces blank lines in an array with the contents of the 'blank' section. +lines_type replaceBlankLines(const lines_type lines) +{ + lines_type result; + for (lines_type::const_iterator l = lines.begin(); l != lines.end(); ++l) { + if (l->empty()) { + result.push_back(SEC_BLANK); + } else { + result.push_back(*l); + } + } + return result; +} + + +/// Printing functions +void printHeader() { + std::cout << SEC_HEADER << "\n"; +} + +void printSeparator() { + std::cout << SEC_SEPARATOR << "\n"; +} + +void printMessage(const std::string& msgid) +{ + //In the message ID, replace "<" and ">" with XML-safe versions and + // substitute into the data. + const std::string m0 = SEC_MESSAGE; + const std::string m1 = replaceShell(m0, 'I', replaceTag(msgid)); + + // Do the same for the message text. + std::string m2 = replaceShell(m1, 'T', + replaceTag(dictionary[msgid].text)); + + // Do the same for the description then replace blank lines with the + // specified separator. (We do this in that order to avoid replacing + // the "<" and ">" in the XML tags in the separator.) + lines_type desc0 = dictionary[msgid].description; + lines_type desc1; + for (lines_type::iterator l = desc0.begin(); l != desc0.end(); ++l) { + desc1.push_back(replaceTag(*l)); + } + lines_type desc2 = replaceBlankLines(desc1); + + // Join the lines together to form a single string and insert into + // current text. + std::string m3; + for (lines_type::iterator l = desc2.begin(); l != desc2.end(); ++l) { + m3.append(*l); + m3.push_back('\n'); + } + + std::cout << replaceShell(m2, 'D', m3) << "\n"; +} + +void printTrailer() { + std::cout << SEC_TRAILER << "\n"; +} + + +/// Removes leading and trailing empty lines. +/// +/// A list of strings is passed as argument, some of which may be empty. +/// This function removes from the start and end of list a contiguous +/// sequence of empty lines and returns the result. Embedded sequence of +/// empty lines are not touched. +/// +/// Parameters: +/// lines List of strings to be modified. +/// +/// Return: +/// Input list of strings with leading/trailing blank line sequences +/// removed. +lines_type removeEmptyLeadingTrailing(lines_type lines) +{ + lines_type retlines = lines; + + // Dispose of degenerate case of empty array + if (retlines.empty()) { + return retlines; + } + + // Search for first non-blank line + for (;;) { + lines_type::iterator start = retlines.begin(); + if (start == retlines.end()) { + return retlines; + } + if (start->empty()) { + retlines.erase(start); + } else { + break; + } + } + + // Search for last non-blank line + for (;;) { + lines_type::reverse_iterator finish = retlines.rbegin(); + if (finish == retlines.rend()) { + return retlines; + } + if (finish->empty()) { + retlines.erase(retlines.end() - 1); + } else { + break; + } + } + + return retlines; +} + +/// Add the current message ID and associated information to the global +/// dictionary. If a message with that ID already exists, loop appending +/// suffixes of the form "(n)" to it until one is found that doesn't. +/// +/// Parameters: +/// msgid Message ID +/// msgtext Message text +/// desc Message description +/// filename File from which the message came. Currently this is +/// not used, but a future enhancement may wish to include the +/// name of the message file in the messages manual. +void addToDictionary(const std::string& msgid, + const std::string& msgtext, + const lines_type& desc, + const std::string& filename) +{ + // If the ID is in the dictionary, append a "(n)" to the name - this will + // flag that there are multiple instances. (However, this is an error - + // each ID should be unique in the code.) + std::string key = msgid; + if (dictionary.count(key) > 0) { + int i = 1; + std::string s = boost::lexical_cast(i); + key = msgid + " (" + s + ")"; + while (dictionary.count(key) > 0) { + i = i + 1; + s = boost::lexical_cast(i); + key = msgid + " (" + s + ")"; + } + } + + // Remove leading and trailing blank lines in the description, then + // add everything into a subdictionary which is then added to the main + // one. + Details details; + details.text = msgtext; + details.description = removeEmptyLeadingTrailing(desc); + details.filename = filename; + dictionary.insert(std::pair(key, details)); +} + + +/// Processes file content. Messages and descriptions are identified and +/// added to a dictionary (keyed by message ID). If the key already exists, +/// a numeric suffix is added to it. +/// +/// Parameters: +/// filename Name of the message file being processed +/// lines Lines read from the file +void processFileContent(const std::string& filename, + const lines_type& lines) +{ + std::string prefix; // Last prefix encountered + std::string msgid; // Last message ID encountered + std::string msgtext; // Text of the message + lines_type description; // Description + + for (lines_type::const_iterator l = lines.begin(); l != lines.end(); ++l) { + if (l->empty()) { + description.push_back(*l); + } else if (l->at(0) == '$') { + // Starts with "$". Ignore anything other than $PREFIX + char* line = new char [l->size() + 1]; + std::strcpy(line, l->c_str()); + char* word0 = strtok(line, " \t\r\n\t\v"); + if (strcasecmp(word0, "$PREFIX") == 0) { + char* word1 = strtok(NULL, " \t\r\n\t\v"); + prefix = word1; + } + } else if (l->at(0) == '%') { + // Start of a message. Add the message we were processing to the + // dictionary and clear everything apart from the file name. + if (!msgid.empty()) { + addToDictionary(msgid, msgtext, description, filename); + } + msgid.clear(); + msgtext.clear(); + description.clear(); + // Start of a message + char* line = new char [l->size() + 1]; + std::strcpy(line, l->c_str()); + // Remove "%" and trim leading spaces + size_t start = l->find_first_not_of(" \t\r\n\t\v", 1); + if (start == std::string::npos) { + reportError(filename, "Line with single % found"); + continue; + } + // Split into words. The first word is the message ID + char* word0 = strtok(line + start, " \t\r\n\t\v"); + msgid = prefix; + msgid.append(word0); + std::transform(msgid.begin(), msgid.end(), + msgid.begin(), toupper); + char* word1 = strtok(NULL, " \t\r\n\t\v"); + start = word1 - line; + size_t finish = l->find_last_not_of(" \t\r\n\t\v"); + msgtext = l->substr(start, finish + 1 - start); + } else { + // Part of a description, so add to the current description array + description.push_back(*l); + } + } + + // All done, add the last message to the global dictionaty. + if (!msgid.empty()) { + addToDictionary(msgid, msgtext, description, filename); + } +} + + +/// Processes a file by reading it in and stripping out all comments and +/// and directives. Leading and trailing blank lines in the file are removed +/// and the remainder passed for message processing. +/// +/// Parameters: +/// filename Name of the message file to process +void processFile(const std::string& filename) +{ + std::ifstream cin; + cin.open(filename, std::ios::in); + lines_type lines0; + while (!cin.eof()) { + std::string line; + getline(cin, line); + lines0.push_back(line); + } + + // Trim leading and trailing spaces from each line, and remove comments. + lines_type lines1; + for (lines_type::iterator l = lines0.begin(); l != lines0.end(); ++l) { + std::string line = *l; + if (line.empty()) { + lines1.push_back(line); + continue; + } + size_t start = line.find_first_not_of(" \t\r\n\t\v"); + if (start != 0) { + line.erase(0, start); + } + if (line.empty()) { + lines1.push_back(line); + continue; + } + size_t finish = line.find_last_not_of(" \t\r\n\t\v"); + if ((finish != std::string::npos) && + (finish + 1 != line.size())) { + line.erase(finish + 1); + } + if (line.empty()) { + lines1.push_back(line); + continue; + } + if (line[0] != '#') { + lines1.push_back(line); + } + } + + // Remove leading/trailing empty line sequences from the result + lines_type lines2 = removeEmptyLeadingTrailing(lines1); + + // Interpret content + processFileContent(filename, lines2); +} + +/// Iterates through all files in the tree starting at the given root and +/// calls processFile for all .mes files found. +/// +/// Parameters: +/// root Directory that is the root of the source tree +void processAllFiles(const boost::filesystem::path& root) +{ + boost::filesystem::directory_iterator endd; + for (boost::filesystem::directory_iterator file(root); + file != endd; + ++file) { + boost::filesystem::path path = file->path(); + if (boost::filesystem::is_directory(path)) { + processAllFiles(path); + } else if (boost::filesystem::is_regular_file(path)) { + boost::filesystem::path extension = path.extension(); + // Identify message files + if (extension == ".mes") { + processFile(path.native()); + } + } + } +} + +void usage(char* progname) +{ + std::cerr << "Usage: " << progname << + " [--help | options] [root|files]\n"; + std::cerr << " options: --output file: " << + "output file name (default to stdout)\n"; +} + +int main(int argc, char* argv[]) +{ + char* progname = argv[0]; + std::ofstream fout; + while (argc > 1) { + --argc; + ++argv; + if (strcmp(argv[0], "--help") == 0) { + usage(progname); + exit(0); + } + // Redirect output if specified (errors are written to stderr) + if ((strcmp(argv[0], "-o") == 0) || + (strcmp(argv[0], "--output") == 0)) { + --argc; + ++argv; + if (argc == 0) { + usage(progname); + exit(-1); + } + fout.open(argv[0], std::ofstream::out | std::ofstream::trunc); + std::cout.rdbuf(fout.rdbuf()); + } + } + + if (argc == 0) { + usage(progname); + exit(-1); + } + if (argc > 1) { + for (int i = 0; i < argc; ++i) { + processFile(argv[i]); + } + } else { + boost::filesystem::path root(argv[0]); + if (boost::filesystem::is_directory(root)) { + processAllFiles(root); + } else { + processFile(argv[0]); + } + } + + // Now just print out everything we've read (in alphabetical order). + bool first = true; + printHeader(); + for (dictionary_type::iterator it = dictionary.begin(); + it != dictionary.end(); + ++it) { + if (!first) { + printSeparator(); + } + first = false; + printMessage(it->first); + } + printTrailer(); + exit(0); +}