Hello community, here is the log from the commit of package crudini for openSUSE:Factory checked in at 2014-10-11 19:26:43 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/crudini (Old) and /work/SRC/openSUSE:Factory/.crudini.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "crudini" Changes: -------- --- /work/SRC/openSUSE:Factory/crudini/crudini.changes 2013-10-01 08:09:45.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.crudini.new/crudini.changes 2014-10-11 19:28:34.000000000 +0200 @@ -1,0 +2,18 @@ +Fri Oct 10 19:21:16 UTC 2014 - dmuel...@suse.com + +- update to 0.4: + * add --format=lines to support line by line processing + * doc: tweak readme to mention --format=lines + * Declare encoding to avoid fatal error + * fix duplicate DEFAULT section header being output + * ensure edited ini file contents are always complete + * split out --options from synopsis + * send --help to stdout + * provide alternative --rewrite file editing option + * provide --output option to allow redirecting output + * use only the base 'crudini' name in --help + * ensure writes to the edited ini are never lost + * add a new --list option to update a list of values + * honor case when merging new parameters + +------------------------------------------------------------------- Old: ---- crudini-0.3.tar.gz New: ---- crudini-0.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ crudini.spec ++++++ --- /var/tmp/diff_new_pack.QWuJC2/_old 2014-10-11 19:28:35.000000000 +0200 +++ /var/tmp/diff_new_pack.QWuJC2/_new 2014-10-11 19:28:35.000000000 +0200 @@ -1,7 +1,7 @@ # # spec file for package crudini # -# Copyright (c) 2013 SUSE LINUX Products GmbH, Nuernberg, Germany. +# Copyright (c) 2014 SUSE LINUX Products GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: crudini -Version: 0.3 +Version: 0.4 Release: 0 Summary: CRUD for .ini files License: GPL-2.0 ++++++ crudini-0.3.tar.gz -> crudini-0.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/EXAMPLES new/crudini-0.4/EXAMPLES --- old/crudini-0.3/EXAMPLES 1970-01-01 01:00:00.000000000 +0100 +++ new/crudini-0.4/EXAMPLES 2014-09-05 19:17:30.000000000 +0200 @@ -0,0 +1,35 @@ +Examples: + +# Add/Update a var + crudini --set config_file section parameter value + +# Update an existing var + crudini --set --existing config_file section parameter value + +# Delete a var + crudini --del config_file section parameter + +# Delete a section + crudini --del config_file section + +# output a value + crudini --get config_file section parameter + +# output a global value not in a section + crudini --get config_file '' parameter + +# output a section + crudini --get config_file section + +# output a section, parseable by shell + eval $(crudini --get --format=sh config_file section) + +# update an ini file from shell variable(s) + echo name="$name" | crudini --merge config_file section + +# merge an ini file from another ini + crudini --merge config_file < another.ini + +# compare two ini files using standard UNIX text processing + diff <(crudini --get --format=lines file1.ini|sort) \ + <(crudini --get --format=lines file2.ini|sort) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/Makefile new/crudini-0.4/Makefile --- old/crudini-0.3/Makefile 1970-01-01 01:00:00.000000000 +0100 +++ new/crudini-0.4/Makefile 2014-09-05 19:17:30.000000000 +0200 @@ -0,0 +1,12 @@ +name = crudini +version = 0.4 + +all: + help2man -n "manipulate ini files" -o crudini.1 -N ./crudini-help + ./crudini-help --help > README + +dist: all + mkdir ${name}-${version} + { git ls-files; echo crudini.1; } | xargs cp -a --parents --target=${name}-${version} + tar -czf ${name}-${version}.tar.gz ${name}-${version} + rm -Rf ${name}-${version} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/README new/crudini-0.4/README --- old/crudini-0.3/README 2013-03-08 14:48:41.000000000 +0100 +++ new/crudini-0.4/README 2014-09-05 19:17:30.000000000 +0200 @@ -1,40 +1,55 @@ -A utility for manipulating ini files - - -crudini --set [--existing] config_file section [param] [value] - --get [--format=sh|ini] config_file [section] [param] - --del [--existing] config_file section [param] - --merge [--existing] config_file [section] +crudini - A utility for manipulating ini files +Usage: crudini --set [OPTION]... config_file section [param] [value] + or: crudini --get [OPTION]... config_file [section] [param] + or: crudini --del [OPTION]... config_file section [param] [value] + or: crudini --merge [OPTION]... config_file [section] + +Options: + + --existing For --set, --del and --merge fail if the + section or param is not present + --format=FMT For --get, select the output FMT. + Formats are sh,ini,lines + --inplace Lock and write files in place. + This is not atomic but has less restrictions + than the default replacement method. + --list For --set and --del, update a list (set) of values + --list-sep=STR Delimit list values with "STR" instead of " ," + --output=FILE Write output to FILE instead. '-' means stdout Examples: # Add/Update a var -crudini --set config_file section parameter value + crudini --set config_file section parameter value # Update an existing var -crudini --set --existing config_file section parameter value + crudini --set --existing config_file section parameter value # Delete a var -crudini --del config_file section parameter + crudini --del config_file section parameter # Delete a section -crudini --del config_file section + crudini --del config_file section # output a value -crudini --get config_file section parameter + crudini --get config_file section parameter # output a global value not in a section -crudini --get config_file '' parameter + crudini --get config_file '' parameter # output a section -crudini --get config_file section + crudini --get config_file section # output a section, parseable by shell -eval $(crudini --get --format=sh config_file section) + eval $(crudini --get --format=sh config_file section) # update an ini file from shell variable(s) -echo name="$name" | crudini --merge config_file section + echo name="$name" | crudini --merge config_file section # merge an ini file from another ini -crudini --merge config_file < another.ini + crudini --merge config_file < another.ini + +# compare two ini files using standard UNIX text processing + diff <(crudini --get --format=lines file1.ini|sort) \ + <(crudini --get --format=lines file2.ini|sort) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/TODO new/crudini-0.4/TODO --- old/crudini-0.3/TODO 2013-03-08 14:48:41.000000000 +0100 +++ new/crudini-0.4/TODO 2014-09-05 19:17:30.000000000 +0200 @@ -5,4 +5,17 @@ support multiple files passed to --merge -possibly support --format=sh|json with --mergea +possibly support --format=sh|json with --merge + +possibly support multiple duplicate names per section +to support MultiStrOpt in openstack config files file example. +This could be interfaced using the --list=multiname option. +Also have --list autodetect multiline lists as used by yum like: + name = val1, val2 + val3 +I.E. split on a combo of [\n,] + +possibly support --lower to output normalised case for +--get and --sort + +support python3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/crudini new/crudini-0.4/crudini --- old/crudini-0.3/crudini 2013-03-08 14:48:41.000000000 +0100 +++ new/crudini-0.4/crudini 2014-09-05 19:17:30.000000000 +0200 @@ -1,19 +1,34 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- # vim:fileencoding=utf8 # -# Copyright (C) 2013, Pádraig Brady <p...@draigbrady.com> +# Copyright (C) 2014, Pádraig Brady <p...@draigbrady.com> # # This program is free software; you can redistribute it and/or modify it # under the terms of the GPLv2, the GNU General Public License version 2, as # published by the Free Software Foundation. http://gnu.org/licenses/gpl.html +import os import sys +import errno +import contextlib import ConfigParser import getopt import iniparse import pipes +import shutil import string +import tempfile from cStringIO import StringIO +import time + +# The following exits cleanly on Ctrl-C, +# while treating other exceptions as before. +def cli_exception(type, value, tb): + if not issubclass(type, KeyboardInterrupt): + sys.__excepthook__(type, value, tb) +if sys.stdin.isatty(): + sys.excepthook=cli_exception try: iniparse.DEFAULTSECT @@ -21,12 +36,28 @@ iniparse.DEFAULTSECT = 'DEFAULT' def usage(exitval=0): - cmd = sys.argv[0] - sys.stderr.write( - cmd + " --set [--existing] config_file section [param] [value]\n" + - cmd + " --get [--format=sh|ini] config_file [section] [param]\n" + - cmd + " --del [--existing] config_file section [param]\n" + - cmd + " --merge [--existing] config_file [section]\n" + cmd = os.path.basename(sys.argv[0]) + output = sys.stderr if exitval else sys.stdout + output.write( + "A utility for manipulating ini files\n" + "\n" + + "Usage: " + cmd + " --set [OPTION]... config_file section [param] [value]\n" + + " or: " + cmd + " --get [OPTION]... config_file [section] [param]\n" + + " or: " + cmd + " --del [OPTION]... config_file section [param] [value]\n" + + " or: " + cmd + " --merge [OPTION]... config_file [section]\n" + + "\n" + "Options:\n" + "\n" + " --existing For --set, --del and --merge fail if the\n" + " section or param is not present\n" + " --format=FMT For --get, select the output FMT.\n" + " Formats are sh,ini,lines\n" + " --inplace Lock and write files in place.\n" + " This is not atomic but has less restrictions\n" + " than the default replacement method.\n" + " --list For --set and --del, update a list (set) of values\n" + " --list-sep=STR Delimit list values with \"STR\" instead of \" ,\"\n" + " --output=FILE Write output to FILE instead. '-' means stdout\n" ) sys.exit(exitval) @@ -49,8 +80,20 @@ else: print section -def print_name_value(name, value): - if fmt == 'sh': +def print_name_value(name, value, section=None): + if fmt == 'lines': + # Both unambiguous and easily parseable by shell. Caveat is + # that sections and values with spaces are awkward to split in shell + if section: + line = '[ %s ]' % section + if name: + line += ' ' + if name: + line += '%s' % name + if value: + line += ' = %s' % value.replace('\n','\\n') + print line + elif fmt == 'sh': # Note we provide validation of the output indentifiers # as it's dangerous to leave validation to shell. # consider for example doing eval on this in shell: @@ -64,24 +107,26 @@ else: print name or value -mode = fmt = update = cfgfile = section = param = value = None +mode = fmt = update = inplace = cfgfile = output = section = param = \ +value = vlist = listsep = None def parse_options(): try: - long_options = ['set', 'del', 'get', 'merge', 'existing', 'format=', - 'help', 'version'] + long_options = ['set', 'del', 'get', 'list', 'list-sep=', 'merge', + 'existing', 'format=', 'output=', 'inplace', 'help', 'version'] opts, args = getopt.getopt(sys.argv[1:], '', long_options) except getopt.GetoptError, e: error(str(e)) usage(1) - global mode, fmt, update, cfgfile, section, param, value + global mode, fmt, update, inplace, cfgfile, output, section, param, \ + value, vlist, listsep for o, a in opts: if o in ('--help',): usage(0) elif o in ('--version',): - print 'crudini 0.3' + print 'crudini 0.4' sys.exit(0) elif o in ('--set', '--del', '--get', '--merge'): if mode: @@ -90,11 +135,19 @@ mode = o elif o in ('--format',): fmt = a - if fmt not in ('sh','ini'): + if fmt not in ('sh','ini','lines'): error('--format not recognized: %s' % fmt) usage(1) elif o in ('--existing',): update = True + elif o in ('--inplace',): + inplace = True + elif o in ('--list',): + vlist = "set" #TODO support combos of list, sorted, ... + elif o in ('--list-sep',): + listsep = a + elif o in ('--output',): + output = a if not mode: error('One of --set|--del|--get|--merge must be specified') @@ -108,15 +161,19 @@ except IndexError: pass + if not output: + output = cfgfile + if cfgfile is None: usage(1) if section is None and mode in ('--del', '--set'): usage(1) - if param is not None and mode in ('--merge'): - usage(1) - if value is not None and mode not in ('--set'): - error('A value should not be specified with %s' % mode) + if param is not None and mode in ('--merge',): usage(1) + if value is not None and mode not in ('--set',): + if not (mode == '--del' and vlist): + error('A value should not be specified with %s' % mode) + usage(1) if mode == '--merge' and fmt == 'sh': # I'm not sure how useful is is to support this. @@ -148,44 +205,211 @@ else: return self.fp.readline() + def seek(self, *args): + self.fp.seek(*args) + stdin = "" -def _parse_file(filename, add_default=False): - # Note we use RawConfigParser rather than SafeConfigParser - # to avoid unwanted variable interpolation. - # Note iniparse doesn't currently support allow_no_value=True. +def has_default_section(locked_file): + try: + if locked_file is None: + fp = StringIO(stdin) + else: + fp = locked_file.fp + + for line in fp: + if line.startswith('[%s]' % iniparse.DEFAULTSECT): + return True + + return False + + except IOError as e: + error(str(e)) + sys.exit(1) + finally: + fp.seek(0) + +def delete_if_exists(path): + """Delete a file, but ignore file not found error. + """ + try: + os.unlink(path) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + print str(e) + raise + +class FileLock(object): + """Advisory file based locking. This should be reasonably cross platform + and also work over distributed file systems.""" + def __init__(self, exclusive=False, separated=False): + # In inplace mode, the process must be careful to not close this fp + # until finished, nor open and close another fp associated with the file. + self.fp = None + self.locked = False + self.die = delete_if_exists # reference so available at teardown + + # Note we can't combine these methods to provide separated locks + # which are immune to stale file deadlock, as once the separated + # file is unlinked or renamed, you introduce a race with 3 or more users + # if there is an associated fcntl lock. + + if separated: + import signal + def cleanup(signum, frame): + sys.exit(1) + if hasattr(signal, "SIGTERM"): + signal.signal(signal.SIGTERM, cleanup) + + def lock(self): + while True: + try: + os.open(self.lockpath, os.O_EXCL | os.O_CREAT, 0) + except EnvironmentError as e: + if e.errno == errno.EEXIST: + time.sleep(1) + else: + raise + else: + self.locked = True + break + + def unlock(self): + if self.locked: # Don't clobber other locks on ctrl-c etc. + self.die (self.lockpath) + self.locked = False + + elif os.name == 'nt': + import msvcrt + def lock(self): + msvcrt.locking(self.fp, msvcrt.LK_LOCK, 1) + self.locked = True + + def unlock(self): + if self.locked: + msvcrt.locking(self.fp, msvcrt.LK_UNLCK, 1) + self.locked = False + + else: + import fcntl + def lock(self): + fcntl.lockf(self.fp, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH) + self.locked = True + + def unlock(self): + if self.locked: + fcntl.lockf(self.fp, fcntl.LOCK_UN) + self.locked = False + + FileLock.lock = lock + FileLock.unlock = unlock + + +class LockedFile(FileLock): + """Open a file with advisory locking. This provides the Isolation + property of ACID, to avoid missing writes. In addition this provides AC + properties of ACID if crudini is the only logic accessing the ini file. + This should work on most platforms and distributed file systems. + + Caveats in --inplace mode: + - File must be writeable + - File should be generally non readable to avoid read lock DoS. + Caveats in replace mode: + - Possibility of stale lock files left on crash leading to deadlock. + - Less responsive when there is contention.""" + + def __init__(self, filename, operation, inplace): + + self.filename = filename + self.operation = operation + + FileLock.__init__(self, operation != "--get", not inplace) + + if inplace: + self.lockpath = filename + else: + self.lockpath = os.path.join(os.path.dirname(filename), + '.' + os.path.basename(filename) + '.crudini.lck') + + if inplace: + open_mode = 'r' + if operation != "--get": + open_mode += '+' + + try: + if inplace: + self.fp = open(self.lockpath, open_mode) + # In general readers are protected by file_replace() + # but using read lock here gives AC of the ACID propserties + # when only accessing the file through crudini even with file_rewrite(). + self.lock() + else: + self.lock() + self.fp = open(self.filename) + except EnvironmentError as e: + error(str(e)) + sys.exit(1) + + def __del__(self): + # explicit close so closed in correct order + # if taking lock multiple times + self.unlock() + if self.fp: + self.fp.close() + + +locked_file = None + +# Note we use RawConfigParser rather than SafeConfigParser +# to avoid unwanted variable interpolation. +# Note iniparse doesn't currently support allow_no_value=True. +class CrudiniConfigParser(iniparse.RawConfigParser): + def __init__(self, preserve_case=False): + iniparse.RawConfigParser.__init__(self) + if preserve_case: + self.optionxform = str + +def _parse_file(filename, add_default=False, preserve_case=False): try: if filename == '-': fp = StringIO(stdin) else: - fp = open(filename) + global locked_file + fp = locked_file.fp if add_default: fp = add_default_section(fp) - conf = iniparse.RawConfigParser() + conf = CrudiniConfigParser(preserve_case=preserve_case) conf.readfp(fp) return conf - except IOError as e: + except EnvironmentError as e: error(str(e)) sys.exit(1) + finally: + fp.seek(0) # in case we need to reparse -def parse_file(filename): +def parse_file(filename, preserve_case=False): global added_default_section added_default_section = False + global locked_file + if filename != '-': + locked_file = LockedFile (filename, mode, inplace) + try: - conf = _parse_file(filename) + conf = _parse_file(filename, preserve_case=preserve_case) if not conf.items(iniparse.DEFAULTSECT): - # reparse with inserted [DEFAULT] to be able to add global opts etc. - # XXX: We don't distinguish the edge case where - # there is just [DEFAULT] in a file with no name=values. - # In that case a redundant [DEFAULT] will be output. - conf = _parse_file(filename, add_default=True) - added_default_section = True + # Check if there is just [DEFAULT] in a file with no + # name=values to avoid adding a duplicate section. + if not has_default_section(locked_file): + # reparse with inserted [DEFAULT] to be able to add global opts etc. + conf = _parse_file(filename, add_default=True, + preserve_case=preserve_case) + added_default_section = True except ConfigParser.MissingSectionHeaderError: - conf = _parse_file(filename, add_default=True) + conf = _parse_file(filename, add_default=True, preserve_case=preserve_case) added_default_section = True except ConfigParser.ParsingError as e: error(str(e)) @@ -197,7 +421,7 @@ if mode == '--merge': stdin = sys.stdin.read() # read all upfront so that we can reparse if needed - mconf = parse_file('-') + mconf = parse_file('-', preserve_case=True) madded_default_section = added_default_section conf = parse_file(cfgfile) @@ -206,20 +430,61 @@ and not madded_default_section and mconf.items(iniparse.DEFAULTSECT): added_default_section = madded_default_section +# TODO item should be items and split also +# especially in merge mode +def update_list(curr_val, item, mode, sep): + curr_items = [] + use_space = True + if curr_val: + if sep is None: + use_space = ' ' in curr_val or ',' not in curr_val + curr_items = [v.strip() for v in curr_val.split(",")] + else: + curr_items = curr_val.split(sep) + + if mode == "--set": + if item not in curr_items: + curr_items.append(item) + elif mode == "--del": + try: + curr_items.remove(item) + except ValueError: + pass + + if sep is None: + sep = "," + if use_space: + sep += " " + + return sep.join(curr_items) + def set_name_value(section, param, value): + curr_val = None + if update: if param is None: _sec = section == iniparse.DEFAULTSECT or conf.has_section(section) if not _sec: raise ConfigParser.NoSectionError(section) else: - _val = conf.get(section, param) + curr_val = conf.get(section, param) elif section != iniparse.DEFAULTSECT and not conf.has_section(section): - conf.add_section(section) + if mode == "--del": + raise ConfigParser.NoSectionError(section) + else: + conf.add_section(section) if param is not None: + if not update: + try: + curr_val = conf.get(section, param) + except ConfigParser.NoOptionError: + if mode == "--del": + return if value is None: value = '' + if vlist: + value = update_list(curr_val, value, mode, listsep) conf.set(section, param, value) try: @@ -240,7 +505,7 @@ ignore_errs = (ConfigParser.NoOptionError,) if section is not None: msection = section - else: + elif not update: ignore_errs += (ConfigParser.NoSectionError,) try: set_param = True @@ -261,7 +526,9 @@ elif value is None: if not conf.remove_option(section, param) and update: raise ConfigParser.NoOptionError(section, param) - elif mode == '--get': + else: # remove item from list + set_name_value(section, param, value) + elif mode == '--get' and fmt != 'lines': if section is None: if conf.defaults(): print_section_header(iniparse.DEFAULTSECT) @@ -290,25 +557,125 @@ else: name = None print_name_value(name, val) -except ConfigParser.NoSectionError: - error('Section not found: %s' % section) + elif mode == '--get' and fmt == 'lines': + if section is None: + sections = conf.sections() + if conf.defaults(): + sections.insert(0, iniparse.DEFAULTSECT) + else: + sections = (section,) + if param is not None: + val = conf.get(section, param) + print_name_value(param, val, section) + else: + for section in sections: + if section == iniparse.DEFAULTSECT: + defaults_to_strip = {} + else: + defaults_to_strip = conf.defaults() + items = False + for item in conf.items(section): + # XXX: Note this strips an item from section + # if matching value also in default (global) section. + if defaults_to_strip.get(item[0]) != item[1]: + val = item[1] + print_name_value(item[0], val, section) + items = True + if not items: + print_name_value(None, None, section) + +except ConfigParser.NoSectionError as e: + error('Section not found: %s' % e.section) sys.exit(1) except ConfigParser.NoOptionError: error('Parameter not found: %s' % param) sys.exit(1) +@contextlib.contextmanager +def remove_file_on_error(path): + """Protect code that wants to operate on PATH atomically. + Any exception will cause PATH to be removed. + """ + try: + yield + except Exception: + t, v, tb = sys.exc_info() + delete_if_exists(path) + raise t, v, tb + +def file_replace(name, data): + """Replace file as atomically as possible, + fulfilling and AC properties of ACID. + This is essentially using method 9 from: + http://www.pixelbeat.org/docs/unix_file_replacement.html + + Caveats: + - Changes ownership of the file being edited + by non root users (due to POSIX interface limitations). + - Loses any extended attributes of the original file + (due to the simplicity of this implementation). + - Existing hardlinks will be separated from the + newly replaced file. + - Ignores the write permissions of the original file. + - Requires write permission on the directory as well as the file. + - With python2 on windows we don't fulfill the A ACID property. + + To avoid the above caveats see the --inplace option. + """ + (f, tmp) = tempfile.mkstemp(".tmp", prefix=name+".", dir=".") + + with remove_file_on_error(tmp): + shutil.copystat(name, tmp) + + if hasattr(os, 'fchown') and os.geteuid() == 0: + st = os.stat(name) + os.fchown(f, st.st_uid, st.st_gid) + + os.write(f, data) + os.close(f) + + if hasattr(os,'replace'): # >= python 3.3 + os.replace(tmp, name) # atomic even on windos + elif os.name == 'posix': + os.rename(tmp, name) # atomic on POSIX + else: + backup = tmp+'.backup' + os.rename(name, backup) + os.rename(tmp, name) + delete_if_exists(backup) + +def file_rewrite(name, data): + """Rewrite file inplace avoiding the caveats + noted in file_replace(). + + Caveats: + - Not Atomic as readers may see incomplete data for a while. + - Not Consistent as multiple writers may overlap. + - Less Durable as exisiting data truncated before I/O completes. + - Requires write access to file rather than write access to dir. + """ + with open(name, 'w') as f: + f.write(data) + if mode != '--get': - with open(cfgfile, 'w') as f: - # XXX: Ideally we should just do conf.write(f) here, - # but to avoid iniparse issues, we massage the data a little here - str_data = str(conf.data) - if len(str_data) and str_data[-1] != '\n': - str_data += '\n' - - if ( - (added_default_section and not (section_explicit_default and mode in ('--set', '--merge'))) - or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None) - ): - str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1) + # XXX: Ideally we should just do conf.write(f) here, + # but to avoid iniparse issues, we massage the data a little here + str_data = str(conf.data) + if len(str_data) and str_data[-1] != '\n': + str_data += '\n' + + if ( + (added_default_section and not (section_explicit_default and mode in ('--set', '--merge'))) + or (mode == '--del' and section == iniparse.DEFAULTSECT and param is None) + ): + str_data = str_data.replace('[%s]\n' % iniparse.DEFAULTSECT, '', 1) - f.write(str_data) + try: + if output == '-': + sys.stdout.write(str_data) + else: + file_edit = file_rewrite if inplace else file_replace + file_edit(output, str_data) + except EnvironmentError as e: + error(str(e)) + sys.exit(1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/crudini-help new/crudini-0.4/crudini-help --- old/crudini-0.3/crudini-help 1970-01-01 01:00:00.000000000 +0100 +++ new/crudini-0.4/crudini-help 2014-09-05 19:17:30.000000000 +0200 @@ -0,0 +1,13 @@ +#!/bin/sh + +# crudini --help generator for help2man and README + +if [ "$1" = '--help' ]; then + printf '%s' 'crudini - ' + ./crudini --help + echo + cat EXAMPLES +elif [ "$1" = '--version' ]; then + ./crudini --version +fi + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/example.ini new/crudini-0.4/example.ini --- old/crudini-0.3/example.ini 2013-03-08 14:48:41.000000000 +0100 +++ new/crudini-0.4/example.ini 2014-09-05 19:17:30.000000000 +0200 @@ -31,8 +31,14 @@ [section1] combine=sections +[empty section] + [non-sh-compat] space name=val útf8name=val 1num=val ls;name=val + +[list] +list1 = v1, v2 +list2 = v1,v2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/setup.py new/crudini-0.4/setup.py --- old/crudini-0.3/setup.py 1970-01-01 01:00:00.000000000 +0100 +++ new/crudini-0.4/setup.py 2014-09-05 19:17:30.000000000 +0200 @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +import os +from setuptools import setup + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = "crudini", + version = "0.4", + author = "Pádraig Brady", + author_email = "p...@draigbrady.com", + description = ("A utility for manipulating ini files"), + license = "GPLv2", + keywords = "ini config edit", + url = "http://github.com/pixelb/crudini", + long_description=read('README'), + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Topic :: Utilities", + "Topic :: System :: Systems Administration", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Programming Language :: Python :: 2", + ], + install_requires = ['iniparse'], + scripts=["crudini"] +) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/tests/example.lines new/crudini-0.4/tests/example.lines --- old/crudini-0.3/tests/example.lines 1970-01-01 01:00:00.000000000 +0100 +++ new/crudini-0.4/tests/example.lines 2014-09-05 19:17:30.000000000 +0200 @@ -0,0 +1,28 @@ +[ DEFAULT ] global = supported +[ section1 ] dup1 = val1 +[ section1 ] dup2 = val2 +[ section1 ] nospace = val +[ section1 ] multiline = with\nleading\nspace +[ section1 ] nmultiline = not supported with\ +[ section1 ] comment_after1 = val +[ section1 ] comment_after2 = val;not a comment +[ section1 ] comment_after3 = val #not a comment +[ section1 ] escaped_not_processed = test \nescape +[ section1 ] colon = val +[ section1 ] double_quotes = "not removed" +[ section1 ] single_quotes = 'not removed' +[ section1 ] spaces_stripped = val +[ section1 ] internal_not_stripped = v al +[ section1 ] notempty1 = ;comment=val +[ section1 ] empty +[ section1 ] python_interpolate = %(dup1)s/blah +[ section1 ] interpolate2 = ${dup1}/blah +[ section1 ] caps = not significant +[ section1 ] combine = sections +[ empty section ] +[ non-sh-compat ] space name = val +[ non-sh-compat ] útf8name = val +[ non-sh-compat ] 1num = val +[ non-sh-compat ] ls;name = val +[ list ] list1 = v1, v2 +[ list ] list2 = v1,v2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/tests/section1.lines new/crudini-0.4/tests/section1.lines --- old/crudini-0.3/tests/section1.lines 1970-01-01 01:00:00.000000000 +0100 +++ new/crudini-0.4/tests/section1.lines 2014-09-05 19:17:30.000000000 +0200 @@ -0,0 +1,20 @@ +[ section1 ] dup1 = val1 +[ section1 ] dup2 = val2 +[ section1 ] nospace = val +[ section1 ] multiline = with\nleading\nspace +[ section1 ] nmultiline = not supported with\ +[ section1 ] comment_after1 = val +[ section1 ] comment_after2 = val;not a comment +[ section1 ] comment_after3 = val #not a comment +[ section1 ] escaped_not_processed = test \nescape +[ section1 ] colon = val +[ section1 ] double_quotes = "not removed" +[ section1 ] single_quotes = 'not removed' +[ section1 ] spaces_stripped = val +[ section1 ] internal_not_stripped = v al +[ section1 ] notempty1 = ;comment=val +[ section1 ] empty +[ section1 ] python_interpolate = %(dup1)s/blah +[ section1 ] interpolate2 = ${dup1}/blah +[ section1 ] caps = not significant +[ section1 ] combine = sections diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/tests/test.sh new/crudini-0.4/tests/test.sh --- old/crudini-0.3/tests/test.sh 2013-03-08 14:48:41.000000000 +0100 +++ new/crudini-0.4/tests/test.sh 2014-09-05 19:17:30.000000000 +0200 @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash trap "exit 130" INT cleanup() { rm -f test.ini good.ini example.ini; exit; } @@ -8,17 +8,16 @@ test=0 -fail() { test=$(($test+1)); echo "Test $test FAIL"; exit 1; } -ok() { test=$(($test+1)); echo "Test $test OK"; } +fail() { test=$(($test+1)); echo "Test $test FAIL (line ${BASH_LINENO[-2]})"; exit 1; } +ok() { test=$(($test+1)); echo "Test $test OK (line ${BASH_LINENO[-2]})"; } cp ../example.ini . # invalid params ---------------------------------------- -# 1 :> test.ini crudini 2>/dev/null && fail -crudini --met test.init 2>/dev/null && fail # bad mode +crudini --met test.ini 2>/dev/null && fail # bad mode crudini --set 2>/dev/null && fail # no file crudini --set test.ini 2>/dev/null && fail # no section crudini --get 2>/dev/null && fail # no file @@ -34,96 +33,100 @@ # --set ------------------------------------------------- -# 2 :> test.ini crudini --set test.ini '' name val printf '%s\n' 'name = val' > good.ini diff -u test.ini good.ini && ok || fail -# 3 :> test.ini crudini --set test.ini DEFAULT name val printf '%s\n' '[DEFAULT]' 'name = val' > good.ini diff -u test.ini good.ini && ok || fail -# 4 # Note blank line inserted at start :> test.ini crudini --set test.ini nonDEFAULT name val printf '%s\n' '' '[nonDEFAULT]' 'name = val' > good.ini diff -u test.ini good.ini && ok || fail -# 5 printf '%s\n' 'global=val' > test.ini crudini --set test.ini '' global valnew printf '%s\n' 'global=valnew' > good.ini diff -u test.ini good.ini && ok || fail -# 6 printf '%s\n' 'global=val' > test.ini crudini --set test.ini DEFAULT global valnew printf '%s\n' '[DEFAULT]' 'global=valnew' > good.ini diff -u test.ini good.ini && ok || fail -# 7 printf '%s\n' '[DEFAULT]' 'global=val' > test.ini crudini --set test.ini DEFAULT global valnew printf '%s\n' '[DEFAULT]' 'global=valnew' > good.ini diff -u test.ini good.ini && ok || fail -# 8 printf '%s\n' 'global=val' '' '[nonDEFAULT]' 'name=val' > test.ini crudini --set test.ini '' global valnew printf '%s\n' 'global=valnew' '' '[nonDEFAULT]' 'name=val' > good.ini diff -u test.ini good.ini && ok || fail -# 9 +# do these --sets which test [DEFAULT] handling also with --inplace +for mode in '' '--inplace'; do # Add '[DEFAULT]' if explicitly specified -printf '%s\n' 'global=val' '' '[nonDEFAULT]' 'name=val' > test.ini -crudini --set test.ini DEFAULT global valnew -printf '%s\n' '[DEFAULT]' 'global=valnew' '' '[nonDEFAULT]' 'name=val' > good.ini -diff -u test.ini good.ini && ok || fail + printf '%s\n' 'global=val' '' '[nonDEFAULT]' 'name=val' > test.ini + crudini $mode --set test.ini DEFAULT global valnew + printf '%s\n' '[DEFAULT]' 'global=valnew' '' '[nonDEFAULT]' 'name=val' > good.ini + diff -u test.ini good.ini && ok || fail -# 10 -printf '%s\n' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > test.ini -crudini --set test.ini DEFAULT global val -printf '%s\n' '[DEFAULT]' 'global = val' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > good.ini -diff -u test.ini good.ini && ok || fail + printf '%s\n' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > test.ini + crudini $mode --set test.ini DEFAULT global val + printf '%s\n' '[DEFAULT]' 'global = val' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > good.ini + diff -u test.ini good.ini && ok || fail -# 11 -printf '%s\n' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > test.ini -crudini --set test.ini '' global val -printf '%s\n' 'global = val' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > good.ini -diff -u test.ini good.ini && ok || fail + printf '%s\n' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > test.ini + crudini $mode --set test.ini '' global val + printf '%s\n' 'global = val' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > good.ini + diff -u test.ini good.ini && ok || fail -# 12 XXX: Extraneous [DEFAULT] output in this edge case -printf '%s\n' '[DEFAULT]' > test.ini -crudini --set test.ini DEFAULT global val -printf '%s\n' '[DEFAULT]' '[DEFAULT]' 'global = val' > good.ini -diff -u test.ini good.ini && ok || fail + # Ensure '[DEFAULT]' is not duplicated + printf '%s\n' '[DEFAULT]' > test.ini + crudini $mode --set test.ini DEFAULT global val + printf '%s\n' '[DEFAULT]' 'global = val' > good.ini + diff -u test.ini good.ini && ok || fail + + # Ensure '[DEFAULT]' is not duplicated when trailing space is present + printf '%s\n' '[DEFAULT] ' > test.ini + crudini $mode --set test.ini DEFAULT global val + printf '%s\n' '[DEFAULT] ' 'global = val' > good.ini + diff -u test.ini good.ini && ok || fail -# 13 Maintain colon separation -crudini --set example.ini section1 colon val -grep -q '^colon:val' example.ini && ok || fail + # Ensure '[DEFAULT]' is not duplicated when a trailing comment is present + printf '%s\n' '[DEFAULT] #comment' > test.ini + crudini $mode --set test.ini DEFAULT global val + printf '%s\n' '[DEFAULT] #comment' 'global = val' > good.ini + diff -u test.ini good.ini && ok || fail -# 14 Maintain space separation -crudini --set example.ini section1 nospace val -grep -q '^nospace=val' example.ini && ok || fail + # Maintain colon separation + crudini $mode --set example.ini section1 colon val + grep -q '^colon:val' example.ini && ok || fail + + # Maintain space separation + crudini $mode --set example.ini section1 nospace val + grep -q '^nospace=val' example.ini && ok || fail +done -# 15 value is optional +# value is optional :> test.ini crudini --set test.ini '' name printf '%s\n' 'name = ' > good.ini diff -u test.ini good.ini && ok || fail -# 16 # value is optional printf '%s\n' 'name=val' > test.ini crudini --set test.ini '' name printf '%s\n' 'name=' > good.ini diff -u test.ini good.ini && ok || fail -# 17 --existing +# --existing :> test.ini crudini --set test.ini '' gname val crudini --set --existing test.ini '' gname val2 @@ -134,20 +137,20 @@ printf '%s\n' 'gname = val2' '' '' '[section1]' 'name = val2' > good.ini diff -u test.ini good.ini && ok || fail -# 18 missing +# missing crudini --set missing.ini '' name val 2>/dev/null && fail || ok # --get ------------------------------------------------- -# 19 basic get +# basic get test "$(crudini --get example.ini section1 cAps)" = 'not significant' && ok || fail -# 20 get sections +# get sections crudini --get example.ini > test.ini -printf '%s\n' DEFAULT section1 non-sh-compat > good.ini +printf '%s\n' DEFAULT section1 'empty section' non-sh-compat list > good.ini diff -u test.ini good.ini && ok || fail -# 21 get implicit default section +# get implicit default section crudini --get example.ini '' > test.ini printf '%s\n' 'global' > good.ini diff -u test.ini good.ini || fail @@ -156,7 +159,7 @@ diff -u test.ini good.ini || fail ok -# 22 get explicit default section +# get explicit default section crudini --get example.ini DEFAULT > test.ini printf '%s\n' 'global' > good.ini diff -u test.ini good.ini || fail @@ -165,22 +168,22 @@ diff -u test.ini good.ini || fail ok -# 23 get section1 in ini format +# get section1 in ini format crudini --format=ini --get example.ini section1 > test.ini diff -u test.ini section1.ini && ok || fail -# 24 get section1 in sh format +# get section1 in sh format crudini --format=sh --get example.ini section1 > test.ini diff -u test.ini section1.sh && ok || fail -# 24 empty DEFAULT is not printed +# empty DEFAULT is not printed printf '%s\n' '[DEFAULT]' '#comment' '[section1]' > test.ini test "$(crudini --get test.ini)" = 'section1' || fail printf '%s\n' '#comment' '[section1]' > test.ini test "$(crudini --get test.ini)" = 'section1' || fail ok -# 26 missing bits +# missing bits :> test.ini crudini --get missing.ini 2>/dev/null && fail test "$(crudini --get test.ini)" = '' || fail @@ -190,113 +193,122 @@ # --merge ----------------------------------------------- -# 27 XXX: An empty default section isn't merged +# XXX: An empty default section isn't merged :> test.ini printf '%s\n' '[DEFAULT]' '#comment' '[section1]' | crudini --merge test.ini || fail printf '%s\n' '' '[section1]' > good.ini diff -u test.ini good.ini && ok || fail -# 28 :> test.ini printf '%s\n' '[DEFAULT]' 'name=val' '[section1]' | crudini --merge test.ini || fail printf '%s\n' '[DEFAULT]' 'name = val' '' '[section1]' > good.ini diff -u test.ini good.ini && ok || fail -# 29 :> test.ini printf '%s\n' 'name=val' | crudini --merge test.ini || fail printf '%s\n' 'name = val' > good.ini diff -u test.ini good.ini && ok || fail -# 30 printf '%s\n' 'name=val1' > test.ini printf '%s\n' 'name = val2' | crudini --merge test.ini || fail printf '%s\n' 'name=val2' > good.ini diff -u test.ini good.ini && ok || fail -# 31 printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini printf '%s\n' 'name=val2' | crudini --merge test.ini || fail printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini diff -u test.ini good.ini && ok || fail -# 32 printf '%s\n' 'name = val1' > test.ini printf '%s\n' 'name=val2' | crudini --merge test.ini '' || fail printf '%s\n' 'name = val2' > good.ini diff -u test.ini good.ini && ok || fail -# 33 printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini printf '%s\n' '[DEFAULT]' 'name=val2' | crudini --merge test.ini || fail printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini diff -u test.ini good.ini && ok || fail -# 34 printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini printf '%s\n' '[DEFAULT]' 'name=val2' | crudini --merge test.ini '' || fail printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini diff -u test.ini good.ini && ok || fail -# 35 printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini printf '%s\n' 'name=val2' | crudini --merge test.ini '' || fail printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini diff -u test.ini good.ini && ok || fail -# 36 printf '%s\n' 'name=val1' > test.ini printf '%s\n' 'name=val2' | crudini --merge test.ini DEFAULT || fail printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini diff -u test.ini good.ini && ok || fail -# 37 printf '%s\n' 'name=val1' > test.ini printf '%s\n' 'name=val2' | crudini --merge test.ini new || fail printf '%s\n' 'name=val1' '' '' '[new]' 'name = val2' > good.ini diff -u test.ini good.ini && ok || fail -# 38 printf '%s\n' 'name=val1' > test.ini printf '%s\n' 'name=val2' | crudini --merge --existing test.ini new 2>/dev/null && fail || ok -# 39 printf '%s\n' 'name=val1' > test.ini printf '%s\n' 'name2=val2' | crudini --merge --existing test.ini || fail printf '%s\n' 'name=val1' > good.ini diff -u test.ini good.ini && ok || fail -# 40 printf '%s\n' 'name=val1' '[section1]' 'name=val2' > test.ini printf '%s\n' 'name=val1a' '[section1]' 'name=val2a' | crudini --merge --existing test.ini || fail printf '%s\n' 'name=val1a' '[section1]' 'name=val2a' > good.ini diff -u test.ini good.ini && ok || fail -# 41 All input sections merged to a specific section +# All input sections merged to a specific section printf '%s\n' 'name=val1' '[section1]' 'name=val2' > test.ini printf '%s\n' 'name=val2a' '[section2]' 'name2=val' | crudini --merge test.ini 'section1' || fail printf '%s\n' 'name=val1' '[section1]' 'name=val2a' 'name2 = val' > good.ini diff -u test.ini good.ini && ok || fail +# Maintain case for existing parameters +printf '%s\n' '[section]' 'name=val' > test.ini +printf '%s\n' '[section]' 'Name=val' | +crudini --merge test.ini || fail +printf '%s\n' '[section]' 'name=val'> good.ini +diff -u test.ini good.ini && ok || fail + +# Honor case for new parameters (spacing not currently honored) +printf '%s\n' '[section]' 'name1=val' > test.ini +printf '%s\n' '[section]' 'Name2=val' | +crudini --merge test.ini || fail +printf '%s\n' '[section]' 'name1=val' 'Name2 = val' > good.ini +diff -u test.ini good.ini && ok || fail + +# Note iniparse currently matches sections case insensitively +printf '%s\n' '[section1]' 'name=val1' > test.ini +printf '%s\n' '[Section1]' 'name=val2' | +crudini --merge --existing 2>/dev/null test.ini && fail || ok +printf '%s\n' '[Section1]' 'name=val2' | +crudini --merge test.ini || fail +printf '%s\n' '[section1]' 'name=val1' '' '' '[Section1]' 'name = val2' > good.ini +diff -u test.ini good.ini && ok || fail + # --del ------------------------------------------------- for sec in '' '[DEFAULT]'; do -# 42 46 printf '%s\n' $sec 'name = val' > test.ini crudini --del test.ini '' noname || fail crudini --del --existing test.ini '' noname 2>/dev/null && fail @@ -305,7 +317,6 @@ [ "$sec" ] && printf '%s\n' $sec > good.ini diff -u test.ini good.ini && ok || fail -# 43 47 printf '%s\n' $sec 'name = val' > test.ini crudini --del test.ini 'DEFAULT' noname || fail crudini --del --existing test.ini 'DEFAULT' noname 2>/dev/null && fail @@ -314,7 +325,6 @@ [ "$sec" ] && printf '%s\n' $sec > good.ini diff -u test.ini good.ini && ok || fail -# 44 48 printf '%s\n' $sec 'name = val' > test.ini crudini --del test.ini nosect || fail crudini --del --existing test.ini nosect 2>/dev/null && fail @@ -322,7 +332,6 @@ :> good.ini diff -u test.ini good.ini && ok || fail -# 45 49 printf '%s\n' $sec 'name = val' > test.ini crudini --del test.ini nosect || fail crudini --del --existing test.ini nosect 2>/dev/null && fail @@ -330,3 +339,54 @@ :> good.ini diff -u test.ini good.ini && ok || fail done + +# --get-lines -------------------------------------------- + +crudini --get --format=lines example.ini section1 > test.ini || fail +diff -u test.ini section1.lines && ok || fail + +crudini --get --format=lines example.ini > test.ini || fail +diff -u test.ini example.lines && ok || fail + +# --list ------------------------------------------------- + +# Add new item to list +crudini --list --set example.ini list list1 v3 || fail +test "$(crudini --get example.ini list list1)" = 'v1, v2, v3' && ok || fail + +# Ensure item in list +crudini --list --set example.ini list list1 v3 || fail +test "$(crudini --get example.ini list list1)" = 'v1, v2, v3' && ok || fail + +# Delete item from list +crudini --list --del example.ini list list1 v3 || fail +test "$(crudini --get example.ini list list1)" = 'v1, v2' && ok || fail + +# Delete non existing item from list +for existing in '' '--existing'; do + crudini $existing --list --del example.ini list list1 v3 || fail + test "$(crudini --get example.ini list list1)" = 'v1, v2' && ok || fail +done + +# Add new item to list without spacing +# auto +crudini --list --set example.ini list list2 v3 || fail +test "$(crudini --get example.ini list list2)" = 'v1,v2,v3' && ok || fail +crudini --set example.ini list list2 'v1,v2' || fail +# explicit +crudini --list --list-sep=, --set example.ini list list2 v3 || fail +test "$(crudini --get example.ini list list2)" = 'v1,v2,v3' && ok || fail + + +# Delete item from list without spacing +# auto +crudini --list --del example.ini list list2 v3 || fail +test "$(crudini --get example.ini list list2)" = 'v1,v2' && ok || fail +crudini --set example.ini list list2 'v1,v2,v3' || fail +# explicit +crudini --list --list-sep=, --del example.ini list list2 v3 || fail +test "$(crudini --get example.ini list list2)" = 'v1,v2' && ok || fail + +# Delete honoring --existing +crudini --list --existing --del example.ini nolist list1 v3 2>/dev/null && fail || ok +crudini --list --existing --del example.ini list nolist1 v3 2>/dev/null && fail || ok diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crudini-0.3/tox.ini new/crudini-0.4/tox.ini --- old/crudini-0.3/tox.ini 1970-01-01 01:00:00.000000000 +0100 +++ new/crudini-0.4/tox.ini 2014-09-05 19:17:30.000000000 +0200 @@ -0,0 +1,6 @@ +[tox] +envlist = py26,py27 + +[testenv] +deps=iniparse +commands=/bin/bash -c 'cd tests && ./test.sh' -- To unsubscribe, e-mail: opensuse-commit+unsubscr...@opensuse.org For additional commands, e-mail: opensuse-commit+h...@opensuse.org