qubes-installer-qubes-os/anaconda/pyanaconda/simpleconfig.py

239 lines
7.9 KiB
Python
Raw Normal View History

#
# simpleconifg.py - representation of a simple configuration file (sh-like)
#
# Copyright (C) 1999-2015 Red Hat, Inc.
#
# 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.
#
import os
import shlex
import string # pylint: disable=deprecated-module
import tempfile
from pyanaconda.iutil import upperASCII
_SAFECHARS = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')
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('"', '\\"')+'"'
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
def write_tmpfile(filename, data):
# Create a temporary in the same directory as the target file to ensure
# the new file is on the same filesystem
tmpf = tempfile.NamedTemporaryFile(mode="w", delete=False,
dir=os.path.dirname(filename) or '.', prefix="." + os.path.basename(filename))
tmpf.write(data)
tmpf.close()
# Change the permissions (currently 0600) to match the original file
if os.path.exists(filename):
m = os.stat(filename).st_mode
else:
m = 0o0644
os.chmod(tmpf.name, m)
# Move the temporary file over the top of the original
os.rename(tmpf.name, filename)
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)
key, value, _comment = self._parseline(line)
if key:
self.info[key] = value
def write(self, filename=None, use_tmp=True):
""" passing filename will override the filename passed to init.
"""
filename = filename or self.filename
if not filename:
return None
if use_tmp:
write_tmpfile(filename, str(self))
else:
# write directly to the file
with open(filename, "w") as fobj:
fobj.write(str(self))
def set(self, *args):
for key, value in args:
self.info[upperASCII(key)] = value
def unset(self, *keys):
for key in (upperASCII(k) for k in keys):
if key in self.info:
del self.info[key]
def get(self, key):
return self.info.get(upperASCII(key), "")
def _parseline(self, line):
""" parse a line into a key, value and comment
:param str line: Line to be parsed
:returns: Tuple of key, value, comment
:rtype: tuple
Handle comments and optionally unquote quoted strings
Returns (key, value, comment) or (None, None, comment)
key is always UPPERCASE and comment may by "" if none was found.
"""
s = line.strip()
# 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
key, eq, val = s.partition('=')
key = key.strip()
val = val.strip()
if self.read_unquote:
val = unquote(val)
if key != '' and eq == '=':
return (upperASCII(key), val, comment)
else:
return (None, None, comment)
def _kvpair(self, key, comment=""):
value = self.info[key]
if self.write_quote or self.always_quote:
value = quote(value, self.always_quote)
if comment:
comment = " " + comment
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, _value, comment = self._parseline(line)
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
def simple_replace(fname, keys, add=True, add_comment="# Added by Anaconda"):
""" Replace lines in a file, optionally adding if missing.
:param str fname: Filename to operate on
:param list keys: List of (key, string) tuples to search and replace
:param bool add: When True add strings that were not replaced
This will read all the lines in a file, looking for ones that start
with keys and replacing the line with the associated string. The string
should be a COMPLETE replacement for the line, not just a value.
When add is True any keys that haven't been found will be appended
to the end of the file along with the add_comment.
"""
# Helper to return the line or the first matching key's string
def _replace(l):
r = [s for k,s in keys if l.startswith(k)]
if r:
return r[0]
else:
return l
# Replace lines that match any of the keys
with open(fname, "r") as f:
lines = [_replace(l.strip()) for l in f]
# Add any strings that weren't already in the file
if add:
append = [s for k,s in keys if not any(l.startswith(k) for l in lines)]
if append:
lines += [add_comment]
lines += append
write_tmpfile(fname, "\n".join(lines)+"\n")