Browse Source

[trac3516] C++ version of system_messages.py

Francis Dupont 10 years ago
parent
commit
638666c9be
1 changed files with 579 additions and 0 deletions
  1. 579 0
      tools/kt_system_messages.cc

+ 579 - 0
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 <output-file>] [<top-source-directory>|<files>]
+//
+// If no output file is specified, output is written to stdout.
+
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <boost/filesystem.hpp>
+#include <boost/lexical_cast.hpp>
+
+typedef std::vector<std::string> 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<const std::string, Details> 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 =
+"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
+<!DOCTYPE book PUBLIC \"-//OASIS//DTD DocBook XML V4.2//EN\"\n\
+\"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd\" [\n\
+<!ENTITY mdash  \"&#x2014;\" >\n\
+<!ENTITY % version SYSTEM \"version.ent\">\n\
+%version;\n\
+]>\n\
+<!--\n\
+     This XML document is generated using the system_messages.py tool\n\
+     based on the .mes message files.\n\
+\n\
+     Do not edit this file.\n\
+-->\n\
+<book>\n\
+  <?xml-stylesheet href=\"kea-guide.css\" type=\"text/css\"?>\n\
+\n\
+  <bookinfo>\n\
+    <title>Kea Messages Manual</title>\n\
+\n\
+    <copyright>\n\
+      <year>2011-2014</year><holder>Internet Systems Consortium, Inc.</holder>\n\
+    </copyright>\n\
+\n\
+    <abstract>\n\
+      <para>\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\
+        <ulink url=\"http://kea.isc.org/docs\"/>.\n\
+      </para>\n\
+    </abstract>\n\
+\n\
+    <releaseinfo>This is the messages manual for Kea version\n\
+        &__VERSION__;.</releaseinfo>\n\
+  </bookinfo>\n\
+\n\
+  <chapter id=\"intro\">\n\
+    <title>Introduction</title>\n\
+    <para>\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\
+      <screen>IDENTIFICATION message-text</screen>\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\
+    </para>\n\
+    <para>\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\
+    </para>\n\
+    <para>\n\
+      For information on configuring and using Kea logging,\n\
+      refer to the <ulink url=\"kea-guide.html\">Kea Guide</ulink>.\n\
+    </para>\n\
+  </chapter>\n\
+\n\
+  <chapter id=\"messages\">\n\
+    <title>Kea Log Messages</title>\n\
+    <para>\n\
+      <variablelist>\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 =
+"<varlistentry id=\"$I\">\n\
+<term>$I $T</term>\n\
+<listitem><para>\n\
+$D</para></listitem>\n\
+</varlistentry>";
+
+// 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 = "</para><para>";
+
+// 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 =
+"      </variablelist>\n\
+    </para>\n\
+  </chapter>\n\
+</book>";
+
+/// 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 &lt; and &gt; 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("&lt;");
+        } else if (*it == '>') {
+            result.append("&gt;");
+        } 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<std::string>(i);
+        key = msgid + " (" + s + ")";
+        while (dictionary.count(key) > 0) {
+            i = i + 1;
+            s = boost::lexical_cast<std::string>(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<const std::string, Details>(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);
+}