Browse Source

[master] Finished merge of trac3516 (trac3516)

Francis Dupont 9 years ago
parent
commit
0dea9af8ef
7 changed files with 667 additions and 416 deletions
  1. 4 0
      ChangeLog
  2. 1 1
      Makefile.am
  3. 1 0
      configure.ac
  4. 3 1
      doc/guide/Makefile.am
  5. 16 0
      tools/Makefile.am
  6. 642 0
      tools/system_messages.cc
  7. 0 414
      tools/system_messages.py

+ 4 - 0
ChangeLog

@@ -1,3 +1,7 @@
+1031.	[build]		fdupont
+	Rewrote the system_messages tool from Python to C++.
+	(trac #3516, git xxx)
+
 1030.	[bug]		marcin
 	Fixed failing 'reclaimExpiredLeasesTimeout' unit tests on
 	some virtual machines.

+ 1 - 1
Makefile.am

@@ -2,7 +2,7 @@ ACLOCAL_AMFLAGS = -I m4macros ${ACLOCAL_FLAGS}
 # ^^^^^^^^ This has to be the first line and cannot come later in this
 # Makefile.am due to some bork in some versions of autotools.
 
-SUBDIRS = compatcheck doc . ext src m4macros
+SUBDIRS = compatcheck tools doc . ext src m4macros
 USE_LCOV=@USE_LCOV@
 LCOV=@LCOV@
 GENHTML=@GENHTML@

+ 1 - 0
configure.ac

@@ -1478,6 +1478,7 @@ AC_CONFIG_FILES([compatcheck/Makefile
                  src/lib/util/threads/Makefile
                  src/lib/util/threads/tests/Makefile
                  src/lib/util/unittests/Makefile
+                 tools/Makefile
                  tools/path_replacer.sh
 ])
 

+ 3 - 1
doc/guide/Makefile.am

@@ -13,7 +13,8 @@ EXTRA_DIST = $(DOCBOOK)
 DISTCLEANFILES = $(HTMLDOCS) $(DOCS) kea-messages.xml
 
 kea-messages.xml:
-	$(PYTHON) $(top_srcdir)/tools/system_messages.py -o $@ $(top_srcdir)
+	$(top_srcdir)/tools/system_messages -o $@ \
+	`find $(top_srcdir) -name "*.mes" -print`
 
 # This is not a "man" manual, but reuse this for now for docbook.
 if GENERATE_DOCS
@@ -35,6 +36,7 @@ kea-messages.html: kea-messages.xml
 	@XSLTPROC@ --novalid --xinclude --nonet \
 		--path $(top_builddir)/doc \
 		-o $@ \
+		--stringparam generate.toc "book toc" \
 		--stringparam html.stylesheet kea-guide.css \
 		http://docbook.sourceforge.net/release/xsl/current/html/docbook.xsl \
 		kea-messages.xml

+ 16 - 0
tools/Makefile.am

@@ -0,0 +1,16 @@
+AM_CPPFLAGS = $(BOOST_INCLUDES)
+
+AM_CXXFLAGS = $(KEA_CXXFLAGS)
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+CLEANFILES = *.gcno *.gcda
+
+if GENERATE_DOCS
+
+noinst_PROGRAMS = system_messages
+system_messages_SOURCES = system_messages.cc
+
+endif

+ 642 - 0
tools/system_messages.cc

@@ -0,0 +1,642 @@
+// 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 message files given on the command line.
+// 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:
+//
+// system_messages [-o <output-file>] <files>
+//
+// If no output file is specified, output is written to stdout.
+// The produced format is docbook XML.
+
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <boost/lexical_cast.hpp>
+
+typedef std::vector<std::string> LinesType;
+
+/// @brief dictionary values
+struct Details {
+    std::string text;
+    LinesType description;
+    std::string sname;
+    std::string filename;
+};
+
+/// @brief Main dictionary holding all the messages.
+/// The messages are accumulated here before being printed in
+/// alphabetical order.
+typedef std::map<const std::string, Details> DictionaryType;
+DictionaryType dictionary;
+
+/// @brief The structure of the output page
+//
+///        header
+///           section header
+///                 message
+///              separator
+///                 message
+///              separator
+///                :
+///              separator
+///                 message
+///           section trailer
+///           separator
+///           section header
+///             :
+///           section trailer
+///        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:
+
+/// @name Constants for the output page
+//@{
+
+/// @brief File header
+/// this is output before anything else.
+const std::string FILE_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-2015</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\
+            The most up-to-date version of this document, along with\n\
+            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";
+
+/// @brief Section header
+/// This is output one for each module. $M substitution token is the name.
+const std::string SECTION_HEADER = "  <section id=\"$M\">\n\
+    <title>$M Module</title>\n\
+    <para>\n\
+      <variablelist>\n";
+
+/// @brief message ID
+/// 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 ID_MESSAGE =
+"<varlistentry id=\"$I\">\n\
+<term>$I $T</term>\n\
+<listitem><para>\n\
+$D</para></listitem>\n\
+</varlistentry>";
+
+/// @brief Blank line
+/// A description may contain blank lines intended to separate
+/// paragraphs.  If so, each blank line is replaced by the following.
+const std::string BLANK = "</para><para>";
+
+/// @brief Separator
+/// The separator is copied to the output verbatim after each message except
+/// the last.
+const std::string SEPARATOR = "";
+
+/// @brief Section trailer
+/// The trailer is copied to the output verbatim after the last message.
+const std::string SECTION_TRAILER =
+"      </variablelist>\n\
+    </para>\n\
+  </section>";
+
+/// @brief File trailer
+/// The trailier is copied to the output verbatim after the last section.
+const std::string FILE_TRAILER =
+"  </chapter>\n\
+</book>";
+
+//@}
+
+/// @name Utility routines
+//@{
+
+/// @brief 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);
+}
+
+/// @brief Replace tag
+/// 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);
+}
+
+/// @brief Replace shell
+/// Replace $c in a string (or with other words performs macro expansion
+/// with '$' for introducing a macro followed by a character selecting
+/// a specific macro.
+///
+/// @param src  source string
+/// @param c    character selecting a macro when it follows '$'
+/// @param val  value which
+///
+/// @return     the source string where all occurrences of $c were
+///             replaced by val
+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);
+}
+
+/// @brief Replace blank lines
+/// Replaces blank lines in an array with the contents of the 'blank' section.
+LinesType replaceBlankLines(const LinesType lines)
+{
+    LinesType result;
+    for (LinesType::const_iterator l = lines.begin(); l != lines.end(); ++l) {
+        if (l->empty()) {
+            result.push_back(BLANK);
+        } else {
+            result.push_back(*l);
+        }
+    }
+    return (result);
+}
+
+//@}
+
+/// @name Printing functions
+//@{
+
+/// @brief Print file header
+void printHeader() {
+    std::cout << FILE_HEADER << "\n";
+}
+
+/// @brief Print separator
+void printSeparator() {
+    std::cout << SEPARATOR << "\n";
+}
+
+/// @brief Print section header
+void printSectionHeader(const std::string& sname)
+{
+    // In the section name, replace "<" and ">" with XML-safe versions and
+    // substitute into the data.
+    std::cout << replaceShell(SECTION_HEADER, 'M', replaceTag(sname));
+}
+
+/// @brief print message id
+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 = ID_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.)
+    LinesType desc0 = dictionary[msgid].description;
+    LinesType desc1;
+    for (LinesType::iterator l = desc0.begin(); l != desc0.end(); ++l) {
+        desc1.push_back(replaceTag(*l));
+    }
+    LinesType desc2 = replaceBlankLines(desc1);
+
+    // Join the lines together to form a single string and insert into
+    // current text.
+    std::string m3;
+    for (LinesType::iterator l = desc2.begin(); l != desc2.end(); ++l) {
+        m3.append(*l);
+        m3.push_back('\n');
+    }
+
+    std::cout << replaceShell(m2, 'D', m3) << "\n";
+}
+
+/// @brief print section trailer
+void printSectionTrailer() {
+    std::cout << SECTION_TRAILER << "\n";
+}
+
+/// @brief print file trailer
+void printTrailer() {
+    std::cout << FILE_TRAILER << "\n";
+}
+
+//@}
+
+/// @brief 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.
+///
+/// @param lines List of strings to be modified.
+///
+/// @return Input list of strings with leading/trailing blank line
+/// sequences removed.
+LinesType removeEmptyLeadingTrailing(const LinesType& lines)
+{
+    LinesType retlines = lines;
+
+    // Dispose of degenerate case of empty array
+    if (retlines.empty()) {
+        return (retlines);
+    }
+
+    // Search for first non-blank line
+    for (;;) {
+        LinesType::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 (;;) {
+        LinesType::reverse_iterator finish = retlines.rbegin();
+        if (finish == retlines.rend()) {
+            return (retlines);
+        }
+        if (finish->empty()) {
+            retlines.erase(retlines.end() - 1);
+        } else {
+            break;
+        }
+    }
+
+    return (retlines);
+}
+
+
+/// @brief 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.
+///
+///  @param msgid        Message ID
+///  @param msgtext      Message text
+///  @param desc         Message description
+///  @param 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 LinesType& 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);
+    size_t underscore = msgid.find_first_of('_');
+    details.sname = msgid.substr(0, underscore);
+    details.filename = filename;
+    dictionary.insert(std::pair<const std::string, Details>(key, details));
+}
+
+
+/// @brief 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.
+
+/// The format of .mes files is fully described in src/lib/log/logging.dox
+///
+/// @param filename     Name of the message file being processed
+/// @param lines        Lines read from the file
+void processFileContent(const std::string& filename,
+                        const LinesType& lines)
+{
+    std::string prefix;         // Last prefix encountered
+    std::string msgid;          // Last message ID encountered
+    std::string msgtext;        // Text of the message
+    LinesType description;      // Description
+
+    for (LinesType::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);
+    }
+}
+
+/// @brief Process a file
+/// Read it in and strip out all comments and and directives.  Leading
+/// and trailing blank lines in the file are removed and the remainder
+/// passed for message processing.
+///
+/// @param filename     Name of the message file to process
+void processFile(const std::string& filename)
+{
+    std::ifstream cin;
+    cin.open(filename, std::ios::in);
+    if (!cin.is_open()) {
+        reportError(filename, "open for read failure");
+    }
+    LinesType lines0;
+    while (!cin.eof()) {
+        std::string line;
+        getline(cin, line);
+        lines0.push_back(line);
+    }
+    cin.close();
+
+    // Trim leading and trailing spaces from each line, and remove comments.
+    LinesType lines1;
+    for (LinesType::iterator l = lines0.begin(); l != lines0.end(); ++l) {
+        std::string line = *l;
+	// Empty lines have no spaces so are processed
+        if (line.empty()) {
+            lines1.push_back(line);
+            continue;
+        }
+	// Trim leading spaces
+        size_t start = line.find_first_not_of(" \t\r\n\t\v");
+        if (start != 0) {
+            line.erase(0, start);
+        }
+	// Done?
+        if (line.empty()) {
+            lines1.push_back(line);
+            continue;
+        }
+	// Trim trailing spaces
+        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);
+        }
+	// Done
+        if (line.empty()) {
+            lines1.push_back(line);
+            continue;
+        }
+	// Skip comments
+        if (line[0] != '#') {
+            lines1.push_back(line);
+        }
+    }  
+
+    // Remove leading/trailing empty line sequences from the result
+    LinesType lines2 = removeEmptyLeadingTrailing(lines1);
+
+    // Interpret content
+    processFileContent(filename, lines2);
+}
+
+/// @brief Usage error routine
+void usage(char* progname)
+{
+    std::cerr << "Usage: " << progname <<
+        " [--help | options] files\n";
+    std::cerr << " options: --output file: " <<
+        "output file name (default to stdout)\n";
+}
+
+/// @brief Main (entry point)
+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);
+            if (!fout.is_open()) {
+                reportError(argv[0], "open for write failure");
+            }
+            std::cout.rdbuf(fout.rdbuf());
+            --argc;
+            ++argv;
+            break;
+        }
+    }
+
+    if (argc == 0) {
+        usage(progname);
+        exit(-1);
+    }
+    for (int i = 0; i < argc; ++i) {
+        processFile(argv[i]);
+    }
+
+    // Now just print out everything we've read (in alphabetical order).
+    bool first = true;
+    std::string sname;
+    printHeader();
+    for (DictionaryType::iterator it = dictionary.begin();
+         it != dictionary.end();
+         ++it) {
+        if (sname.compare(it->second.sname) != 0) {
+            if (!sname.empty()) {
+                printSectionTrailer();
+                printSeparator();
+            }
+            sname = it->second.sname;
+            printSectionHeader(sname);
+            first = true;
+        }
+        if (!first) {
+            printSeparator();
+        }
+        first = false;
+        printMessage(it->first);
+    }
+    if (!sname.empty()) {
+        printSectionTrailer();
+    }
+    printTrailer();
+    exit(0);
+}

+ 0 - 414
tools/system_messages.py

@@ -1,414 +0,0 @@
-# Copyright (C) 2011  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:
-#
-# python system_messages.py [-o <output-file>] <top-source-directory>
-#
-# If no output file is specified, output is written to stdout.
-
-import re
-import os
-import sys
-from optparse import OptionParser
-
-# Main dictionary holding all the messages.  The messages are accumulated here
-# before being printed in alphabetical order.
-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.
-SEC_HEADER="""<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
-"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" [
-<!ENTITY mdash  "&#x2014;" >
-<!ENTITY % version SYSTEM "version.ent">
-%version;
-]>
-<!--
-     This XML document is generated using the system_messages.py tool
-     based on the .mes message files.
-
-     Do not edit this file.
--->
-<book>
-  <?xml-stylesheet href="kea-guide.css" type="text/css"?>
-
-  <bookinfo>
-    <title>Kea Messages Manual</title>
-
-    <copyright>
-      <year>2011-2015</year><holder>Internet Systems Consortium, Inc.</holder>
-    </copyright>
-
-    <abstract>
-      <para>
-        This is the messages manual for Kea version &__VERSION__;.
-	    The most up-to-date version of this document, along with
-	    other documents for Kea, can be found at
-        <ulink url="http://kea.isc.org/docs"/>.
-      </para>
-    </abstract>
-
-    <releaseinfo>This is the messages manual for Kea version
-        &__VERSION__;.</releaseinfo>
-  </bookinfo>
-
-  <chapter id="intro">
-    <title>Introduction</title>
-    <para>
-      This document lists each message that can be logged by the
-      programs in the Kea package.  Each entry in this manual
-      is of the form:
-      <screen>IDENTIFICATION message-text</screen>
-      ... where "IDENTIFICATION" is the message identification included
-      in each message logged and "message-text" is the accompanying
-      message text.  The "message-text" may include placeholders of the
-      form "%1", "%2" etc.; these parameters are replaced by relevant
-      values when the message is logged.
-    </para>
-    <para>
-      Each entry is also accompanied by a description giving more
-      information about the circumstances that result in the message
-      being logged.
-    </para>
-    <para>
-      For information on configuring and using Kea logging,
-      refer to the <ulink url="kea-guide.html">Kea Guide</ulink>.
-    </para>
-  </chapter>
-
-  <chapter id="messages">
-    <title>Kea Log Messages</title>
-    <para>
-      <variablelist>
-"""
-
-# 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.
-SEC_MESSAGE = """<varlistentry id="$I">
-<term>$I $T</term>
-<listitem><para>
-$D
-</para></listitem>
-</varlistentry>"""
-
-# A description may contain blank lines intended to separate paragraphs.  If so,
-# each blank line is replaced by the following.
-SEC_BLANK = "</para><para>"
-
-# The separator is copied to the output verbatim after each message except
-# the last.
-SEC_SEPARATOR = ""
-
-# The trailier is copied to the output verbatim after the last message.
-SEC_TRAILER = """      </variablelist>
-    </para>
-  </chapter>
-</book>"""
-
-
-def reportError(filename, what):
-    """Report an error and exit"""
-    print("*** ERROR in ", filename, file=sys.stderr)
-    print("*** REASON: ", what, file=sys.stderr)
-    print("*** System message generator terminating", file=sys.stderr)
-    sys.exit(1)
-
-
-
-def replaceTag(string):
-    """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.
-    """
-    string1 = string.replace("<", "&lt;")
-    string2 = string1.replace(">", "&gt;")
-    return string2
-
-
-
-def replaceBlankLines(lines):
-    """Replaces blank lines in an array with the contents of the 'blank'
-       section.
-    """
-    result = []
-    for l in lines:
-        if len(l) == 0:
-            result.append(SEC_BLANK)
-        else:
-            result.append(l)
-
-    return result
-
-
-
-# Printing functions
-def printHeader():
-    print(SEC_HEADER)
-
-def printSeparator():
-    print(SEC_SEPARATOR)
-
-def printMessage(msgid):
-    # In the message ID, replace "<" and ">" with XML-safe versions and
-    # substitute into the data.
-    m1 = SEC_MESSAGE.replace("$I", replaceTag(msgid))
-
-    # Do the same for the message text.
-    m2 = m1.replace("$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.)
-    desc1 = [replaceTag(l) for l in dictionary[msgid]['description']]
-    desc2 = replaceBlankLines(desc1)
-
-    # Join the lines together to form a single string and insert into
-    # current text.
-    m3 = m2.replace("$D", "\n".join(desc2))
-
-    print(m3)
-
-def printTrailer():
-    print(SEC_TRAILER)
-
-
-
-def removeEmptyLeadingTrailing(lines):
-    """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.
-    """
-
-    retlines = []
-
-    # Dispose of degenerate case of empty array
-    if len(lines) == 0:
-        return retlines
-
-    # Search for first non-blank line
-    start = 0
-    while start < len(lines):
-        if len(lines[start]) > 0:
-            break
-        start = start + 1
-
-    # Handle case when entire list is empty
-    if start >= len(lines):
-        return retlines
-
-    # Search for last non-blank line
-    finish = len(lines) - 1
-    while finish >= 0:
-        if len(lines[finish]) > 0:
-            break
-        finish = finish - 1
-
-    retlines = lines[start:finish + 1]
-    return retlines
-
-
-
-def addToDictionary(msgid, msgtext, desc, filename):
-    """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.
-    """
-
-    # 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.)
-    if msgid in dictionary:
-        i = 1
-        while msgid + " (" + str(i) + ")" in dictionary:
-            i = i + 1
-        msgid = msgid + " (" + str(i) + ")"
-
-    # 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['text'] = msgtext
-    details['description'] = removeEmptyLeadingTrailing(desc)
-    details['filename'] = filename
-    dictionary[msgid] = details
-
-
-
-def processFileContent(filename, lines):
-    """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
-    """
-
-    prefix = ""         # Last prefix encountered
-    msgid = ""          # Last message ID encountered
-    msgtext = ""        # Text of the message
-    description = []    # Description
-
-    for l in lines:
-        if l.startswith("$"):
-            # Starts with "$".  Ignore anything other than $PREFIX
-            words = re.split("\s+", l)
-            if words[0].upper() == "$PREFIX":
-                if len(words) == 1:
-                    prefix = ""
-                else:
-                    prefix = words[1]
-
-        elif l.startswith("%"):
-            # Start of a message.  Add the message we were processing to the
-            # dictionary and clear everything apart from the file name.
-            if msgid != "":
-                addToDictionary(msgid, msgtext, description, filename)
-
-            msgid = ""
-            msgtext = ""
-            description = []
-
-            # Start of a message
-            l = l[1:].strip()       # Remove "%" and trim leading spaces
-            if len(l) == 0:
-                printError(filename, "Line with single % found")
-                next
-
-            # Split into words.  The first word is the message ID
-            words = re.split("\s+", l)
-            msgid = (prefix + words[0]).upper()
-            msgtext = l[len(words[0]):].strip()
-
-        else:
-            # Part of a description, so add to the current description array
-            description.append(l)
-
-    # All done, add the last message to the global dictionaty.
-    if msgid != "":
-        addToDictionary(msgid, msgtext, description, filename)
-
-
-
-def processFile(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
-    """
-    lines = open(filename).readlines();
-
-    # Trim leading and trailing spaces from each line, and remove comments.
-    lines = [l.strip() for l in lines]
-    lines = [l for l in lines if not l.startswith("#")]
-
-    # Remove leading/trailing empty line sequences from the result
-    lines = removeEmptyLeadingTrailing(lines)
-
-    # Interpret content
-    processFileContent(filename, lines)
-
-
-
-def processAllFiles(root):
-    """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
-    """
-    for (path, dirs, files) in os.walk(root):
-
-        # Identify message files
-        mes_files = [f for f in files if f.endswith(".mes")]
-
-        # ... and process each file in the list
-        for m in mes_files:
-            processFile(path + os.sep + m)
-
-
-# Main program
-if __name__ == "__main__":
-    parser = OptionParser(usage="Usage: %prog [--help | options] root")
-    parser.add_option("-o", "--output", dest="output", default=None,
-                      metavar="FILE", 
-                      help="output file name (default to stdout)")
-    (options, args) = parser.parse_args()
-
-    if len(args) == 0:
-        parser.error("Must supply directory at which to begin search")
-    elif len(args) > 1:
-        parser.error("Only a single root directory can be given")
-
-    # Redirect output if specified (errors are written to stderr)
-    if options.output is not None:
-        sys.stdout = open(options.output, 'w')
-
-    # Read the files and load the data
-    processAllFiles(args[0])
-
-    # Now just print out everything we've read (in alphabetical order).
-    count = 1
-    printHeader()
-    for msgid in sorted(dictionary):
-        if count > 1:
-            printSeparator()
-        count = count + 1
-        printMessage(msgid)
-    printTrailer()