crudini-0.9.3/0000775000175000017500000000000013532204577012772 5ustar padraigpadraigcrudini-0.9.3/crudini0000775000175000017500000010632313532203771014355 0ustar padraigpadraig#!/usr/bin/env python # -*- coding: utf-8 -*- # vim:fileencoding=utf8 # # Copyright © Pádraig Brady # # 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 from __future__ import print_function import atexit import sys import contextlib import errno import getopt import hashlib import iniparse import os import pipes import shutil import string import tempfile if sys.version_info[0] >= 3: from io import StringIO import configparser else: from cStringIO import StringIO import ConfigParser as configparser def error(message=None): if message: sys.stderr.write(message + '\n') 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 # TODO: support configurable options for various ini variants. # For now just support parameters without '=' specified class CrudiniInputFilter(): def __init__(self, fp): self.fp = fp self.crudini_no_arg = False def readline(self): line = self.fp.readline() # XXX: This doesn't handle ;inline comments. # Really should be done within inparse. if (line and line[0] not in '[ \t#;\n\r' and '=' not in line and ':' not in line): self.crudini_no_arg = True line = line[:-1] + ' = crudini_no_arg\n' return line # XXX: should be done in iniparse. Used to # add support for ini files without a section class AddDefaultSection(CrudiniInputFilter): def __init__(self, fp): CrudiniInputFilter.__init__(self, fp) self.first = True def readline(self): if self.first: self.first = False return '[%s]' % iniparse.DEFAULTSECT else: return CrudiniInputFilter.readline(self) 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): # 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 if 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): operation = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH fcntl.lockf(self.fp, operation) 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: - Less responsive when there is contention.""" def __init__(self, filename, operation, inplace, create): self.fp_cmp = None self.filename = filename self.operation = operation FileLock.__init__(self, operation != "--get") atexit.register(self.delete) open_mode = os.O_RDONLY if operation != "--get": # We're only reading here, but we check now for write # permissions we'll need in --inplace case to avoid # redundant processing. # Also an exlusive lock needs write perms anyway. open_mode = os.O_RDWR if create and operation != '--del': open_mode += os.O_CREAT try: self.fp = os.fdopen(os.open(self.filename, open_mode, 0o666)) if inplace: # In general readers (--get) are protected by file_replace(), # but using read lock here gives AC of the ACID properties # when only accessing the file through crudini even with # file_rewrite(). self.lock() else: # The file may have been renamed since the open so recheck while True: self.lock() fpnew = os.fdopen(os.open(self.filename, open_mode, 0o666)) if os.path.sameopenfile(self.fp.fileno(), fpnew.fileno()): # Note we don't fpnew.close() here as that would break # any existing fcntl lock (fcntl.lockf is an fcntl lock # despite the name). We don't use flock() at present # as that's less consistent across platforms and may # be an fcntl lock on NFS anyway for example. self.fp_cmp = fpnew break else: self.fp.close() self.fp = fpnew except EnvironmentError as e: # Treat --del on a non existing file as operating on NULL data # which will be deemed unchanged, and thus not re{written,created} # We don't exit early here so that --verbose is also handled. if create and operation == '--del' \ and e.errno in (errno.ENOTDIR, errno.ENOENT): self.fp = StringIO('') else: error(str(e)) sys.exit(1) def delete(self): # explicit close so closed in correct order if taking lock multiple # times, and also explicit "delete" needed to avoid implicit __del__ # after os module is unloaded. self.unlock() if self.fp: self.fp.close() if self.fp_cmp: self.fp_cmp.close() # 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) # Without the following we can't have params starting with "rem"! # We ignore lines starting with '%' which mercurial uses to include iniparse.change_comment_syntax('%;#', allow_rem=False) if preserve_case: self.optionxform = str class Print(): """Use for default output format.""" def section_header(self, section): """Print section header. :param section: str """ print(section) def name_value(self, name, value, section=None): """Print parameter. :param name: str :param value: str :param section: str (default 'None') """ if value == 'crudini_no_arg': value = '' print(name or value) class PrintIni(Print): """Use for ini output format.""" def section_header(self, section): print("[%s]" % section) def name_value(self, name, value, section=None): if value == 'crudini_no_arg': value = '' print(name, '=', value.replace('\n', '\n ')) class PrintLines(Print): """Use for lines output format.""" def name_value(self, name, value, section=None): # 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 == 'crudini_no_arg': value = '' if value: line += ' = %s' % value.replace('\n', '\\n') print(line) class PrintSh(Print): """Use for shell output format.""" @staticmethod def _valid_sh_identifier( i, safe_chars=frozenset(string.ascii_letters + string.digits + '_') ): """Provide validation of the output identifiers as it's dangerous to leave validation to shell. Consider for example doing eval on this in shell: rm -Rf /;oops=val :param i: str :param sh_safe_id_chars: frozenset :return: bool """ if i[0] in string.digits: return False for c in i: if c not in safe_chars: return False return True def name_value(self, name, value, section=None): if not PrintSh._valid_sh_identifier(name): error('Invalid sh identifier: %s' % name) sys.exit(1) if value == 'crudini_no_arg': value = '' sys.stdout.write("%s=%s\n" % (name, pipes.quote(value))) class Crudini(): mode = fmt = update = inplace = cfgfile = output = section = param = \ value = vlist = listsep = verbose = None locked_file = None section_explicit_default = False data = None conf = None added_default_section = False _print = None # The following exits cleanly on Ctrl-C, # while treating other exceptions as before. @staticmethod def cli_exception(type, value, tb): if not issubclass(type, KeyboardInterrupt): sys.__excepthook__(type, value, tb) @staticmethod @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).with_traceback(tb) @staticmethod 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 fulfil the A ACID property. To avoid the above caveats see the --inplace option. """ (f, tmp) = tempfile.mkstemp(".tmp", prefix=name + ".", dir=".") with Crudini.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) if sys.version_info[0] >= 3: os.write(f, bytearray(data, 'utf-8')) else: os.write(f, data) # We assume the existing file is persisted, # so sync here to ensure new data is persisted # before referencing it. Otherwise the metadata could # be written first, referencing the new data, which # would be nothing if a crash occured before the # data was allocated/persisted. os.fsync(f) 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) # Sync out the new directory entry to provide # better durability that the new inode is referenced # rather than continuing to reference the old inode. # This also provides verification in exit status that # this update completes. O_DIRECTORY = os.O_DIRECTORY if hasattr(os, 'O_DIRECTORY') else 0 dirfd = os.open(os.path.dirname(name) or '.', O_DIRECTORY) os.fsync(dirfd) os.close(dirfd) @staticmethod 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 existing 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) f.flush() os.fsync(f.fileno()) @staticmethod def init_iniparse_defaultsect(): try: iniparse.DEFAULTSECT except AttributeError: iniparse.DEFAULTSECT = 'DEFAULT' # TODO item should be items and split also # especially in merge mode @staticmethod 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 usage(self, exitval=0): cmd = os.path.basename(sys.argv[0]) output = sys.stderr if exitval else sys.stdout output.write("""\ A utility for manipulating ini files Usage: %s --set [OPTION]... config_file section [param] [value] or: %s --get [OPTION]... config_file [section] [param] or: %s --del [OPTION]... config_file section [param] [list value] or: %s --merge [OPTION]... config_file [section] Options: --existing[=WHAT] For --set, --del and --merge, fail if item is missing, where WHAT is 'file', 'section', or 'param', or if not specified; all specified items. --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 --verbose Indicate on stderr if changes were made --help Write this help to stdout --version Write version to stdout """ % (cmd, cmd, cmd, cmd) ) sys.exit(exitval) def parse_options(self): # Handle optional arg to long option # The gettopt module should really support this for i, opt in enumerate(sys.argv): if opt == '--existing': sys.argv[i] = '--existing=' elif opt == '--': break try: long_options = [ 'del', 'existing=', 'format=', 'get', 'help', 'inplace', 'list', 'list-sep=', 'merge', 'output=', 'set', 'verbose', 'version' ] opts, args = getopt.getopt(sys.argv[1:], '', long_options) except getopt.GetoptError as e: error(str(e)) self.usage(1) for o, a in opts: if o in ('--help',): self.usage(0) elif o in ('--version',): print('crudini 0.9.3') sys.exit(0) elif o in ('--verbose',): self.verbose = True elif o in ('--set', '--del', '--get', '--merge'): if self.mode: error('One of --set|--del|--get|--merge can be specified') self.usage(1) self.mode = o elif o in ('--format',): self.fmt = a if self.fmt not in ('sh', 'ini', 'lines'): error('--format not recognized: %s' % self.fmt) self.usage(1) elif o in ('--existing',): self.update = a or 'param' # 'param' implies all must exist if self.update not in ('file', 'section', 'param'): error('--existing item not recognized: %s' % self.update) self.usage(1) elif o in ('--inplace',): self.inplace = True elif o in ('--list',): self.vlist = "set" # TODO support combos of list, sorted, ... elif o in ('--list-sep',): self.listsep = a elif o in ('--output',): self.output = a if not self.mode: error('One of --set|--del|--get|--merge must be specified') self.usage(1) try: self.cfgfile = args[0] self.section = args[1] self.param = args[2] self.value = args[3] except IndexError: pass if not self.output: self.output = self.cfgfile if self.cfgfile is None: self.usage(1) if self.section is None and self.mode in ('--del', '--set'): self.usage(1) if self.param is not None and self.mode in ('--merge',): self.usage(1) if self.value is not None and self.mode not in ('--set',): if not (self.mode == '--del' and self.vlist): error('A value should not be specified with %s' % self.mode) self.usage(1) if self.mode == '--merge' and self.fmt == 'sh': # I'm not sure how useful is is to support this. # printenv will already generate a mostly compat ini format. # If you want to also include non exported vars (from `set`), # then there is a format change. error('sh format input is not supported at present') sys.exit(1) # Protect against generating non parseable ini files if self.section and ('[' in self.section or ']' in self.section): error("section names should not contain '[' or ']': %s" % self.section) sys.exit(1) if self.param and self.param.startswith('['): error("param names should not start with '[': %s" % self.param) sys.exit(1) if self.fmt == 'lines': self._print = PrintLines() elif self.fmt == 'sh': self._print = PrintSh() elif self.fmt == 'ini': self._print = PrintIni() else: self._print = Print() def _has_default_section(self): fp = StringIO(self.data) for line in fp: if line.startswith('[%s]' % iniparse.DEFAULTSECT): return True return False def _chksum(self, data): h = hashlib.sha256() if sys.version_info[0] >= 3: h.update(bytearray(data, 'utf-8')) else: h.update(data) return h.digest() def _parse_file(self, filename, add_default=False, preserve_case=False): try: if self.data is None: # Read all data up front as this is done by iniparse anyway # Doing it here will avoid rereads on reparse and support # correct parsing of stdin if filename == '-': self.data = sys.stdin.read() else: self.data = self.locked_file.fp.read() if self.mode != '--get': # compare checksums to flag any changes # (even spacing or case adjustments) with --verbose, # and to avoid rewriting the file if not necessary self.chksum = self._chksum(self.data) if self.data.startswith('\n'): self.newline_at_start = True else: self.newline_at_start = False fp = StringIO(self.data) if add_default: fp = AddDefaultSection(fp) else: fp = CrudiniInputFilter(fp) conf = CrudiniConfigParser(preserve_case=preserve_case) conf.readfp(fp) self.crudini_no_arg = fp.crudini_no_arg return conf except EnvironmentError as e: error(str(e)) sys.exit(1) def parse_file(self, filename, preserve_case=False): self.added_default_section = False self.data = None if filename != '-': self.locked_file = LockedFile(filename, self.mode, self.inplace, not self.update) try: conf = self._parse_file(filename, preserve_case=preserve_case) if not conf.items(iniparse.DEFAULTSECT): # Check if there is just [DEFAULT] in a file with no # name=values to avoid adding a duplicate section. if not self._has_default_section(): # reparse with inserted [DEFAULT] to be able to add global # opts etc. conf = self._parse_file( filename, add_default=True, preserve_case=preserve_case ) self.added_default_section = True except configparser.MissingSectionHeaderError: conf = self._parse_file( filename, add_default=True, preserve_case=preserve_case ) self.added_default_section = True except configparser.ParsingError as e: error(str(e)) sys.exit(1) self.data = None return conf def set_name_value(self, section, param, value): curr_val = None if self.update in ('param', 'section'): if param is None: if not ( section == iniparse.DEFAULTSECT or self.conf.has_section(section) ): raise configparser.NoSectionError(section) else: try: curr_val = self.conf.get(section, param) except configparser.NoSectionError: if self.update == 'section': raise except configparser.NoOptionError: if self.update == 'param': raise elif (section != iniparse.DEFAULTSECT and not self.conf.has_section(section)): if self.mode == "--del": return else: # Note this always adds a '\n' before the section name # resulting in double spaced sections or blank line at # the start of a new file to which a new section is added. # We handle the empty file case at least when writing. self.conf.add_section(section) if param is not None: if self.update not in ('param', 'section'): try: curr_val = self.conf.get(section, param) except configparser.NoOptionError: if self.mode == "--del": return if value is None: value = 'crudini_no_arg' if self.crudini_no_arg else '' if self.vlist: value = self.update_list( curr_val, value, self.mode, self.listsep ) self.conf.set(section, param, value) def command_set(self): """Insert a section/parameter.""" self.set_name_value(self.section, self.param, self.value) def command_merge(self): """Merge an ini file from another ini.""" for msection in [iniparse.DEFAULTSECT] + self.mconf.sections(): if msection == iniparse.DEFAULTSECT: defaults_to_strip = {} else: defaults_to_strip = self.mconf.defaults() items = self.mconf.items(msection) set_param = False for item in items: # XXX: Note this doesn't update an item in section # if matching value also in default (global) section. if defaults_to_strip.get(item[0]) != item[1]: ignore_errs = (configparser.NoOptionError,) if self.section is not None: msection = self.section elif self.update not in ('param', 'section'): ignore_errs += (configparser.NoSectionError,) try: set_param = True self.set_name_value(msection, item[0], item[1]) except ignore_errs: pass # For empty sections ensure the section header is added if not set_param and self.section is None: self.set_name_value(msection, None, None) def command_del(self): """Delete a section/parameter.""" if self.param is None: if self.section == iniparse.DEFAULTSECT: for name in self.conf.defaults(): self.conf.remove_option(iniparse.DEFAULTSECT, name) else: if not self.conf.remove_section(self.section) \ and self.update in ('param', 'section'): raise configparser.NoSectionError(self.section) elif self.value is None: try: if not self.conf.remove_option(self.section, self.param) \ and self.update == 'param': raise configparser.NoOptionError(self.section, self.param) except configparser.NoSectionError: if self.update in ('param', 'section'): raise else: # remove item from list self.set_name_value(self.section, self.param, self.value) def command_get(self): """Output a section/parameter""" if self.fmt != 'lines': if self.section is None: if self.conf.defaults(): self._print.section_header(iniparse.DEFAULTSECT) for item in self.conf.sections(): self._print.section_header(item) elif self.param is None: if self.fmt == 'ini': self._print.section_header(self.section) if self.section == iniparse.DEFAULTSECT: defaults_to_strip = {} else: defaults_to_strip = self.conf.defaults() for item in self.conf.items(self.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]: if self.fmt: val = item[1] else: val = None self._print.name_value(item[0], val) else: val = self.conf.get(self.section, self.param) if self.fmt: name = self.param else: name = None self._print.name_value(name, val) else: if self.section is None: sections = self.conf.sections() if self.conf.defaults(): sections.insert(0, iniparse.DEFAULTSECT) else: sections = (self.section,) if self.param is not None: val = self.conf.get(self.section, self.param) self._print.name_value(self.param, val, self.section) else: for section in sections: if section == iniparse.DEFAULTSECT: defaults_to_strip = {} else: defaults_to_strip = self.conf.defaults() items = False for item in self.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] self._print.name_value(item[0], val, section) items = True if not items: self._print.name_value(None, None, section) def run(self): if sys.stdin.isatty(): sys.excepthook = Crudini.cli_exception Crudini.init_iniparse_defaultsect() self.parse_options() self.section_explicit_default = False if self.section == '': self.section = iniparse.DEFAULTSECT elif self.section == iniparse.DEFAULTSECT: self.section_explicit_default = True if self.mode == '--merge': self.mconf = self.parse_file('-', preserve_case=True) self.madded_default_section = self.added_default_section try: if self.mode == '--get' and self.param is None: # Maintain case when outputting params. # Note sections are handled case sensitively # even if optionxform is not set. preserve_case = True else: preserve_case = False self.conf = self.parse_file(self.cfgfile, preserve_case=preserve_case) # Take the [DEFAULT] header from the input if present if ( self.mode == '--merge' and self.update not in ('param', 'section') and not self.madded_default_section and self.mconf.items(iniparse.DEFAULTSECT) ): self.added_default_section = self.madded_default_section if self.mode == '--set': self.command_set() elif self.mode == '--merge': self.command_merge() elif self.mode == '--del': self.command_del() elif self.mode == '--get': self.command_get() if self.mode != '--get': # 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(self.conf.data) if len(str_data) and str_data[-1] != '\n': str_data += '\n' if ( ( self.added_default_section and not ( self.section_explicit_default and self.mode in ('--set', '--merge') ) ) or ( self.mode == '--del' and self.section == iniparse.DEFAULTSECT and self.param is None ) ): # See note at add_section() call above detailing # where this extra \n comes from that we handle # here for the edge case of new files. default_sect = '[%s]\n' % iniparse.DEFAULTSECT if not self.newline_at_start and \ str_data.startswith(default_sect + '\n'): str_data = str_data[len(default_sect) + 1:] else: str_data = str_data.replace(default_sect, '', 1) if self.crudini_no_arg: # This is the main case str_data = str_data.replace(' = crudini_no_arg', '') # Handle setting empty values for existing param= format str_data = str_data.replace('=crudini_no_arg', '=') # Handle setting empty values for existing colon: format str_data = str_data.replace(':crudini_no_arg', ':') changed = self.chksum != self._chksum(str_data) if self.output == '-': sys.stdout.write(str_data) elif changed: if self.inplace: self.file_rewrite(self.output, str_data) else: self.file_replace(os.path.realpath(self.output), str_data) if self.verbose: def quote_val(val): return pipes.quote(val).replace('\n', '\\n') what = ' '.join(map(quote_val, list(filter(bool, [self.mode, self.cfgfile, self.section, self.param, self.value])))) sys.stderr.write('%s: %s\n' % (('unchanged', 'changed')[changed], what)) # Finish writing now to consistently handle errors here # (and while excepthook is set) sys.stdout.flush() except configparser.ParsingError as e: error('Error parsing %s: %s' % (self.cfgfile, e.message)) sys.exit(1) except configparser.NoSectionError as e: error('Section not found: %s' % e.section) sys.exit(1) except configparser.NoOptionError: error('Parameter not found: %s' % self.param) sys.exit(1) except EnvironmentError as e: # Handle EPIPE as python 2 doesn't catch SIGPIPE if e.errno != errno.EPIPE: error(str(e)) sys.exit(1) # Python3 fix for exception on exit: # https://docs.python.org/3/library/signal.html#note-on-sigpipe nullf = os.open(os.devnull, os.O_WRONLY) os.dup2(nullf, sys.stdout.fileno()) def main(): crudini = Crudini() return crudini.run() if __name__ == "__main__": sys.exit(main()) crudini-0.9.3/example.ini0000664000175000017500000000140713023601154015113 0ustar padraigpadraigglobal=supported [section1] dup1 = val1 dup1 = val1 dup2 = val1 dup2 = val2 nospace=val multiline = with leading space nmultiline=not supported with\ ;comment #comment comment_after1=val ;a comment comment_after2=val;not a comment comment_after3=val #not a comment escaped_not_processed=test \nescape colon:val ; supported also double_quotes = "not removed" single_quotes = 'not removed' spaces_stripped = val ; internal_not_stripped = v al ; notempty1= ;comment=val ;Note iniparse requires the = following empty empty= python_interpolate = %(dup1)s/blah interpolate2 = ${dup1}/blah Caps = not significant [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 crudini-0.9.3/COPYING0000664000175000017500000004325411352247755014040 0ustar padraigpadraig GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 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. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. crudini-0.9.3/Makefile0000664000175000017500000000052613532203744014430 0ustar padraigpadraigname = crudini version = 0.9.3 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} crudini-0.9.3/crudini-help0000775000175000017500000000033412344576772015314 0ustar padraigpadraig#!/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 crudini-0.9.3/TODO0000664000175000017500000000263013023764424013460 0ustar padraigpadraigsupport --set,--merge of #commented name=value with operation controlled with --with-comment=always To add new item under comment even if same value as comment --with-comment[=ifchanged] To add new item under comment only if different. Would support --list also to add to default list. --new-comment To add an (additional) comment line for item no matter where, or if, it's added. Ensure above any #commented name=value though. support multiple files passed to --merge 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 Have a look at cliff output formatters? csv and shell at least: http://blog.oddbit.com/2013/11/22/a-unified-cli-for-op --format=json to output json format. Note ini format supports name:value so quite a lot of overlap have pip install put man page in place Support spacing options for new items. I.E. allow to set=like_this rather than just set = like_this. This would have significance for shell like config formats when adding new parmeters. Maybe have --format=sh control this. crudini-0.9.3/setup.py0000664000175000017500000000146013532203753014500 0ustar padraigpadraig# -*- 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.9.3", 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>=0.3.2'], scripts=["crudini"] ) crudini-0.9.3/NEWS0000664000175000017500000000474213532204007013464 0ustar padraigpadraigcrudini NEWS -*- outline -*- * Noteworthy changes in release 0.9.3 (2019-08-30) ** Bug fixes Reading ini files with windows line endings is again supported. Regression added in v0.9. ** Improvements python 3 support. * Noteworthy changes in release 0.9 (2016-12-13) ** Bug fixes Write errors to stdout are diagnosed correctly and consistently. Replacing symlinks now replaces the target rather than the symlink itself. ** Changes in behavior The case of parameters is maintained with --get. ** Improvements Single token parameters (without equals) are now supported, which are used in mysql config for example. * Noteworthy changes in release 0.8 (2016-11-23) ** Bug fixes crudini now handles parameters starting with "rem". Previously an entry such as "remote = 1" would be ignored. ** New features Support mercurial config files by treating lines starting with '%' as comments, thus ignoring mercurial '%include' and '%unset' directives. * Noteworthy changes in release 0.7 (2015-06-14) ** Bug fixes crudini no longer removes a blank line from the start of a file which has no sections, or options outside a section. [bug introduced in version 0.5] Files are now synced after writing for better Durability. Separate locking files are no longer used which avoids deadlock in cases where the system is stopped in the small window where these files exist. * Noteworthy changes in release 0.5 (2015-01-28) ** Bug fixes Lock files are cleaned up robustly. Previously there was a race condition resulting in blocked subsequent edits, due to a lingering lock file. --del will ignore requests to delete a parameter in a non-existing section (unless --existing is used). Previously it failed citing the missing section. ** New features The --existing option takes parameters to give more control over what needs to pre-exist. So you can specify for example that a file needs to exist, but any items within it are created as needed. A new --verbose option was added to indicate on stderr wether the request resulted in a config change or not. This can be used to determine whether to restart programs etc. ** Changes in behavior Files are created by default if missing, unless --existing is specified. ** Improvements Protections against creating unparseable ini files were added. stdin can be parsed just as with normal files. File writes are avoided if there are no changes to the config. crudini-0.9.3/noequals.ini0000664000175000017500000000047613023772756015335 0ustar padraigpadraig# Differences from mysql.conf # #comments can't be at middle of line # single/double quotes are not stripped # leading spaces on line are not ignored !include directives treated as parameters [noequals] param1 param2= param3 = colon1: colon2 : space param never #comment not ;comment multiline=val spaceval tabval crudini-0.9.3/README0000664000175000017500000000412213532204577013651 0ustar padraigpadraigcrudini - 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] [list value] or: crudini --merge [OPTION]... config_file [section] Options: --existing[=WHAT] For --set, --del and --merge, fail if item is missing, where WHAT is 'file', 'section', or 'param', or if not specified; all specified items. --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 --verbose Indicate on stderr if changes were made --help Write this help to stdout --version Write version to stdout 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) crudini-0.9.3/tox.ini0000664000175000017500000000026213016267427014305 0ustar padraigpadraig[tox] envlist = py26,py27,pep8 [testenv] deps = iniparse>=0.3.2 commands = /bin/bash -c 'cd tests && ./test.sh' [testenv:pep8] deps = flake8 commands = flake8 crudini setup.py crudini-0.9.3/tests/0000775000175000017500000000000013532204577014134 5ustar padraigpadraigcrudini-0.9.3/tests/section1.sh0000664000175000017500000000075113023022243016177 0ustar padraigpadraigdup1=val1 dup2=val2 nospace=val multiline='with leading space' nmultiline='not supported with\' comment_after1=val comment_after2='val;not a comment' comment_after3='val #not a comment' escaped_not_processed='test \nescape' colon=val double_quotes='"not removed"' single_quotes=''"'"'not removed'"'"'' spaces_stripped=val internal_not_stripped='v al' notempty1=';comment=val' empty='' python_interpolate='%(dup1)s/blah' interpolate2='${dup1}/blah' Caps='not significant' combine=sections crudini-0.9.3/tests/example.lines0000664000175000017500000000172413023022331016604 0ustar padraigpadraig[ 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 crudini-0.9.3/tests/section1.lines0000664000175000017500000000136213023022276016704 0ustar padraigpadraig[ 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 crudini-0.9.3/tests/section1.ini0000664000175000017500000000077413023022217016352 0ustar padraigpadraig[section1] dup1 = val1 dup2 = val2 nospace = val multiline = with leading space nmultiline = not supported with\ comment_after1 = val comment_after2 = val;not a comment comment_after3 = val #not a comment escaped_not_processed = test \nescape colon = val double_quotes = "not removed" single_quotes = 'not removed' spaces_stripped = val internal_not_stripped = v al notempty1 = ;comment=val empty = python_interpolate = %(dup1)s/blah interpolate2 = ${dup1}/blah Caps = not significant combine = sections crudini-0.9.3/tests/test.sh0000775000175000017500000004602113412700767015454 0ustar padraigpadraig#!/bin/bash trap "exit 130" INT cleanup() { rm -f err noequals*.ini test.ini ltest.ini good.ini example.ini; exit; } trap cleanup EXIT export PATH=..:$PATH test=0 fail() { test=$(($test+1)); echo "Test $test FAIL (line ${BASH_LINENO[0]})"; exit 1; } ok() { test=$(($test+1)); echo "Test $test OK (line ${BASH_LINENO[0]})"; } cp ../example.ini . # invalid params ---------------------------------------- :> test.ini crudini 2>/dev/null && fail 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 crudini --get test.ini '' 'name' 'val' 2>/dev/null && fail # value crudini --get --format=bad test.ini 2>/dev/null && fail # bad format crudini --del 2>/dev/null && fail # no file crudini --del test.ini 2>/dev/null && fail # no section crudini --del test.ini '' 'name' 'val' 2>/dev/null && fail # value crudini --merge 2>/dev/null && fail # no file crudini --merge test.ini '' 'name' 2>/dev/null && fail # param crudini --del test.ini '' 'name' 'val' 2>/dev/null && fail # value crudini --get --format=ggg test.ini 2>&1 | grep -q 'format not recognized' || fail crudini --get test.ini 'DEFAULT' missing 2>&1 | grep -q 'Parameter not found' || fail ok # --set ------------------------------------------------- :> test.ini crudini --set test.ini '' name val printf '%s\n' 'name = val' > good.ini diff -u test.ini good.ini && ok || fail :> 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 :> 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 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 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 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 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 # 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 $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 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 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 # 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 # 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 # 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 # value is optional :> test.ini crudini --set test.ini '' name printf '%s\n' 'name = ' > good.ini diff -u test.ini good.ini && ok || fail # 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 # Protect against creating non parseable files (with nested [[]]) :> test.ini crudini --set test.ini '[section]' name val 2>/dev/null && fail test -s test.ini && fail printf '%s\n' '[[section]]' 'name=val' > test.ini crudini --get test.ini '[section]' name 2>/dev/null && fail printf '%s\n' '[section]' '[name=val' > test.ini crudini --get test.ini 'section' '[name' 2>/dev/null && fail printf '%s\n' '[section]' 'n[ame=val' > test.ini test $(crudini --get test.ini 'section' 'n[ame') = 'val' && ok || fail # --existing with file creation for mode in '' '--inplace'; do crudini $mode --set missing.ini '' name val 2>/dev/null && ok || fail rm -f missing.ini for emode in '' 'file' 'section' 'param'; do crudini $mode --existing="$emode" --set missing.ini '' name val \ 2>/dev/null && fail || ok test -f missing.ini && fail done rm -f missing.ini done # --existing[=param] :> test.ini crudini --set test.ini '' gname val crudini --set --existing test.ini '' gname val2 crudini --set --existing=inval test.ini '' gname val3 2>/dev/null && fail crudini --set --existing test.ini '' gname2 val 2>/dev/null && fail crudini --set test.ini section1 name val crudini --set --existing test.ini section1 name val2 crudini --set --existing test.ini section1 name2 val 2>/dev/null && fail printf '%s\n' 'gname = val2' '' '' '[section1]' 'name = val2' > good.ini diff -u test.ini good.ini && ok || fail # --existing=section :> test.ini crudini --set test.ini '' gname val crudini --set --existing='section' test.ini '' gname val2 crudini --set --existing='section' test.ini '' gname2 val 2>/dev/null || fail crudini --set test.ini section1 name val crudini --set --existing='section' test.ini section1 name val2 crudini --set --existing='section' test.ini section1 name2 val 2>/dev/null || fail printf '%s\n' 'gname = val2' 'gname2 = val' \ '' '' '[section1]' 'name = val2' 'name2 = val' > good.ini diff -u test.ini good.ini && ok || fail # --get ------------------------------------------------- # basic get test "$(crudini --get example.ini section1 cAps)" = 'not significant' && ok || fail # get sections crudini --get example.ini > test.ini printf '%s\n' DEFAULT section1 'empty section' non-sh-compat list > good.ini diff -u test.ini good.ini && ok || fail # get implicit default section crudini --get example.ini '' > test.ini printf '%s\n' 'global' > good.ini diff -u test.ini good.ini || fail crudini --format=ini --get example.ini '' > test.ini printf '%s\n' '[DEFAULT]' 'global = supported' > good.ini diff -u test.ini good.ini || fail ok # get explicit default section crudini --get example.ini DEFAULT > test.ini printf '%s\n' 'global' > good.ini diff -u test.ini good.ini || fail crudini --get --format ini example.ini DEFAULT > test.ini printf '%s\n' '[DEFAULT]' 'global = supported' > good.ini diff -u test.ini good.ini || fail ok # get section1 in ini format crudini --format=ini --get example.ini section1 > test.ini diff -u test.ini section1.ini && ok || fail # get section1 in sh format crudini --format=sh --get example.ini section1 > test.ini diff -u test.ini section1.sh && ok || fail # 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 # Ensure we handle comments correctly printf '%s\n' '[DEFAULT]' '#c1' ';c2' '%inc1' > test.ini test "$(crudini --get test.ini)" = '' || fail printf '%s\n' '[section1]' 'remote=1' > test.ini test "$(crudini --get test.ini 'section1')" = 'remote' || fail ok # missing bits :> test.ini crudini --get missing.ini 2>/dev/null && fail test "$(crudini --get test.ini)" = '' || fail crudini --get test.ini '' || fail crudini --get test.ini '' 'missing' 2>/dev/null && fail ok # --merge ----------------------------------------------- # 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 :> 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 :> 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 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 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 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 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 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 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 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 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 printf '%s\n' 'name=val1' > test.ini printf '%s\n' 'name=val2' | crudini --merge --existing test.ini new 2>/dev/null && fail || ok 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 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 # 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 printf '%s\n' $sec 'name = val' > test.ini crudini --del test.ini '' noname || fail crudini --del --existing test.ini '' noname 2>/dev/null && fail crudini --del test.ini '' name || fail :> good.ini [ "$sec" ] && printf '%s\n' $sec > good.ini diff -u test.ini good.ini && ok || fail 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 crudini --del test.ini 'DEFAULT' name || fail :> good.ini [ "$sec" ] && printf '%s\n' $sec > good.ini diff -u test.ini good.ini && ok || fail printf '%s\n' $sec 'name = val' > test.ini crudini --del test.ini nosect || fail crudini --del --existing=file test.ini nosect || fail crudini --del --existing=section test.ini nosect 2>/dev/null && fail crudini --del --existing=param test.ini '' noname 2>/dev/null && fail crudini --del --existing test.ini nosect 2>/dev/null 2>/dev/null && fail crudini --del --existing=param test.ini '' name || fail crudini --del test.ini '' || fail :> good.ini diff -u test.ini good.ini && ok || fail printf '%s\n' $sec 'name = val' > test.ini crudini --del test.ini nosect || fail crudini --del --existing=file test.ini nosect || fail crudini --del --existing=section test.ini nosect 2>/dev/null && fail crudini --del --existing=param test.ini 'DEFAULT' noname 2>/dev/null && fail crudini --del --existing test.ini nosect 2>/dev/null && fail crudini --del test.ini 'DEFAULT' || fail :> good.ini diff -u test.ini good.ini && ok || fail done # --del non existing sections/params shouldn't give an error printf '%s\n' '[section]' 'name = val' > test.ini crudini --verbose --del test.ini nosect 2>&1 | grep -q ^unchanged || fail crudini --verbose --del test.ini nosect noname 2>&1 | grep -q ^unchanged || fail crudini --verbose --del test.ini section noname 2>&1 | grep -q ^unchanged || fail crudini --verbose --del test.ini section noname 2>&1 | grep -q ^unchanged || fail crudini --verbose --del --list test.ini section noname val 2>&1 | grep -q ^unchanged || fail crudini --verbose --del --list test.ini nosect noname val 2>&1 | grep -q ^unchanged || fail crudini --verbose --del test.ini section 2>&1 | grep -q ^changed || fail test -s test.ini && fail || ok # --del non existing file shouldn't create an empty file crudini --verbose --del missing.ini section 2>&1 | grep -q ^unchanged || fail crudini --existing --del missing.ini section 2>/dev/null && fail test -f missing.ini && fail || ok # --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 # support parsing from stdin test "$(printf '%s\n' global=1 | crudini --get - '' global)" = 1 && ok || fail # --verbose printf '%s\n' '[section]' 'param = value' > test.ini crudini --verbose --set test.ini section param value 2>&1 | grep -q ^unchanged && ok || fail crudini --verbose --set test.ini section param valuE 2>&1 | grep -q ^changed && ok || fail crudini --verbose --del test.ini section param 2>&1 | grep -q ^changed && ok || fail crudini --verbose --del test.ini section param 2>&1 | grep -q ^unchanged && ok || fail crudini --verbose --del test.ini section $'multiline\nchanged:' 2>&1 | grep -q ^changed && fail || ok # ensure leading blank lines maintained with global settings printf '%s\n' '' 'option=1' > file.conf printf '%s\n' '' 'option=2' > good.conf crudini --set file.conf '' option 2 || fail diff -u good.conf file.conf && ok || fail rm file.conf good.conf # ensure errors diagnosed correctly crudini --get example.ini 2>err | : ! test -s err && ok || fail #EPIPE ignored if test -e /dev/full; then crudini --get example.ini 2>err >/dev/full grep -q 'No space left' err && ok || fail fi # ensure symlinks handled correctly in file replace mode printf '%s\n' '[section]' 'param = value' > test.ini ln -s test.ini ltest.ini crudini --set ltest.ini section param newvalue || fail test "$(crudini --get test.ini section param)" = 'newvalue' && ok || fail crudini --output=ltest.ini --set ltest.ini section param newvalue2 || fail test "$(crudini --get test.ini section param)" = 'newvalue2' && ok || fail # Test single token parameters (without equals) cp ../noequals.ini . crudini --get noequals.ini >/dev/null && ok || fail cp noequals.ini noequals_new.ini printf '%s\n' 'new' 'new_equals = ' >> noequals_new.ini for param in param{1..3} colon{1..2} new; do crudini --set noequals.ini noequals $param || fail done crudini --set noequals.ini noequals new_equals '' || fail diff -u noequals.ini noequals_new.ini && ok || fail # Test can read windows format files printf '%s\r\n' '' 'param = value' > test.ini crudini --get test.ini DEFAULT param > /dev/null || fail crudini-0.9.3/crudini.10000664000175000017500000000504513532204577014515 0ustar padraigpadraig.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. .TH CRUDINI "1" "August 2019" "crudini 0.9.3" "User Commands" .SH NAME crudini \- manipulate ini files .SH SYNOPSIS .B crudini \fI\,--set \/\fR[\fI\,OPTION\/\fR]... \fI\,config_file section \/\fR[\fI\,param\/\fR] [\fI\,value\/\fR] .br .B crudini \fI\,--get \/\fR[\fI\,OPTION\/\fR]... \fI\,config_file \/\fR[\fI\,section\/\fR] [\fI\,param\/\fR] .br .B crudini \fI\,--del \/\fR[\fI\,OPTION\/\fR]... \fI\,config_file section \/\fR[\fI\,param\/\fR] [\fI\,list value\/\fR] .br .B crudini \fI\,--merge \/\fR[\fI\,OPTION\/\fR]... \fI\,config_file \/\fR[\fI\,section\/\fR] .SH DESCRIPTION crudini \- A utility for manipulating ini files .SH OPTIONS .TP \fB\-\-existing\fR[=\fI\,WHAT\/\fR] For \fB\-\-set\fR, \fB\-\-del\fR and \fB\-\-merge\fR, fail if item is missing, where WHAT is 'file', 'section', or 'param', or if not specified; all specified items. .TP \fB\-\-format\fR=\fI\,FMT\/\fR For \fB\-\-get\fR, select the output FMT. Formats are sh,ini,lines .TP \fB\-\-inplace\fR Lock and write files in place. This is not atomic but has less restrictions than the default replacement method. .TP \fB\-\-list\fR For \fB\-\-set\fR and \fB\-\-del\fR, update a list (set) of values .TP \fB\-\-list\-sep\fR=\fI\,STR\/\fR Delimit list values with "STR" instead of " ," .TP \fB\-\-output\fR=\fI\,FILE\/\fR Write output to FILE instead. '\-' means stdout .TP \fB\-\-verbose\fR Indicate on stderr if changes were made .TP \fB\-\-help\fR Write this help to stdout .TP \fB\-\-version\fR Write version to stdout .SH EXAMPLES # Add/Update a var .IP crudini \-\-set config_file section parameter value .PP # Update an existing var .IP crudini \-\-set \-\-existing config_file section parameter value .PP # Delete a var .IP crudini \-\-del config_file section parameter .PP # Delete a section .IP crudini \-\-del config_file section .PP # output a value .IP crudini \-\-get config_file section parameter .PP # output a global value not in a section .IP crudini \-\-get config_file '' parameter .PP # output a section .IP crudini \-\-get config_file section .PP # output a section, parseable by shell .IP eval $(crudini \-\-get \-\-format=sh config_file section) .PP # update an ini file from shell variable(s) .IP echo name="$name" | crudini \-\-merge config_file section .PP # merge an ini file from another ini .IP crudini \-\-merge config_file < another.ini .PP # compare two ini files using standard UNIX text processing .IP diff <(crudini \-\-get \-\-format=lines file1.ini|sort) \e .IP <(crudini \-\-get \-\-format=lines file2.ini|sort) crudini-0.9.3/EXAMPLES0000664000175000017500000000166012344577071014140 0ustar padraigpadraigExamples: # 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)