pax_global_header00006660000000000000000000000064147777457770014552gustar00rootroot0000000000000052 comment=f3811bc0b2c19be373d0cdaf85378c823ecc8187 crudini-0.9.6/000077500000000000000000000000001477774577700132235ustar00rootroot00000000000000crudini-0.9.6/COPYING000066400000000000000000000432541477774577700142660ustar00rootroot00000000000000 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.6/EXAMPLES000066400000000000000000000043211477774577700143640ustar00rootroot00000000000000Examples: # Add/Update a var crudini --set config_file section parameter value # Add/Update a var in the root or global area. # I.e. that's not under a [section]. crudini --set config_file "" parameter value # Update an existing var crudini --set --existing config_file section parameter value # Add/Update/Delete multiple variables atomically crudini --set config_file section parameter1 value \ --set config_file section parameter2 value \ --del config_file section parameter3 # Get multiple items from stdin env | crudini --get - '' USER --get - '' SHELL # Add/Append a value to a comma separated list # Note any whitespace around commas is ignored crudini --set --list config_file section parameter a_value # Add/Append a value to a whitespace separated list # Note multiline lists are supported (as newline is whitespace) crudini --set --list --list-sep= config_file section parameter a_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) # Rewrite ini file to use name=value format rather than name = value crudini --ini-options=nospace --set config_file "" # Add/Update a var, ensuring complete file in name=value format crudini --ini-options=nospace --set config_file section parameter value # Rewrite ini file to ensure a single blank line between sections, # and no leading or trailing blank lines crudini --ini-options=sectionspace --set config_file "" # Read indented ini file, like .gitconfig crudini --ini-options=ignoreindent --format=lines --get ~/.gitconfig crudini-0.9.6/INSTALL000066400000000000000000000015201477774577700142520ustar00rootroot00000000000000## Installation On windows ensure a python interpreter is installed. For example installing from https://www.python.org/downloads/ will put the py launcher and pip in the PATH. Then ensure the iniparse module is installed by running the following from a "cmd" prompt: ``` pip install iniparse ``` Then crudini can be invoked by downloading just the crudini.py file and running like: ``` py crudini.py --help ``` On Linux systems crudini is generally available from your standard package manager, and installing will also ensure the iniparse dependency is appropriately installed on your system. You can also download and run the single crudini.py file directly to use latest version. On any system you should be able to pip install the latest code from github like: ``` pip install git+https://github.com/pixelb/crudini.git#egg=crudini ``` crudini-0.9.6/Makefile000066400000000000000000000005351477774577700146660ustar00rootroot00000000000000name = crudini version = 0.9.6 all: help2man -n "manipulate ini files" -o crudini.1 -N ./crudini-help ./crudini-help --markdown > README.md 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.6/NEWS000066400000000000000000000103121477774577700137170ustar00rootroot00000000000000crudini NEWS -*- outline -*- * Noteworthy changes in release 0.9.6 (2025-04-16) Support BOM correctly. Previously we would have stripped any Byte Order Mark, and incorrectly matched items on the first line. Extraneous blank lines are avoided when deleting a section. Previously blank lines preceeding a [section] were not removed. Note this will collapse multiple empty lines preceding all sections. Support creating a section called "default". Previously this would have been disallowed, with an invalid section name error. Support ensuring a single space in all 'name = value' entries in the file with --ini-options=space. This is symmetric and opposite to the existing --ini-options=nospace option. Support ensuring a single blank line between sections, and no blank lines at the start or end of the file, with --ini-options=sectionspace. * Noteworthy changes in release 0.9.5 (2023-10-04) ** Improvements Support for multiple --set and --del, or --get operations, allowing for more efficient and atomic updates to multiple parameters. Support indented ini files with --ini-options=ignoreindent. Indentation is ignored (and maintained) in the ini file. --format=sh will now attempt to output a whole ini file in sh format. Previously it would have only printed the section names. * Noteworthy changes in release 0.9.4 (2022-12-23) ** Bug fixes Fix updating of flag only parameters so they don't have '=' or '=crudini_no_arg' added added on update. Handle closed stdin/stdout gracefully, without giving errors. ** Improvements Windows support. Windows line endings are maintained. Lists can be delimited with arbitrary whitespace with `--list-sep=`. Support for unspaced "name=val" format with `--ini-options=nospace`. * 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.6/README.md000066400000000000000000000116141477774577700145050ustar00rootroot00000000000000# crudini - A utility for manipulating ini files ## Usage: ``` crudini --set [OPTION]... config_file section [param] [value] crudini --get [OPTION]... config_file [section] [param] crudini --del [OPTION]... config_file section [param] [list value] crudini --merge [OPTION]... config_file [section] SECTION can be empty ("") or "DEFAULT" in which case, params not in a section, i.e. global parameters are operated on. If 'DEFAULT' is used with --set, an explicit [DEFAULT] section is added. Multiple --set|--del|--get operations for a config_file can be specified. ``` ## Options: ``` --existing[=WHAT] For --set, --del and --merge, fail if item is missing, where WHAT is 'file', 'section', or 'param', or if WHAT not specified; all specified items. --format=FMT For --get, select the output FMT. Formats are 'sh','ini','lines' --ini-options=OPT Set options for handling ini files. Options are: 'nospace': use format name=value not name = value 'space': ensure name = value format 'sectionspace': ensure one blank line between sections 'ignoreindent': ignore leading whitespace --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 " ,". An empty STR means any whitespace is a delimiter. --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 # Add/Update a var in the root or global area. # I.e. that's not under a [section]. crudini --set config_file "" parameter value # Update an existing var crudini --set --existing config_file section parameter value # Add/Update/Delete multiple variables atomically crudini --set config_file section parameter1 value \ --set config_file section parameter2 value \ --del config_file section parameter3 # Get multiple items from stdin env | crudini --get - '' USER --get - '' SHELL # Add/Append a value to a comma separated list # Note any whitespace around commas is ignored crudini --set --list config_file section parameter a_value # Add/Append a value to a whitespace separated list # Note multiline lists are supported (as newline is whitespace) crudini --set --list --list-sep= config_file section parameter a_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) # Rewrite ini file to use name=value format rather than name = value crudini --ini-options=nospace --set config_file "" # Add/Update a var, ensuring complete file in name=value format crudini --ini-options=nospace --set config_file section parameter value # Rewrite ini file to ensure a single blank line between sections, # and no leading or trailing blank lines crudini --ini-options=sectionspace --set config_file "" # Read indented ini file, like .gitconfig crudini --ini-options=ignoreindent --format=lines --get ~/.gitconfig ``` ## Installation On windows ensure a python interpreter is installed. For example installing from https://www.python.org/downloads/ will put the py launcher and pip in the PATH. Then ensure the iniparse module is installed by running the following from a "cmd" prompt: ``` pip install iniparse ``` Then crudini can be invoked by downloading just the crudini.py file and running like: ``` py crudini.py --help ``` On Linux systems crudini is generally available from your standard package manager, and installing will also ensure the iniparse dependency is appropriately installed on your system. You can also download and run the single crudini.py file directly to use latest version. On any system you should be able to pip install the latest code from github like: ``` pip install git+https://github.com/pixelb/crudini.git#egg=crudini ``` crudini-0.9.6/TODO000066400000000000000000000022461477774577700137170ustar00rootroot00000000000000support --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 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 crudini-0.9.6/crudini-help000077500000000000000000000013101477774577700155270ustar00rootroot00000000000000#!/bin/sh # crudini --help generator for help2man and README.md if [ "$1" = '--help' ]; then { printf '%s' 'crudini - ' ./crudini.py --help | sed 's/crudini\.py/crudini/g;' echo cat EXAMPLES } elif [ "$1" = '--markdown' ]; then { printf '%s' '# crudini - ' ./crudini.py --help | sed 's/crudini\.py/crudini/g;' echo cat EXAMPLES } | sed 's/^\([^ ]\+:\)\( \+\)/\1\n/' | # Sections on own line sed 's/^[^ ]\+:/```\n## &\n```/' | # Markup sections sed 's/^ *or: *//' | # Delete help2man synopsis formatting sed '0,/```/{/```/d;}' # Delete first ``` echo '```' # Add terminating ``` cat INSTALL # Already marked up elif [ "$1" = '--version' ]; then ./crudini.py --version fi crudini-0.9.6/crudini.py000077500000000000000000001472611477774577700152500ustar00rootroot00000000000000#!/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 io import os import re import shutil import string import tempfile if sys.version_info[0] >= 3: import shlex as pipes import configparser else: import codecs import pipes import ConfigParser as configparser user_encoding = 'utf-8' # user specified items file_encoding = 'utf-8' # encoding of ini file contents # Python 2/3 wrapper to convert strings to unicode try: # Python 2 unicode def s2u(s, e=user_encoding): return unicode(s, e) # Also add conversion wrapper for print() sys.stdout = codecs.getwriter(user_encoding)(sys.stdout) except NameError: # Python 3 def s2u(s, e=user_encoding): return str(s) unicode = str 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 def file_is_closed(stdfile): if not stdfile: # python3 sets sys.stdin etc. to None if closed return True else: # python2 needs to be checked try: os.fstat(stdfile.fileno()) except EnvironmentError as e: if e.errno == errno.EBADF: return True return False # Adjustments to iniparse to optionally use name=value format (nospace) # and support for parameters without '=' specified class CrudiniInputFilter(): def __init__(self, fp, iniopt): self.fp = fp self.iniopt = iniopt self.crudini_no_arg = False self.indented = False self.last_section = 'DEFAULT' self.section_indents = {} self.windows_eol = None self.bom = None # Note [ \t] used rather than \s to avoid adjusting \r\n when no value # Unicode spacing around the delimiter would be very unusual anyway self.delimiter_spacing = re.compile(r'(.*?)[ \t]*([:=])[ \t]*(.*)') self.leading_whitespace = re.compile(r'([ \t]+)(.+)') self.replace_leading = re.compile(r'^(.+) ;crudini_indent>(.*)<$', flags=re.MULTILINE) self.section_match = re.compile(r'[ \t]*\[([^]]+)\].*') def readline(self): line = self.fp.readline() # Strip BOM. iniparse tracks it but simpler for us to replace later # as we're munging the data in various ways. if self.bom is None: if line and line[0] == u'\ufeff': line = line[1:] self.bom = True else: self.bom = False # XXX: This doesn't handle ;inline comments. # Really should be done within iniparse. # Detect windows format files # so we can undo iniparse auto conversion to unix if self.windows_eol is None: if line: self.windows_eol = len(line) >= 2 and line[-2] == '\r' else: self.windows_eol = os.name == 'nt' if line.strip() and line[0] not in '#;%': if 'ignoreindent' in self.iniopt: section = line.lstrip()[0] == '[' else: section = line[0] == '[' if section: section_name = self.section_match.sub(r'\1', line.rstrip()) if not section_name: return line self.last_section = section_name if line[0] in ' \t' and 'ignoreindent' not in self.iniopt: return line if not section and '=' not in line and ':' not in line: self.crudini_no_arg = True line = line.rstrip() + ' = crudini_no_arg\n' if not section and 'nospace' in self.iniopt: # Convert _all_ existing params. New params are handled in # the iniparse specialization in CrudiniConfigParser() # Note if we wanted an option to only convert specified params # we could do it with special ${value}_crudini_no_space values # that were then adjusted on output like for crudini_no_arg # But if need to remove the spacing, then should for all params line = self.delimiter_spacing.sub(r'\1\2\3', line) elif not section and 'space' in self.iniopt: # Convert _all_ existing params. New params will be correct line = self.delimiter_spacing.sub(r'\1 \2 \3', line) if line[0] in ' \t': self.indented = True # Set default indent for section to last indent leading_ws = self.leading_whitespace.sub(r'\1', line.rstrip()) self.section_indents[self.last_section] = leading_ws # match leading spaces and put in trailing ;crudini_indent=... reorder_ws = r'\2 ;crudini_indent>\1<' line = self.leading_whitespace.sub(reorder_ws, line) 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, iniopt): CrudiniInputFilter.__init__(self, fp, iniopt) self.first = True def readline(self): if self.first: self.first = False return s2u('[%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': # XXX: msvcrt.locking is problematic on windows # See the history of the portalocker implementation for example: # https://github.com/WoLpH/portalocker/commits/develop/portalocker # That would involve needing a new pywin32 dependency, # so instead we avoid locking on windows for now. def lock(self): self.locked = True def unlock(self): 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 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 exclusive lock needs write perms anyway. open_mode = os.O_RDWR if create and operation != '--del': open_mode += os.O_CREAT try: # Note we open in binary mode to avoid newline processing, # and also to give more control over the decoding process later. # This avoids platform encoding inconsistencies as per PEP 597. self.fp = os.fdopen(os.open(self.filename, open_mode, 0o666), 'rb') 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), 'rb') if (os.name == 'nt' or 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 = io.BytesIO(b'') 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. # Note iniparse doesn't currently support space_around_delimiters=False. class CrudiniConfigParser(iniparse.RawConfigParser): def __init__(self, preserve_case=False, space_around_delimiters=True): 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 = lambda x: x # Adjust iniparse separator to default to no space around equals # Note this does NOT convert existing params with spaces, # that's done in CrudiniInputFilter.readline(). # XXX: This couples with iniparse internals. if not space_around_delimiters: def new_ol_init(self, name, value, separator="=", *args, **kw): orig_ol_init(self, name, value, separator, *args, **kw) orig_ol_init = iniparse.ini.OptionLine.__init__ iniparse.ini.OptionLine.__init__ = new_ol_init 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 __init__(self): self.sep = ' ' 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 '), sep=self.sep) class PrintIniNoSpace(PrintIni): """Use for ini output format with no space around equals""" def __init__(self): self.sep = '' 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 section and name: identifier = "%s_%s" % (section, name) else: identifier = name if not PrintSh._valid_sh_identifier(identifier): error('Invalid sh identifier "%s"' % identifier) sys.exit(1) if value == 'crudini_no_arg': value = '' sys.stdout.write("%s=%s\n" % (identifier, pipes.quote(value))) class Crudini(): mode = fmt = update = iniopt = inplace = cfgfile = output = section = \ param = value = vlist = listsep = verbose = None locked_file = None section_explicit_default = None data = None conf = None added_default_section = False default_adjust = False removed_section = False ini_section_blanks = [] _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) os.write(f, bytearray(data, file_encoding)) # 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 windows 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. if os.name != 'nt': O_DIRECTORY = 0 if hasattr(os, 'O_DIRECTORY'): O_DIRECTORY = os.O_DIRECTORY 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, 'wb') as f: f.write(bytearray(data, file_encoding)) 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 # Perhaps have 'nospace' set this default? if curr_val and curr_val != 'crudini_no_arg': if sep is None: # Default to comma separated use_space = ' ' in curr_val or ',' not in curr_val curr_items = [v.strip() for v in curr_val.split(",")] elif sep == '': # Empty means whitespace separated curr_items = curr_val.split(None) # Find first run of whitespace to maintain current delimiter whitespace_re = re.compile(r'\S*(\s+)') first_whitespace = whitespace_re.match(curr_val) if first_whitespace: sep = first_whitespace.group(1) else: sep = ' ' # Maintain empty `param =` line if present if sep == '\n' or sep == '\r\n': if curr_val.startswith(sep): curr_items.insert(0, '') 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]) if exitval or file_is_closed(sys.stdout): output = sys.stderr else: output = 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] SECTION can be empty ("") or "DEFAULT" in which case, params not in a section, i.e. global parameters are operated on. If 'DEFAULT' is used with --set, an explicit [DEFAULT] section is added. Multiple --set|--del|--get operations for a config_file can be specified. Options: --existing[=WHAT] For --set, --del and --merge, fail if item is missing, where WHAT is 'file', 'section', or 'param', or if WHAT not specified; all specified items. --format=FMT For --get, select the output FMT. Formats are 'sh','ini','lines' --ini-options=OPT Set options for handling ini files. Options are: 'nospace': use format name=value not name = value 'space': ensure name = value format 'sectionspace': ensure one blank line between sections 'ignoreindent': ignore leading whitespace --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 \" ,\". An empty STR means any whitespace is a delimiter. --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 set_operation(self, operation): self.mode = None self.cfgfile = self.section = self.param = self.value = None try: self.mode = operation[0] self.cfgfile = operation[1] # Convert the following to unicode as # we process in unicode explicitly in python2. # Not needed on python3 where all strings are unicode. self.section = s2u(operation[2]) self.param = s2u(operation[3]) self.value = s2u(operation[4]) except IndexError: pass 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 long_options = [ 'del', 'existing=', 'format=', 'get', 'help', 'ini-options=', 'inplace', 'list', 'list-sep=', 'merge', 'output=', 'set', 'verbose', 'version' ] # Group args into options and operations options = [] operations = [] next_is_option_param = False for i, opt in enumerate(sys.argv[1:]): if next_is_option_param: options.append(opt) next_is_option_param = False elif opt in ('--get', '--set', '--del', '--merge'): operations.append([opt]) elif opt == '--': if operations: operations[-1].extend(sys.argv[i+2:]) # else discard as was done before multi operation support break elif opt.startswith('--'): options.append(opt) if '=' not in opt and opt[2:]+'=' in long_options: next_is_option_param = True else: if operations: operations[-1].append(opt) else: error('Unknown operation: %s' % opt) break try: opts, args = getopt.getopt(options, '', long_options) except getopt.GetoptError as e: error(str(e)) self.usage(1) self.iniopt = () for o, a in opts: if o in ('--help',): self.usage(0) elif o in ('--version',): print('crudini 0.9.6') sys.exit(0) elif o in ('--verbose',): self.verbose = True 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 ('--ini-options',): self.iniopt = a.split(',') for opt in self.iniopt: if opt not in ('', 'nospace', 'space', 'sectionspace', 'ignoreindent'): error('--ini-options not recognized: %s' % opt) self.usage(1) if 'nospace' in self.iniopt and 'space' in self.iniopt: error('--ini-options=space,nospace are mutually exclusive') sys.exit(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 operations: error('One of --set|--del|--get|--merge must be specified') self.usage(1) if self.fmt == 'lines': self._print = PrintLines() elif self.fmt == 'sh': self._print = PrintSh() elif self.fmt == 'ini': if 'nospace' in self.iniopt: self._print = PrintIniNoSpace() else: self._print = PrintIni() else: self._print = Print() # Validate all operations combinations for o in operations: if self.mode and self.mode != o[0]: mixable = ('--set', '--del') if self.mode not in mixable or o[0] not in mixable: error("--get|--merge modes can't be mixed" "with --set|--del") self.usage(1) elif self.mode == '--merge': error("--merge mode can't be repeated") self.usage(1) if self.cfgfile and len(o) > 1 and self.cfgfile != o[1]: error("Can't operate on multiple files") self.usage(1) self.set_operation(o) 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) # Convert secion '' to 'DEFAULT', # ensuring no conflicting DEFAULT section specs if self.section_explicit_default is None: if self.section == '': o[2] = self.section = iniparse.DEFAULTSECT self.section_explicit_default = False elif self.section == iniparse.DEFAULTSECT: self.section_explicit_default = True elif self.section is not None: if ((self.section == '' and self.section_explicit_default) or (self.section == iniparse.DEFAULTSECT and not self.section_explicit_default)): error("Conflicting %s section specifications" % iniparse.DEFAULTSECT) sys.exit(1) elif self.section == '': o[2] = self.section = iniparse.DEFAULTSECT 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) # A "param=with=equals = value" line can not be found with --get # so avoid the ambiguity. Note this precludes the "nospace" hack # in https://github.com/pixelb/crudini/issues/33#issuecomment-\ # 1151253988 if self.param and '=' in self.param: error("param names should not contain '=': %s" % self.param) if 'nospace' not in self.iniopt: error("Use --ini-options=nospace if you want that format") sys.exit(1) if self.section_explicit_default is None: self.section_explicit_default = False if not self.output: self.output = self.cfgfile if file_is_closed(sys.stdout) \ and (self.output == '-' or self.mode == '--get'): error("stdout is closed") sys.exit(1) return operations def _has_default_section(self): fp = io.StringIO(self.data) for line in fp: if line.startswith('[%s]' % iniparse.DEFAULTSECT): return True return False def _chksum(self, data): h = hashlib.sha256() h.update(bytearray(data, file_encoding)) 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 == '-': ifp = os.fdopen(sys.stdin.fileno(), 'rb') else: ifp = self.locked_file.fp self.data = ifp.read().decode(file_encoding) 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 # newline='' =-> don't convert line endings fp = io.StringIO(self.data, newline='') if add_default: fp = AddDefaultSection(fp, self.iniopt) else: fp = CrudiniInputFilter(fp, self.iniopt) conf = CrudiniConfigParser(preserve_case=preserve_case, space_around_delimiters=( 'nospace' not in self.iniopt)) conf.readfp(fp) self.crudini_no_arg = fp.crudini_no_arg self.indented = fp.indented self.replace_leading = fp.replace_leading self.section_indents = fp.section_indents self.windows_eol = fp.windows_eol self.bom = fp.bom 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) elif file_is_closed(sys.stdin): error("stdin is closed") sys.exit(1) 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 ignore_indent = 'ignoreindent' in self.iniopt # Since indents stripped on read, ensure no ambiguities # Also allow to set default indent on params with explicit indent # We don't support this for sections as in all cases they # can have [ leading spaces] in their names. TODO: Perhaps should # support specifying ' [spaces before brackets]' for sections? if ignore_indent and param: stripped_param = param.lstrip() current_indent = self.section_indents.get(section or 'DEFAULT') if not current_indent: leading_ws = param[:len(param)-len(stripped_param)] if leading_ws: self.indented = True self.section_indents[section] = leading_ws param = stripped_param 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: # Adjust to allow adding a "default" section (issue #80) skip_section_add = False if section.lower() == "default": section = "crudini_default_adjust_%s" % section self.default_adjust = True if self.conf.has_section(section): # We already added skip_section_add = True # 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. # List the sections here to adjust when writing. if not skip_section_add: self.ini_section_blanks.append(section) self.conf.add_section(section) if param is not None: try: curr_val = self.conf.get(section, param) except configparser.NoOptionError: if self.mode == "--del": if self.update not in ('param', 'section'): return if value is None: # Unspecified param should clear list. This will also force # existing param "flags" or new params to use '=' delimiter. if self.vlist: curr_val = '' if curr_val == 'crudini_no_arg': # param already exists without delimiter return elif curr_val is None and self.crudini_no_arg: # some params exist without delimiter # so default new param to not use one value = 'crudini_no_arg' else: # Otherwise use a delimeter value = '' # Add a default indent through an inline comment, to later replace section_indent = self.section_indents.get(section) if curr_val is None and ignore_indent and section_indent: value += ' ;crudini_indent>%s<' % section_indent 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): if self.update in ('param', 'section'): raise configparser.NoSectionError(self.section) else: self.removed_section = True 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' and self.fmt != 'sh': 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) print_section = self.section if self.fmt == 'sh': print_section = None self._print.name_value(self.param, val, print_section) else: for section in sections: print_section = section if self.fmt == 'sh': if self.section or section == iniparse.DEFAULTSECT: print_section = None 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, print_section) items = True if not items and self.fmt != 'sh': self._print.name_value(None, None, print_section) def run(self): if not file_is_closed(sys.stdin) and sys.stdin.isatty(): sys.excepthook = Crudini.cli_exception Crudini.init_iniparse_defaultsect() operations = self.parse_options() # --set takes precedence to create file etc. if self.mode == '--del': for o in operations: if o[0] == '--set': self.mode = '--set' break 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 for o in operations: self.set_operation(o) 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': # Del possible extraneous blank line left with removed section # XXX: This may collapse existing multiple blank lines if self.removed_section and 'sectionspace' not in self.iniopt: iniparse.tidy(self.conf) # XXX: Ideally we should just do conf.write(f) here, but to # avoid iniparse issues, we massage the data a little here if sys.version_info[0] >= 3: str_data = str(self.conf.data) else: # XXX: Can't use uc() here as can't specify encoding str_data = unicode(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) # Handle creation of non special "default" section if self.default_adjust: str_data = str_data.replace('crudini_default_adjust_', '') # Process blank lines around sections if 'sectionspace' in self.iniopt: # Ensure a single blank line before sections str_data = re.sub(r'\n*(\[[^\]]+\])', r'\n\n\1', str_data) str_data = str_data.lstrip('\n') # remove leading \n str_data = str_data.rstrip('\n') + '\n' # ensure \n at end else: # Remove extraneous blanks iniparse adds when adding sects for section in self.ini_section_blanks: section_ = '\n[%s]\n' % section str_data = str_data.replace(section_, section_[1:], 1) if self.windows_eol: # iniparse uses '\n' for new/updated items # so reset all to windows format str_data = str_data.replace('\r\n', '\n') str_data = str_data.replace('\n', '\r\n') if self.indented: str_data = self.replace_leading.sub(r'\2\1', str_data) if self.crudini_no_arg: spacing = '' if 'nospace' in self.iniopt else ' ' str_data = str_data.replace('%s=%scrudini_no_arg' % (spacing, spacing), '') if self.bom: str_data = u'\ufeff%s' % str_data changed = self.chksum != self._chksum(str_data) if self.output == '-': sys.stdout.write(str_data) elif changed: if os.name == 'nt': # Close input file as Windows gives access errors on # open files. For e.g. see caveats noted at: # https://bugs.python.org/issue46003 self.locked_file.delete() 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) if not file_is_closed(sys.stdout): 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 if not file_is_closed(sys.stdout): 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.6/example.ini000066400000000000000000000014271477774577700153630ustar00rootroot00000000000000global=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 list3 = v1 v2 crudini-0.9.6/noequals.ini000066400000000000000000000004761477774577700155620ustar00rootroot00000000000000# 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.6/pyproject.toml000066400000000000000000000002241477774577700161350ustar00rootroot00000000000000[build-system] requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] crudini-0.9.6/setup.cfg000066400000000000000000000014651477774577700150520ustar00rootroot00000000000000[metadata] name = crudini author = Pádraig Brady author_email = P@draigBrady.com license = GPLv2 version = 0.9.6 description = A utility for manipulating ini files keywords = ini, config, edit url = http://github.com/pixelb/crudini long_description = file: README.md long_description_content_type = text/markdown 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 Programming Language :: Python :: 3 [options] install_requires = iniparse>=0.5 py_modules = crudini setup_requires = setuptools>=44; wheel; setuptools_scm[toml]>=3.4.3 [bdist_wheel] universal = 1 [options.entry_points] console_scripts = crudini = crudini:main crudini-0.9.6/setup.py000066400000000000000000000001361477774577700147350ustar00rootroot00000000000000# -*- coding: utf-8 -*- from setuptools import setup if __name__ == "__main__": setup() crudini-0.9.6/tests/000077500000000000000000000000001477774577700143655ustar00rootroot00000000000000crudini-0.9.6/tests/example.lines000066400000000000000000000017561477774577700170650ustar00rootroot00000000000000[ 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 [ list ] list3 = \nv1\nv2 crudini-0.9.6/tests/nospace-in.ini000066400000000000000000000001411477774577700171160ustar00rootroot00000000000000#comment = ignore p1 = 1 p2 = 2 = 2 p3 : 3 = 3 p 4 = 4 p5 = 5 p6 : 6 : 6 a multiline = ignore crudini-0.9.6/tests/nospace-out.ini000066400000000000000000000001341477774577700173210ustar00rootroot00000000000000#comment = ignore p1=1 p2=2 = 2 p3:3 = 3 p 4=4 p5=5 p6:6 : 6 a multiline = ignore new=val crudini-0.9.6/tests/section1.ini000066400000000000000000000007741477774577700166230ustar00rootroot00000000000000[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.6/tests/section1.lines000066400000000000000000000013621477774577700171500ustar00rootroot00000000000000[ 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.6/tests/section1.sh000066400000000000000000000007511477774577700164510ustar00rootroot00000000000000dup1=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.6/tests/sections.sh000066400000000000000000000012351477774577700165510ustar00rootroot00000000000000section1_dup1=val1 section1_dup2=val2 section1_nospace=val section1_multiline='with leading space' 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.6/tests/space-in.ini000066400000000000000000000001151477774577700165620ustar00rootroot00000000000000#comment=ignore p1=1 p2=2=2 p3:3=3 p 4=4 p5 = 5 p6:6:6 a multiline=ignore crudini-0.9.6/tests/space-out.ini000066400000000000000000000001401477774577700167610ustar00rootroot00000000000000#comment=ignore p1 = 1 p2 = 2=2 p3 : 3=3 p 4 = 4 p5 = 5 p6 : 6:6 a multiline=ignore new = val crudini-0.9.6/tests/test.sh000077500000000000000000000656751477774577700157260ustar00rootroot00000000000000#!/bin/bash trap "exit 130" INT cleanup() { rm -f err noequals*.ini test.ini ltest.ini good.ini example.ini; exit "$1"; } trap 'cleanup $?' EXIT crudini() { ../crudini.py "$@"; } test=0 fail() { test=$(($test+1)) printf "Test $test \033[1;31mFAIL\033[m (line ${BASH_LINENO[0]})\n" 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 for bom in '' $'\xef\xbb\xbf'; do printf '%s%s\n' "$bom" 'global=val' > test.ini crudini --set test.ini '' global valnew printf '%s%s\n' "$bom" 'global=valnew' > good.ini diff -u test.ini good.ini && ok || fail printf '%s%s\n' "$bom" 'global=val' > test.ini crudini --set test.ini DEFAULT global valnew printf '%s%s\n' "$bom" '[DEFAULT]' 'global=valnew' > good.ini diff -u test.ini good.ini && ok || fail printf '%s%s\n' "$bom" '[DEFAULT]' 'global=val' > test.ini crudini --set test.ini DEFAULT global valnew printf '%s%s\n' "$bom" '[DEFAULT]' 'global=valnew' > good.ini diff -u test.ini good.ini && ok || fail printf '%s%s\n' "$bom" 'global=val' '' '[nonDEFAULT]' 'name=val' > test.ini crudini --set test.ini '' global valnew printf '%s%s\n' "$bom" 'global=valnew' '' '[nonDEFAULT]' 'name=val' > good.ini diff -u test.ini good.ini && ok || fail done # 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 crudini $mode --ini-options= --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 # unicode get test "$(crudini --get example.ini non-sh-compat útf8name)" = 'val' && 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 # get all in sh format crudini --format=sh --get section1.ini > test.ini diff -u test.ini sections.sh && ok || fail # get default in sh format crudini --format=sh --get example.ini '' > test.ini printf '%s\n' 'global=supported' > good.ini diff -u test.ini good.ini && 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 # whitespace separated (while maintaining newline separation) crudini --list --list-sep= --set example.ini list list3 v2 || fail # ignored crudini --list --list-sep= --set example.ini list list3 v3 || fail test "$(crudini --get example.ini list list3)" = $'\nv1\nv2\nv3' && 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 clearing a list by not specifing a value printf '%s\n' 'empty' 'nonempty = 1' > test.ini crudini --list --set test.ini '' nonempty 2>/dev/null && ok || fail test "$(crudini --get test.ini '' nonempty 2>&1)" = '' && ok || fail crudini --list --set test.ini '' empty 'v1' 2>/dev/null && ok || fail test "$(crudini --get test.ini '' empty 2>&1)" = 'v1' && ok || fail # -------------------------------------------------------- # 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 updating of single token parameters # Ensure no delimitier added with unspecified value test "$(crudini --output=- --set noequals.ini noequals param1 \ | grep param1)" = 'param1' && ok || fail # Ensure delimitier added with unspecified value with --list test "$(crudini --list --output=- --set noequals.ini noequals param1 \ | grep param1)" = 'param1 = ' && ok || fail # Ensure delimitier added with empty value test "$(crudini --output=- --set noequals.ini noequals param1 '' \ | grep param1)" = 'param1 = ' && ok || fail # Ensure correct spacing with --ini-options=nospace test "$(crudini --ini-options=nospace --output=- --set noequals.ini \ noequals param1 value1 \ | grep param1)" = 'param1=value1' && ok || fail # Ensure correct spacing with --ini-options=space test "$(crudini --ini-options=space --output=- --set noequals.ini \ noequals param1 value1 \ | grep param1)" = 'param1 = value1' && ok || fail # Test can read windows format files printf '%s\r\n' '' 'empty' 'param=value' > test.ini test "$(crudini --get test.ini DEFAULT param)" = 'value' && ok || fail # Test can maintain windows format files diff -u <(crudini --output=- --ini-options=nospace \ --set test.ini '' param value) \ test.ini && ok || fail printf '%s\r\n' '' 'empty=' > test.ini diff -u <(crudini --output=- --ini-options=nospace \ --set test.ini '' empty '') \ test.ini && ok || fail printf '%s\r\n' '' 'empty' > test.ini diff -u <(crudini --output=- --ini-options=nospace \ --set test.ini '' empty) \ test.ini && ok || fail # Test closed stdin (0<&- crudini --help >/dev/null) && ok || fail # Test closed stdout (>&- crudini --get test.ini section param value 2>/dev/null) && fail || ok (>&- crudini --set - section param value 2>/dev/null) && fail || ok (>&- crudini --set test.ini section param value) && ok || fail # Test --ini-options=nospace diff -u <(crudini --output=- --ini-options=nospace \ --set nospace-in.ini '' new val) \ nospace-out.ini && ok || fail # Test --ini-options=space diff -u <(crudini --output=- --ini-options=space \ --set space-out.ini '' new val) \ space-out.ini && ok || fail # - Mixed space / nospace not allowed crudini --get file.conf '' param1 --ini-options=space,nospace \ 2>/dev/null && fail || ok # Test multi operation # - Multiple set printf '%s\n' '' 'param1=?' 'param2=?' > file.conf printf '%s\n' '' 'param1=1' 'param2=2' > good.conf crudini --set file.conf '' param1 1 \ --set file.conf '' param2 2 || fail diff -u good.conf file.conf && ok || fail rm file.conf good.conf # - Multiple get printf '%s\n' 'param1=1' 'param2=2' > file.conf crudini --get file.conf '' param1 \ --get file.conf '' param2 \ --format=sh > out.conf || fail diff -u out.conf file.conf && ok || fail rm file.conf out.conf # - Mixed set / del printf '%s\n' '' 'param1=?' 'param2=?' > file.conf printf '%s\n' '' 'param2=2' > good.conf crudini --del file.conf '' param1 \ --set file.conf '' param2 2 || fail diff -u good.conf file.conf && ok || fail rm file.conf good.conf # - Mixed set / del with non existing file printf '%s\n' 'param1 = ' > good.conf crudini --set file.conf '' param1 \ --del file.conf '' param3 || fail diff -u good.conf file.conf && ok || fail rm file.conf good.conf # - Mixed set / get not allowed crudini --set file.conf '' param1 \ --get file.conf '' param1 2>/dev/null && fail || ok # - Multiple files not allowed printf '%s\n' '[section]' > file1.conf printf '%s\n' '[section]' > file2.conf crudini --del file1.conf section --del file2.conf section 2>/dev/null \ && fail || ok rm -f file1.conf file2.conf # - Multiple --merge not allowed crudini --merge file.conf --merge file.conf < /dev/null 2>/dev/null \ && fail || ok rm -f file.conf # - Interspersed options supported printf '%s\n' 'param1=1' 'param2=2' > good.conf crudini --set file.conf '' param1 1 \ --ini-options nospace \ --set file.conf '' param2 2 || fail diff -u good.conf file.conf && ok || fail rm file.conf good.conf # - Conflicting DEFAULT section specs not allowed crudini --set file.conf '' param1 value \ --set file.conf DEFAULT param2 value 2>/dev/null \ && fail || ok crudini --set file.conf DEFAULT param1 value \ --set file.conf '' param2 value 2>/dev/null \ && fail || ok rm -f file.conf # Test indentation support # - --get indented printf ' %s\n' '[section]' 'param1=1' > file.conf crudini --ini-options=ignoreindent \ --get file.conf >/dev/null && ok || fail # - --set maintaining indented printf ' %s\n' '[section]' 'param1=a' > good.conf crudini --ini-options=ignoreindent \ --set file.conf section param1 a && ok || fail diff -u good.conf file.conf && ok || fail # - --set copying indentation printf ' %s\n' '[section]' 'param1=a' 'param2=b' > good.conf crudini --ini-options=ignoreindent,nospace \ --set file.conf section param2 b && ok || fail diff -u good.conf file.conf && ok || fail # - --set new indentation for param (on new section) printf '%s\n' '[section]' ' param1 = a' > good.conf rm -f file.conf crudini --ini-options=ignoreindent \ --set file.conf section ' param1' a && ok || fail diff -u good.conf file.conf && ok || fail # - --set copying indentation from param printf '%s\n' '[section]' ' param1 = a' ' param2 = b' > good.conf crudini --ini-options=ignoreindent \ --set file.conf section 'param2' b && ok || fail diff -u good.conf file.conf && ok || fail # - --set new indentation for param (on new default section) printf '%s\n' ' param1 = a' > good.conf rm -f file.conf crudini --ini-options=ignoreindent \ --set file.conf '' ' param1' a && ok || fail diff -u good.conf file.conf && ok || fail rm file.conf good.conf # Test removal of extraneous empty lines # implicitly enabled with --del printf '\n[%s]\n' 1 2 > good.conf cp good.conf file.conf for i in 1 2; do crudini --del file.conf $i; crudini --set file.conf $i; done diff -u good.conf file.conf && ok || fail rm file.conf good.conf # Test addition/removal of empty lines with sectionspace printf '\n[%s]\n[%s]\n\n[%s]\n\n\n[%s]\n\n' 1 2 3 4 > file.conf printf '[%s]\n\n[%s]\n\n[%s]\n\n[%s]\n' 1 2 3 4 > good.conf crudini --set --ini-options=sectionspace file.conf '' diff -u good.conf file.conf && ok || fail rm file.conf good.conf # Test creation of a "default" section printf '[%s]\n' 'default' > good.conf crudini --set file.conf default # new diff -u good.conf file.conf && ok || fail crudini --set file.conf default # existing diff -u good.conf file.conf && ok || fail rm file.conf crudini --set file.conf default --set file.conf default # double new diff -u good.conf file.conf && ok || fail rm file.conf good.conf crudini-0.9.6/tox.ini000066400000000000000000000002601477774577700145340ustar00rootroot00000000000000[tox] envlist = py26,py27,pep8 [testenv] deps = iniparse>=0.5 commands = /bin/bash -c 'cd tests && ./test.sh' [testenv:pep8] deps = flake8 commands = flake8 crudini setup.py