system_messages.py 12.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 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 BIND 10 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.
54
55
56
57
58
59
60
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;
]>
61
62
63
64
65
66
<!--
     This XML document is generated using the system_messages.py tool
     based on the .mes message files.

     Do not edit this file.
-->
67
68
69
70
71
72
73
<book>
  <?xml-stylesheet href="bind10-guide.css" type="text/css"?>

  <bookinfo>
    <title>BIND 10 Messages Manual</title>

    <copyright>
74
      <year>2011-2013</year><holder>Internet Systems Consortium, Inc.</holder>
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
    </copyright>

    <abstract>
      <para>
        This is the messages manual for BIND 10 version &__VERSION__;.
	    The most up-to-date version of this document, along with
	    other documents for BIND 10, can be found at
        <ulink url="http://bind10.isc.org/docs"/>.
      </para>
    </abstract>

    <releaseinfo>This is the messages manual for BIND 10 version
        &__VERSION__;.</releaseinfo>
  </bookinfo>

  <chapter id="intro">
    <title>Introduction</title>
    <para>
93
      This document lists each message that can be logged by the
94
95
      programs in the BIND 10 package.  Each entry in this manual
      is of the form:
96
      <screen>IDENTIFICATION message-text</screen>
97
98
      ... where "IDENTIFICATION" is the message identification included
      in each message logged and "message-text" is the accompanying
99
      message text.  The "message-text" may include placeholders of the
100
101
102
103
104
105
106
107
      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>
108
109
110
111
    <para>
      For information on configuring and using BIND 10 logging,
      refer to the <ulink url="bind10-guide.html">BIND 10 Guide</ulink>.
    </para>
112
113
114
115
116
117
  </chapter>

  <chapter id="messages">
    <title>BIND 10 Messages</title>
    <para>
      <variablelist>
118
119
120
121
122
"""

# 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.
123
SEC_MESSAGE = """<varlistentry id="$I">
124
<term>$I $T</term>
125
126
127
128
<listitem><para>
$D
</para></listitem>
</varlistentry>"""
129
130
131

# A description may contain blank lines intended to separate paragraphs.  If so,
# each blank line is replaced by the following.
132
SEC_BLANK = "</para><para>"
133
134
135

# The separator is copied to the output verbatim after each message except
# the last.
136
SEC_SEPARATOR = ""
137
138

# The trailier is copied to the output verbatim after the last message.
139
140
141
SEC_TRAILER = """      </variablelist>
    </para>
  </chapter>
142
</book>"""
143
144
145
146
147
148
149
150
151
152
153


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)



154
155
156
157
158
159
160
161
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179



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



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# 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)



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
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.
    """

271
    # If the ID is in the dictionary, append a "(n)" to the name - this will
272
273
274
275
276
277
278
279
    # flag that there are multiple instances.  (However, this is an error -
    # each ID should be unique in BIND-10.)
    if msgid in dictionary:
        i = 1
        while msgid + " (" + str(i) + ")" in dictionary:
            i = i + 1
        msgid = msgid + " (" + str(i) + ")"

280
281
282
    # Remove leading and trailing blank lines in the description, then
    # add everything into a subdictionary which is then added to the main
    # one.
283
284
    details = {}
    details['text'] = msgtext
285
    details['description'] = removeEmptyLeadingTrailing(desc)
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
    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 BIND-10 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])

406
    # Now just print out everything we've read (in alphabetical order).
407
408
409
410
411
412
413
414
    count = 1
    printHeader()
    for msgid in sorted(dictionary):
        if count > 1:
            printSeparator()
        count = count + 1
        printMessage(msgid)
    printTrailer()