system_messages.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. # Copyright (C) 2011, 2015 Internet Systems Consortium, Inc. ("ISC")
  2. #
  3. # Permission to use, copy, modify, and/or distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
  8. # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  9. # AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
  10. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  11. # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
  12. # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  13. # PERFORMANCE OF THIS SOFTWARE.
  14. # Produce System Messages Manual
  15. #
  16. # This tool reads all the .mes files in the directory tree whose root is given
  17. # on the command line and interprets them as message files. It pulls
  18. # all the messages and description out, sorts them by message ID, and writes
  19. # them out as a single (formatted) file.
  20. #
  21. # Invocation:
  22. # The code is invoked using the command line:
  23. #
  24. # python system_messages.py [-o <output-file>] <top-source-directory>
  25. #
  26. # If no output file is specified, output is written to stdout.
  27. import re
  28. import os
  29. import sys
  30. from optparse import OptionParser
  31. # Main dictionary holding all the messages. The messages are accumulated here
  32. # before being printed in alphabetical order.
  33. dictionary = {}
  34. # The structure of the output page is:
  35. #
  36. # header
  37. # section header
  38. # message
  39. # separator
  40. # message
  41. # separator
  42. # :
  43. # separator
  44. # message
  45. # section trailer
  46. # separator
  47. # section header
  48. # :
  49. # section trailer
  50. # trailer
  51. #
  52. # (Indentation is not relevant - it has only been added to the above
  53. # illustration to make the structure clearer.) The text of these section is:
  54. # Header - this is output before anything else.
  55. FILE_HEADER = """<?xml version="1.0" encoding="UTF-8"?>
  56. <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
  57. "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd" [
  58. <!ENTITY mdash "&#x2014;" >
  59. <!ENTITY % version SYSTEM "version.ent">
  60. %version;
  61. ]>
  62. <!--
  63. This XML document is generated using the system_messages.py tool
  64. based on the .mes message files.
  65. Do not edit this file.
  66. -->
  67. <book>
  68. <?xml-stylesheet href="kea-guide.css" type="text/css"?>
  69. <bookinfo>
  70. <title>Kea Messages Manual</title>
  71. <copyright>
  72. <year>2011-2014</year><holder>Internet Systems Consortium, Inc.</holder>
  73. </copyright>
  74. <abstract>
  75. <para>
  76. This is the messages manual for Kea version &__VERSION__;.
  77. The most up-to-date version of this document, along with
  78. other documents for Kea, can be found at
  79. <ulink url="http://kea.isc.org/docs"/>.
  80. </para>
  81. </abstract>
  82. <releaseinfo>This is the messages manual for Kea version
  83. &__VERSION__;.</releaseinfo>
  84. </bookinfo>
  85. <chapter id="intro">
  86. <title>Introduction</title>
  87. <para>
  88. This document lists each message that can be logged by the
  89. programs in the Kea package. Each entry in this manual
  90. is of the form:
  91. <screen>IDENTIFICATION message-text</screen>
  92. ... where "IDENTIFICATION" is the message identification included
  93. in each message logged and "message-text" is the accompanying
  94. message text. The "message-text" may include placeholders of the
  95. form "%1", "%2" etc.; these parameters are replaced by relevant
  96. values when the message is logged.
  97. </para>
  98. <para>
  99. Each entry is also accompanied by a description giving more
  100. information about the circumstances that result in the message
  101. being logged.
  102. </para>
  103. <para>
  104. For information on configuring and using Kea logging,
  105. refer to the <ulink url="kea-guide.html">Kea Guide</ulink>.
  106. </para>
  107. </chapter>
  108. <chapter id="messages">
  109. <title>Kea Log Messages</title>
  110. """
  111. # This is output one for each module. $M substitution token is the name.
  112. SECTION_HEADER = """ <section id="$M">
  113. <title>$M Module</title>
  114. <para>
  115. <variablelist>"""
  116. # This is output once for each message. The string contains substitution
  117. # tokens: $I is replaced by the message identification, $T by the message text,
  118. # and $D by the message description.
  119. ID_MESSAGE = """<varlistentry id="$I">
  120. <term>$I $T</term>
  121. <listitem><para>
  122. $D
  123. </para></listitem>
  124. </varlistentry>"""
  125. # A description may contain blank lines intended to separate
  126. # paragraphs. If so, each blank line is replaced by the following.
  127. BLANK = "</para><para>"
  128. # The separator is copied to the output verbatim after each message or
  129. # section except the last.
  130. SEPARATOR = ""
  131. # The trailer is copied to the output verbatim after the last message.
  132. SECTION_TRAILER = """ </variablelist>
  133. </para>
  134. </section>"""
  135. # The trailer is copied to the output verbatim after the last section.
  136. FILE_TRAILER = """ </chapter>
  137. </book>"""
  138. def reportError(filename, what):
  139. """Report an error and exit"""
  140. print("*** ERROR in ", filename, file=sys.stderr)
  141. print("*** REASON: ", what, file=sys.stderr)
  142. print("*** System message generator terminating", file=sys.stderr)
  143. sys.exit(1)
  144. def replaceTag(string):
  145. """Replaces the '<' and '>' in text about to be inserted into the template
  146. sections above with &lt; and &gt; to avoid problems with message text
  147. being interpreted as XML text.
  148. """
  149. string1 = string.replace("<", "&lt;")
  150. string2 = string1.replace(">", "&gt;")
  151. return string2
  152. def replaceBlankLines(lines):
  153. """Replaces blank lines in an array with the contents of the 'blank'
  154. section.
  155. """
  156. result = []
  157. for l in lines:
  158. if len(l) == 0:
  159. result.append(BLANK)
  160. else:
  161. result.append(l)
  162. return result
  163. # Printing functions
  164. def printHeader():
  165. print(FILE_HEADER)
  166. def printSeparator():
  167. print(SEPARATOR)
  168. def printSectionHeader(sname):
  169. # In the section name, replace "<" and ">" with XML-safe versions and
  170. # substitute into the data.
  171. m = SECTION_HEADER.replace("$M", replaceTag(sname));
  172. print(m)
  173. def printMessage(msgid):
  174. # In the message ID, replace "<" and ">" with XML-safe versions and
  175. # substitute into the data.
  176. m1 = ID_MESSAGE.replace("$I", replaceTag(msgid))
  177. # Do the same for the message text.
  178. m2 = m1.replace("$T", replaceTag(dictionary[msgid]['text']))
  179. # Do the same for the description then replace blank lines with the
  180. # specified separator. (We do this in that order to avoid replacing
  181. # the "<" and ">" in the XML tags in the separator.)
  182. desc1 = [replaceTag(l) for l in dictionary[msgid]['description']]
  183. desc2 = replaceBlankLines(desc1)
  184. # Join the lines together to form a single string and insert into
  185. # current text.
  186. m3 = m2.replace("$D", "\n".join(desc2))
  187. print(m3)
  188. def printSectionTrailer():
  189. print(SECTION_TRAILER)
  190. def printTrailer():
  191. print(FILE_TRAILER)
  192. def removeEmptyLeadingTrailing(lines):
  193. """Removes leading and trailing empty lines.
  194. A list of strings is passed as argument, some of which may be empty.
  195. This function removes from the start and end of list a contiguous
  196. sequence of empty lines and returns the result. Embedded sequence of
  197. empty lines are not touched.
  198. Parameters:
  199. lines List of strings to be modified.
  200. Return:
  201. Input list of strings with leading/trailing blank line sequences
  202. removed.
  203. """
  204. retlines = []
  205. # Dispose of degenerate case of empty array
  206. if len(lines) == 0:
  207. return retlines
  208. # Search for first non-blank line
  209. start = 0
  210. while start < len(lines):
  211. if len(lines[start]) > 0:
  212. break
  213. start = start + 1
  214. # Handle case when entire list is empty
  215. if start >= len(lines):
  216. return retlines
  217. # Search for last non-blank line
  218. finish = len(lines) - 1
  219. while finish >= 0:
  220. if len(lines[finish]) > 0:
  221. break
  222. finish = finish - 1
  223. retlines = lines[start:finish + 1]
  224. return retlines
  225. def addToDictionary(msgid, msgtext, desc, filename):
  226. """Add the current message ID and associated information to the global
  227. dictionary. If a message with that ID already exists, loop appending
  228. suffixes of the form "(n)" to it until one is found that doesn't.
  229. Parameters:
  230. msgid Message ID
  231. msgtext Message text
  232. desc Message description
  233. sname Section name (part before the first _ of the ID)
  234. filename File from which the message came. Currently this is
  235. not used, but a future enhancement may wish to include the
  236. name of the message file in the messages manual.
  237. """
  238. # If the ID is in the dictionary, append a "(n)" to the name - this will
  239. # flag that there are multiple instances. (However, this is an error -
  240. # each ID should be unique in the code.)
  241. if msgid in dictionary:
  242. i = 1
  243. while msgid + " (" + str(i) + ")" in dictionary:
  244. i = i + 1
  245. msgid = msgid + " (" + str(i) + ")"
  246. # Remove leading and trailing blank lines in the description, then
  247. # add everything into a subdictionary which is then added to the main
  248. # one.
  249. details = {}
  250. words = re.split("_", msgid)
  251. details['text'] = msgtext
  252. details['description'] = removeEmptyLeadingTrailing(desc)
  253. details['sname'] = words[0]
  254. details['filename'] = filename
  255. dictionary[msgid] = details
  256. def processFileContent(filename, lines):
  257. """Processes file content. Messages and descriptions are identified and
  258. added to a dictionary (keyed by message ID). If the key already exists,
  259. a numeric suffix is added to it.
  260. Parameters:
  261. filename Name of the message file being processed
  262. lines Lines read from the file
  263. """
  264. prefix = "" # Last prefix encountered
  265. msgid = "" # Last message ID encountered
  266. msgtext = "" # Text of the message
  267. description = [] # Description
  268. for l in lines:
  269. if l.startswith("$"):
  270. # Starts with "$". Ignore anything other than $PREFIX
  271. words = re.split("\s+", l)
  272. if words[0].upper() == "$PREFIX":
  273. if len(words) == 1:
  274. prefix = ""
  275. else:
  276. prefix = words[1]
  277. elif l.startswith("%"):
  278. # Start of a message. Add the message we were processing to the
  279. # dictionary and clear everything apart from the file name.
  280. if msgid != "":
  281. addToDictionary(msgid, msgtext, description, filename)
  282. msgid = ""
  283. msgtext = ""
  284. description = []
  285. # Start of a message
  286. l = l[1:].strip() # Remove "%" and trim leading spaces
  287. if len(l) == 0:
  288. printError(filename, "Line with single % found")
  289. next
  290. # Split into words. The first word is the message ID
  291. words = re.split("\s+", l)
  292. msgid = (prefix + words[0]).upper()
  293. msgtext = l[len(words[0]):].strip()
  294. else:
  295. # Part of a description, so add to the current description array
  296. description.append(l)
  297. # All done, add the last message to the global dictionaty.
  298. if msgid != "":
  299. addToDictionary(msgid, msgtext, description, filename)
  300. def processFile(filename):
  301. """Processes a file by reading it in and stripping out all comments and
  302. and directives. Leading and trailing blank lines in the file are removed
  303. and the remainder passed for message processing.
  304. Parameters:
  305. filename Name of the message file to process
  306. """
  307. lines = open(filename).readlines();
  308. # Trim leading and trailing spaces from each line, and remove comments.
  309. lines = [l.strip() for l in lines]
  310. lines = [l for l in lines if not l.startswith("#")]
  311. # Remove leading/trailing empty line sequences from the result
  312. lines = removeEmptyLeadingTrailing(lines)
  313. # Interpret content
  314. processFileContent(filename, lines)
  315. def processAllFiles(root):
  316. """Iterates through all files in the tree starting at the given root and
  317. calls processFile for all .mes files found.
  318. Parameters:
  319. root Directory that is the root of the source tree
  320. """
  321. for (path, dirs, files) in os.walk(root):
  322. # Identify message files
  323. mes_files = [f for f in files if f.endswith(".mes")]
  324. # ... and process each file in the list
  325. for m in mes_files:
  326. processFile(path + os.sep + m)
  327. # Main program
  328. if __name__ == "__main__":
  329. parser = OptionParser(usage="Usage: %prog [--help | options] root")
  330. parser.add_option("-o", "--output", dest="output", default=None,
  331. metavar="FILE",
  332. help="output file name (default to stdout)")
  333. (options, args) = parser.parse_args()
  334. if len(args) == 0:
  335. parser.error("Must supply directory at which to begin search")
  336. elif len(args) > 1:
  337. parser.error("Only a single root directory can be given")
  338. # Redirect output if specified (errors are written to stderr)
  339. if options.output is not None:
  340. sys.stdout = open(options.output, 'w')
  341. # Read the files and load the data
  342. processAllFiles(args[0])
  343. # Now just print out everything we've read (in alphabetical order).
  344. sname = ""
  345. printHeader()
  346. for msgid in sorted(dictionary):
  347. if dictionary[msgid]['sname'] != sname:
  348. if sname != "":
  349. printSectionTrailer()
  350. printSeparator()
  351. sname = dictionary[msgid]['sname']
  352. printSectionHeader(sname)
  353. count = 1
  354. if count > 1:
  355. printSeparator()
  356. count = count + 1
  357. printMessage(msgid)
  358. if sname !="":
  359. printSectionTrailer()
  360. printTrailer()