├── COPYING ├── EXAMPLES ├── INSTALL ├── Makefile ├── NEWS ├── README.md ├── TODO ├── crudini-help ├── crudini.py ├── example.ini ├── noequals.ini ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests ├── example.lines ├── nospace-in.ini ├── nospace-out.ini ├── section1.ini ├── section1.lines ├── section1.sh ├── sections.sh ├── space-in.ini ├── space-out.ini └── test.sh └── tox.ini /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /EXAMPLES: -------------------------------------------------------------------------------- 1 | Examples: 2 | 3 | # Add/Update a var 4 | crudini --set config_file section parameter value 5 | 6 | # Add/Update a var in the root or global area. 7 | # I.e. that's not under a [section]. 8 | crudini --set config_file "" parameter value 9 | 10 | # Update an existing var 11 | crudini --set --existing config_file section parameter value 12 | 13 | # Add/Update/Delete multiple variables atomically 14 | crudini --set config_file section parameter1 value \ 15 | --set config_file section parameter2 value \ 16 | --del config_file section parameter3 17 | 18 | # Get multiple items from stdin 19 | env | crudini --get - '' USER --get - '' SHELL 20 | 21 | # Add/Append a value to a comma separated list 22 | # Note any whitespace around commas is ignored 23 | crudini --set --list config_file section parameter a_value 24 | 25 | # Add/Append a value to a whitespace separated list 26 | # Note multiline lists are supported (as newline is whitespace) 27 | crudini --set --list --list-sep= config_file section parameter a_value 28 | 29 | # Delete a var 30 | crudini --del config_file section parameter 31 | 32 | # Delete a section 33 | crudini --del config_file section 34 | 35 | # output a value 36 | crudini --get config_file section parameter 37 | 38 | # output a global value not in a section 39 | crudini --get config_file "" parameter 40 | 41 | # output a section 42 | crudini --get config_file section 43 | 44 | # output a section, parseable by shell 45 | eval "$(crudini --get --format=sh config_file section)" 46 | 47 | # update an ini file from shell variable(s) 48 | echo name="$name" | crudini --merge config_file section 49 | 50 | # merge an ini file from another ini 51 | crudini --merge config_file < another.ini 52 | 53 | # compare two ini files using standard UNIX text processing 54 | diff <(crudini --get --format=lines file1.ini|sort) \ 55 | <(crudini --get --format=lines file2.ini|sort) 56 | 57 | # Rewrite ini file to use name=value format rather than name = value 58 | crudini --ini-options=nospace --set config_file "" 59 | 60 | # Add/Update a var, ensuring complete file in name=value format 61 | crudini --ini-options=nospace --set config_file section parameter value 62 | 63 | # Rewrite ini file to ensure a single blank line between sections, 64 | # and no leading or trailing blank lines 65 | crudini --ini-options=sectionspace --set config_file "" 66 | 67 | # Read indented ini file, like .gitconfig 68 | crudini --ini-options=ignoreindent --format=lines --get ~/.gitconfig 69 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | On windows ensure a python interpreter is installed. 4 | For example installing from https://www.python.org/downloads/ 5 | will put the py launcher and pip in the PATH. 6 | 7 | Then ensure the iniparse module is installed by 8 | running the following from a "cmd" prompt: 9 | 10 | ``` 11 | pip install iniparse 12 | ``` 13 | 14 | Then crudini can be invoked by downloading just the crudini.py 15 | file and running like: 16 | 17 | ``` 18 | py crudini.py --help 19 | ``` 20 | 21 | On Linux systems crudini is generally available from your standard 22 | package manager, and installing will also ensure the iniparse 23 | dependency is appropriately installed on your system. 24 | You can also download and run the single crudini.py file directly 25 | to use latest version. 26 | 27 | On any system you should be able to pip install 28 | the latest code from github like: 29 | 30 | ``` 31 | pip install git+https://github.com/pixelb/crudini.git#egg=crudini 32 | ``` 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | name = crudini 2 | version = 0.9.6 3 | 4 | all: 5 | help2man -n "manipulate ini files" -o crudini.1 -N ./crudini-help 6 | ./crudini-help --markdown > README.md 7 | 8 | dist: all 9 | mkdir ${name}-${version} 10 | { git ls-files; echo crudini.1; } | xargs cp -a --parents --target=${name}-${version} 11 | tar -czf ${name}-${version}.tar.gz ${name}-${version} 12 | rm -Rf ${name}-${version} 13 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | crudini NEWS -*- outline -*- 2 | 3 | * Noteworthy changes in release 0.9.7 (????-??-??) 4 | 5 | Support non UTF-8 encoded files. Previously non UTF-8 6 | encoded files would have thrown an exception. 7 | 8 | 9 | * Noteworthy changes in release 0.9.6 (2025-04-16) 10 | 11 | Support BOM correctly. Previously we would have stripped any 12 | Byte Order Mark, and incorrectly matched items on the first line. 13 | 14 | Extraneous blank lines are avoided when deleting a section. 15 | Previously blank lines preceeding a [section] were not removed. 16 | Note this will collapse multiple empty lines preceding all sections. 17 | 18 | Support creating a section called "default". Previously this 19 | would have been disallowed, with an invalid section name error. 20 | 21 | Support ensuring a single space in all 'name = value' entries in the file 22 | with --ini-options=space. This is symmetric and opposite to the existing 23 | --ini-options=nospace option. 24 | 25 | Support ensuring a single blank line between sections, and no blank lines 26 | at the start or end of the file, with --ini-options=sectionspace. 27 | 28 | 29 | * Noteworthy changes in release 0.9.5 (2023-10-04) 30 | 31 | ** Improvements 32 | 33 | Support for multiple --set and --del, or --get operations, 34 | allowing for more efficient and atomic updates to multiple parameters. 35 | 36 | Support indented ini files with --ini-options=ignoreindent. 37 | Indentation is ignored (and maintained) in the ini file. 38 | 39 | --format=sh will now attempt to output a whole ini file in sh format. 40 | Previously it would have only printed the section names. 41 | 42 | 43 | * Noteworthy changes in release 0.9.4 (2022-12-23) 44 | 45 | ** Bug fixes 46 | 47 | Fix updating of flag only parameters so they 48 | don't have '=' or '=crudini_no_arg' added added on update. 49 | 50 | Handle closed stdin/stdout gracefully, without giving errors. 51 | 52 | ** Improvements 53 | 54 | Windows support. 55 | 56 | Windows line endings are maintained. 57 | 58 | Lists can be delimited with arbitrary whitespace with `--list-sep=`. 59 | 60 | Support for unspaced "name=val" format with `--ini-options=nospace`. 61 | 62 | 63 | * Noteworthy changes in release 0.9.3 (2019-08-30) 64 | 65 | ** Bug fixes 66 | 67 | Reading ini files with windows line endings is again supported. 68 | Regression added in v0.9. 69 | 70 | ** Improvements 71 | 72 | python 3 support. 73 | 74 | 75 | * Noteworthy changes in release 0.9 (2016-12-13) 76 | 77 | ** Bug fixes 78 | 79 | Write errors to stdout are diagnosed correctly and consistently. 80 | 81 | Replacing symlinks now replaces the target rather than the symlink itself. 82 | 83 | ** Changes in behavior 84 | 85 | The case of parameters is maintained with --get. 86 | 87 | ** Improvements 88 | 89 | Single token parameters (without equals) are now supported, 90 | which are used in mysql config for example. 91 | 92 | 93 | * Noteworthy changes in release 0.8 (2016-11-23) 94 | 95 | ** Bug fixes 96 | 97 | crudini now handles parameters starting with "rem". 98 | Previously an entry such as "remote = 1" would be ignored. 99 | 100 | ** New features 101 | 102 | Support mercurial config files by treating lines starting 103 | with '%' as comments, thus ignoring mercurial '%include' 104 | and '%unset' directives. 105 | 106 | 107 | * Noteworthy changes in release 0.7 (2015-06-14) 108 | 109 | ** Bug fixes 110 | 111 | crudini no longer removes a blank line from the start of a file 112 | which has no sections, or options outside a section. 113 | [bug introduced in version 0.5] 114 | 115 | Files are now synced after writing for better Durability. 116 | 117 | Separate locking files are no longer used which avoids 118 | deadlock in cases where the system is stopped in the 119 | small window where these files exist. 120 | 121 | 122 | * Noteworthy changes in release 0.5 (2015-01-28) 123 | 124 | ** Bug fixes 125 | 126 | Lock files are cleaned up robustly. Previously there was a race condition 127 | resulting in blocked subsequent edits, due to a lingering lock file. 128 | 129 | --del will ignore requests to delete a parameter in a non-existing section 130 | (unless --existing is used). Previously it failed citing the missing section. 131 | 132 | ** New features 133 | 134 | The --existing option takes parameters to give more control over what needs 135 | to pre-exist. So you can specify for example that a file needs to exist, 136 | but any items within it are created as needed. 137 | 138 | A new --verbose option was added to indicate on stderr wether the 139 | request resulted in a config change or not. This can be used to 140 | determine whether to restart programs etc. 141 | 142 | ** Changes in behavior 143 | 144 | Files are created by default if missing, unless --existing is specified. 145 | 146 | ** Improvements 147 | 148 | Protections against creating unparseable ini files were added. 149 | 150 | stdin can be parsed just as with normal files. 151 | 152 | File writes are avoided if there are no changes to the config. 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crudini - A utility for manipulating ini files 2 | 3 | ## Usage: 4 | ``` 5 | crudini --set [OPTION]... config_file section [param] [value] 6 | crudini --get [OPTION]... config_file [section] [param] 7 | crudini --del [OPTION]... config_file section [param] [list value] 8 | crudini --merge [OPTION]... config_file [section] 9 | 10 | SECTION can be empty ("") or "DEFAULT" in which case, 11 | params not in a section, i.e. global parameters are operated on. 12 | If 'DEFAULT' is used with --set, an explicit [DEFAULT] section is added. 13 | 14 | Multiple --set|--del|--get operations for a config_file can be specified. 15 | 16 | ``` 17 | ## Options: 18 | ``` 19 | 20 | --existing[=WHAT] For --set, --del and --merge, fail if item is missing, 21 | where WHAT is 'file', 'section', or 'param', 22 | or if WHAT not specified; all specified items. 23 | --format=FMT For --get, select the output FMT. 24 | Formats are 'sh','ini','lines' 25 | --ini-options=OPT Set options for handling ini files. Options are: 26 | 'nospace': use format name=value not name = value 27 | 'space': ensure name = value format 28 | 'sectionspace': ensure one blank line between sections 29 | 'ignoreindent': ignore leading whitespace 30 | --inplace Lock and write files in place. 31 | This is not atomic but has less restrictions 32 | than the default replacement method. 33 | --list For --set and --del, update a list (set) of values 34 | --list-sep=STR Delimit list values with "STR" instead of " ,". 35 | An empty STR means any whitespace is a delimiter. 36 | --output=FILE Write output to FILE instead. '-' means stdout 37 | --verbose Indicate on stderr if changes were made 38 | --help Write this help to stdout 39 | --version Write version to stdout 40 | 41 | ``` 42 | ## Examples: 43 | ``` 44 | 45 | # Add/Update a var 46 | crudini --set config_file section parameter value 47 | 48 | # Add/Update a var in the root or global area. 49 | # I.e. that's not under a [section]. 50 | crudini --set config_file "" parameter value 51 | 52 | # Update an existing var 53 | crudini --set --existing config_file section parameter value 54 | 55 | # Add/Update/Delete multiple variables atomically 56 | crudini --set config_file section parameter1 value \ 57 | --set config_file section parameter2 value \ 58 | --del config_file section parameter3 59 | 60 | # Get multiple items from stdin 61 | env | crudini --get - '' USER --get - '' SHELL 62 | 63 | # Add/Append a value to a comma separated list 64 | # Note any whitespace around commas is ignored 65 | crudini --set --list config_file section parameter a_value 66 | 67 | # Add/Append a value to a whitespace separated list 68 | # Note multiline lists are supported (as newline is whitespace) 69 | crudini --set --list --list-sep= config_file section parameter a_value 70 | 71 | # Delete a var 72 | crudini --del config_file section parameter 73 | 74 | # Delete a section 75 | crudini --del config_file section 76 | 77 | # output a value 78 | crudini --get config_file section parameter 79 | 80 | # output a global value not in a section 81 | crudini --get config_file "" parameter 82 | 83 | # output a section 84 | crudini --get config_file section 85 | 86 | # output a section, parseable by shell 87 | eval "$(crudini --get --format=sh config_file section)" 88 | 89 | # update an ini file from shell variable(s) 90 | echo name="$name" | crudini --merge config_file section 91 | 92 | # merge an ini file from another ini 93 | crudini --merge config_file < another.ini 94 | 95 | # compare two ini files using standard UNIX text processing 96 | diff <(crudini --get --format=lines file1.ini|sort) \ 97 | <(crudini --get --format=lines file2.ini|sort) 98 | 99 | # Rewrite ini file to use name=value format rather than name = value 100 | crudini --ini-options=nospace --set config_file "" 101 | 102 | # Add/Update a var, ensuring complete file in name=value format 103 | crudini --ini-options=nospace --set config_file section parameter value 104 | 105 | # Rewrite ini file to ensure a single blank line between sections, 106 | # and no leading or trailing blank lines 107 | crudini --ini-options=sectionspace --set config_file "" 108 | 109 | # Read indented ini file, like .gitconfig 110 | crudini --ini-options=ignoreindent --format=lines --get ~/.gitconfig 111 | ``` 112 | ## Installation 113 | 114 | On windows ensure a python interpreter is installed. 115 | For example installing from https://www.python.org/downloads/ 116 | will put the py launcher and pip in the PATH. 117 | 118 | Then ensure the iniparse module is installed by 119 | running the following from a "cmd" prompt: 120 | 121 | ``` 122 | pip install iniparse 123 | ``` 124 | 125 | Then crudini can be invoked by downloading just the crudini.py 126 | file and running like: 127 | 128 | ``` 129 | py crudini.py --help 130 | ``` 131 | 132 | On Linux systems crudini is generally available from your standard 133 | package manager, and installing will also ensure the iniparse 134 | dependency is appropriately installed on your system. 135 | You can also download and run the single crudini.py file directly 136 | to use latest version. 137 | 138 | On any system you should be able to pip install 139 | the latest code from github like: 140 | 141 | ``` 142 | pip install git+https://github.com/pixelb/crudini.git#egg=crudini 143 | ``` 144 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | support --set,--merge of #commented name=value 2 | with operation controlled with 3 | --with-comment=always 4 | To add new item under comment even if same value as comment 5 | --with-comment[=ifchanged] 6 | To add new item under comment only if different. 7 | Would support --list also to add to default list. 8 | --new-comment 9 | To add an (additional) comment line for item no matter where, 10 | or if, it's added. Ensure above any #commented name=value though. 11 | 12 | support multiple files passed to --merge 13 | 14 | possibly support --format=sh|json with --merge 15 | 16 | possibly support multiple duplicate names per section 17 | to support MultiStrOpt in openstack config files file example. 18 | This could be interfaced using the --list=multiname option. 19 | Also have --list autodetect multiline lists as used by yum like: 20 | name = val1, val2 21 | val3 22 | I.E. split on a combo of [\n,] 23 | 24 | possibly support --lower to output normalised case for 25 | --get and --sort 26 | 27 | Have a look at cliff output formatters? csv and shell at least: 28 | http://blog.oddbit.com/2013/11/22/a-unified-cli-for-op 29 | 30 | --format=json to output json format. 31 | Note ini format supports name:value so quite a lot of overlap 32 | 33 | have pip install put man page in place 34 | -------------------------------------------------------------------------------- /crudini-help: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # crudini --help generator for help2man and README.md 4 | 5 | if [ "$1" = '--help' ]; then 6 | { 7 | printf '%s' 'crudini - ' 8 | ./crudini.py --help | sed 's/crudini\.py/crudini/g;' 9 | echo 10 | cat EXAMPLES 11 | } 12 | elif [ "$1" = '--markdown' ]; then 13 | { 14 | printf '%s' '# crudini - ' 15 | ./crudini.py --help | sed 's/crudini\.py/crudini/g;' 16 | echo 17 | cat EXAMPLES 18 | } | 19 | sed 's/^\([^ ]\+:\)\( \+\)/\1\n/' | # Sections on own line 20 | sed 's/^[^ ]\+:/```\n## &\n```/' | # Markup sections 21 | sed 's/^ *or: *//' | # Delete help2man synopsis formatting 22 | sed '0,/```/{/```/d;}' # Delete first ``` 23 | echo '```' # Add terminating ``` 24 | cat INSTALL # Already marked up 25 | elif [ "$1" = '--version' ]; then 26 | ./crudini.py --version 27 | fi 28 | 29 | -------------------------------------------------------------------------------- /crudini.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim:fileencoding=utf8 4 | # 5 | # Copyright © Pádraig Brady 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GPLv2, the GNU General Public License version 2, as 9 | # published by the Free Software Foundation. http://gnu.org/licenses/gpl.html 10 | from __future__ import print_function 11 | 12 | import atexit 13 | import sys 14 | import contextlib 15 | import errno 16 | import getopt 17 | import hashlib 18 | import iniparse 19 | import io 20 | import locale 21 | import os 22 | import re 23 | import shutil 24 | import string 25 | import tempfile 26 | 27 | if sys.version_info[0] >= 3: 28 | import shlex as pipes 29 | import configparser 30 | else: 31 | import codecs 32 | import pipes 33 | import ConfigParser as configparser 34 | 35 | 36 | # We try utf-8 by default, but allow the user to override. 37 | # See https://peps.python.org/pep-0686/ for details. 38 | def env_encoding(): 39 | try: 40 | return locale.getencoding() 41 | except: 42 | return locale.getpreferredencoding(do_setlocale=False) 43 | 44 | user_encoding = 'utf-8' # user specified items 45 | file_encoding = 'utf-8' # encoding of ini file contents 46 | 47 | 48 | # Python 2/3 wrapper to convert strings to unicode 49 | try: # Python 2 50 | unicode 51 | 52 | def s2u(s, e=user_encoding): 53 | return unicode(s, e) 54 | # Also add conversion wrapper for print() 55 | sys.stdout = codecs.getwriter(user_encoding)(sys.stdout) 56 | except NameError: # Python 3 57 | def s2u(s, e=user_encoding): 58 | return str(s) 59 | unicode = str 60 | 61 | 62 | def error(message=None): 63 | if message: 64 | sys.stderr.write(message + '\n') 65 | 66 | 67 | def delete_if_exists(path): 68 | """Delete a file, but ignore file not found error. 69 | """ 70 | try: 71 | os.unlink(path) 72 | except EnvironmentError as e: 73 | if e.errno != errno.ENOENT: 74 | print(str(e)) 75 | raise 76 | 77 | 78 | def file_is_closed(stdfile): 79 | if not stdfile: 80 | # python3 sets sys.stdin etc. to None if closed 81 | return True 82 | else: 83 | # python2 needs to be checked 84 | try: 85 | os.fstat(stdfile.fileno()) 86 | except EnvironmentError as e: 87 | if e.errno == errno.EBADF: 88 | return True 89 | return False 90 | 91 | 92 | # Adjustments to iniparse to optionally use name=value format (nospace) 93 | # and support for parameters without '=' specified 94 | class CrudiniInputFilter(): 95 | def __init__(self, fp, iniopt): 96 | self.fp = fp 97 | self.iniopt = iniopt 98 | self.crudini_no_arg = False 99 | self.indented = False 100 | self.last_section = 'DEFAULT' 101 | self.section_indents = {} 102 | self.windows_eol = None 103 | self.bom = None 104 | # Note [ \t] used rather than \s to avoid adjusting \r\n when no value 105 | # Unicode spacing around the delimiter would be very unusual anyway 106 | self.delimiter_spacing = re.compile(r'(.*?)[ \t]*([:=])[ \t]*(.*)') 107 | self.leading_whitespace = re.compile(r'([ \t]+)(.+)') 108 | self.replace_leading = re.compile(r'^(.+) ;crudini_indent>(.*)<$', 109 | flags=re.MULTILINE) 110 | self.section_match = re.compile(r'[ \t]*\[([^]]+)\].*') 111 | 112 | def readline(self): 113 | line = self.fp.readline() 114 | 115 | # Strip BOM. iniparse tracks it but simpler for us to replace later 116 | # as we're munging the data in various ways. 117 | if self.bom is None: 118 | if line and line[0] == u'\ufeff': 119 | line = line[1:] 120 | self.bom = True 121 | else: 122 | self.bom = False 123 | 124 | # XXX: This doesn't handle ;inline comments. 125 | # Really should be done within iniparse. 126 | 127 | # Detect windows format files 128 | # so we can undo iniparse auto conversion to unix 129 | if self.windows_eol is None: 130 | if line: 131 | self.windows_eol = len(line) >= 2 and line[-2] == '\r' 132 | else: 133 | self.windows_eol = os.name == 'nt' 134 | 135 | if line.strip() and line[0] not in '#;%': 136 | 137 | if 'ignoreindent' in self.iniopt: 138 | section = line.lstrip()[0] == '[' 139 | else: 140 | section = line[0] == '[' 141 | if section: 142 | section_name = self.section_match.sub(r'\1', line.rstrip()) 143 | if not section_name: 144 | return line 145 | self.last_section = section_name 146 | 147 | if line[0] in ' \t' and 'ignoreindent' not in self.iniopt: 148 | return line 149 | 150 | if not section and '=' not in line and ':' not in line: 151 | self.crudini_no_arg = True 152 | line = line.rstrip() + ' = crudini_no_arg\n' 153 | 154 | if not section and 'nospace' in self.iniopt: 155 | # Convert _all_ existing params. New params are handled in 156 | # the iniparse specialization in CrudiniConfigParser() 157 | 158 | # Note if we wanted an option to only convert specified params 159 | # we could do it with special ${value}_crudini_no_space values 160 | # that were then adjusted on output like for crudini_no_arg 161 | # But if need to remove the spacing, then should for all params 162 | 163 | line = self.delimiter_spacing.sub(r'\1\2\3', line) 164 | elif not section and 'space' in self.iniopt: 165 | # Convert _all_ existing params. New params will be correct 166 | 167 | line = self.delimiter_spacing.sub(r'\1 \2 \3', line) 168 | 169 | if line[0] in ' \t': 170 | self.indented = True 171 | 172 | # Set default indent for section to last indent 173 | leading_ws = self.leading_whitespace.sub(r'\1', line.rstrip()) 174 | self.section_indents[self.last_section] = leading_ws 175 | 176 | # match leading spaces and put in trailing ;crudini_indent=... 177 | reorder_ws = r'\2 ;crudini_indent>\1<' 178 | line = self.leading_whitespace.sub(reorder_ws, line) 179 | 180 | return line 181 | 182 | 183 | # XXX: should be done in iniparse. Used to 184 | # add support for ini files without a section 185 | class AddDefaultSection(CrudiniInputFilter): 186 | def __init__(self, fp, iniopt): 187 | CrudiniInputFilter.__init__(self, fp, iniopt) 188 | self.first = True 189 | 190 | def readline(self): 191 | if self.first: 192 | self.first = False 193 | return s2u('[%s]' % iniparse.DEFAULTSECT) 194 | else: 195 | return CrudiniInputFilter.readline(self) 196 | 197 | 198 | class FileLock(object): 199 | """Advisory file based locking. This should be reasonably cross platform 200 | and also work over distributed file systems.""" 201 | def __init__(self, exclusive=False): 202 | # In inplace mode, the process must be careful to not close this fp 203 | # until finished, nor open and close another fp associated with the 204 | # file. 205 | self.fp = None 206 | self.locked = False 207 | 208 | if os.name == 'nt': 209 | # XXX: msvcrt.locking is problematic on windows 210 | # See the history of the portalocker implementation for example: 211 | # https://github.com/WoLpH/portalocker/commits/develop/portalocker 212 | # That would involve needing a new pywin32 dependency, 213 | # so instead we avoid locking on windows for now. 214 | def lock(self): 215 | self.locked = True 216 | 217 | def unlock(self): 218 | self.locked = False 219 | 220 | else: 221 | import fcntl 222 | 223 | def lock(self): 224 | operation = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH 225 | fcntl.lockf(self.fp, operation) 226 | self.locked = True 227 | 228 | def unlock(self): 229 | if self.locked: 230 | fcntl.lockf(self.fp, fcntl.LOCK_UN) 231 | self.locked = False 232 | 233 | FileLock.lock = lock 234 | FileLock.unlock = unlock 235 | 236 | 237 | class LockedFile(FileLock): 238 | """Open a file with advisory locking. This provides the Isolation 239 | property of ACID, to avoid missing writes. In addition this provides AC 240 | properties of ACID if crudini is the only logic accessing the ini file. 241 | This should work on most platforms and distributed file systems. 242 | 243 | Caveats in --inplace mode: 244 | - File must be writeable 245 | - File should be generally non readable to avoid read lock DoS. 246 | Caveats in replace mode: 247 | - Less responsive when there is contention.""" 248 | 249 | def __init__(self, filename, operation, inplace, create): 250 | 251 | self.fp_cmp = None 252 | self.filename = filename 253 | 254 | FileLock.__init__(self, operation != "--get") 255 | 256 | atexit.register(self.delete) 257 | 258 | open_mode = os.O_RDONLY 259 | if operation != "--get": 260 | # We're only reading here, but we check now for write 261 | # permissions we'll need in --inplace case to avoid 262 | # redundant processing. 263 | # Also an exclusive lock needs write perms anyway. 264 | open_mode = os.O_RDWR 265 | 266 | if create and operation != '--del': 267 | open_mode += os.O_CREAT 268 | 269 | try: 270 | # Note we open in binary mode to avoid newline processing, 271 | # and also to give more control over the decoding process later. 272 | # This avoids platform encoding inconsistencies as per PEP 597. 273 | self.fp = os.fdopen(os.open(self.filename, open_mode, 0o666), 'rb') 274 | if inplace: 275 | # In general readers (--get) are protected by file_replace(), 276 | # but using read lock here gives AC of the ACID properties 277 | # when only accessing the file through crudini even with 278 | # file_rewrite(). 279 | self.lock() 280 | else: 281 | # The file may have been renamed since the open so recheck 282 | while True: 283 | self.lock() 284 | fpnew = os.fdopen(os.open(self.filename, open_mode, 0o666), 285 | 'rb') 286 | if (os.name == 'nt' or 287 | os.path.sameopenfile(self.fp.fileno(), fpnew.fileno())): 288 | # Note we don't fpnew.close() here as that would break 289 | # any existing fcntl lock (fcntl.lockf is an fcntl lock 290 | # despite the name). We don't use flock() at present 291 | # as that's less consistent across platforms and may 292 | # be an fcntl lock on NFS anyway for example. 293 | self.fp_cmp = fpnew 294 | break 295 | else: 296 | self.fp.close() 297 | self.fp = fpnew 298 | except EnvironmentError as e: 299 | # Treat --del on a non existing file as operating on NULL data 300 | # which will be deemed unchanged, and thus not re{written,created} 301 | # We don't exit early here so that --verbose is also handled. 302 | if create and operation == '--del' \ 303 | and e.errno in (errno.ENOTDIR, errno.ENOENT): 304 | self.fp = io.BytesIO(b'') 305 | else: 306 | error(str(e)) 307 | sys.exit(1) 308 | 309 | def delete(self): 310 | # explicit close so closed in correct order if taking lock multiple 311 | # times, and also explicit "delete" needed to avoid implicit __del__ 312 | # after os module is unloaded. 313 | self.unlock() 314 | if self.fp: 315 | self.fp.close() 316 | if self.fp_cmp: 317 | self.fp_cmp.close() 318 | 319 | 320 | # Note we use RawConfigParser rather than SafeConfigParser 321 | # to avoid unwanted variable interpolation. 322 | # Note iniparse doesn't currently support allow_no_value=True. 323 | # Note iniparse doesn't currently support space_around_delimiters=False. 324 | class CrudiniConfigParser(iniparse.RawConfigParser): 325 | def __init__(self, preserve_case=False, space_around_delimiters=True): 326 | iniparse.RawConfigParser.__init__(self) 327 | # Without the following we can't have params starting with "rem"! 328 | # We ignore lines starting with '%' which mercurial uses to include 329 | iniparse.change_comment_syntax('%;#', allow_rem=False) 330 | if preserve_case: 331 | self.optionxform = lambda x: x 332 | # Adjust iniparse separator to default to no space around equals 333 | # Note this does NOT convert existing params with spaces, 334 | # that's done in CrudiniInputFilter.readline(). 335 | # XXX: This couples with iniparse internals. 336 | if not space_around_delimiters: 337 | 338 | def new_ol_init(self, name, value, separator="=", *args, **kw): 339 | orig_ol_init(self, name, value, separator, *args, **kw) 340 | orig_ol_init = iniparse.ini.OptionLine.__init__ 341 | iniparse.ini.OptionLine.__init__ = new_ol_init 342 | 343 | 344 | class Print(): 345 | """Use for default output format.""" 346 | 347 | def section_header(self, section): 348 | """Print section header. 349 | 350 | :param section: str 351 | """ 352 | 353 | print(section) 354 | 355 | def name_value(self, name, value, section=None): 356 | """Print parameter. 357 | 358 | :param name: str 359 | :param value: str 360 | :param section: str (default 'None') 361 | """ 362 | 363 | if value == 'crudini_no_arg': 364 | value = '' 365 | print(name or value) 366 | 367 | 368 | class PrintIni(Print): 369 | """Use for ini output format.""" 370 | 371 | def __init__(self): 372 | self.sep = ' ' 373 | 374 | def section_header(self, section): 375 | print("[%s]" % section) 376 | 377 | def name_value(self, name, value, section=None): 378 | if value == 'crudini_no_arg': 379 | value = '' 380 | print(name, '=', value.replace('\n', '\n '), sep=self.sep) 381 | 382 | 383 | class PrintIniNoSpace(PrintIni): 384 | """Use for ini output format with no space around equals""" 385 | 386 | def __init__(self): 387 | self.sep = '' 388 | 389 | 390 | class PrintLines(Print): 391 | """Use for lines output format.""" 392 | 393 | def name_value(self, name, value, section=None): 394 | # Both unambiguous and easily parseable by shell. Caveat is 395 | # that sections and values with spaces are awkward to split in shell 396 | if section: 397 | line = '[ %s ]' % section 398 | if name: 399 | line += ' ' 400 | if name: 401 | line += '%s' % name 402 | if value == 'crudini_no_arg': 403 | value = '' 404 | if value: 405 | line += ' = %s' % value.replace('\n', '\\n') 406 | print(line) 407 | 408 | 409 | class PrintSh(Print): 410 | """Use for shell output format.""" 411 | 412 | @staticmethod 413 | def _valid_sh_identifier( 414 | i, 415 | safe_chars=frozenset(string.ascii_letters + string.digits + '_') 416 | ): 417 | """Provide validation of the output identifiers as it's dangerous to 418 | leave validation to shell. Consider for example doing eval on this in 419 | shell: rm -Rf /;oops=val 420 | 421 | :param i: str 422 | :param sh_safe_id_chars: frozenset 423 | :return: bool 424 | """ 425 | 426 | if i[0] in string.digits: 427 | return False 428 | for c in i: 429 | if c not in safe_chars: 430 | return False 431 | return True 432 | 433 | def name_value(self, name, value, section=None): 434 | if section and name: 435 | identifier = "%s_%s" % (section, name) 436 | else: 437 | identifier = name 438 | if not PrintSh._valid_sh_identifier(identifier): 439 | error('Invalid sh identifier "%s"' % identifier) 440 | sys.exit(1) 441 | if value == 'crudini_no_arg': 442 | value = '' 443 | sys.stdout.write("%s=%s\n" % (identifier, pipes.quote(value))) 444 | 445 | 446 | class Crudini(): 447 | mode = fmt = update = iniopt = inplace = cfgfile = output = section = \ 448 | param = value = vlist = listsep = verbose = None 449 | 450 | locked_file = None 451 | section_explicit_default = None 452 | data = None 453 | conf = None 454 | added_default_section = False 455 | default_adjust = False 456 | removed_section = False 457 | ini_section_blanks = [] 458 | _print = None 459 | 460 | # The following exits cleanly on Ctrl-C, 461 | # while treating other exceptions as before. 462 | @staticmethod 463 | def cli_exception(type, value, tb): 464 | if not issubclass(type, KeyboardInterrupt): 465 | sys.__excepthook__(type, value, tb) 466 | 467 | @staticmethod 468 | @contextlib.contextmanager 469 | def remove_file_on_error(path): 470 | """Protect code that wants to operate on PATH atomically. 471 | Any exception will cause PATH to be removed. 472 | """ 473 | try: 474 | yield 475 | except Exception: 476 | t, v, tb = sys.exc_info() 477 | delete_if_exists(path) 478 | raise t(v).with_traceback(tb) 479 | 480 | @staticmethod 481 | def file_replace(name, data): 482 | """Replace file as atomically as possible, 483 | fulfilling and AC properties of ACID. 484 | This is essentially using method 9 from: 485 | http://www.pixelbeat.org/docs/unix_file_replacement.html 486 | 487 | Caveats: 488 | - Changes ownership of the file being edited 489 | by non root users (due to POSIX interface limitations). 490 | - Loses any extended attributes of the original file 491 | (due to the simplicity of this implementation). 492 | - Existing hardlinks will be separated from the 493 | newly replaced file. 494 | - Ignores the write permissions of the original file. 495 | - Requires write permission on the directory as well as the file. 496 | - With python2 on windows we don't fulfil the A ACID property. 497 | 498 | To avoid the above caveats see the --inplace option. 499 | """ 500 | (f, tmp) = tempfile.mkstemp(".tmp", prefix=name + ".", dir=".") 501 | 502 | with Crudini.remove_file_on_error(tmp): 503 | shutil.copystat(name, tmp) 504 | 505 | if hasattr(os, 'fchown') and os.geteuid() == 0: 506 | st = os.stat(name) 507 | os.fchown(f, st.st_uid, st.st_gid) 508 | 509 | os.write(f, bytearray(data, file_encoding)) 510 | 511 | # We assume the existing file is persisted, 512 | # so sync here to ensure new data is persisted 513 | # before referencing it. Otherwise the metadata could 514 | # be written first, referencing the new data, which 515 | # would be nothing if a crash occured before the 516 | # data was allocated/persisted. 517 | os.fsync(f) 518 | os.close(f) 519 | 520 | if hasattr(os, 'replace'): # >= python 3.3 521 | os.replace(tmp, name) # atomic even on windows 522 | elif os.name == 'posix': 523 | os.rename(tmp, name) # atomic on POSIX 524 | else: 525 | backup = tmp + '.backup' 526 | os.rename(name, backup) 527 | os.rename(tmp, name) 528 | delete_if_exists(backup) 529 | 530 | # Sync out the new directory entry to provide 531 | # better durability that the new inode is referenced 532 | # rather than continuing to reference the old inode. 533 | # This also provides verification in exit status that 534 | # this update completes. 535 | if os.name != 'nt': 536 | O_DIRECTORY = 0 537 | if hasattr(os, 'O_DIRECTORY'): 538 | O_DIRECTORY = os.O_DIRECTORY 539 | dirfd = os.open(os.path.dirname(name) or '.', O_DIRECTORY) 540 | os.fsync(dirfd) 541 | os.close(dirfd) 542 | 543 | @staticmethod 544 | def file_rewrite(name, data): 545 | """Rewrite file inplace avoiding the caveats 546 | noted in file_replace(). 547 | 548 | Caveats: 549 | - Not Atomic as readers may see incomplete data for a while. 550 | - Not Consistent as multiple writers may overlap. 551 | - Less Durable as existing data truncated before I/O completes. 552 | - Requires write access to file rather than write access to dir. 553 | """ 554 | 555 | with open(name, 'wb') as f: 556 | f.write(bytearray(data, file_encoding)) 557 | f.flush() 558 | os.fsync(f.fileno()) 559 | 560 | @staticmethod 561 | def init_iniparse_defaultsect(): 562 | try: 563 | iniparse.DEFAULTSECT 564 | except AttributeError: 565 | iniparse.DEFAULTSECT = 'DEFAULT' 566 | 567 | # TODO item should be items and split also 568 | # especially in merge mode 569 | @staticmethod 570 | def update_list(curr_val, item, mode, sep): 571 | curr_items = [] 572 | use_space = True # Perhaps have 'nospace' set this default? 573 | if curr_val and curr_val != 'crudini_no_arg': 574 | if sep is None: # Default to comma separated 575 | use_space = ' ' in curr_val or ',' not in curr_val 576 | curr_items = [v.strip() for v in curr_val.split(",")] 577 | elif sep == '': # Empty means whitespace separated 578 | curr_items = curr_val.split(None) 579 | 580 | # Find first run of whitespace to maintain current delimiter 581 | whitespace_re = re.compile(r'\S*(\s+)') 582 | first_whitespace = whitespace_re.match(curr_val) 583 | if first_whitespace: 584 | sep = first_whitespace.group(1) 585 | else: 586 | sep = ' ' 587 | 588 | # Maintain empty `param =` line if present 589 | if sep == '\n' or sep == '\r\n': 590 | if curr_val.startswith(sep): 591 | curr_items.insert(0, '') 592 | else: 593 | curr_items = curr_val.split(sep) 594 | 595 | if mode == "--set": 596 | if item not in curr_items: 597 | curr_items.append(item) 598 | elif mode == "--del": 599 | try: 600 | curr_items.remove(item) 601 | except ValueError: 602 | pass 603 | 604 | if sep is None: 605 | sep = "," 606 | if use_space: 607 | sep += " " 608 | 609 | return sep.join(curr_items) 610 | 611 | def usage(self, exitval=0): 612 | cmd = os.path.basename(sys.argv[0]) 613 | if exitval or file_is_closed(sys.stdout): 614 | output = sys.stderr 615 | else: 616 | output = sys.stdout 617 | output.write("""\ 618 | A utility for manipulating ini files 619 | 620 | Usage: %s --set [OPTION]... config_file section [param] [value] 621 | or: %s --get [OPTION]... config_file [section] [param] 622 | or: %s --del [OPTION]... config_file section [param] [list value] 623 | or: %s --merge [OPTION]... config_file [section] 624 | 625 | SECTION can be empty ("") or "DEFAULT" in which case, 626 | params not in a section, i.e. global parameters are operated on. 627 | If 'DEFAULT' is used with --set, an explicit [DEFAULT] section is added. 628 | 629 | Multiple --set|--del|--get operations for a config_file can be specified. 630 | 631 | Options: 632 | 633 | --existing[=WHAT] For --set, --del and --merge, fail if item is missing, 634 | where WHAT is 'file', 'section', or 'param', 635 | or if WHAT not specified; all specified items. 636 | --format=FMT For --get, select the output FMT. 637 | Formats are 'sh','ini','lines' 638 | --ini-options=OPT Set options for handling ini files. Options are: 639 | 'nospace': use format name=value not name = value 640 | 'space': ensure name = value format 641 | 'sectionspace': ensure one blank line between sections 642 | 'ignoreindent': ignore leading whitespace 643 | --inplace Lock and write files in place. 644 | This is not atomic but has less restrictions 645 | than the default replacement method. 646 | --list For --set and --del, update a list (set) of values 647 | --list-sep=STR Delimit list values with \"STR\" instead of \" ,\". 648 | An empty STR means any whitespace is a delimiter. 649 | --output=FILE Write output to FILE instead. '-' means stdout 650 | --verbose Indicate on stderr if changes were made 651 | --help Write this help to stdout 652 | --version Write version to stdout 653 | """ % (cmd, cmd, cmd, cmd) 654 | ) 655 | sys.exit(exitval) 656 | 657 | def set_operation(self, operation): 658 | self.mode = None 659 | self.cfgfile = self.section = self.param = self.value = None 660 | try: 661 | self.mode = operation[0] 662 | self.cfgfile = operation[1] 663 | # Convert the following to unicode as 664 | # we process in unicode explicitly in python2. 665 | # Not needed on python3 where all strings are unicode. 666 | self.section = s2u(operation[2]) 667 | self.param = s2u(operation[3]) 668 | self.value = s2u(operation[4]) 669 | except IndexError: 670 | pass 671 | 672 | def parse_options(self): 673 | 674 | # Handle optional arg to long option 675 | # The gettopt module should really support this 676 | for i, opt in enumerate(sys.argv): 677 | if opt == '--existing': 678 | sys.argv[i] = '--existing=' 679 | elif opt == '--': 680 | break 681 | 682 | long_options = [ 683 | 'del', 684 | 'existing=', 685 | 'format=', 686 | 'get', 687 | 'help', 688 | 'ini-options=', 689 | 'inplace', 690 | 'list', 691 | 'list-sep=', 692 | 'merge', 693 | 'output=', 694 | 'set', 695 | 'verbose', 696 | 'version' 697 | ] 698 | 699 | # Group args into options and operations 700 | options = [] 701 | operations = [] 702 | next_is_option_param = False 703 | for i, opt in enumerate(sys.argv[1:]): 704 | if next_is_option_param: 705 | options.append(opt) 706 | next_is_option_param = False 707 | elif opt in ('--get', '--set', '--del', '--merge'): 708 | operations.append([opt]) 709 | elif opt == '--': 710 | if operations: 711 | operations[-1].extend(sys.argv[i+2:]) 712 | # else discard as was done before multi operation support 713 | break 714 | elif opt.startswith('--'): 715 | options.append(opt) 716 | if '=' not in opt and opt[2:]+'=' in long_options: 717 | next_is_option_param = True 718 | else: 719 | if operations: 720 | operations[-1].append(opt) 721 | else: 722 | error('Unknown operation: %s' % opt) 723 | break 724 | 725 | try: 726 | opts, args = getopt.getopt(options, '', long_options) 727 | except getopt.GetoptError as e: 728 | error(str(e)) 729 | self.usage(1) 730 | 731 | self.iniopt = () 732 | for o, a in opts: 733 | if o in ('--help',): 734 | self.usage(0) 735 | elif o in ('--version',): 736 | print('crudini 0.9.6') 737 | sys.exit(0) 738 | elif o in ('--verbose',): 739 | self.verbose = True 740 | elif o in ('--format',): 741 | self.fmt = a 742 | if self.fmt not in ('sh', 'ini', 'lines'): 743 | error('--format not recognized: %s' % self.fmt) 744 | self.usage(1) 745 | elif o in ('--ini-options',): 746 | self.iniopt = a.split(',') 747 | for opt in self.iniopt: 748 | if opt not in ('', 'nospace', 'space', 'sectionspace', 749 | 'ignoreindent'): 750 | error('--ini-options not recognized: %s' % opt) 751 | self.usage(1) 752 | if 'nospace' in self.iniopt and 'space' in self.iniopt: 753 | error('--ini-options=space,nospace are mutually exclusive') 754 | sys.exit(1) 755 | elif o in ('--existing',): 756 | self.update = a or 'param' # 'param' implies all must exist 757 | if self.update not in ('file', 'section', 'param'): 758 | error('--existing item not recognized: %s' % self.update) 759 | self.usage(1) 760 | elif o in ('--inplace',): 761 | self.inplace = True 762 | elif o in ('--list',): 763 | self.vlist = "set" # TODO support combos of list, sorted, ... 764 | elif o in ('--list-sep',): 765 | self.listsep = a 766 | elif o in ('--output',): 767 | self.output = a 768 | 769 | if not operations: 770 | error('One of --set|--del|--get|--merge must be specified') 771 | self.usage(1) 772 | 773 | if self.fmt == 'lines': 774 | self._print = PrintLines() 775 | elif self.fmt == 'sh': 776 | self._print = PrintSh() 777 | elif self.fmt == 'ini': 778 | if 'nospace' in self.iniopt: 779 | self._print = PrintIniNoSpace() 780 | else: 781 | self._print = PrintIni() 782 | else: 783 | self._print = Print() 784 | 785 | # Validate all operations combinations 786 | for o in operations: 787 | if self.mode and self.mode != o[0]: 788 | mixable = ('--set', '--del') 789 | if self.mode not in mixable or o[0] not in mixable: 790 | error("--get|--merge modes can't be mixed" 791 | "with --set|--del") 792 | self.usage(1) 793 | elif self.mode == '--merge': 794 | error("--merge mode can't be repeated") 795 | self.usage(1) 796 | 797 | if self.cfgfile and len(o) > 1 and self.cfgfile != o[1]: 798 | error("Can't operate on multiple files") 799 | self.usage(1) 800 | 801 | self.set_operation(o) 802 | 803 | if self.cfgfile is None: 804 | self.usage(1) 805 | if self.section is None and self.mode in ('--del', '--set'): 806 | self.usage(1) 807 | if self.param is not None and self.mode in ('--merge',): 808 | self.usage(1) 809 | if self.value is not None and self.mode not in ('--set',): 810 | if not (self.mode == '--del' and self.vlist): 811 | error('A value should not be specified with %s' 812 | % self.mode) 813 | self.usage(1) 814 | 815 | # Convert secion '' to 'DEFAULT', 816 | # ensuring no conflicting DEFAULT section specs 817 | if self.section_explicit_default is None: 818 | if self.section == '': 819 | o[2] = self.section = iniparse.DEFAULTSECT 820 | self.section_explicit_default = False 821 | elif self.section == iniparse.DEFAULTSECT: 822 | self.section_explicit_default = True 823 | elif self.section is not None: 824 | if ((self.section == '' and self.section_explicit_default) 825 | or (self.section == iniparse.DEFAULTSECT 826 | and not self.section_explicit_default)): 827 | error("Conflicting %s section specifications" % 828 | iniparse.DEFAULTSECT) 829 | sys.exit(1) 830 | elif self.section == '': 831 | o[2] = self.section = iniparse.DEFAULTSECT 832 | 833 | if self.mode == '--merge' and self.fmt == 'sh': 834 | # I'm not sure how useful is is to support this. 835 | # printenv will already generate a mostly compat ini format. 836 | # If you want to also include non exported vars (from `set`), 837 | # then there is a format change. 838 | error('sh format input is not supported at present') 839 | sys.exit(1) 840 | 841 | # Protect against generating non parseable ini files 842 | if self.section and ('[' in self.section or ']' in self.section): 843 | error("section names should not contain '[' or ']': %s" % 844 | self.section) 845 | sys.exit(1) 846 | if self.param and self.param.startswith('['): 847 | error("param names should not start with '[': %s" % self.param) 848 | sys.exit(1) 849 | 850 | # A "param=with=equals = value" line can not be found with --get 851 | # so avoid the ambiguity. Note this precludes the "nospace" hack 852 | # in https://github.com/pixelb/crudini/issues/33#issuecomment-\ 853 | # 1151253988 854 | if self.param and '=' in self.param: 855 | error("param names should not contain '=': %s" % self.param) 856 | if 'nospace' not in self.iniopt: 857 | error("Use --ini-options=nospace if you want that format") 858 | sys.exit(1) 859 | 860 | if self.section_explicit_default is None: 861 | self.section_explicit_default = False 862 | 863 | if not self.output: 864 | self.output = self.cfgfile 865 | 866 | if file_is_closed(sys.stdout) \ 867 | and (self.output == '-' or self.mode == '--get'): 868 | error("stdout is closed") 869 | sys.exit(1) 870 | 871 | return operations 872 | 873 | def _has_default_section(self): 874 | fp = io.StringIO(self.data) 875 | for line in fp: 876 | if line.startswith('[%s]' % iniparse.DEFAULTSECT): 877 | return True 878 | return False 879 | 880 | def _chksum(self, data): 881 | h = hashlib.sha256() 882 | h.update(bytearray(data, file_encoding)) 883 | return h.digest() 884 | 885 | def _parse_file(self, filename, add_default=False, preserve_case=False): 886 | try: 887 | if self.data is None: 888 | # Read all data up front as this is done by iniparse anyway 889 | # Doing it here will avoid rereads on reparse and support 890 | # correct parsing of stdin 891 | if filename == '-': 892 | ifp = os.fdopen(sys.stdin.fileno(), 'rb') 893 | else: 894 | ifp = self.locked_file.fp 895 | 896 | _data = ifp.read() 897 | 898 | global file_encoding 899 | # latin1 should work for all files as a fallback. 900 | for file_encoding in ('utf-8', env_encoding(), 'latin1'): 901 | try: 902 | self.data = _data.decode(file_encoding) 903 | except UnicodeDecodeError: 904 | continue 905 | else: 906 | break 907 | if self.mode != '--get': 908 | # compare checksums to flag any changes 909 | # (even spacing or case adjustments) with --verbose, 910 | # and to avoid rewriting the file if not necessary 911 | self.chksum = self._chksum(self.data) 912 | 913 | if self.data.startswith('\n'): 914 | self.newline_at_start = True 915 | else: 916 | self.newline_at_start = False 917 | 918 | # newline='' =-> don't convert line endings 919 | fp = io.StringIO(self.data, newline='') 920 | if add_default: 921 | fp = AddDefaultSection(fp, self.iniopt) 922 | else: 923 | fp = CrudiniInputFilter(fp, self.iniopt) 924 | 925 | conf = CrudiniConfigParser(preserve_case=preserve_case, 926 | space_around_delimiters=( 927 | 'nospace' not in self.iniopt)) 928 | conf.readfp(fp) 929 | self.crudini_no_arg = fp.crudini_no_arg 930 | self.indented = fp.indented 931 | self.replace_leading = fp.replace_leading 932 | self.section_indents = fp.section_indents 933 | self.windows_eol = fp.windows_eol 934 | self.bom = fp.bom 935 | return conf 936 | except EnvironmentError as e: 937 | error(str(e)) 938 | sys.exit(1) 939 | 940 | def parse_file(self, filename, preserve_case=False): 941 | self.added_default_section = False 942 | self.data = None 943 | 944 | if filename != '-': 945 | self.locked_file = LockedFile(filename, self.mode, self.inplace, 946 | not self.update) 947 | elif file_is_closed(sys.stdin): 948 | error("stdin is closed") 949 | sys.exit(1) 950 | 951 | try: 952 | conf = self._parse_file(filename, preserve_case=preserve_case) 953 | 954 | if not conf.items(iniparse.DEFAULTSECT): 955 | # Check if there is just [DEFAULT] in a file with no 956 | # name=values to avoid adding a duplicate section. 957 | if not self._has_default_section(): 958 | # reparse with inserted [DEFAULT] to be able to add global 959 | # opts etc. 960 | conf = self._parse_file( 961 | filename, 962 | add_default=True, 963 | preserve_case=preserve_case 964 | ) 965 | self.added_default_section = True 966 | 967 | except configparser.MissingSectionHeaderError: 968 | conf = self._parse_file( 969 | filename, 970 | add_default=True, 971 | preserve_case=preserve_case 972 | ) 973 | self.added_default_section = True 974 | except configparser.ParsingError as e: 975 | error(str(e)) 976 | sys.exit(1) 977 | 978 | self.data = None 979 | return conf 980 | 981 | def set_name_value(self, section, param, value): 982 | curr_val = None 983 | ignore_indent = 'ignoreindent' in self.iniopt 984 | 985 | # Since indents stripped on read, ensure no ambiguities 986 | # Also allow to set default indent on params with explicit indent 987 | # We don't support this for sections as in all cases they 988 | # can have [ leading spaces] in their names. TODO: Perhaps should 989 | # support specifying ' [spaces before brackets]' for sections? 990 | if ignore_indent and param: 991 | stripped_param = param.lstrip() 992 | current_indent = self.section_indents.get(section or 'DEFAULT') 993 | if not current_indent: 994 | leading_ws = param[:len(param)-len(stripped_param)] 995 | if leading_ws: 996 | self.indented = True 997 | self.section_indents[section] = leading_ws 998 | param = stripped_param 999 | 1000 | if self.update in ('param', 'section'): 1001 | if param is None: 1002 | if not ( 1003 | section == iniparse.DEFAULTSECT or 1004 | self.conf.has_section(section) 1005 | ): 1006 | raise configparser.NoSectionError(section) 1007 | else: 1008 | try: 1009 | curr_val = self.conf.get(section, param) 1010 | except configparser.NoSectionError: 1011 | if self.update == 'section': 1012 | raise 1013 | except configparser.NoOptionError: 1014 | if self.update == 'param': 1015 | raise 1016 | elif (section != iniparse.DEFAULTSECT and 1017 | not self.conf.has_section(section)): 1018 | if self.mode == "--del": 1019 | return 1020 | else: 1021 | # Adjust to allow adding a "default" section (issue #80) 1022 | skip_section_add = False 1023 | if section.lower() == "default": 1024 | section = "crudini_default_adjust_%s" % section 1025 | self.default_adjust = True 1026 | if self.conf.has_section(section): # We already added 1027 | skip_section_add = True 1028 | 1029 | # Note this always adds a '\n' before the section name 1030 | # resulting in double spaced sections or blank line at 1031 | # the start of a new file to which a new section is added. 1032 | # List the sections here to adjust when writing. 1033 | if not skip_section_add: 1034 | self.ini_section_blanks.append(section) 1035 | self.conf.add_section(section) 1036 | 1037 | if param is not None: 1038 | try: 1039 | curr_val = self.conf.get(section, param) 1040 | except configparser.NoOptionError: 1041 | if self.mode == "--del": 1042 | if self.update not in ('param', 'section'): 1043 | return 1044 | 1045 | if value is None: 1046 | # Unspecified param should clear list. This will also force 1047 | # existing param "flags" or new params to use '=' delimiter. 1048 | if self.vlist: 1049 | curr_val = '' 1050 | 1051 | if curr_val == 'crudini_no_arg': 1052 | # param already exists without delimiter 1053 | return 1054 | elif curr_val is None and self.crudini_no_arg: 1055 | # some params exist without delimiter 1056 | # so default new param to not use one 1057 | value = 'crudini_no_arg' 1058 | else: 1059 | # Otherwise use a delimeter 1060 | value = '' 1061 | 1062 | # Add a default indent through an inline comment, to later replace 1063 | section_indent = self.section_indents.get(section) 1064 | if curr_val is None and ignore_indent and section_indent: 1065 | value += ' ;crudini_indent>%s<' % section_indent 1066 | 1067 | if self.vlist: 1068 | value = self.update_list( 1069 | curr_val, 1070 | value, 1071 | self.mode, 1072 | self.listsep 1073 | ) 1074 | self.conf.set(section, param, value) 1075 | 1076 | def command_set(self): 1077 | """Insert a section/parameter.""" 1078 | 1079 | self.set_name_value(self.section, self.param, self.value) 1080 | 1081 | def command_merge(self): 1082 | """Merge an ini file from another ini.""" 1083 | 1084 | for msection in [iniparse.DEFAULTSECT] + self.mconf.sections(): 1085 | if msection == iniparse.DEFAULTSECT: 1086 | defaults_to_strip = {} 1087 | else: 1088 | defaults_to_strip = self.mconf.defaults() 1089 | items = self.mconf.items(msection) 1090 | set_param = False 1091 | for item in items: 1092 | # XXX: Note this doesn't update an item in section 1093 | # if matching value also in default (global) section. 1094 | if defaults_to_strip.get(item[0]) != item[1]: 1095 | ignore_errs = (configparser.NoOptionError,) 1096 | if self.section is not None: 1097 | msection = self.section 1098 | elif self.update not in ('param', 'section'): 1099 | ignore_errs += (configparser.NoSectionError,) 1100 | try: 1101 | set_param = True 1102 | self.set_name_value(msection, item[0], item[1]) 1103 | except ignore_errs: 1104 | pass 1105 | # For empty sections ensure the section header is added 1106 | if not set_param and self.section is None: 1107 | self.set_name_value(msection, None, None) 1108 | 1109 | def command_del(self): 1110 | """Delete a section/parameter.""" 1111 | 1112 | if self.param is None: 1113 | if self.section == iniparse.DEFAULTSECT: 1114 | for name in self.conf.defaults(): 1115 | self.conf.remove_option(iniparse.DEFAULTSECT, name) 1116 | else: 1117 | if not self.conf.remove_section(self.section): 1118 | if self.update in ('param', 'section'): 1119 | raise configparser.NoSectionError(self.section) 1120 | else: 1121 | self.removed_section = True 1122 | elif self.value is None: 1123 | try: 1124 | if not self.conf.remove_option(self.section, self.param) \ 1125 | and self.update == 'param': 1126 | raise configparser.NoOptionError(self.section, self.param) 1127 | except configparser.NoSectionError: 1128 | if self.update in ('param', 'section'): 1129 | raise 1130 | else: # remove item from list 1131 | self.set_name_value(self.section, self.param, self.value) 1132 | 1133 | def command_get(self): 1134 | """Output a section/parameter""" 1135 | 1136 | if self.fmt != 'lines' and self.fmt != 'sh': 1137 | if self.section is None: 1138 | if self.conf.defaults(): 1139 | self._print.section_header(iniparse.DEFAULTSECT) 1140 | for item in self.conf.sections(): 1141 | self._print.section_header(item) 1142 | elif self.param is None: 1143 | if self.fmt == 'ini': 1144 | self._print.section_header(self.section) 1145 | if self.section == iniparse.DEFAULTSECT: 1146 | defaults_to_strip = {} 1147 | else: 1148 | defaults_to_strip = self.conf.defaults() 1149 | for item in self.conf.items(self.section): 1150 | # XXX: Note this strips an item from section 1151 | # if matching value also in default (global) section. 1152 | if defaults_to_strip.get(item[0]) != item[1]: 1153 | if self.fmt: 1154 | val = item[1] 1155 | else: 1156 | val = None 1157 | self._print.name_value(item[0], val) 1158 | else: 1159 | val = self.conf.get(self.section, self.param) 1160 | if self.fmt: 1161 | name = self.param 1162 | else: 1163 | name = None 1164 | self._print.name_value(name, val) 1165 | else: 1166 | if self.section is None: 1167 | sections = self.conf.sections() 1168 | if self.conf.defaults(): 1169 | sections.insert(0, iniparse.DEFAULTSECT) 1170 | else: 1171 | sections = (self.section,) 1172 | if self.param is not None: 1173 | val = self.conf.get(self.section, self.param) 1174 | print_section = self.section 1175 | if self.fmt == 'sh': 1176 | print_section = None 1177 | self._print.name_value(self.param, val, print_section) 1178 | else: 1179 | for section in sections: 1180 | print_section = section 1181 | if self.fmt == 'sh': 1182 | if self.section or section == iniparse.DEFAULTSECT: 1183 | print_section = None 1184 | if section == iniparse.DEFAULTSECT: 1185 | defaults_to_strip = {} 1186 | else: 1187 | defaults_to_strip = self.conf.defaults() 1188 | items = False 1189 | for item in self.conf.items(section): 1190 | # XXX: Note this strips an item from section 1191 | # if matching value also in default (global) section. 1192 | if defaults_to_strip.get(item[0]) != item[1]: 1193 | val = item[1] 1194 | self._print.name_value(item[0], val, print_section) 1195 | items = True 1196 | if not items and self.fmt != 'sh': 1197 | self._print.name_value(None, None, print_section) 1198 | 1199 | def run(self): 1200 | if not file_is_closed(sys.stdin) and sys.stdin.isatty(): 1201 | sys.excepthook = Crudini.cli_exception 1202 | 1203 | Crudini.init_iniparse_defaultsect() 1204 | operations = self.parse_options() 1205 | 1206 | # --set takes precedence to create file etc. 1207 | if self.mode == '--del': 1208 | for o in operations: 1209 | if o[0] == '--set': 1210 | self.mode = '--set' 1211 | break 1212 | 1213 | if self.mode == '--merge': 1214 | self.mconf = self.parse_file('-', preserve_case=True) 1215 | 1216 | self.madded_default_section = self.added_default_section 1217 | 1218 | try: 1219 | if self.mode == '--get' and self.param is None: 1220 | # Maintain case when outputting params. 1221 | # Note sections are handled case sensitively 1222 | # even if optionxform is not set. 1223 | preserve_case = True 1224 | else: 1225 | preserve_case = False 1226 | self.conf = self.parse_file(self.cfgfile, 1227 | preserve_case=preserve_case) 1228 | 1229 | # Take the [DEFAULT] header from the input if present 1230 | if ( 1231 | self.mode == '--merge' and 1232 | self.update not in ('param', 'section') and 1233 | not self.madded_default_section and 1234 | self.mconf.items(iniparse.DEFAULTSECT) 1235 | ): 1236 | self.added_default_section = self.madded_default_section 1237 | 1238 | for o in operations: 1239 | self.set_operation(o) 1240 | 1241 | if self.mode == '--set': 1242 | self.command_set() 1243 | elif self.mode == '--merge': 1244 | self.command_merge() 1245 | elif self.mode == '--del': 1246 | self.command_del() 1247 | elif self.mode == '--get': 1248 | self.command_get() 1249 | 1250 | if self.mode != '--get': 1251 | # Del possible extraneous blank line left with removed section 1252 | # XXX: This may collapse existing multiple blank lines 1253 | if self.removed_section and 'sectionspace' not in self.iniopt: 1254 | iniparse.tidy(self.conf) 1255 | 1256 | # XXX: Ideally we should just do conf.write(f) here, but to 1257 | # avoid iniparse issues, we massage the data a little here 1258 | if sys.version_info[0] >= 3: 1259 | str_data = str(self.conf.data) 1260 | else: 1261 | # XXX: Can't use uc() here as can't specify encoding 1262 | str_data = unicode(self.conf.data) 1263 | if len(str_data) and str_data[-1] != '\n': 1264 | str_data += '\n' 1265 | 1266 | if ( 1267 | ( 1268 | self.added_default_section and 1269 | not ( 1270 | self.section_explicit_default and 1271 | self.mode in ('--set', '--merge') 1272 | ) 1273 | ) or 1274 | ( 1275 | self.mode == '--del' and 1276 | self.section == iniparse.DEFAULTSECT and 1277 | self.param is None 1278 | ) 1279 | ): 1280 | # See note at add_section() call above detailing 1281 | # where this extra \n comes from that we handle 1282 | # here for the edge case of new files. 1283 | default_sect = '[%s]\n' % iniparse.DEFAULTSECT 1284 | if not self.newline_at_start and \ 1285 | str_data.startswith(default_sect + '\n'): 1286 | str_data = str_data[len(default_sect) + 1:] 1287 | else: 1288 | str_data = str_data.replace(default_sect, '', 1) 1289 | 1290 | # Handle creation of non special "default" section 1291 | if self.default_adjust: 1292 | str_data = str_data.replace('crudini_default_adjust_', '') 1293 | 1294 | # Process blank lines around sections 1295 | if 'sectionspace' in self.iniopt: 1296 | # Ensure a single blank line before sections 1297 | str_data = re.sub(r'\n*(\[[^\]]+\])', r'\n\n\1', str_data) 1298 | str_data = str_data.lstrip('\n') # remove leading \n 1299 | str_data = str_data.rstrip('\n') + '\n' # ensure \n at end 1300 | else: 1301 | # Remove extraneous blanks iniparse adds when adding sects 1302 | for section in self.ini_section_blanks: 1303 | section_ = '\n[%s]\n' % section 1304 | str_data = str_data.replace(section_, section_[1:], 1) 1305 | 1306 | if self.windows_eol: 1307 | # iniparse uses '\n' for new/updated items 1308 | # so reset all to windows format 1309 | str_data = str_data.replace('\r\n', '\n') 1310 | str_data = str_data.replace('\n', '\r\n') 1311 | 1312 | if self.indented: 1313 | str_data = self.replace_leading.sub(r'\2\1', str_data) 1314 | 1315 | if self.crudini_no_arg: 1316 | spacing = '' if 'nospace' in self.iniopt else ' ' 1317 | str_data = str_data.replace('%s=%scrudini_no_arg' % 1318 | (spacing, spacing), '') 1319 | 1320 | if self.bom: 1321 | str_data = u'\ufeff%s' % str_data 1322 | 1323 | changed = self.chksum != self._chksum(str_data) 1324 | 1325 | if self.output == '-': 1326 | sys.stdout.write(str_data) 1327 | elif changed: 1328 | if os.name == 'nt': 1329 | # Close input file as Windows gives access errors on 1330 | # open files. For e.g. see caveats noted at: 1331 | # https://bugs.python.org/issue46003 1332 | self.locked_file.delete() 1333 | 1334 | if self.inplace: 1335 | self.file_rewrite(self.output, str_data) 1336 | else: 1337 | self.file_replace(os.path.realpath(self.output), 1338 | str_data) 1339 | 1340 | if self.verbose: 1341 | def quote_val(val): 1342 | return pipes.quote(val).replace('\n', '\\n') 1343 | what = ' '.join(map(quote_val, 1344 | list(filter(bool, 1345 | [self.mode, self.cfgfile, 1346 | self.section, self.param, 1347 | self.value])))) 1348 | sys.stderr.write('%s: %s\n' % 1349 | (('unchanged', 'changed')[changed], what)) 1350 | 1351 | # Finish writing now to consistently handle errors here 1352 | # (and while excepthook is set) 1353 | if not file_is_closed(sys.stdout): 1354 | sys.stdout.flush() 1355 | except configparser.ParsingError as e: 1356 | error('Error parsing %s: %s' % (self.cfgfile, e.message)) 1357 | sys.exit(1) 1358 | except configparser.NoSectionError as e: 1359 | error('Section not found: %s' % e.section) 1360 | sys.exit(1) 1361 | except configparser.NoOptionError: 1362 | error('Parameter not found: %s' % self.param) 1363 | sys.exit(1) 1364 | except EnvironmentError as e: 1365 | # Handle EPIPE as python 2 doesn't catch SIGPIPE 1366 | if e.errno != errno.EPIPE: 1367 | error(str(e)) 1368 | sys.exit(1) 1369 | # Python3 fix for exception on exit: 1370 | # https://docs.python.org/3/library/signal.html#note-on-sigpipe 1371 | if not file_is_closed(sys.stdout): 1372 | nullf = os.open(os.devnull, os.O_WRONLY) 1373 | os.dup2(nullf, sys.stdout.fileno()) 1374 | 1375 | 1376 | def main(): 1377 | crudini = Crudini() 1378 | return crudini.run() 1379 | 1380 | 1381 | if __name__ == "__main__": 1382 | sys.exit(main()) 1383 | -------------------------------------------------------------------------------- /example.ini: -------------------------------------------------------------------------------- 1 | global=supported 2 | 3 | [section1] 4 | dup1 = val1 5 | dup1 = val1 6 | dup2 = val1 7 | dup2 = val2 8 | nospace=val 9 | multiline = with 10 | leading 11 | space 12 | nmultiline=not supported with\ 13 | ;comment 14 | #comment 15 | comment_after1=val ;a comment 16 | comment_after2=val;not a comment 17 | comment_after3=val #not a comment 18 | escaped_not_processed=test \nescape 19 | colon:val ; supported also 20 | double_quotes = "not removed" 21 | single_quotes = 'not removed' 22 | spaces_stripped = val ; 23 | internal_not_stripped = v al ; 24 | notempty1= ;comment=val 25 | ;Note iniparse requires the = following empty 26 | empty= 27 | python_interpolate = %(dup1)s/blah 28 | interpolate2 = ${dup1}/blah 29 | Caps = not significant 30 | 31 | [section1] 32 | combine=sections 33 | 34 | [empty section] 35 | 36 | [non-sh-compat] 37 | space name=val 38 | útf8name=val 39 | 1num=val 40 | ls;name=val 41 | 42 | [list] 43 | list1 = v1, v2 44 | list2 = v1,v2 45 | list3 = 46 | v1 47 | v2 48 | -------------------------------------------------------------------------------- /noequals.ini: -------------------------------------------------------------------------------- 1 | # Differences from mysql.conf 2 | # #comments can't be at middle of line 3 | # single/double quotes are not stripped 4 | # leading spaces on line are not ignored 5 | 6 | !include directives treated as parameters 7 | 8 | [noequals] 9 | param1 10 | param2= 11 | param3 = 12 | colon1: 13 | colon2 : 14 | space param 15 | never #comment 16 | not ;comment 17 | multiline=val 18 | spaceval 19 | tabval 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = crudini 3 | author = Pádraig Brady 4 | author_email = P@draigBrady.com 5 | license = GPLv2 6 | version = 0.9.6 7 | description = A utility for manipulating ini files 8 | keywords = ini, config, edit 9 | url = http://github.com/pixelb/crudini 10 | long_description = file: README.md 11 | long_description_content_type = text/markdown 12 | classifiers = 13 | Development Status :: 5 - Production/Stable 14 | Topic :: Utilities 15 | Topic :: System :: Systems Administration 16 | License :: OSI Approved :: GNU General Public License v2 (GPLv2) 17 | Programming Language :: Python :: 2 18 | Programming Language :: Python :: 3 19 | 20 | [options] 21 | install_requires = iniparse>=0.5 22 | py_modules = crudini 23 | setup_requires = setuptools>=44; wheel; setuptools_scm[toml]>=3.4.3 24 | 25 | [bdist_wheel] 26 | universal = 1 27 | 28 | [options.entry_points] 29 | console_scripts = crudini = crudini:main 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup 4 | 5 | if __name__ == "__main__": 6 | setup() 7 | -------------------------------------------------------------------------------- /tests/example.lines: -------------------------------------------------------------------------------- 1 | [ DEFAULT ] global = supported 2 | [ section1 ] dup1 = val1 3 | [ section1 ] dup2 = val2 4 | [ section1 ] nospace = val 5 | [ section1 ] multiline = with\nleading\nspace 6 | [ section1 ] nmultiline = not supported with\ 7 | [ section1 ] comment_after1 = val 8 | [ section1 ] comment_after2 = val;not a comment 9 | [ section1 ] comment_after3 = val #not a comment 10 | [ section1 ] escaped_not_processed = test \nescape 11 | [ section1 ] colon = val 12 | [ section1 ] double_quotes = "not removed" 13 | [ section1 ] single_quotes = 'not removed' 14 | [ section1 ] spaces_stripped = val 15 | [ section1 ] internal_not_stripped = v al 16 | [ section1 ] notempty1 = ;comment=val 17 | [ section1 ] empty 18 | [ section1 ] python_interpolate = %(dup1)s/blah 19 | [ section1 ] interpolate2 = ${dup1}/blah 20 | [ section1 ] Caps = not significant 21 | [ section1 ] combine = sections 22 | [ empty section ] 23 | [ non-sh-compat ] space name = val 24 | [ non-sh-compat ] útf8name = val 25 | [ non-sh-compat ] 1num = val 26 | [ non-sh-compat ] ls;name = val 27 | [ list ] list1 = v1, v2 28 | [ list ] list2 = v1,v2 29 | [ list ] list3 = \nv1\nv2 30 | -------------------------------------------------------------------------------- /tests/nospace-in.ini: -------------------------------------------------------------------------------- 1 | #comment = ignore 2 | p1 = 1 3 | p2 = 2 = 2 4 | p3 : 3 = 3 5 | p 4 = 4 6 | p5 = 5 7 | p6 : 6 : 6 a 8 | multiline = ignore 9 | -------------------------------------------------------------------------------- /tests/nospace-out.ini: -------------------------------------------------------------------------------- 1 | #comment = ignore 2 | p1=1 3 | p2=2 = 2 4 | p3:3 = 3 5 | p 4=4 6 | p5=5 7 | p6:6 : 6 a 8 | multiline = ignore 9 | new=val 10 | -------------------------------------------------------------------------------- /tests/section1.ini: -------------------------------------------------------------------------------- 1 | [section1] 2 | dup1 = val1 3 | dup2 = val2 4 | nospace = val 5 | multiline = with 6 | leading 7 | space 8 | nmultiline = not supported with\ 9 | comment_after1 = val 10 | comment_after2 = val;not a comment 11 | comment_after3 = val #not a comment 12 | escaped_not_processed = test \nescape 13 | colon = val 14 | double_quotes = "not removed" 15 | single_quotes = 'not removed' 16 | spaces_stripped = val 17 | internal_not_stripped = v al 18 | notempty1 = ;comment=val 19 | empty = 20 | python_interpolate = %(dup1)s/blah 21 | interpolate2 = ${dup1}/blah 22 | Caps = not significant 23 | combine = sections 24 | -------------------------------------------------------------------------------- /tests/section1.lines: -------------------------------------------------------------------------------- 1 | [ section1 ] dup1 = val1 2 | [ section1 ] dup2 = val2 3 | [ section1 ] nospace = val 4 | [ section1 ] multiline = with\nleading\nspace 5 | [ section1 ] nmultiline = not supported with\ 6 | [ section1 ] comment_after1 = val 7 | [ section1 ] comment_after2 = val;not a comment 8 | [ section1 ] comment_after3 = val #not a comment 9 | [ section1 ] escaped_not_processed = test \nescape 10 | [ section1 ] colon = val 11 | [ section1 ] double_quotes = "not removed" 12 | [ section1 ] single_quotes = 'not removed' 13 | [ section1 ] spaces_stripped = val 14 | [ section1 ] internal_not_stripped = v al 15 | [ section1 ] notempty1 = ;comment=val 16 | [ section1 ] empty 17 | [ section1 ] python_interpolate = %(dup1)s/blah 18 | [ section1 ] interpolate2 = ${dup1}/blah 19 | [ section1 ] Caps = not significant 20 | [ section1 ] combine = sections 21 | -------------------------------------------------------------------------------- /tests/section1.sh: -------------------------------------------------------------------------------- 1 | dup1=val1 2 | dup2=val2 3 | nospace=val 4 | multiline='with 5 | leading 6 | space' 7 | nmultiline='not supported with\' 8 | comment_after1=val 9 | comment_after2='val;not a comment' 10 | comment_after3='val #not a comment' 11 | escaped_not_processed='test \nescape' 12 | colon=val 13 | double_quotes='"not removed"' 14 | single_quotes=''"'"'not removed'"'"'' 15 | spaces_stripped=val 16 | internal_not_stripped='v al' 17 | notempty1=';comment=val' 18 | empty='' 19 | python_interpolate='%(dup1)s/blah' 20 | interpolate2='${dup1}/blah' 21 | Caps='not significant' 22 | combine=sections 23 | -------------------------------------------------------------------------------- /tests/sections.sh: -------------------------------------------------------------------------------- 1 | section1_dup1=val1 2 | section1_dup2=val2 3 | section1_nospace=val 4 | section1_multiline='with 5 | leading 6 | space' 7 | section1_nmultiline='not supported with\' 8 | section1_comment_after1=val 9 | section1_comment_after2='val;not a comment' 10 | section1_comment_after3='val #not a comment' 11 | section1_escaped_not_processed='test \nescape' 12 | section1_colon=val 13 | section1_double_quotes='"not removed"' 14 | section1_single_quotes=''"'"'not removed'"'"'' 15 | section1_spaces_stripped=val 16 | section1_internal_not_stripped='v al' 17 | section1_notempty1=';comment=val' 18 | section1_empty='' 19 | section1_python_interpolate='%(dup1)s/blah' 20 | section1_interpolate2='${dup1}/blah' 21 | section1_Caps='not significant' 22 | section1_combine=sections 23 | -------------------------------------------------------------------------------- /tests/space-in.ini: -------------------------------------------------------------------------------- 1 | #comment=ignore 2 | p1=1 3 | p2=2=2 4 | p3:3=3 5 | p 4=4 6 | p5 = 5 7 | p6:6:6 a 8 | multiline=ignore 9 | -------------------------------------------------------------------------------- /tests/space-out.ini: -------------------------------------------------------------------------------- 1 | #comment=ignore 2 | p1 = 1 3 | p2 = 2=2 4 | p3 : 3=3 5 | p 4 = 4 6 | p5 = 5 7 | p6 : 6:6 a 8 | multiline=ignore 9 | new = val 10 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | trap "exit 130" INT 4 | cleanup() { rm -f err noequals*.ini test.ini ltest.ini good.ini example.ini; exit "$1"; } 5 | trap 'cleanup $?' EXIT 6 | 7 | crudini() { ../crudini.py "$@"; } 8 | 9 | test=0 10 | 11 | fail() { 12 | test=$(($test+1)) 13 | printf "Test $test \033[1;31mFAIL\033[m (line ${BASH_LINENO[0]})\n" 14 | exit 1 15 | } 16 | ok() { test=$(($test+1)); echo "Test $test OK (line ${BASH_LINENO[0]})"; } 17 | 18 | cp ../example.ini . 19 | 20 | # invalid params ---------------------------------------- 21 | 22 | :> test.ini 23 | crudini 2>/dev/null && fail 24 | crudini --met test.ini 2>/dev/null && fail # bad mode 25 | crudini --set 2>/dev/null && fail # no file 26 | crudini --set test.ini 2>/dev/null && fail # no section 27 | crudini --get 2>/dev/null && fail # no file 28 | crudini --get test.ini '' 'name' 'val' 2>/dev/null && fail # value 29 | crudini --get --format=bad test.ini 2>/dev/null && fail # bad format 30 | crudini --del 2>/dev/null && fail # no file 31 | crudini --del test.ini 2>/dev/null && fail # no section 32 | crudini --del test.ini '' 'name' 'val' 2>/dev/null && fail # value 33 | crudini --merge 2>/dev/null && fail # no file 34 | crudini --merge test.ini '' 'name' 2>/dev/null && fail # param 35 | crudini --del test.ini '' 'name' 'val' 2>/dev/null && fail # value 36 | crudini --get --format=ggg test.ini 2>&1 | grep -q 'format not recognized' || fail 37 | crudini --get test.ini 'DEFAULT' missing 2>&1 | grep -q 'Parameter not found' || fail 38 | ok 39 | 40 | # --set ------------------------------------------------- 41 | 42 | :> test.ini 43 | crudini --set test.ini '' name val 44 | printf '%s\n' 'name = val' > good.ini 45 | diff -u test.ini good.ini && ok || fail 46 | 47 | :> test.ini 48 | crudini --set test.ini DEFAULT name val 49 | printf '%s\n' '[DEFAULT]' 'name = val' > good.ini 50 | diff -u test.ini good.ini && ok || fail 51 | 52 | :> test.ini 53 | crudini --set test.ini nonDEFAULT name val 54 | printf '%s\n' '[nonDEFAULT]' 'name = val' > good.ini 55 | diff -u test.ini good.ini && ok || fail 56 | 57 | for bom in '' $'\xef\xbb\xbf'; do 58 | printf '%s%s\n' "$bom" 'global=val' > test.ini 59 | crudini --set test.ini '' global valnew 60 | printf '%s%s\n' "$bom" 'global=valnew' > good.ini 61 | diff -u test.ini good.ini && ok || fail 62 | 63 | printf '%s%s\n' "$bom" 'global=val' > test.ini 64 | crudini --set test.ini DEFAULT global valnew 65 | printf '%s%s\n' "$bom" '[DEFAULT]' 'global=valnew' > good.ini 66 | diff -u test.ini good.ini && ok || fail 67 | 68 | printf '%s%s\n' "$bom" '[DEFAULT]' 'global=val' > test.ini 69 | crudini --set test.ini DEFAULT global valnew 70 | printf '%s%s\n' "$bom" '[DEFAULT]' 'global=valnew' > good.ini 71 | diff -u test.ini good.ini && ok || fail 72 | 73 | printf '%s%s\n' "$bom" 'global=val' '' '[nonDEFAULT]' 'name=val' > test.ini 74 | crudini --set test.ini '' global valnew 75 | printf '%s%s\n' "$bom" 'global=valnew' '' '[nonDEFAULT]' 'name=val' > good.ini 76 | diff -u test.ini good.ini && ok || fail 77 | done 78 | 79 | # do these --sets which test [DEFAULT] handling also with --inplace 80 | for mode in '' '--inplace'; do 81 | # Add '[DEFAULT]' if explicitly specified 82 | printf '%s\n' 'global=val' '' '[nonDEFAULT]' 'name=val' > test.ini 83 | crudini $mode --set test.ini DEFAULT global valnew 84 | printf '%s\n' '[DEFAULT]' 'global=valnew' '' '[nonDEFAULT]' 'name=val' > good.ini 85 | diff -u test.ini good.ini && ok || fail 86 | 87 | printf '%s\n' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > test.ini 88 | crudini $mode --set test.ini DEFAULT global val 89 | printf '%s\n' '[DEFAULT]' 'global = val' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > good.ini 90 | diff -u test.ini good.ini && ok || fail 91 | 92 | printf '%s\n' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > test.ini 93 | crudini $mode --set test.ini '' global val 94 | printf '%s\n' 'global = val' '[nonDEFAULT1]' 'name=val' '[nonDEFAULT2]' 'name=val' > good.ini 95 | diff -u test.ini good.ini && ok || fail 96 | 97 | # Ensure '[DEFAULT]' is not duplicated 98 | printf '%s\n' '[DEFAULT]' > test.ini 99 | crudini $mode --set test.ini DEFAULT global val 100 | printf '%s\n' '[DEFAULT]' 'global = val' > good.ini 101 | diff -u test.ini good.ini && ok || fail 102 | 103 | # Ensure '[DEFAULT]' is not duplicated when trailing space is present 104 | printf '%s\n' '[DEFAULT] ' > test.ini 105 | crudini $mode --set test.ini DEFAULT global val 106 | printf '%s\n' '[DEFAULT] ' 'global = val' > good.ini 107 | diff -u test.ini good.ini && ok || fail 108 | 109 | # Ensure '[DEFAULT]' is not duplicated when a trailing comment is present 110 | printf '%s\n' '[DEFAULT] #comment' > test.ini 111 | crudini $mode --set test.ini DEFAULT global val 112 | printf '%s\n' '[DEFAULT] #comment' 'global = val' > good.ini 113 | diff -u test.ini good.ini && ok || fail 114 | 115 | # Maintain colon separation 116 | crudini $mode --set example.ini section1 colon val 117 | grep -q '^colon:val' example.ini && ok || fail 118 | 119 | # Maintain space separation 120 | crudini $mode --set example.ini section1 nospace val 121 | grep -q '^nospace=val' example.ini && ok || fail 122 | 123 | crudini $mode --ini-options= --set example.ini section1 nospace val 124 | grep -q '^nospace=val' example.ini && ok || fail 125 | done 126 | 127 | # value is optional 128 | :> test.ini 129 | crudini --set test.ini '' name 130 | printf '%s\n' 'name = ' > good.ini 131 | diff -u test.ini good.ini && ok || fail 132 | 133 | # value is optional 134 | printf '%s\n' 'name=val' > test.ini 135 | crudini --set test.ini '' name 136 | printf '%s\n' 'name=' > good.ini 137 | diff -u test.ini good.ini && ok || fail 138 | 139 | # Protect against creating non parseable files (with nested [[]]) 140 | :> test.ini 141 | crudini --set test.ini '[section]' name val 2>/dev/null && fail 142 | test -s test.ini && fail 143 | printf '%s\n' '[[section]]' 'name=val' > test.ini 144 | crudini --get test.ini '[section]' name 2>/dev/null && fail 145 | printf '%s\n' '[section]' '[name=val' > test.ini 146 | crudini --get test.ini 'section' '[name' 2>/dev/null && fail 147 | printf '%s\n' '[section]' 'n[ame=val' > test.ini 148 | test $(crudini --get test.ini 'section' 'n[ame') = 'val' && ok || fail 149 | 150 | # --existing with file creation 151 | for mode in '' '--inplace'; do 152 | crudini $mode --set missing.ini '' name val 2>/dev/null && ok || fail 153 | rm -f missing.ini 154 | for emode in '' 'file' 'section' 'param'; do 155 | crudini $mode --existing="$emode" --set missing.ini '' name val \ 156 | 2>/dev/null && fail || ok 157 | test -f missing.ini && fail 158 | done 159 | rm -f missing.ini 160 | done 161 | 162 | # --existing[=param] 163 | :> test.ini 164 | crudini --set test.ini '' gname val 165 | crudini --set --existing test.ini '' gname val2 166 | crudini --set --existing=inval test.ini '' gname val3 2>/dev/null && fail 167 | crudini --set --existing test.ini '' gname2 val 2>/dev/null && fail 168 | crudini --set test.ini section1 name val 169 | crudini --set --existing test.ini section1 name val2 170 | crudini --set --existing test.ini section1 name2 val 2>/dev/null && fail 171 | printf '%s\n' 'gname = val2' '' '[section1]' 'name = val2' > good.ini 172 | diff -u test.ini good.ini && ok || fail 173 | 174 | # --existing=section 175 | :> test.ini 176 | crudini --set test.ini '' gname val 177 | crudini --set --existing='section' test.ini '' gname val2 178 | crudini --set --existing='section' test.ini '' gname2 val 2>/dev/null || fail 179 | crudini --set test.ini section1 name val 180 | crudini --set --existing='section' test.ini section1 name val2 181 | crudini --set --existing='section' test.ini section1 name2 val 2>/dev/null || fail 182 | printf '%s\n' 'gname = val2' 'gname2 = val' \ 183 | '' '[section1]' 'name = val2' 'name2 = val' > good.ini 184 | diff -u test.ini good.ini && ok || fail 185 | 186 | # --get ------------------------------------------------- 187 | 188 | # basic get 189 | test "$(crudini --get example.ini section1 cAps)" = 'not significant' && ok || fail 190 | 191 | # unicode get 192 | test "$(crudini --get example.ini non-sh-compat útf8name)" = 'val' && ok || fail 193 | 194 | # get sections 195 | crudini --get example.ini > test.ini 196 | printf '%s\n' DEFAULT section1 'empty section' non-sh-compat list > good.ini 197 | diff -u test.ini good.ini && ok || fail 198 | 199 | # get implicit default section 200 | crudini --get example.ini '' > test.ini 201 | printf '%s\n' 'global' > good.ini 202 | diff -u test.ini good.ini || fail 203 | crudini --format=ini --get example.ini '' > test.ini 204 | printf '%s\n' '[DEFAULT]' 'global = supported' > good.ini 205 | diff -u test.ini good.ini || fail 206 | ok 207 | 208 | # get explicit default section 209 | crudini --get example.ini DEFAULT > test.ini 210 | printf '%s\n' 'global' > good.ini 211 | diff -u test.ini good.ini || fail 212 | crudini --get --format ini example.ini DEFAULT > test.ini 213 | printf '%s\n' '[DEFAULT]' 'global = supported' > good.ini 214 | diff -u test.ini good.ini || fail 215 | ok 216 | 217 | # get section1 in ini format 218 | crudini --format=ini --get example.ini section1 > test.ini 219 | diff -u test.ini section1.ini && ok || fail 220 | 221 | # get section1 in sh format 222 | crudini --format=sh --get example.ini section1 > test.ini 223 | diff -u test.ini section1.sh && ok || fail 224 | # get all in sh format 225 | crudini --format=sh --get section1.ini > test.ini 226 | diff -u test.ini sections.sh && ok || fail 227 | # get default in sh format 228 | crudini --format=sh --get example.ini '' > test.ini 229 | printf '%s\n' 'global=supported' > good.ini 230 | diff -u test.ini good.ini && ok || fail 231 | 232 | # empty DEFAULT is not printed 233 | printf '%s\n' '[DEFAULT]' '#comment' '[section1]' > test.ini 234 | test "$(crudini --get test.ini)" = 'section1' || fail 235 | printf '%s\n' '#comment' '[section1]' > test.ini 236 | test "$(crudini --get test.ini)" = 'section1' || fail 237 | ok 238 | 239 | # Ensure we handle comments correctly 240 | printf '%s\n' '[DEFAULT]' '#c1' ';c2' '%inc1' > test.ini 241 | test "$(crudini --get test.ini)" = '' || fail 242 | printf '%s\n' '[section1]' 'remote=1' > test.ini 243 | test "$(crudini --get test.ini 'section1')" = 'remote' || fail 244 | ok 245 | 246 | # missing bits 247 | :> test.ini 248 | crudini --get missing.ini 2>/dev/null && fail 249 | test "$(crudini --get test.ini)" = '' || fail 250 | crudini --get test.ini '' || fail 251 | crudini --get test.ini '' 'missing' 2>/dev/null && fail 252 | ok 253 | 254 | # --merge ----------------------------------------------- 255 | 256 | # XXX: An empty default section isn't merged 257 | :> test.ini 258 | printf '%s\n' '[DEFAULT]' '#comment' '[section1]' | 259 | crudini --merge test.ini || fail 260 | printf '%s\n' '[section1]' > good.ini 261 | diff -u test.ini good.ini && ok || fail 262 | 263 | :> test.ini 264 | printf '%s\n' '[DEFAULT]' 'name=val' '[section1]' | 265 | crudini --merge test.ini || fail 266 | printf '%s\n' '[DEFAULT]' 'name = val' '[section1]' > good.ini 267 | diff -u test.ini good.ini && ok || fail 268 | 269 | :> test.ini 270 | printf '%s\n' 'name=val' | 271 | crudini --merge test.ini || fail 272 | printf '%s\n' 'name = val' > good.ini 273 | diff -u test.ini good.ini && ok || fail 274 | 275 | printf '%s\n' 'name=val1' > test.ini 276 | printf '%s\n' 'name = val2' | 277 | crudini --merge test.ini || fail 278 | printf '%s\n' 'name=val2' > good.ini 279 | diff -u test.ini good.ini && ok || fail 280 | 281 | printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini 282 | printf '%s\n' 'name=val2' | 283 | crudini --merge test.ini || fail 284 | printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini 285 | diff -u test.ini good.ini && ok || fail 286 | 287 | printf '%s\n' 'name = val1' > test.ini 288 | printf '%s\n' 'name=val2' | 289 | crudini --merge test.ini '' || fail 290 | printf '%s\n' 'name = val2' > good.ini 291 | diff -u test.ini good.ini && ok || fail 292 | 293 | printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini 294 | printf '%s\n' '[DEFAULT]' 'name=val2' | 295 | crudini --merge test.ini || fail 296 | printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini 297 | diff -u test.ini good.ini && ok || fail 298 | 299 | printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini 300 | printf '%s\n' '[DEFAULT]' 'name=val2' | 301 | crudini --merge test.ini '' || fail 302 | printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini 303 | diff -u test.ini good.ini && ok || fail 304 | 305 | printf '%s\n' '[DEFAULT]' 'name=val1' > test.ini 306 | printf '%s\n' 'name=val2' | 307 | crudini --merge test.ini '' || fail 308 | printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini 309 | diff -u test.ini good.ini && ok || fail 310 | 311 | printf '%s\n' 'name=val1' > test.ini 312 | printf '%s\n' 'name=val2' | 313 | crudini --merge test.ini DEFAULT || fail 314 | printf '%s\n' '[DEFAULT]' 'name=val2' > good.ini 315 | diff -u test.ini good.ini && ok || fail 316 | 317 | printf '%s\n' 'name=val1' > test.ini 318 | printf '%s\n' 'name=val2' | 319 | crudini --merge test.ini new || fail 320 | printf '%s\n' 'name=val1' '' '[new]' 'name = val2' > good.ini 321 | diff -u test.ini good.ini && ok || fail 322 | 323 | printf '%s\n' 'name=val1' > test.ini 324 | printf '%s\n' 'name=val2' | 325 | crudini --merge --existing test.ini new 2>/dev/null && fail || ok 326 | 327 | printf '%s\n' 'name=val1' > test.ini 328 | printf '%s\n' 'name2=val2' | 329 | crudini --merge --existing test.ini || fail 330 | printf '%s\n' 'name=val1' > good.ini 331 | diff -u test.ini good.ini && ok || fail 332 | 333 | printf '%s\n' 'name=val1' '[section1]' 'name=val2' > test.ini 334 | printf '%s\n' 'name=val1a' '[section1]' 'name=val2a' | 335 | crudini --merge --existing test.ini || fail 336 | printf '%s\n' 'name=val1a' '[section1]' 'name=val2a' > good.ini 337 | diff -u test.ini good.ini && ok || fail 338 | 339 | # All input sections merged to a specific section 340 | printf '%s\n' 'name=val1' '[section1]' 'name=val2' > test.ini 341 | printf '%s\n' 'name=val2a' '[section2]' 'name2=val' | 342 | crudini --merge test.ini 'section1' || fail 343 | printf '%s\n' 'name=val1' '[section1]' 'name=val2a' 'name2 = val' > good.ini 344 | diff -u test.ini good.ini && ok || fail 345 | 346 | # Maintain case for existing parameters 347 | printf '%s\n' '[section]' 'name=val' > test.ini 348 | printf '%s\n' '[section]' 'Name=val' | 349 | crudini --merge test.ini || fail 350 | printf '%s\n' '[section]' 'name=val'> good.ini 351 | diff -u test.ini good.ini && ok || fail 352 | 353 | # Honor case for new parameters (spacing not currently honored) 354 | printf '%s\n' '[section]' 'name1=val' > test.ini 355 | printf '%s\n' '[section]' 'Name2=val' | 356 | crudini --merge test.ini || fail 357 | printf '%s\n' '[section]' 'name1=val' 'Name2 = val' > good.ini 358 | diff -u test.ini good.ini && ok || fail 359 | 360 | # Note iniparse currently matches sections case insensitively 361 | printf '%s\n' '[section1]' 'name=val1' > test.ini 362 | printf '%s\n' '[Section1]' 'name=val2' | 363 | crudini --merge --existing 2>/dev/null test.ini && fail || ok 364 | printf '%s\n' '[Section1]' 'name=val2' | 365 | crudini --merge test.ini || fail 366 | printf '%s\n' '[section1]' 'name=val1' '' '[Section1]' 'name = val2' > good.ini 367 | diff -u test.ini good.ini && ok || fail 368 | 369 | # --del ------------------------------------------------- 370 | 371 | for sec in '' '[DEFAULT]'; do 372 | printf '%s\n' $sec 'name = val' > test.ini 373 | crudini --del test.ini '' noname || fail 374 | crudini --del --existing test.ini '' noname 2>/dev/null && fail 375 | crudini --del test.ini '' name || fail 376 | :> good.ini 377 | [ "$sec" ] && printf '%s\n' $sec > good.ini 378 | diff -u test.ini good.ini && ok || fail 379 | 380 | printf '%s\n' $sec 'name = val' > test.ini 381 | crudini --del test.ini 'DEFAULT' noname || fail 382 | crudini --del --existing test.ini 'DEFAULT' noname 2>/dev/null && fail 383 | crudini --del test.ini 'DEFAULT' name || fail 384 | :> good.ini 385 | [ "$sec" ] && printf '%s\n' $sec > good.ini 386 | diff -u test.ini good.ini && ok || fail 387 | 388 | printf '%s\n' $sec 'name = val' > test.ini 389 | crudini --del test.ini nosect || fail 390 | crudini --del --existing=file test.ini nosect || fail 391 | crudini --del --existing=section test.ini nosect 2>/dev/null && fail 392 | crudini --del --existing=param test.ini '' noname 2>/dev/null && fail 393 | crudini --del --existing test.ini nosect 2>/dev/null 2>/dev/null && fail 394 | crudini --del --existing=param test.ini '' name || fail 395 | crudini --del test.ini '' || fail 396 | :> good.ini 397 | diff -u test.ini good.ini && ok || fail 398 | 399 | printf '%s\n' $sec 'name = val' > test.ini 400 | crudini --del test.ini nosect || fail 401 | crudini --del --existing=file test.ini nosect || fail 402 | crudini --del --existing=section test.ini nosect 2>/dev/null && fail 403 | crudini --del --existing=param test.ini 'DEFAULT' noname 2>/dev/null && fail 404 | crudini --del --existing test.ini nosect 2>/dev/null && fail 405 | crudini --del test.ini 'DEFAULT' || fail 406 | :> good.ini 407 | diff -u test.ini good.ini && ok || fail 408 | done 409 | 410 | # --del non existing sections/params shouldn't give an error 411 | printf '%s\n' '[section]' 'name = val' > test.ini 412 | crudini --verbose --del test.ini nosect 2>&1 | grep -q ^unchanged || fail 413 | crudini --verbose --del test.ini nosect noname 2>&1 | grep -q ^unchanged || fail 414 | crudini --verbose --del test.ini section noname 2>&1 | grep -q ^unchanged || fail 415 | crudini --verbose --del test.ini section noname 2>&1 | grep -q ^unchanged || fail 416 | crudini --verbose --del --list test.ini section noname val 2>&1 | grep -q ^unchanged || fail 417 | crudini --verbose --del --list test.ini nosect noname val 2>&1 | grep -q ^unchanged || fail 418 | crudini --verbose --del test.ini section 2>&1 | grep -q ^changed || fail 419 | test -s test.ini && fail || ok 420 | 421 | # --del non existing file shouldn't create an empty file 422 | crudini --verbose --del missing.ini section 2>&1 | grep -q ^unchanged || fail 423 | crudini --existing --del missing.ini section 2>/dev/null && fail 424 | test -f missing.ini && fail || ok 425 | 426 | # --get-lines -------------------------------------------- 427 | 428 | crudini --get --format=lines example.ini section1 > test.ini || fail 429 | diff -u test.ini section1.lines && ok || fail 430 | 431 | crudini --get --format=lines example.ini > test.ini || fail 432 | diff -u test.ini example.lines && ok || fail 433 | 434 | # --list ------------------------------------------------- 435 | 436 | # Add new item to list 437 | crudini --list --set example.ini list list1 v3 || fail 438 | test "$(crudini --get example.ini list list1)" = 'v1, v2, v3' && ok || fail 439 | 440 | # Ensure item in list 441 | crudini --list --set example.ini list list1 v3 || fail 442 | test "$(crudini --get example.ini list list1)" = 'v1, v2, v3' && ok || fail 443 | 444 | # Delete item from list 445 | crudini --list --del example.ini list list1 v3 || fail 446 | test "$(crudini --get example.ini list list1)" = 'v1, v2' && ok || fail 447 | 448 | # Delete non existing item from list 449 | for existing in '' '--existing'; do 450 | crudini $existing --list --del example.ini list list1 v3 || fail 451 | test "$(crudini --get example.ini list list1)" = 'v1, v2' && ok || fail 452 | done 453 | 454 | # Add new item to list without spacing 455 | # auto 456 | crudini --list --set example.ini list list2 v3 || fail 457 | test "$(crudini --get example.ini list list2)" = 'v1,v2,v3' && ok || fail 458 | crudini --set example.ini list list2 'v1,v2' || fail 459 | # explicit 460 | crudini --list --list-sep=, --set example.ini list list2 v3 || fail 461 | test "$(crudini --get example.ini list list2)" = 'v1,v2,v3' && ok || fail 462 | 463 | 464 | # Delete item from list without spacing 465 | # auto 466 | crudini --list --del example.ini list list2 v3 || fail 467 | test "$(crudini --get example.ini list list2)" = 'v1,v2' && ok || fail 468 | crudini --set example.ini list list2 'v1,v2,v3' || fail 469 | # explicit 470 | crudini --list --list-sep=, --del example.ini list list2 v3 || fail 471 | test "$(crudini --get example.ini list list2)" = 'v1,v2' && ok || fail 472 | # whitespace separated (while maintaining newline separation) 473 | crudini --list --list-sep= --set example.ini list list3 v2 || fail # ignored 474 | crudini --list --list-sep= --set example.ini list list3 v3 || fail 475 | test "$(crudini --get example.ini list list3)" = $'\nv1\nv2\nv3' && ok || fail 476 | 477 | # Delete honoring --existing 478 | crudini --list --existing --del example.ini nolist list1 v3 2>/dev/null && fail || ok 479 | crudini --list --existing --del example.ini list nolist1 v3 2>/dev/null && fail || ok 480 | 481 | # Support clearing a list by not specifing a value 482 | printf '%s\n' 'empty' 'nonempty = 1' > test.ini 483 | crudini --list --set test.ini '' nonempty 2>/dev/null && ok || fail 484 | test "$(crudini --get test.ini '' nonempty 2>&1)" = '' && ok || fail 485 | crudini --list --set test.ini '' empty 'v1' 2>/dev/null && ok || fail 486 | test "$(crudini --get test.ini '' empty 2>&1)" = 'v1' && ok || fail 487 | 488 | # -------------------------------------------------------- 489 | 490 | # support parsing from stdin 491 | test "$(printf '%s\n' global=1 | crudini --get - '' global)" = 1 && ok || fail 492 | 493 | # --verbose 494 | printf '%s\n' '[section]' 'param = value' > test.ini 495 | crudini --verbose --set test.ini section param value 2>&1 | grep -q ^unchanged && ok || fail 496 | crudini --verbose --set test.ini section param valuE 2>&1 | grep -q ^changed && ok || fail 497 | crudini --verbose --del test.ini section param 2>&1 | grep -q ^changed && ok || fail 498 | crudini --verbose --del test.ini section param 2>&1 | grep -q ^unchanged && ok || fail 499 | crudini --verbose --del test.ini section $'multiline\nchanged:' 2>&1 | grep -q ^changed && fail || ok 500 | 501 | # ensure leading blank lines maintained with global settings 502 | printf '%s\n' '' 'option=1' > file.conf 503 | printf '%s\n' '' 'option=2' > good.conf 504 | crudini --set file.conf '' option 2 || fail 505 | diff -u good.conf file.conf && ok || fail 506 | rm file.conf good.conf 507 | 508 | # ensure errors diagnosed correctly 509 | crudini --get example.ini 2>err | : 510 | ! test -s err && ok || fail #EPIPE ignored 511 | if test -e /dev/full; then 512 | crudini --get example.ini 2>err >/dev/full 513 | grep -q 'No space left' err && ok || fail 514 | fi 515 | 516 | # ensure symlinks handled correctly in file replace mode 517 | printf '%s\n' '[section]' 'param = value' > test.ini 518 | ln -s test.ini ltest.ini 519 | crudini --set ltest.ini section param newvalue || fail 520 | test "$(crudini --get test.ini section param)" = 'newvalue' && ok || fail 521 | crudini --output=ltest.ini --set ltest.ini section param newvalue2 || fail 522 | test "$(crudini --get test.ini section param)" = 'newvalue2' && ok || fail 523 | 524 | # Test single token parameters (without equals) 525 | cp ../noequals.ini . 526 | crudini --get noequals.ini >/dev/null && ok || fail 527 | cp noequals.ini noequals_new.ini 528 | printf '%s\n' 'new' 'new_equals = ' >> noequals_new.ini 529 | for param in param{1..3} colon{1..2} new; do 530 | crudini --set noequals.ini noequals $param || fail 531 | done 532 | crudini --set noequals.ini noequals new_equals '' || fail 533 | diff -u noequals.ini noequals_new.ini && ok || fail 534 | 535 | # Test updating of single token parameters 536 | # Ensure no delimitier added with unspecified value 537 | test "$(crudini --output=- --set noequals.ini noequals param1 \ 538 | | grep param1)" = 'param1' && ok || fail 539 | # Ensure delimitier added with unspecified value with --list 540 | test "$(crudini --list --output=- --set noequals.ini noequals param1 \ 541 | | grep param1)" = 'param1 = ' && ok || fail 542 | # Ensure delimitier added with empty value 543 | test "$(crudini --output=- --set noequals.ini noequals param1 '' \ 544 | | grep param1)" = 'param1 = ' && ok || fail 545 | # Ensure correct spacing with --ini-options=nospace 546 | test "$(crudini --ini-options=nospace --output=- --set noequals.ini \ 547 | noequals param1 value1 \ 548 | | grep param1)" = 'param1=value1' && ok || fail 549 | # Ensure correct spacing with --ini-options=space 550 | test "$(crudini --ini-options=space --output=- --set noequals.ini \ 551 | noequals param1 value1 \ 552 | | grep param1)" = 'param1 = value1' && ok || fail 553 | 554 | # Test can read windows format files 555 | printf '%s\r\n' '' 'empty' 'param=value' > test.ini 556 | test "$(crudini --get test.ini DEFAULT param)" = 'value' && ok || fail 557 | # Test can maintain windows format files 558 | diff -u <(crudini --output=- --ini-options=nospace \ 559 | --set test.ini '' param value) \ 560 | test.ini && ok || fail 561 | printf '%s\r\n' '' 'empty=' > test.ini 562 | diff -u <(crudini --output=- --ini-options=nospace \ 563 | --set test.ini '' empty '') \ 564 | test.ini && ok || fail 565 | printf '%s\r\n' '' 'empty' > test.ini 566 | diff -u <(crudini --output=- --ini-options=nospace \ 567 | --set test.ini '' empty) \ 568 | test.ini && ok || fail 569 | 570 | # Test closed stdin 571 | (0<&- crudini --help >/dev/null) && ok || fail 572 | 573 | # Test closed stdout 574 | (>&- crudini --get test.ini section param value 2>/dev/null) && fail || ok 575 | (>&- crudini --set - section param value 2>/dev/null) && fail || ok 576 | (>&- crudini --set test.ini section param value) && ok || fail 577 | 578 | # Test --ini-options=nospace 579 | diff -u <(crudini --output=- --ini-options=nospace \ 580 | --set nospace-in.ini '' new val) \ 581 | nospace-out.ini && ok || fail 582 | # Test --ini-options=space 583 | diff -u <(crudini --output=- --ini-options=space \ 584 | --set space-out.ini '' new val) \ 585 | space-out.ini && ok || fail 586 | # - Mixed space / nospace not allowed 587 | crudini --get file.conf '' param1 --ini-options=space,nospace \ 588 | 2>/dev/null && fail || ok 589 | 590 | # Test multi operation 591 | # - Multiple set 592 | printf '%s\n' '' 'param1=?' 'param2=?' > file.conf 593 | printf '%s\n' '' 'param1=1' 'param2=2' > good.conf 594 | crudini --set file.conf '' param1 1 \ 595 | --set file.conf '' param2 2 || fail 596 | diff -u good.conf file.conf && ok || fail 597 | rm file.conf good.conf 598 | 599 | # - Multiple get 600 | printf '%s\n' 'param1=1' 'param2=2' > file.conf 601 | crudini --get file.conf '' param1 \ 602 | --get file.conf '' param2 \ 603 | --format=sh > out.conf || fail 604 | diff -u out.conf file.conf && ok || fail 605 | rm file.conf out.conf 606 | 607 | # - Mixed set / del 608 | printf '%s\n' '' 'param1=?' 'param2=?' > file.conf 609 | printf '%s\n' '' 'param2=2' > good.conf 610 | crudini --del file.conf '' param1 \ 611 | --set file.conf '' param2 2 || fail 612 | diff -u good.conf file.conf && ok || fail 613 | rm file.conf good.conf 614 | 615 | # - Mixed set / del with non existing file 616 | printf '%s\n' 'param1 = ' > good.conf 617 | crudini --set file.conf '' param1 \ 618 | --del file.conf '' param3 || fail 619 | diff -u good.conf file.conf && ok || fail 620 | rm file.conf good.conf 621 | 622 | # - Mixed set / get not allowed 623 | crudini --set file.conf '' param1 \ 624 | --get file.conf '' param1 2>/dev/null && fail || ok 625 | 626 | # - Multiple files not allowed 627 | printf '%s\n' '[section]' > file1.conf 628 | printf '%s\n' '[section]' > file2.conf 629 | crudini --del file1.conf section --del file2.conf section 2>/dev/null \ 630 | && fail || ok 631 | rm -f file1.conf file2.conf 632 | 633 | # - Multiple --merge not allowed 634 | crudini --merge file.conf --merge file.conf < /dev/null 2>/dev/null \ 635 | && fail || ok 636 | rm -f file.conf 637 | 638 | # - Interspersed options supported 639 | printf '%s\n' 'param1=1' 'param2=2' > good.conf 640 | crudini --set file.conf '' param1 1 \ 641 | --ini-options nospace \ 642 | --set file.conf '' param2 2 || fail 643 | diff -u good.conf file.conf && ok || fail 644 | rm file.conf good.conf 645 | 646 | # - Conflicting DEFAULT section specs not allowed 647 | crudini --set file.conf '' param1 value \ 648 | --set file.conf DEFAULT param2 value 2>/dev/null \ 649 | && fail || ok 650 | crudini --set file.conf DEFAULT param1 value \ 651 | --set file.conf '' param2 value 2>/dev/null \ 652 | && fail || ok 653 | rm -f file.conf 654 | 655 | # Test indentation support 656 | # - --get indented 657 | printf ' %s\n' '[section]' 'param1=1' > file.conf 658 | crudini --ini-options=ignoreindent \ 659 | --get file.conf >/dev/null && ok || fail 660 | 661 | # - --set maintaining indented 662 | printf ' %s\n' '[section]' 'param1=a' > good.conf 663 | crudini --ini-options=ignoreindent \ 664 | --set file.conf section param1 a && ok || fail 665 | diff -u good.conf file.conf && ok || fail 666 | 667 | # - --set copying indentation 668 | printf ' %s\n' '[section]' 'param1=a' 'param2=b' > good.conf 669 | crudini --ini-options=ignoreindent,nospace \ 670 | --set file.conf section param2 b && ok || fail 671 | diff -u good.conf file.conf && ok || fail 672 | 673 | # - --set new indentation for param (on new section) 674 | printf '%s\n' '[section]' ' param1 = a' > good.conf 675 | rm -f file.conf 676 | crudini --ini-options=ignoreindent \ 677 | --set file.conf section ' param1' a && ok || fail 678 | diff -u good.conf file.conf && ok || fail 679 | 680 | # - --set copying indentation from param 681 | printf '%s\n' '[section]' ' param1 = a' ' param2 = b' > good.conf 682 | crudini --ini-options=ignoreindent \ 683 | --set file.conf section 'param2' b && ok || fail 684 | diff -u good.conf file.conf && ok || fail 685 | 686 | # - --set new indentation for param (on new default section) 687 | printf '%s\n' ' param1 = a' > good.conf 688 | rm -f file.conf 689 | crudini --ini-options=ignoreindent \ 690 | --set file.conf '' ' param1' a && ok || fail 691 | diff -u good.conf file.conf && ok || fail 692 | rm file.conf good.conf 693 | 694 | # Test removal of extraneous empty lines 695 | # implicitly enabled with --del 696 | printf '\n[%s]\n' 1 2 > good.conf 697 | cp good.conf file.conf 698 | for i in 1 2; do crudini --del file.conf $i; crudini --set file.conf $i; done 699 | diff -u good.conf file.conf && ok || fail 700 | rm file.conf good.conf 701 | 702 | # Test addition/removal of empty lines with sectionspace 703 | printf '\n[%s]\n[%s]\n\n[%s]\n\n\n[%s]\n\n' 1 2 3 4 > file.conf 704 | printf '[%s]\n\n[%s]\n\n[%s]\n\n[%s]\n' 1 2 3 4 > good.conf 705 | crudini --set --ini-options=sectionspace file.conf '' 706 | diff -u good.conf file.conf && ok || fail 707 | rm file.conf good.conf 708 | 709 | # Test creation of a "default" section 710 | printf '[%s]\n' 'default' > good.conf 711 | crudini --set file.conf default # new 712 | diff -u good.conf file.conf && ok || fail 713 | crudini --set file.conf default # existing 714 | diff -u good.conf file.conf && ok || fail 715 | rm file.conf 716 | crudini --set file.conf default --set file.conf default # double new 717 | diff -u good.conf file.conf && ok || fail 718 | rm file.conf good.conf 719 | 720 | # Test non UTF-8 encoded file 721 | printf '9d = \x9d' > latin1.conf 722 | # Gets may be output as single or multi-byte depending on locale 723 | crudini --get latin1.conf '' 9d || fail 724 | # Note setting works on linux at least, but needs matching locale 725 | # LC_ALL=en_US.iso-8859-1 crudini.py --set latin1.conf '' 9d $'\xe1' 726 | rm latin1.conf 727 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,pep8 3 | 4 | [testenv] 5 | deps = iniparse>=0.5 6 | commands = /bin/bash -c 'cd tests && ./test.sh' 7 | 8 | [testenv:pep8] 9 | deps = flake8 10 | commands = flake8 crudini setup.py 11 | --------------------------------------------------------------------------------