123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- // Copyright (C) 2015-2017 Internet Systems Consortium, Inc. ("ISC")
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this
- // file, You can obtain one at http://mozilla.org/MPL/2.0/.
- // 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 <cstring>
- #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 \"—\" >\n\
- <!ENTITY % version SYSTEM \"version.ent\">\n\
- %version;\n\
- ]>\n\
- <!--\n\
- This XML document is generated using the system_messages 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 < 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);
- }
- /// @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;
- }
- delete[] line;
- } 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 dictionary.
- 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.c_str(), 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);
- }
|