[PATCH 1/5] simpleconfig: rewrite to better support commented config files

Brian C. Lane bcl at redhat.com
Wed Aug 8 21:49:02 UTC 2012


From: "Brian C. Lane" <bcl at redhat.com>

This takes some of my code, and some of wwoods' code and rewrites
SimpleConfigFile to support a wider range of uses. It can now
preserve comments and whitespaces in config files and can quote
(or not) values when reading and writing them.
---
 pyanaconda/simpleconfig.py |  222 ++++++++++++++++++++++++++++----------------
 1 files changed, 143 insertions(+), 79 deletions(-)

diff --git a/pyanaconda/simpleconfig.py b/pyanaconda/simpleconfig.py
index 4bd665a..db0dea2 100644
--- a/pyanaconda/simpleconfig.py
+++ b/pyanaconda/simpleconfig.py
@@ -3,8 +3,10 @@
 #
 # Matt Wilson <msw at redhat.com>
 # Jeremy Katz <katzj at redhat.com>
+# Will Woods <wwoods at redhat.com>
+# Brian C. Lane <bcl at redhat.com>
 #
-# Copyright 1999-2002 Red Hat, Inc.
+# Copyright 1999-2012 Red Hat, Inc.
 #
 # This software may be freely redistributed under the terms of the GNU
 # library public license.
@@ -13,81 +15,157 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 #
-
-import string
 import os
 import shutil
+import string
+import shlex
+from pipes import _safechars
+import tempfile
 
 # use our own ASCII only uppercase function to avoid locale issues
 # not going to be fast but not important
-def uppercase_ASCII_string(str):
+def uppercase_ASCII_string(s):
     newstr = ""
-    for i in range(0,len(str)):
-	if str[i] in string.lowercase:
-	    newstr += chr(ord(str[i])-32)
-	else:
-	    newstr += str[i]
-
+    for c in s:
+        if c in string.lowercase:
+            newstr += chr(ord(c)-32)
+        else:
+            newstr += c
     return newstr
 
-class SimpleConfigFile:
-    def __str__ (self):
-        s = ""
-        keys = self.info.keys ()
-        keys.sort ()
-        for key in keys:
-            # FIXME - use proper escaping
-            if type (self.info[key]) == type(""):
-                s = s + key + "=\"" + self.info[key] + "\"\n"
-        return s
+def unquote(s):
+    return ' '.join(shlex.split(s))
 
-    def __init__ (self):
+def quote(s, always=False):
+    """ If always is set it returns a quoted value
+    """
+    if not always:
+        for c in s:
+            if c not in _safechars:
+                break
+        else:
+            return s
+    return '"'+s.replace('"', '\\"')+'"'
+
+class SimpleConfigFile(object):
+    """ Edit values in a configuration file without changing comments.
+        Supports KEY=VALUE lines and ignores everything else.
+        Supports adding new keys.
+        Supports deleting keys.
+        Preserves comment, blank lines and comments on KEY lines
+        Does not support duplicate key entries.
+    """
+    def __init__(self, filename=None, read_unquote=True, write_quote=True,
+                 always_quote=False):
+        self.filename = filename
+        self.read_unquote = read_unquote
+        self.write_quote = write_quote
+        self.always_quote = always_quote
+        self.reset()
+
+    def reset(self):
+        self._lines = []
         self.info = {}
 
-    def write(self, file):
-        f = open(file, "w")
-        f.write(self.__str__())
-        f.close()
-
-    def read(self, file):
-        if not os.access(file, os.R_OK):
-            return
-
-        f = open(file, "r")
-        lines = f.readlines()
-        f.close()
-
-        for line in lines:
-            fields = line[:-1].split('=', 2)
-            if len(fields) < 2:
-                # how am I supposed to know what to do here?
-                continue
-            key = uppercase_ASCII_string(fields[0])
-            value = fields[1]
-            # XXX hack
-            value = value.replace('"', '')
-            value = value.replace("'", '')
-            self.info[key] = value
-
-    def set (self, *args):
-        for (key, data) in args:
-            self.info[uppercase_ASCII_string(key)] = data
-
-    def unset (self, *keys):
-        for key in keys:
-            key = uppercase_ASCII_string(key)
-            if self.info.has_key (key):
+    def read(self, filename=None):
+        """ passing filename will override the filename passed to init.
+
+            save the lines into self._lines and the key/value pairs into
+            self.info
+        """
+        filename = filename or self.filename
+        with open(filename) as f:
+            for line in f:
+                self._lines.append(line)
+                key, value = self._parseline(line)
+                if key:
+                    self.info[key] = value
+
+    def write(self, filename=None):
+        """ passing filename will override the filename passed to init.
+        """
+        filename = filename or self.filename
+        if not filename:
+            return None
+
+        tmpf = tempfile.NamedTemporaryFile(mode="w", delete=False)
+        tmpf.write(str(self))
+        tmpf.close()
+
+        # Move the temporary file (with 0600 permissions) over the top of the
+        # original and preserve the original's permissions
+        filename = os.path.realpath(filename)
+        m = os.stat(filename)
+        shutil.move(tmpf.name, filename)
+        os.chmod(filename, m.st_mode)
+
+    def set(self, *args):
+        for key, value in args:
+           self.info[uppercase_ASCII_string(key)] = value
+
+    def unset(self, *keys):
+        for key in [uppercase_ASCII_string(k) for k in keys]:
+            if key in self.info:
                del self.info[key]
 
-    def get (self, key):
-        key = uppercase_ASCII_string(key)
-        return self.info.get(key, "")
+    def get(self, key):
+        return self.info.get(uppercase_ASCII_string(key), "")
+
+    def _parseline(self, line):
+        """ parse a line into a key, value pair
+            Handle comments and optionally unquote quoted strings
+            Returns (key, value) or (None, None)
+            key is always UPPERCASE
+        """
+        s = line.strip()
+        if '#' in s:
+            s = s[:s.find('#')] # remove from comment to EOL
+            s = s.strip()       # and any unnecessary whitespace
+        key, eq, val = s.partition('=')
+        if self.read_unquote:
+            val = unquote(val)
+        if key != '' and eq == '=':
+            return (uppercase_ASCII_string(key), val)
+        else:
+            return (None, None)
 
+    def _kvpair(self, key, comment=""):
+        value = self.info[key]
+        if self.write_quote or self.always_quote:
+            value = quote(value, self.always_quote)
+        return key + '=' + value + comment + "\n"
+
+    def __str__(self):
+        """ Return the file that was read, replacing existing keys with new values
+            removing keys that have been deleted and adding new keys.
+        """
+        oldkeys = []
+        s = ""
+        for line in self._lines:
+            key, val = self._parseline(line)
+            if key is None:
+                s += line
+            else:
+                if key not in self.info:
+                    continue
+                oldkeys.append(key)
+                if "#" in line:
+                    comment = " " + line[line.find("#"):]
+                else:
+                    comment = ""
+                s += self._kvpair(key, comment)
+
+        # Add new keys
+        for key in self.info:
+            if key not in oldkeys:
+                s += self._kvpair(key)
+
+        return s
 
-class IfcfgFile(SimpleConfigFile):
 
+class IfcfgFile(SimpleConfigFile):
     def __init__(self, dir, iface):
-        SimpleConfigFile.__init__(self)
+        SimpleConfigFile.__init__(self, always_quote=True)
         self.iface = iface
         self.dir = dir
 
@@ -96,35 +174,21 @@ class IfcfgFile(SimpleConfigFile):
         return os.path.join(self.dir, "ifcfg-%s" % self.iface)
 
     def clear(self):
-        self.info = {}
+        SimpleConfigFile.reset(self)
 
     def read(self):
-        """Reads values from ifcfg file.
+        """ Reads values from ifcfg file.
 
-        returns: number of values read
+            returns: number of values read
         """
-        f = open(self.path, "r")
-        lines = f.readlines()
-        f.close()
-
-        for line in lines:
-            line = line.strip()
-            if line.startswith("#") or line == '':
-                continue
-            fields = line.split('=', 1)
-            key = uppercase_ASCII_string(fields[0])
-            value = fields[1]
-            # XXX hack
-            value = value.replace('"', '')
-            value = value.replace("'", '')
-            self.info[key] = value
-
+        SimpleConfigFile.read(self, self.path)
         return len(self.info)
 
     # ifcfg-rh is using inotify IN_CLOSE_WRITE event
     # so we don't use temporary file for new configuration.
     def write(self, dir=None):
-        """Writes values into ifcfg file."""
+        """ Writes values into ifcfg file.
+        """
 
         if not dir:
             path = self.path
-- 
1.7.7.6



More information about the anaconda-patches mailing list