]> Frank Brehm's Git Trees - my-stuff/isc-config-parser.git/commitdiff
Weitergekommen
authorFrank Brehm <frank@brehm-online.com>
Thu, 27 Jan 2011 17:29:40 +0000 (17:29 +0000)
committerFrank Brehm <frank@brehm-online.com>
Thu, 27 Jan 2011 17:29:40 +0000 (17:29 +0000)
git-svn-id: http://svn.brehm-online.com/svn/my-stuff/python/IscConfigParser/trunk@190 ec8d2aa5-1599-4edb-8739-2b3a1bc399aa

IscConfigParser.py

index 71d53bf731d900f5e62ee2ac445e285c64b423d4..e432f74b8255cf66856c897a0694adb04f3d26fa 100755 (executable)
@@ -15,6 +15,7 @@
 
 import re
 import pprint
+import os
 import os.path
 import sys
 
@@ -42,10 +43,16 @@ class IscConfigParser(LoggingObject):
     """
 
     #------------------------------------------------------
-    def __init__( self, verbose = 0, ):
+    def __init__( self, verbose = 0, base_dir = os.getcwd(), do_includes = False ):
         """Constructor.
+
            @param verbose: verbosity level (default: 0)
            @type verbose: int
+           @param base_dir: base directory for all non absolute includes (must exists)
+           @type base_dir: str
+           @param do_includes: should include directives be evaluated
+           @type do_includes: bool
+
            @return: None
            @rtype: None
         """
@@ -53,73 +60,242 @@ class IscConfigParser(LoggingObject):
         super( IscConfigParser, self ).__init__( verbose = verbose )
 
         self.current_filename = None
+        self.base_dir = base_dir
+        self.do_includes = do_includes
 
         # Other properties
 
     #------------------------------------------------------
-    def parse( self, input ):
-        """Read a file in a ISC-like config style.
+    # Getter method for the base directory property
+    @property
+    def base_dir( self ):
+        """The property base_dir is the base directory for all non absolute includes."""
+        return self._base_dir
+
+    #------------------------------------------------------
+    # Setter method for the base directory property
+    @base_dir.setter
+    def base_dir( self, new_base_dir ):
 
-           The input can be either a file-like object or a string. If a string
-           is used you can optionally also provide a filename, which will be
-           used for raised exceptions.
+        if not os.path.exists( new_base_dir ):
+            raise IscConfigParserError( "Given base directory {0!r} doesn't exists.".format( new_base_dir ) )
+
+        if not os.path.isdir( new_base_dir ):
+            raise IscConfigParserError( "Given base directory {0!r} is not a directory.".format( new_base_dir ) )
+
+        self._base_dir = new_base_dir
+
+    #------------------------------------------------------
+    def parse( self, filename ):
+        """Read a file in a ISC-like config style - it's the main function.
 
            The contents from the file is returned as a standard python dictionary.
 
-           @param input: file-like object or a string
-           @type input: str
+           @param filename: filename of the configuration file or '-' or None for standard input
+           @type filename:  str or None
+
            @return: the contents from the file as a standard python dictionary
-           @rtype: dict
+           @rtype:  dict
         """
 
         result = {}
 
-        if input is not None and not os.path.isfile( input ):
-            raise IscConfigParserError( "File {0!r} not found.".format( input ) )
-
         fd = None
-        if input is not None:
-            fd = open( input, "r" )
-            self.current_filename = input
-        else:
+
+        if ( filename is None ) or ( filename == '-' ):
             fd = sys.stdin
-            self.current_filename = '<stdin>'
-        self.current_fd = fd
-        
+            current_filename = '-'
+        else:
+            fd = self._open_configfile( filename )
+            current_filename = filename
+
+        self._current_filename = current_filename
 
-        self.logger.debug( "Reading configuration file {0!r} ...".format( self.current_filename ) )
+        self._files = { 
+            current_filename: {
+                'fd': fd,
+                'current_rownum': 0,
+                'next_rownum': 1,
+            },
+        }
+        self._in_quoting = False
 
+        self.logger.debug( "Reading configuration file {0!r} ...".format( self._current_filename ) )
+        self._read_current_file()
 
-        if input is not None:
+        if filename is not None:
             fd.close()
-        del self.current_fd
+
+        del self._files
+        del self._current_filename
 
         return result
 
     #------------------------------------------------------
-    def read_next_line( self ):
+    def _read_current_file( self ):
+
+        filename = self._current_filename
+        result = {}
+        self.logger.debug( "Reading file {0!r} ...".format( filename ) )
+
+        pp = pprint.PrettyPrinter( indent = 4, depth = 6, width = 120 )
+        line_tokens = self._read_next_line()
+        while line_tokens is not None:
+            rownum = self._files[filename]['current_rownum']
+            if ( self.verbose > 1 ) and ( len(line_tokens) > 0 ):
+                self.logger.debug( "%4d: %s" % ( rownum, pp.pformat( line_tokens ) ) )
+            line_tokens = self._read_next_line()
+
+        return result
+
+    #------------------------------------------------------
+    def _open_configfile( self, filename ):
+        """Checks the availablity of the given filename, and opens it readonly.
+           Raises an IscConfigParserError exception, if the file isn't available, or another
+           exception if the open fails.
+
+           @param filename: the filename to open
+           @type filename:  str
+
+           @return: The file descriptor of the opened file
+           @rtype:  file object
+        """
+
+        if not os.path.isfile( filename ):
+            raise IscConfigParserError( "File {0!r} not found.".format( filename ) )
+
+        fd = None
+
+        if not os.access( filename, os.R_OK ):
+            raise IscConfigParserError( "File {0!r} is not readable.".format( filename ) )
+
+        fd = open( filename, "r" )
+
+        return fd
+
+    #------------------------------------------------------
+    def _read_next_line( self ):
         """Read the next line from current file handle.
-           @return: the content of this line
-           @rtype: str
+           Comments are removed.
+           The number of the beginning of line is after in
+           self._files[ self._current_filename ]['current_rownum'] (except in case of EOF).
+
+           @return: the content of this line or None if EOF
+           @rtype:  str or None
         """
 
-        fd = self.current_fd
+        filename = self._current_filename
+
+        if not filename in self._files:
+            raise Exception( "File {0!r} is not opened.".format( filename ) )
+
+        fd = self._files[filename]['fd']
+        current_rownum = self._files[filename]['next_rownum']
+        next_rownum = current_rownum + 1
+
+        line = fd.readline()
+
+        # EOF reached
+        if line == '' or line is None:
+            return None
+
+        if ( self.verbose > 2 ):
+            self.logger.debug( "read line {0}: {1!r}".format( current_rownum, line ) )
+
+        res_array = []
+
+        line = line.lstrip()
+        in_comment = False
+        while line != '':
+
+            if ( self.verbose > 3 ):
+                self.logger.debug( "Rest of line: {0!r}".format( line ) )
+
+            # Remoce C++-like comments
+            match = re.search( r'^[^"]*?//', line )
+            if match is not None:
+                line = re.sub( r'^([^"]*?)//.*\s*', r'\1', line )
+                continue
+
+            # Remove Perl-like comments
+            match = re.search( r'^[^"]*?#', line )
+            if match is not None:
+                line = re.sub( r'^([^"]*?)#.*\s*', r'\1', line )
+                continue
+
+            # Remove C-like comments
+            match = re.search( r'^[^"]*?/\*', line )
+            if match is not None:
+
+                line = re.sub( r'^([^"]*?)/\*[^\*]*', r'\1', line )
+
+                do_comment_loop = True
+                while do_comment_loop:
+                    c_comment_end_match = re.search( r'^[^\*]*?\*/', line )
+                    if c_comment_end_match is not None:
+                        line = re.sub( r'^[^\*]*?\*/', '', line )
+                        do_comment_loop = False
+                    else:
+                        new_line = fd.readline()
+                        if new_line == '' or new_line is None:
+                            raise IscConfigParserError( "Unbalanced C-like comment in {0!r}, starting at line {1}.".format( filename, current_rownum ) )
+                        if ( self.verbose > 2 ):
+                            self.logger.debug( "read line {0}: {1!r}".format( next_rownum, new_line ) )
+                        next_rownum += 1
+                        line += new_line
+                        if ( self.verbose > 2 ):
+                            self.logger.debug( "new line: {0!r}".format( line ) )
+
+            line = line.lstrip()
+
+            match = re.search( r'^([^"\s]+)', line )
+            if match is not None:
+                res_array.append( match.group(1) )
+                line = re.sub( r'^[^"\s]+\s*', '', line )
+                continue
+
+            match = re.search( r'^\s*"((?:[^"]|(?<=\\"))*)"', line )
+            if match is not None:
+                res_array.append( match.group(1) )
+                line = re.sub( r'^\s*"(?:[^"]|(?<=\\"))*\s*"', '', line )
+
+#        # remove whitespaces from the beginning and end of the line
+#        if not self._in_quoting:
+#            result = result.strip( " \t\n\r" )
+
+#        # Remove Perl-like comments
+#        if re.search( r'^(:?[^"]*(:?"[^"]*")?)*?#', result ):
+#            result = re.sub( r'^((:?[^"]*(:?"[^"]*")?)*?)#.*', r'\1', result )
+
+#        # Remoce C++-like comments
+#        if re.search( r'^(?:[^"]*(:?"[^"]*")?)*?//', result ):
+#            result = re.sub( r'^((:?[^"]*(:?"[^"]*")?)*?)//.*', r'\1', result )
+
+        self._files[filename]['current_rownum'] = current_rownum
+        self._files[filename]['next_rownum'] = next_rownum
+
+        return res_array
 
 
 #========================================================================
 
-def parse_isc_config( input, verbose = 0 ):
+def parse_isc_config( input, verbose = 0, base_dir = os.getcwd(), do_includes = False ):
     '''Wrapper for IscConfigParser.parse()
 
        @param input: file-like object or a string
        @type input: str
        @param verbose: verbosity level (default: 0)
        @type verbose: int
+       @param base_dir: base directory for all non absolute includes (must exists)
+       @type base_dir: str
+       @param do_includes: should include directives be evaluated
+       @type do_includes: bool
+
        @return: the contents from the file as a standard python dictionary
        @rtype: dict
     '''
 
-    parser = IscConfigParser( verbose = verbose )
+    parser = IscConfigParser( verbose = verbose, base_dir = base_dir, do_includes = do_includes )
     return parser.parse( input )
 
 #========================================================================
@@ -138,12 +314,15 @@ if __name__ == "__main__":
     verbose = opt_parser.options.verbose
 
     files = []
+    pp = pprint.PrettyPrinter( indent = 4, depth = 6, width = 120 )
     if opt_parser.args is not None and len( opt_parser.args ) > 0:
-        files = opt_parser.args[1:]
+        files = opt_parser.args[:]
     else:
         files = [ os.sep + os.path.join( 'etc', 'bind', 'named.conf' ) ]
 
-    pp = pprint.PrettyPrinter( indent = 4, depth = 6, width = 120 )
+    if verbose > 1:
+        print "Configfiles to read: %s" % ( pp.pformat( files ) )
+
     for conffile in files:
         conf = parse_isc_config( conffile, verbose = verbose )
         print "Read configuration: \n%s" % ( pp.pformat( conf ) )