2013-01-23 17:28:19 +00:00
|
|
|
#
|
|
|
|
# simpleconifg.py - representation of a simple configuration file (sh-like)
|
|
|
|
#
|
2015-03-23 11:36:12 +00:00
|
|
|
# Copyright (C) 1999-2014 Red Hat, Inc.
|
2013-01-23 17:28:19 +00:00
|
|
|
#
|
2015-03-23 11:36:12 +00:00
|
|
|
# This copyrighted material is made available to anyone wishing to use,
|
|
|
|
# modify, copy, or redistribute it subject to the terms and conditions of
|
|
|
|
# the GNU General Public License v.2, or (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
|
|
# ANY WARRANTY expressed or implied, including the implied warranties of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
|
|
# Public License for more details. You should have received a copy of the
|
|
|
|
# GNU General Public License along with this program; if not, write to the
|
|
|
|
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
|
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
|
|
|
|
# source code or documentation are not subject to the GNU General Public
|
|
|
|
# License and may only be used or replicated with the express permission of
|
|
|
|
# Red Hat, Inc.
|
2013-01-23 17:28:19 +00:00
|
|
|
#
|
2015-03-23 11:36:12 +00:00
|
|
|
# Author(s): Matt Wilson <msw@redhat.com>
|
|
|
|
# Jeremy Katz <katzj@redhat.com>
|
|
|
|
# Will Woods <wwoods@redhat.com>
|
|
|
|
# Brian C. Lane <bcl@redhat.com>
|
2013-01-23 17:28:19 +00:00
|
|
|
#
|
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import shlex
|
|
|
|
from pipes import _safechars
|
|
|
|
import tempfile
|
2015-03-23 11:36:12 +00:00
|
|
|
from pyanaconda.iutil import upperASCII, eintr_retry_call
|
2013-01-23 17:28:19 +00:00
|
|
|
|
|
|
|
def unquote(s):
|
|
|
|
return ' '.join(shlex.split(s))
|
|
|
|
|
|
|
|
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('"', '\\"')+'"'
|
|
|
|
|
2015-03-23 11:36:12 +00:00
|
|
|
def find_comment(s):
|
|
|
|
""" Look for a # comment outside of a quoted string.
|
|
|
|
If there are no quotes, find the last # in the string.
|
|
|
|
|
|
|
|
:param str s: string to check for comment and quotes
|
|
|
|
:returns: index of comment or None
|
|
|
|
:rtype: int or None
|
|
|
|
|
|
|
|
Handles comments inside quotes and quotes inside quotes.
|
|
|
|
"""
|
|
|
|
q = None
|
|
|
|
for i in range(len(s)):
|
|
|
|
if not q and s[i] == '#':
|
|
|
|
return i
|
|
|
|
|
|
|
|
# Ignore quotes inside other quotes
|
|
|
|
if s[i] in "'\"":
|
|
|
|
if s[i] == q:
|
|
|
|
q = None
|
|
|
|
elif q is None:
|
|
|
|
q = s[i]
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2013-01-23 17:28:19 +00:00
|
|
|
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 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)
|
2015-03-23 11:36:12 +00:00
|
|
|
key, value, _comment = self._parseline(line)
|
2013-01-23 17:28:19 +00:00
|
|
|
if key:
|
|
|
|
self.info[key] = value
|
|
|
|
|
2014-04-07 12:38:09 +00:00
|
|
|
def write(self, filename=None, use_tmp=True):
|
2013-01-23 17:28:19 +00:00
|
|
|
""" passing filename will override the filename passed to init.
|
|
|
|
"""
|
|
|
|
filename = filename or self.filename
|
|
|
|
if not filename:
|
|
|
|
return None
|
|
|
|
|
2014-04-07 12:38:09 +00:00
|
|
|
if use_tmp:
|
|
|
|
tmpf = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
|
|
|
tmpf.write(str(self))
|
|
|
|
tmpf.close()
|
2013-01-23 17:28:19 +00:00
|
|
|
|
2014-04-07 12:38:09 +00:00
|
|
|
# Move the temporary file (with 0600 permissions) over the top of the
|
|
|
|
# original and preserve the original's permissions
|
|
|
|
filename = os.path.realpath(filename)
|
|
|
|
if os.path.exists(filename):
|
|
|
|
m = os.stat(filename).st_mode
|
|
|
|
else:
|
|
|
|
m = int('0100644', 8)
|
|
|
|
shutil.move(tmpf.name, filename)
|
2015-03-23 11:36:12 +00:00
|
|
|
eintr_retry_call(os.chmod, filename, m)
|
2013-01-23 17:28:19 +00:00
|
|
|
else:
|
2014-04-07 12:38:09 +00:00
|
|
|
# write directly to the file
|
|
|
|
with open(filename, "w") as fobj:
|
|
|
|
fobj.write(str(self))
|
2013-01-23 17:28:19 +00:00
|
|
|
|
|
|
|
def set(self, *args):
|
|
|
|
for key, value in args:
|
2014-04-07 12:38:09 +00:00
|
|
|
self.info[upperASCII(key)] = value
|
2013-01-23 17:28:19 +00:00
|
|
|
|
|
|
|
def unset(self, *keys):
|
2014-04-07 12:38:09 +00:00
|
|
|
for key in (upperASCII(k) for k in keys):
|
2013-01-23 17:28:19 +00:00
|
|
|
if key in self.info:
|
2014-04-07 12:38:09 +00:00
|
|
|
del self.info[key]
|
2013-01-23 17:28:19 +00:00
|
|
|
|
|
|
|
def get(self, key):
|
2014-04-07 12:38:09 +00:00
|
|
|
return self.info.get(upperASCII(key), "")
|
2013-01-23 17:28:19 +00:00
|
|
|
|
|
|
|
def _parseline(self, line):
|
2015-03-23 11:36:12 +00:00
|
|
|
""" parse a line into a key, value and comment
|
|
|
|
|
|
|
|
:param str line: Line to be parsed
|
|
|
|
:returns: Tuple of key, value, comment
|
|
|
|
:rtype: tuple
|
|
|
|
|
2013-01-23 17:28:19 +00:00
|
|
|
Handle comments and optionally unquote quoted strings
|
2015-03-23 11:36:12 +00:00
|
|
|
Returns (key, value, comment) or (None, None, comment)
|
|
|
|
key is always UPPERCASE and comment may by "" if none was found.
|
2013-01-23 17:28:19 +00:00
|
|
|
"""
|
|
|
|
s = line.strip()
|
2015-03-23 11:36:12 +00:00
|
|
|
# Look for a # outside any quotes
|
|
|
|
comment = ""
|
|
|
|
comment_index = find_comment(s)
|
|
|
|
if comment_index is not None:
|
|
|
|
comment = s[comment_index:]
|
|
|
|
s = s[:comment_index] # remove from comment to EOL
|
|
|
|
|
2013-01-23 17:28:19 +00:00
|
|
|
key, eq, val = s.partition('=')
|
2015-03-23 11:36:12 +00:00
|
|
|
key = key.strip()
|
|
|
|
val = val.strip()
|
2013-01-23 17:28:19 +00:00
|
|
|
if self.read_unquote:
|
|
|
|
val = unquote(val)
|
|
|
|
if key != '' and eq == '=':
|
2015-03-23 11:36:12 +00:00
|
|
|
return (upperASCII(key), val, comment)
|
2013-01-23 17:28:19 +00:00
|
|
|
else:
|
2015-03-23 11:36:12 +00:00
|
|
|
return (None, None, comment)
|
2013-01-23 17:28:19 +00:00
|
|
|
|
|
|
|
def _kvpair(self, key, comment=""):
|
|
|
|
value = self.info[key]
|
|
|
|
if self.write_quote or self.always_quote:
|
|
|
|
value = quote(value, self.always_quote)
|
2015-03-23 11:36:12 +00:00
|
|
|
if comment:
|
|
|
|
comment = " " + comment
|
2013-01-23 17:28:19 +00:00
|
|
|
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:
|
2015-03-23 11:36:12 +00:00
|
|
|
key, _value, comment = self._parseline(line)
|
2013-01-23 17:28:19 +00:00
|
|
|
if key is None:
|
|
|
|
s += line
|
|
|
|
else:
|
|
|
|
if key not in self.info:
|
|
|
|
continue
|
|
|
|
oldkeys.append(key)
|
|
|
|
s += self._kvpair(key, comment)
|
|
|
|
|
|
|
|
# Add new keys
|
|
|
|
for key in self.info:
|
|
|
|
if key not in oldkeys:
|
|
|
|
s += self._kvpair(key)
|
|
|
|
|
|
|
|
return s
|