├── tests ├── test_more.ini ├── test_multiline.ini ├── test.ini └── test.rs ├── .gitignore ├── .github └── workflows │ └── rust.yaml ├── LICENSE-MIT ├── Cargo.toml ├── CODE_OF_CONDUCT.md ├── src ├── lib.rs └── ini.rs ├── LICENSE-LGPL └── README.md /tests/test_more.ini: -------------------------------------------------------------------------------- 1 | defaultvalues=overwritten 2 | 3 | [topsecret] 4 | KFC = redacted 5 | 6 | [values] 7 | Bool = False 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | Cargo.lock 4 | output.ini 5 | output2.ini 6 | test2.ini 7 | .vscode 8 | output_async.ini 9 | output_sync.ini 10 | pretty_output.ini 11 | pretty_output_async.ini 12 | -------------------------------------------------------------------------------- /tests/test_multiline.ini: -------------------------------------------------------------------------------- 1 | [Section] 2 | Key1: Value1 3 | Key2: Value Two 4 | Key3: this is a haiku 5 | spread across separate lines 6 | # This is a comment 7 | 8 | a single value 9 | Key4: Four 10 | -------------------------------------------------------------------------------- /tests/test.ini: -------------------------------------------------------------------------------- 1 | defaultvalues=defaultvalues 2 | 3 | [topsecret] 4 | KFC = the secret herb is orega- 5 | colon:value after colon 6 | Empty string = 7 | None string 8 | Password=[in-brackets] 9 | 10 | [ spacing ] 11 | indented=indented 12 | not indented = not indented 13 | 14 | [values] 15 | Bool = True 16 | Boolcoerce = 0 17 | Int = -31415 18 | Uint = 31415 19 | Float = 3.1415 20 | -------------------------------------------------------------------------------- /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | continue-on-error: ${{ matrix.rust == 'nightly' }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | rust: 20 | - stable 21 | - beta 22 | - nightly 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: dtolnay/rust-toolchain@master 28 | with: 29 | toolchain: ${{ matrix.rust }} 30 | components: rustfmt 31 | 32 | - uses: Swatinem/rust-cache@v2 33 | with: 34 | cache-on-failure: true 35 | 36 | - name: build 37 | run: cargo build --release --all-features -v 38 | 39 | - name: test 40 | run: cargo test --all-features -v 41 | 42 | - name: fmt 43 | run: cargo fmt --all -- --check 44 | 45 | - name: Security audit 46 | uses: actions-rs/audit-check@v1 47 | with: 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 QEDK 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "configparser" 3 | version = "3.2.0" 4 | authors = ["QEDK "] 5 | edition = "2024" 6 | description = "A simple configuration parsing utility with no dependencies that allows you to parse INI and ini-style syntax. You can use this to write Rust programs which can be customized by end users easily." 7 | homepage = "https://github.com/QEDK/configparser-rs" 8 | repository = "https://github.com/QEDK/configparser-rs" 9 | documentation = "https://docs.rs/configparser" 10 | readme = "README.md" 11 | license = "MIT OR LGPL-3.0-or-later" 12 | keywords = ["config", "ini", "settings", "configuration", "parser"] 13 | categories = ["config", "encoding", "parser-implementations"] 14 | 15 | [badges] 16 | maintenance = { status = "actively-developed" } 17 | 18 | [dependencies] 19 | indexmap = { version = "^2.9", optional = true } 20 | tokio = { version = "^1.44", optional = true, features = ["fs"] } 21 | serde = { version = "^1.0", optional = true } 22 | 23 | [features] 24 | serde = ["dep:serde", "indexmap/serde"] 25 | 26 | [dev-dependencies] 27 | tokio = { version = "^1.44", features = ["fs", "macros", "rt-multi-thread"] } 28 | serde_json = { version = "^1.0" } 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement, 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This crate provides the `Ini` struct which implements a basic configuration language which provides a structure similar to what’s found in Windows' `ini` files. 3 | You can use this to write Rust programs which can be customized by end users easily. 4 | 5 | This is a simple configuration parsing utility with no dependencies built on Rust. It is inspired by Python's `configparser`. 6 | 7 | The current release is stable and changes will take place at a slower pace. We'll be keeping semver in mind for future releases as well. 8 | 9 | ## 🚀 Quick Start 10 | 11 | A basic `ini`-syntax file (we say ini-syntax files because the files don't need to be necessarily `*.ini`) looks like this: 12 | ```INI 13 | [DEFAULT] 14 | key1 = value1 15 | pizzatime = yes 16 | cost = 9 17 | 18 | [topsecrets] 19 | nuclear launch codes = topsecret 20 | 21 | [github.com] 22 | User = QEDK 23 | ``` 24 | Essentially, the syntax consists of sections, each of which can which contains keys with values. The `Ini` struct can read and write such values to 25 | strings as well as files. 26 | 27 | ## ➕ Supported datatypes 28 | `configparser` does not guess the datatype of values in configuration files and stores everything as strings. However, some datatypes are so common 29 | that it's a safe bet that some values need to be parsed in other types. For this, the `Ini` struct provides easy functions like `getint()`, `getuint()`, 30 | `getfloat()` and `getbool()`. The only bit of extra magic involved is that the `getbool()` function will treat boolean values case-insensitively (so 31 | `true` is the same as `True` just like `TRUE`). The crate also provides a stronger `getboolcoerce()` function that parses more values (such as `T`, `yes` and `0`, all case-insensitively), the function's documentation will give you the exact details. 32 | ```rust 33 | use configparser::ini::Ini; 34 | 35 | let mut config = Ini::new(); 36 | config.read(String::from( 37 | "[somesection] 38 | someintvalue = 5")); 39 | let my_value = config.getint("somesection", "someintvalue").unwrap().unwrap(); 40 | assert_eq!(my_value, 5); // value accessible! 41 | 42 | //You can ofcourse just choose to parse the values yourself: 43 | let my_string = String::from("1984"); 44 | let my_int = my_string.parse::().unwrap(); 45 | ``` 46 | 47 | 48 | ## 📝 Supported `ini` file structure 49 | A configuration file can consist of sections, each led by a `[section-name]` header, followed by key-value entries separated by a delimiter (`=` and `:`). By default, section names and key names are case-insensitive. Case-sensitivity can be enabled using the `Ini::new_cs()` constructor. All leading and trailing whitespace is removed from stored keys, values and section names. 50 | Key values can be omitted, in which case the key-value delimiter 51 | may also be left out (but this is different from putting a delimiter, we'll 52 | explain it later). You can use comment symbols (`;` and `#` to denote comments). This can be configured with the `set_comment_symbols()` method in the 53 | API. Keep in mind that key-value pairs or section headers cannot span multiple lines. 54 | Owing to how ini files usually are, this means that `[`, `]`, `=`, `:`, `;` and `#` are special symbols by default (this crate will allow you to use `]` sparingly). 55 | Let's take for example: 56 | ```INI 57 | [section headers are case-insensitive by default] 58 | [ section headers are case-insensitive by default ] 59 | are the section headers above same? = yes 60 | sectionheaders_and_keysarestored_in_lowercase? = yes 61 | keys_are_also_case_insensitive = Values are case sensitive 62 | Case-sensitive_keys_and_sections = using a special constructor 63 | you can also use colons : instead of the equal symbol 64 | ;anything after a comment symbol is ignored 65 | #this is also a comment 66 | spaces in keys=allowed ;and everything before this is still valid! 67 | spaces in values=allowed as well 68 | spaces around the delimiter = also OK 69 | 70 | 71 | [All values are strings] 72 | values like this= 0000 73 | or this= 0.999 74 | are they treated as numbers? = no 75 | integers, floats and booleans are held as= strings 76 | 77 | [value-less?] 78 | a_valueless_key_has_None 79 | this key has an empty string value has Some("") = 80 | 81 | [indented sections] 82 | can_values_be_as_well = True 83 | purpose = formatting for readability 84 | is_this_same = yes 85 | is_this_same=yes 86 | 87 | ``` 88 | An important thing to note is that values with the same keys will get updated, this means that the last inserted key (whether that's a section header 89 | or property key) is the one that remains in the `HashMap`. 90 | The only bit of magic the API does is the section-less properties are put in a section called "default". You can configure this variable via the API. 91 | Keep in mind that a section named "default" is also treated as sectionless so the output files remains consistent with no section header. 92 | 93 | ## Usage 94 | Let's take another simple `ini` file and talk about working with it: 95 | ```INI 96 | [topsecret] 97 | KFC = the secret herb is orega- 98 | 99 | [values] 100 | Uint = 31415 101 | ``` 102 | If you read the above sections carefully, you'll know that 1) all the keys are stored in lowercase, 2) `get()` can make access in a case-insensitive 103 | manner and 3) we can use `getint()` to parse the `Int` value into an `i64`. Let's see that in action. 104 | 105 | ```rust 106 | use configparser::ini::{Ini, WriteOptions}; 107 | use std::error::Error; 108 | 109 | fn main() -> Result<(), Box> { 110 | let mut config = Ini::new(); 111 | 112 | // You can easily load a file to get a clone of the map: 113 | let map = config.load("tests/test.ini")?; 114 | println!("{:?}", map); 115 | // You can also safely not store the reference and access it later with get_map_ref() or get a clone with get_map() 116 | 117 | // If you want to access the value, then you can simply do: 118 | let val = config.get("TOPSECRET", "KFC").unwrap(); 119 | // Notice how get() can access indexes case-insensitively. 120 | 121 | assert_eq!(val, "the secret herb is orega-"); // value accessible! 122 | 123 | // What if you want remove KFC's secret recipe? Just use set(): 124 | config.set("topsecret", "kfc", None); 125 | 126 | assert_eq!(config.get("TOPSECRET", "KFC"), None); // as expected! 127 | 128 | // What if you want to get an unsigned integer? 129 | let my_number = config.getuint("values", "Uint")?.unwrap(); 130 | assert_eq!(my_number, 31415); // and we got it! 131 | // The Ini struct provides more getters for primitive datatypes. 132 | 133 | // You can also access it like a normal hashmap: 134 | let innermap = map["topsecret"].clone(); 135 | // Remember that all indexes are stored in lowercase! 136 | 137 | // You can easily write the currently stored configuration to a file with the `write` method. This creates a compact format with as little spacing as possible: 138 | config.write("output.ini"); 139 | 140 | // You can write the currently stored configuration with different spacing to a file with the `pretty_write` method: 141 | let write_options = WriteOptions::new_with_params(true, 2, 1); 142 | // or you can use the default configuration as `WriteOptions::new()` 143 | config.pretty_write("pretty_output.ini", &write_options); 144 | 145 | // If you want to simply mutate the stored hashmap, you can use get_mut_map() 146 | let map = config.get_mut_map(); 147 | // You can then use normal HashMap functions on this map at your convenience. 148 | // Remember that functions which rely on standard formatting might stop working 149 | // if it's mutated differently. 150 | 151 | // If you want a case-sensitive map, just do: 152 | let mut config = Ini::new_cs(); 153 | // This automatically changes the behaviour of every function and parses the file as case-sensitive. 154 | 155 | Ok(()) 156 | } 157 | ``` 158 | */ 159 | pub mod ini; 160 | -------------------------------------------------------------------------------- /LICENSE-LGPL: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 QEDK 2 | 3 | GNU LESSER GENERAL PUBLIC LICENSE 4 | Version 3, 29 June 2007 5 | 6 | Copyright (C) 2007 Free Software Foundation, Inc. 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | 11 | This version of the GNU Lesser General Public License incorporates 12 | the terms and conditions of version 3 of the GNU General Public 13 | License, supplemented by the additional permissions listed below. 14 | 15 | 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 19 | General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, 22 | other than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | a) under this License, provided that you make a good faith effort to 58 | ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | 62 | b) under the GNU GPL, with none of the additional permissions of 63 | this License applicable to that copy. 64 | 65 | 3. Object Code Incorporating Material from Library Header Files. 66 | 67 | The object code form of an Application may incorporate material from 68 | a header file that is part of the Library. You may convey such object 69 | code under terms of your choice, provided that, if the incorporated 70 | material is not limited to numerical parameters, data structure 71 | layouts and accessors, or small macros, inline functions and templates 72 | (ten or fewer lines in length), you do both of the following: 73 | 74 | a) Give prominent notice with each copy of the object code that the 75 | Library is used in it and that the Library and its use are 76 | covered by this License. 77 | 78 | b) Accompany the object code with a copy of the GNU GPL and this license 79 | document. 80 | 81 | 4. Combined Works. 82 | 83 | You may convey a Combined Work under terms of your choice that, 84 | taken together, effectively do not restrict modification of the 85 | portions of the Library contained in the Combined Work and reverse 86 | engineering for debugging such modifications, if you also do each of 87 | the following: 88 | 89 | a) Give prominent notice with each copy of the Combined Work that 90 | the Library is used in it and that the Library and its use are 91 | covered by this License. 92 | 93 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 94 | document. 95 | 96 | c) For a Combined Work that displays copyright notices during 97 | execution, include the copyright notice for the Library among 98 | these notices, as well as a reference directing the user to the 99 | copies of the GNU GPL and this license document. 100 | 101 | d) Do one of the following: 102 | 103 | 0) Convey the Minimal Corresponding Source under the terms of this 104 | License, and the Corresponding Application Code in a form 105 | suitable for, and under terms that permit, the user to 106 | recombine or relink the Application with a modified version of 107 | the Linked Version to produce a modified Combined Work, in the 108 | manner specified by section 6 of the GNU GPL for conveying 109 | Corresponding Source. 110 | 111 | 1) Use a suitable shared library mechanism for linking with the 112 | Library. A suitable mechanism is one that (a) uses at run time 113 | a copy of the Library already present on the user's computer 114 | system, and (b) will operate properly with a modified version 115 | of the Library that is interface-compatible with the Linked 116 | Version. 117 | 118 | e) Provide Installation Information, but only if you would otherwise 119 | be required to provide such information under section 6 of the 120 | GNU GPL, and only to the extent that such information is 121 | necessary to install and execute a modified version of the 122 | Combined Work produced by recombining or relinking the 123 | Application with a modified version of the Linked Version. (If 124 | you use option 4d0, the Installation Information must accompany 125 | the Minimal Corresponding Source and Corresponding Application 126 | Code. If you use option 4d1, you must provide the Installation 127 | Information in the manner specified by section 6 of the GNU GPL 128 | for conveying Corresponding Source.) 129 | 130 | 5. Combined Libraries. 131 | 132 | You may place library facilities that are a work based on the 133 | Library side by side in a single library together with other library 134 | facilities that are not Applications and are not covered by this 135 | License, and convey such a combined library under terms of your 136 | choice, if you do both of the following: 137 | 138 | a) Accompany the combined library with a copy of the same work based 139 | on the Library, uncombined with any other library facilities, 140 | conveyed under the terms of this License. 141 | 142 | b) Give prominent notice with the combined library that part of it 143 | is a work based on the Library, and explaining where to find the 144 | accompanying uncombined form of the same work. 145 | 146 | 6. Revised Versions of the GNU Lesser General Public License. 147 | 148 | The Free Software Foundation may publish revised and/or new versions 149 | of the GNU Lesser General Public License from time to time. Such new 150 | versions will be similar in spirit to the present version, but may 151 | differ in detail to address new problems or concerns. 152 | 153 | Each version is given a distinguishing version number. If the 154 | Library as you received it specifies that a certain numbered version 155 | of the GNU Lesser General Public License "or any later version" 156 | applies to it, you have the option of following the terms and 157 | conditions either of that published version or of any later version 158 | published by the Free Software Foundation. If the Library as you 159 | received it does not specify a version number of the GNU Lesser 160 | General Public License, you may choose any version of the GNU Lesser 161 | General Public License ever published by the Free Software Foundation. 162 | 163 | If the Library as you received it specifies that a proxy can decide 164 | whether future versions of the GNU Lesser General Public License shall 165 | apply, that proxy's public statement of acceptance of any version is 166 | permanent authorization for you to choose that version for the 167 | Library. 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # configparser 2 | [![Build Status](https://github.com/QEDK/configparser-rs/actions/workflows/rust.yaml/badge.svg)](https://github.com/QEDK/configparser-rs/actions/workflows/rust.yaml) [![Crates.io](https://img.shields.io/crates/l/configparser?color=black)](LICENSE-MIT) [![Crates.io](https://img.shields.io/crates/v/configparser?color=black)](https://crates.io/crates/configparser) [![Released API docs](https://docs.rs/configparser/badge.svg)](https://docs.rs/configparser) [![Maintenance](https://img.shields.io/maintenance/yes/2025)](https://github.com/QEDK/configparser-rs) 3 | 4 | `configparser` is a configuration parsing utility with zero dependencies built in Rust. It is inspired by the similarly named Python `configparser` library. 5 | 6 | This crate provides an `Ini` struct which implements a succinct configuration language and provides a structure similar to what’s found in `ini` files (similar to `.env` files). You can use this to write Rust programs which can be configured by end users. 7 | 8 | We use semantic versioning, so breaking changes will only be introduced in major versions. The crate is stable and has been used in production for a long time. 9 | 10 | ## 🚀 Quick Start 11 | 12 | A basic `ini`-format file looks like this: 13 | ```INI 14 | [DEFAULT] 15 | key1 = value1 16 | pizzatime = yes 17 | cost = 9 18 | 19 | [topsecrets] 20 | nuclear launch codes = topsecret 21 | 22 | [github.com] 23 | User = QEDK 24 | ``` 25 | Essentially, the syntax consists of sections, each of which can which contains keys with values. The `Ini` struct can read and write such values to 26 | strings as well as files. 27 | 28 | ### 🧰 Installation 29 | You can install this easily via `cargo` by including it in your `Cargo.toml` file like: 30 | ```TOML 31 | [dependencies] 32 | configparser = "3.2.0" 33 | ``` 34 | 35 | ## ➕ Supported datatypes 36 | `configparser` does not guess the datatype of values in configuration files and stores everything as strings. However, some datatypes are so common 37 | that it's a safe bet that some values need to be parsed in other types. For this, the `Ini` struct provides easy functions like `getint()`, `getuint()`, 38 | `getfloat()` and `getbool()`. The only bit of extra magic involved is that the `getbool()` function will treat boolean values case-insensitively (so 39 | `true` is the same as `True` just like `TRUE`). The crate also provides a stronger `getboolcoerce()` function that parses more values (such as `T`, `yes` and `0`, all case-insensitively), the function's documentation will give you the exact details. 40 | ```rust 41 | use configparser::ini::Ini; 42 | 43 | let mut config = Ini::new(); 44 | config.read(String::from( 45 | "[somesection] 46 | someintvalue = 5")); 47 | let my_value = config.getint("somesection", "someintvalue").unwrap().unwrap(); 48 | assert_eq!(my_value, 5); // value accessible! 49 | 50 | //You can ofcourse just choose to parse the values yourself: 51 | let my_string = String::from("1984"); 52 | let my_int = my_string.parse::().unwrap(); 53 | ``` 54 | 55 | ## 📝 Supported `ini` file structure 56 | A configuration file can consist of sections, each led by a `[section-name]` header, followed by key-value entries separated by a delimiter (`=` and `:`). By default, section names and key names are case-insensitive. Case-sensitivity can be enabled using the `Ini::new_cs()` constructor. All leading and trailing whitespace is removed from stored keys, values and section names. 57 | Key values can be omitted, in which case the key-value delimiter 58 | may also be left out (but this is different from putting a delimiter, we'll 59 | explain it later). You can use comment symbols (`;` and `#` to denote comments). This can be configured with the `set_comment_symbols()` method in the 60 | API. Keep in mind that key-value pairs or section headers cannot span multiple lines. 61 | Owing to how ini files usually are, this means that `[`, `]`, `=`, `:`, `;` and `#` are special symbols by default (this crate will allow you to use `]` sparingly). 62 | 63 | Let's take for example: 64 | ```INI 65 | [section headers are case-insensitive by default] 66 | [ section headers are case-insensitive by default ] 67 | are the section headers above same? = yes 68 | sectionheaders_and_keysarestored_in_lowercase? = yes 69 | keys_are_also_case_insensitive = Values are case sensitive 70 | Case-sensitive_keys_and_sections = using a special constructor 71 | you can also use colons : instead of the equal symbol 72 | ;anything after a comment symbol is ignored 73 | #this is also a comment 74 | spaces in keys=allowed ;and everything before this is still valid! 75 | spaces in values=allowed as well 76 | spaces around the delimiter = also OK 77 | 78 | 79 | [All values are strings] 80 | values like this= 0000 81 | or this= 0.999 82 | are they treated as numbers? = no 83 | integers, floats and booleans are held as= strings 84 | 85 | [value-less?] 86 | a_valueless_key_has_None 87 | this key has an empty string value has Some("") = 88 | 89 | [indented sections] 90 | can_values_be_as_well = True 91 | purpose = formatting for readability 92 | is_this_same = yes 93 | is_this_same=yes 94 | 95 | ``` 96 | An important thing to note is that values with the same keys will get updated, this means that the last inserted key (whether that's a section header 97 | or property key) is the one that remains in the `HashMap`. 98 | The only bit of magic the API does is the section-less properties are put in a section called "default". You can configure this variable via the API. 99 | Keep in mind that a section named "default" is also treated as sectionless so the output files remains consistent with no section header. 100 | 101 | ## 🛠 Usage 102 | Let's take another simple `ini` file and talk about working with it: 103 | ```INI 104 | [topsecret] 105 | KFC = the secret herb is orega- 106 | 107 | [values] 108 | Uint = 31415 109 | ``` 110 | If you read the above sections carefully, you'll know that 1) all the keys are stored in lowercase, 2) `get()` can make access in a case-insensitive 111 | manner and 3) we can use `getuint()` to parse the `Uint` value into an `u64`. Let's see that in action. 112 | 113 | ```rust 114 | use configparser::ini::{Ini, WriteOptions}; 115 | use std::error::Error; 116 | 117 | fn main() -> Result<(), Box> { 118 | let mut config = Ini::new(); 119 | 120 | // You can easily load a file to get a clone of the map: 121 | let map = config.load("tests/test.ini")?; 122 | println!("{:?}", map); 123 | // You can also safely not store the reference and access it later with get_map_ref() or get a clone with get_map() 124 | 125 | // If you want to access the value, then you can simply do: 126 | let val = config.get("TOPSECRET", "KFC").unwrap(); 127 | // Notice how get() can access indexes case-insensitively. 128 | 129 | assert_eq!(val, "the secret herb is orega-"); // value accessible! 130 | 131 | // What if you want remove KFC's secret recipe? Just use set(): 132 | config.set("topsecret", "kfc", None); 133 | 134 | assert_eq!(config.get("TOPSECRET", "KFC"), None); // as expected! 135 | 136 | // What if you want to get an unsigned integer? 137 | let my_number = config.getuint("values", "Uint")?.unwrap(); 138 | assert_eq!(my_number, 31415); // and we got it! 139 | // The Ini struct provides more getters for primitive datatypes. 140 | 141 | // You can also access it like a normal hashmap: 142 | let innermap = map["topsecret"].clone(); 143 | // Remember that all indexes are stored in lowercase! 144 | 145 | // You can easily write the currently stored configuration to a file with the `write` method. This creates a compact format with as little spacing as possible: 146 | config.write("output.ini"); 147 | 148 | // You can write the currently stored configuration with different spacing to a file with the `pretty_write` method: 149 | let write_options = WriteOptions::new_with_params(true, 2, 1); 150 | // or you can use the default configuration as `WriteOptions::new()` 151 | config.pretty_write("pretty_output.ini", &write_options); 152 | 153 | // If you want to simply mutate the stored hashmap, you can use get_mut_map() 154 | let map = config.get_mut_map(); 155 | // You can then use normal HashMap functions on this map at your convenience. 156 | // Remember that functions which rely on standard formatting might stop working 157 | // if it's mutated differently. 158 | 159 | // If you want a case-sensitive map, just do: 160 | let mut config = Ini::new_cs(); 161 | // This automatically changes the behaviour of every function and parses the file as case-sensitive. 162 | 163 | Ok(()) 164 | } 165 | ``` 166 | The `Ini` struct offers great support for type conversion and type setting safely, as well as map accesses. See the API for more verbose documentation. 167 | 168 | ## 📖Features 169 | 170 | - *indexmap*: Activating the `indexmap` feature allows using [indexmap](https://crates.io/crates/indexmap) in place 171 | of `HashMap` to store the sections and keys. This ensures that insertion order is preserved when iterating on or 172 | serializing the Ini object. 173 | Due to the nature of indexmap, it offers mostly similar performance to stdlib HashMaps but with 174 | [slower lookup times](https://github.com/bluss/indexmap#performance). 175 | 176 | You can activate it by adding it as a feature like this: 177 | ```TOML 178 | [dependencies] 179 | configparser = { version = "3.2.0", features = ["indexmap"] } 180 | ``` 181 | 182 | - *tokio*: Activating the `tokio` feature adds asynchronous functions for reading from (`load_async()`) and 183 | writing to (`write_async()`) files using [tokio](https://crates.io/crates/tokio). 184 | 185 | You can activate it by adding it as a feature like this: 186 | ```TOML 187 | [dependencies] 188 | configparser = { version = "3.2.0", features = ["tokio"] } 189 | ``` 190 | 191 | ## Override Options 192 | 193 | You can change the default configuration options like this. 194 | See the API for more verbose documentation. 195 | 196 | ``` 197 | let mut parser_options = IniDefault::default(); 198 | parser_options.multiline = true; 199 | parser_options.enable_inline_comments = false; 200 | 201 | let mut config = Ini::new_from_defaults(parser_options); 202 | ``` 203 | 204 | ## 📜 License 205 | 206 | Licensed under either of 207 | 208 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 209 | * Lesser General Public license v3.0 or later ([LICENSE-LGPL](LICENSE-LGPL) or https://www.gnu.org/licenses/lgpl-3.0.html) 210 | 211 | at your option. 212 | 213 | ### ✏ Contribution 214 | 215 | Unless you explicitly state otherwise, any contribution intentionally submitted 216 | for inclusion in the work by you, as defined in the LGPL-3.0 license, shall be dual licensed as above, without any 217 | additional terms or conditions. 218 | 219 | ## 🆕 Changelog 220 | 221 | Old changelogs are visible in the commit history. 222 | - 3.0.1 223 | - Uses `CRLF` line endings for Windows files. 224 | - Bumps crate to 2021 edition. 225 | - Adds features to CI pipeline. 226 | - 3.0.2 227 | - Adds support for multi-line key-value pairs. 228 | - Adds `async-std` feature for asynchronous file operations. 229 | - Some performance optimizations. 230 | - 3.0.3 231 | - Add default empty line on empty strings. 232 | - Feature to append to existing `Ini` objects. 233 | - Minor lint fixes. 234 | - 3.0.4 235 | - Adds pretty printing functionality 236 | - Replaces `async-std` with `tokio` as the available async runtime 237 | - *The `async-std` feature will be deprecated in a future release* 238 | - 3.1.0 (**STABLE**) 239 | - `async-std` has been deprecated 240 | - Fixes a bug where multiline values did not preserve newlines 241 | - Fixes a bug where empty sections were removed 242 | - Adds a feature to support inline comments 243 | - 3.2.0 244 | - `async-std` has been removed 245 | - 🎉 Implements serde support 246 | 247 | ### 🔜 Future plans 248 | 249 | - Support for appending sections, coercing them as well. 250 | - Benchmarking against similar packages. 251 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | use configparser::ini::Ini; 2 | use std::collections::HashSet; 3 | use std::error::Error; 4 | 5 | #[cfg(feature = "indexmap")] 6 | use configparser::ini::WriteOptions; 7 | 8 | #[test] 9 | #[allow(clippy::approx_constant)] 10 | fn non_cs() -> Result<(), Box> { 11 | let mut config = Ini::new(); 12 | let map = config.load("tests/test.ini")?; 13 | config.set_comment_symbols(&[';', '#', '!']); 14 | let inpstring = config.read( 15 | "defaultvalues=defaultvalues 16 | [topsecret] 17 | KFC = the secret herb is orega- 18 | colon:value after colon 19 | Empty string = 20 | None string 21 | Password=[in-brackets] 22 | [ spacing ] 23 | indented=indented 24 | not indented = not indented ;testcomment 25 | !modified comment 26 | [values]#another comment 27 | Bool = True 28 | Boolcoerce = 0 29 | Int = -31415 30 | Uint = 31415 31 | Float = 3.1415" 32 | .to_owned(), 33 | )?; 34 | assert_eq!(map, inpstring); 35 | config.set("DEFAULT", "defaultvalues", Some("notdefault".to_owned())); 36 | assert_eq!( 37 | config.get("DEFAULT", "defaultvalues").unwrap(), 38 | "notdefault" 39 | ); 40 | config.setstr("DEFAULT", "defaultvalues", Some("defaultvalues")); 41 | assert_eq!( 42 | config.get("DEFAULT", "defaultvalues").unwrap(), 43 | "defaultvalues" 44 | ); 45 | config.setstr("DEFAULT", "defaultvalues", None); 46 | config.write("output.ini")?; 47 | let map2 = config.clone().load("output.ini")?; 48 | assert_eq!(map2, *config.get_map_ref()); 49 | let map3 = config.clone().read(config.writes())?; 50 | assert_eq!(map2, map3); 51 | assert_eq!(config.sections().len(), 4); 52 | assert_eq!(config.get("DEFAULT", "defaultvalues"), None); 53 | assert_eq!( 54 | config.get("topsecret", "KFC").unwrap(), 55 | "the secret herb is orega-" 56 | ); 57 | assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); 58 | assert_eq!(config.get("topsecret", "None string"), None); 59 | assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); 60 | assert_eq!( 61 | config.get("spacing", "not indented").unwrap(), 62 | "not indented" 63 | ); 64 | assert_eq!( 65 | config.get("topsecret", "colon").unwrap(), 66 | "value after colon" 67 | ); 68 | assert!(config.getbool("values", "Bool")?.unwrap()); 69 | assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); 70 | assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); 71 | assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); 72 | assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); 73 | assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); 74 | assert_eq!( 75 | map["default"]["defaultvalues"].clone().unwrap(), 76 | "defaultvalues" 77 | ); 78 | assert_eq!( 79 | map["topsecret"]["kfc"].clone().unwrap(), 80 | "the secret herb is orega-" 81 | ); 82 | assert_eq!(map["topsecret"]["empty string"].clone().unwrap(), ""); 83 | assert_eq!(map["topsecret"]["none string"], None); 84 | assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); 85 | assert_eq!( 86 | map["spacing"]["not indented"].clone().unwrap(), 87 | "not indented" 88 | ); 89 | let mut config2 = config.clone(); 90 | let val = config2.remove_key("default", "defaultvalues"); 91 | assert_eq!(val, Some(None)); 92 | assert_eq!(config2.get("default", "defaultvalues"), None); 93 | config2.remove_section("default"); 94 | assert_eq!(config2.get("default", "nope"), None); 95 | let mut_map = config.get_mut_map(); 96 | mut_map.get_mut("topsecret").unwrap().insert( 97 | String::from("none string"), 98 | Some(String::from("None string")), 99 | ); 100 | assert_eq!( 101 | mut_map["topsecret"]["none string"].clone().unwrap(), 102 | "None string" 103 | ); 104 | mut_map.clear(); 105 | config2.clear(); 106 | assert_eq!(config.get_map_ref(), config2.get_map_ref()); 107 | 108 | config.load("tests/test.ini")?; 109 | config.read_and_append("defaultvalues=somenewvalue".to_owned())?; 110 | assert_eq!( 111 | config.get("default", "defaultvalues").unwrap(), 112 | "somenewvalue" 113 | ); 114 | assert_eq!( 115 | config.get("topsecret", "KFC").unwrap(), 116 | "the secret herb is orega-" 117 | ); 118 | 119 | let mut config3 = config.clone(); 120 | let mut_map = config3.get_mut_map(); 121 | mut_map.clear(); 122 | config3.load("tests/test.ini")?; 123 | config3.load_and_append("tests/test_more.ini")?; 124 | assert_eq!( 125 | config3.get("default", "defaultvalues").unwrap(), 126 | "overwritten" 127 | ); 128 | assert_eq!(config3.get("topsecret", "KFC").unwrap(), "redacted"); 129 | // spacing -> indented exists in tests/test.ini, but not tests/test_more.ini 130 | assert_eq!(config3.get("spacing", "indented").unwrap(), "indented"); 131 | assert!(!config3.getbool("values", "Bool")?.unwrap()); 132 | 133 | Ok(()) 134 | } 135 | 136 | #[test] 137 | #[allow(clippy::approx_constant)] 138 | fn cs() -> Result<(), Box> { 139 | let mut config = Ini::new_cs(); 140 | let map = config.load("tests/test.ini")?; 141 | config.set_comment_symbols(&[';', '#', '!']); 142 | let inpstring = config.read( 143 | "defaultvalues=defaultvalues 144 | [topsecret] 145 | KFC = the secret herb is orega- 146 | colon:value after colon 147 | Empty string = 148 | None string 149 | Password=[in-brackets] 150 | [ spacing ] 151 | indented=indented 152 | not indented = not indented ;testcomment 153 | !modified comment 154 | [values]#another comment 155 | Bool = True 156 | Boolcoerce = 0 157 | Int = -31415 158 | Uint = 31415 159 | Float = 3.1415" 160 | .to_owned(), 161 | )?; 162 | assert_eq!(map, inpstring); 163 | config.set("default", "defaultvalues", Some("notdefault".to_owned())); 164 | assert_eq!( 165 | config.get("default", "defaultvalues").unwrap(), 166 | "notdefault" 167 | ); 168 | config.setstr("default", "defaultvalues", Some("defaultvalues")); 169 | assert_eq!( 170 | config.get("default", "defaultvalues").unwrap(), 171 | "defaultvalues" 172 | ); 173 | config.setstr("default", "defaultvalues", None); 174 | config.write("output2.ini")?; 175 | let map2 = config.clone().load("output2.ini")?; 176 | assert_eq!(map2, *config.get_map_ref()); 177 | let map3 = config.clone().read(config.writes())?; 178 | assert_eq!(map2, map3); 179 | assert_eq!(config.sections().len(), 4); 180 | assert_eq!(config.get("default", "defaultvalues"), None); 181 | assert_eq!( 182 | config.get("topsecret", "KFC").unwrap(), 183 | "the secret herb is orega-" 184 | ); 185 | assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); 186 | assert_eq!(config.get("topsecret", "None string"), None); 187 | assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); 188 | assert_eq!( 189 | config.get("spacing", "not indented").unwrap(), 190 | "not indented" 191 | ); 192 | assert_eq!( 193 | config.get("topsecret", "colon").unwrap(), 194 | "value after colon" 195 | ); 196 | assert!(config.getbool("values", "Bool")?.unwrap()); 197 | assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); 198 | assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); 199 | assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); 200 | assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); 201 | assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); 202 | assert_eq!( 203 | map["default"]["defaultvalues"].clone().unwrap(), 204 | "defaultvalues" 205 | ); 206 | assert_eq!( 207 | map["topsecret"]["KFC"].clone().unwrap(), 208 | "the secret herb is orega-" 209 | ); 210 | assert_eq!(map["topsecret"]["Empty string"].clone().unwrap(), ""); 211 | assert_eq!(map["topsecret"]["None string"], None); 212 | assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); 213 | assert_eq!( 214 | map["spacing"]["not indented"].clone().unwrap(), 215 | "not indented" 216 | ); 217 | let mut config2 = config.clone(); 218 | let val = config2.remove_key("default", "defaultvalues"); 219 | assert_eq!(val, Some(None)); 220 | assert_eq!(config2.get("default", "defaultvalues"), None); 221 | config2.remove_section("default"); 222 | assert_eq!(config2.get("default", "nope"), None); 223 | let mut_map = config.get_mut_map(); 224 | mut_map.get_mut("topsecret").unwrap().insert( 225 | String::from("none string"), 226 | Some(String::from("None string")), 227 | ); 228 | assert_eq!( 229 | mut_map["topsecret"]["none string"].clone().unwrap(), 230 | "None string" 231 | ); 232 | mut_map.clear(); 233 | config2.clear(); 234 | assert_eq!(config.get_map_ref(), config2.get_map_ref()); 235 | Ok(()) 236 | } 237 | 238 | #[test] 239 | fn ensure_empty_sections_exist() -> Result<(), Box> { 240 | const FILE_CONTENTS: &str = " 241 | [basic_section] 242 | basic_option=basic_value 243 | [empty_section] 244 | "; 245 | 246 | let mut config = Ini::new(); 247 | config.read(FILE_CONTENTS.to_owned())?; 248 | 249 | assert_eq!( 250 | HashSet::from_iter(config.sections().into_iter()), 251 | HashSet::from([String::from("basic_section"), String::from("empty_section")]) 252 | ); 253 | 254 | Ok(()) 255 | } 256 | 257 | #[test] 258 | fn inline_comment_symbols_enabled() -> Result<(), Box> { 259 | const FILE_CONTENTS: &str = " 260 | [basic_section] 261 | ; basic comment 262 | ; comment with space 263 | ! extra_comment= 264 | basic_option=value 265 | basic_with_comment=value ; Simple comment 266 | basic_with_extra_inline=value ! comment 267 | empty_option= 268 | "; 269 | 270 | let mut config = Ini::new(); 271 | config.read(FILE_CONTENTS.to_owned())?; 272 | 273 | assert_eq!( 274 | config.get("basic_section", "basic_option"), 275 | Some(String::from("value")) 276 | ); 277 | assert_eq!( 278 | config.get("basic_section", "basic_with_comment"), 279 | Some(String::from("value")) 280 | ); 281 | assert_eq!( 282 | config.get("basic_section", "basic_with_extra_inline"), 283 | Some(String::from("value ! comment")) 284 | ); 285 | assert_eq!( 286 | config.get("basic_section", "! extra_comment"), 287 | Some(String::from("")) 288 | ); 289 | 290 | assert_eq!( 291 | config.get("basic_section", "empty_option"), 292 | Some(String::from("")) 293 | ); 294 | 295 | config.set_inline_comment_symbols(Some(&['!'])); 296 | 297 | config.read(FILE_CONTENTS.to_owned())?; 298 | 299 | assert_eq!( 300 | config.get("basic_section", "basic_option"), 301 | Some(String::from("value")) 302 | ); 303 | assert_eq!( 304 | config.get("basic_section", "basic_with_comment"), 305 | Some(String::from("value ; Simple comment")) 306 | ); 307 | assert_eq!( 308 | config.get("basic_section", "basic_with_extra_inline"), 309 | Some(String::from("value")) 310 | ); 311 | 312 | config.set_inline_comment_symbols(Some(&[])); 313 | 314 | config.read(FILE_CONTENTS.to_owned())?; 315 | 316 | assert_eq!( 317 | config.get("basic_section", "basic_option"), 318 | Some(String::from("value")) 319 | ); 320 | assert_eq!( 321 | config.get("basic_section", "basic_with_comment"), 322 | Some(String::from("value ; Simple comment")) 323 | ); 324 | assert_eq!( 325 | config.get("basic_section", "basic_with_extra_inline"), 326 | Some(String::from("value ! comment")) 327 | ); 328 | 329 | Ok(()) 330 | } 331 | 332 | #[test] 333 | fn inline_comment_symbols_disabled() -> Result<(), Box> { 334 | use configparser::ini::IniDefault; 335 | 336 | const FILE_CONTENTS: &str = " 337 | [basic_section] 338 | ; basic comment 339 | ; comment with space 340 | basic_option=value 341 | basic_with_comment=value ; Simple comment 342 | basic_with_comment_hash=value # Simple comment 343 | basic_with_extra_inline=value ! comment 344 | empty_option= 345 | "; 346 | 347 | let mut parser_options = IniDefault::default(); 348 | // true tested in inline_comment_symbols_enabled() 349 | parser_options.enable_inline_comments = false; 350 | 351 | let mut config = Ini::new_from_defaults(parser_options); 352 | config.read(FILE_CONTENTS.to_owned())?; 353 | 354 | assert_eq!( 355 | config.get("basic_section", "basic_with_comment"), 356 | Some(String::from("value ; Simple comment")) 357 | ); 358 | assert_eq!( 359 | config.get("basic_section", "basic_with_comment_hash"), 360 | Some(String::from("value # Simple comment")) 361 | ); 362 | assert_eq!( 363 | config.get("basic_section", "basic_with_extra_inline"), 364 | Some(String::from("value ! comment")) 365 | ); 366 | assert_eq!( 367 | config.get("basic_section", "empty_option"), 368 | Some(String::from("")) 369 | ); 370 | 371 | config.set_inline_comment_symbols(Some(&['!'])); 372 | config.read(FILE_CONTENTS.to_owned())?; 373 | 374 | assert_eq!( 375 | config.get("basic_section", "basic_with_comment"), 376 | Some(String::from("value ; Simple comment")) 377 | ); 378 | assert_eq!( 379 | config.get("basic_section", "basic_with_extra_inline"), 380 | Some(String::from("value ! comment")) 381 | ); 382 | 383 | Ok(()) 384 | } 385 | 386 | #[test] 387 | #[cfg(feature = "indexmap")] 388 | fn sort_on_write() -> Result<(), Box> { 389 | let mut config = Ini::new_cs(); 390 | config.load("tests/test.ini")?; 391 | 392 | assert_eq!( 393 | config.writes(), 394 | "defaultvalues=defaultvalues 395 | [topsecret] 396 | KFC=the secret herb is orega- 397 | colon=value after colon 398 | Empty string= 399 | None string 400 | Password=[in-brackets] 401 | [spacing] 402 | indented=indented 403 | not indented=not indented 404 | [values] 405 | Bool=True 406 | Boolcoerce=0 407 | Int=-31415 408 | Uint=31415 409 | Float=3.1415 410 | " 411 | ); 412 | 413 | Ok(()) 414 | } 415 | 416 | #[test] 417 | #[cfg(feature = "indexmap")] 418 | fn pretty_writes_result_is_formatted_correctly() -> Result<(), Box> { 419 | use configparser::ini::IniDefault; 420 | 421 | const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues 422 | [topsecret] 423 | KFC=the secret herb is orega- 424 | Empty string= 425 | None string 426 | Password=[in-brackets] 427 | [Section] 428 | Key1: Value1 429 | Key2: this is a haiku 430 | spread across separate lines 431 | a single value 432 | Key3: another value 433 | "; 434 | 435 | let mut ini_defaults = IniDefault::default(); 436 | ini_defaults.case_sensitive = true; 437 | ini_defaults.multiline = true; 438 | let mut config = Ini::new_from_defaults(ini_defaults); 439 | config.read(OUT_FILE_CONTENTS.to_owned())?; 440 | 441 | let mut write_options = WriteOptions::default(); 442 | write_options.space_around_delimiters = true; 443 | write_options.multiline_line_indentation = 2; 444 | write_options.blank_lines_between_sections = 1; 445 | assert_eq!( 446 | config.pretty_writes(&write_options), 447 | "defaultvalues = defaultvalues 448 | 449 | [topsecret] 450 | KFC = the secret herb is orega- 451 | Empty string = 452 | None string 453 | Password = [in-brackets] 454 | 455 | [Section] 456 | Key1 = Value1 457 | Key2 = this is a haiku 458 | spread across separate lines 459 | a single value 460 | Key3 = another value 461 | " 462 | ); 463 | 464 | Ok(()) 465 | } 466 | 467 | #[test] 468 | #[cfg(feature = "indexmap")] 469 | #[cfg(feature = "tokio")] 470 | fn pretty_write_result_is_formatted_correctly() -> Result<(), Box> { 471 | use configparser::ini::IniDefault; 472 | 473 | const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues 474 | [topsecret] 475 | KFC=the secret herb is orega- 476 | Empty string= 477 | None string 478 | Password=[in-brackets] 479 | [Section] 480 | Key1: Value1 481 | Key2: this is a haiku 482 | spread across separate lines 483 | a single value 484 | Key3: another value 485 | "; 486 | 487 | let mut ini_defaults = IniDefault::default(); 488 | ini_defaults.case_sensitive = true; 489 | ini_defaults.multiline = true; 490 | let mut config = Ini::new_from_defaults(ini_defaults); 491 | config.read(OUT_FILE_CONTENTS.to_owned())?; 492 | 493 | let mut write_options = WriteOptions::default(); 494 | write_options.space_around_delimiters = true; 495 | write_options.multiline_line_indentation = 2; 496 | write_options.blank_lines_between_sections = 1; 497 | config.pretty_write("pretty_output.ini", &write_options)?; 498 | 499 | let file_contents = std::fs::read_to_string("pretty_output.ini")?; 500 | assert_eq!( 501 | file_contents, 502 | "defaultvalues = defaultvalues 503 | 504 | [topsecret] 505 | KFC = the secret herb is orega- 506 | Empty string = 507 | None string 508 | Password = [in-brackets] 509 | 510 | [Section] 511 | Key1 = Value1 512 | Key2 = this is a haiku 513 | spread across separate lines 514 | a single value 515 | Key3 = another value 516 | " 517 | ); 518 | 519 | Ok(()) 520 | } 521 | 522 | #[tokio::test] 523 | #[cfg(feature = "indexmap")] 524 | #[cfg(feature = "tokio")] 525 | async fn async_pretty_print_result_is_formatted_correctly() -> Result<(), Box> { 526 | use configparser::ini::IniDefault; 527 | 528 | const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues 529 | [topsecret] 530 | KFC=the secret herb is orega- 531 | Empty string= 532 | None string 533 | Password=[in-brackets] 534 | [Section] 535 | Key1: Value1 536 | Key2: this is a haiku 537 | spread across separate lines 538 | a single value 539 | Key3: another value 540 | "; 541 | 542 | let mut ini_defaults = IniDefault::default(); 543 | ini_defaults.case_sensitive = true; 544 | ini_defaults.multiline = true; 545 | let mut config = Ini::new_from_defaults(ini_defaults); 546 | config.read(OUT_FILE_CONTENTS.to_owned())?; 547 | 548 | let mut write_options = WriteOptions::default(); 549 | write_options.space_around_delimiters = true; 550 | write_options.multiline_line_indentation = 2; 551 | write_options.blank_lines_between_sections = 1; 552 | config 553 | .pretty_write_async("pretty_output_async.ini", &write_options) 554 | .await 555 | .map_err(|e| e.to_string())?; 556 | 557 | let file_contents = std::fs::read_to_string("pretty_output_async.ini")?; 558 | assert_eq!( 559 | file_contents, 560 | "defaultvalues = defaultvalues 561 | 562 | [topsecret] 563 | KFC = the secret herb is orega- 564 | Empty string = 565 | None string 566 | Password = [in-brackets] 567 | 568 | [Section] 569 | Key1 = Value1 570 | Key2 = this is a haiku 571 | spread across separate lines 572 | a single value 573 | Key3 = another value 574 | " 575 | ); 576 | 577 | Ok(()) 578 | } 579 | 580 | #[tokio::test] 581 | #[cfg(feature = "tokio")] 582 | async fn async_load_write() -> Result<(), Box> { 583 | const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues 584 | [topsecret] 585 | KFC = the secret herb is orega- 586 | colon:value after colon 587 | Empty string = 588 | None string 589 | Password=[in-brackets] 590 | [ spacing ] 591 | indented=indented 592 | not indented = not indented ;testcomment 593 | !modified comment 594 | [values]#another comment 595 | Bool = True 596 | Boolcoerce = 0 597 | Int = -31415 598 | Uint = 31415 599 | Float = 3.1415"; 600 | 601 | let mut config = Ini::new(); 602 | config.read(OUT_FILE_CONTENTS.to_owned())?; 603 | config.write("output_sync.ini")?; 604 | 605 | let mut config_async = Ini::new(); 606 | config_async.read(OUT_FILE_CONTENTS.to_owned())?; 607 | config_async 608 | .write_async("output_async.ini") 609 | .await 610 | .map_err(|e| e.to_string())?; 611 | 612 | let mut sync_content = Ini::new(); 613 | sync_content.load("output_sync.ini")?; 614 | 615 | let mut async_content = Ini::new(); 616 | async_content.load_async("output_async.ini").await?; 617 | 618 | assert_eq!(sync_content, async_content); 619 | 620 | Ok(()) 621 | } 622 | 623 | #[tokio::test] 624 | #[cfg(feature = "tokio")] 625 | async fn async_load_and_append() -> Result<(), Box> { 626 | let mut sync_content = Ini::new(); 627 | sync_content.load("tests/test.ini")?; 628 | sync_content.load_and_append("tests/test_more.ini")?; 629 | 630 | let mut async_content = Ini::new(); 631 | async_content.load_async("tests/test.ini").await?; 632 | async_content 633 | .load_and_append_async("tests/test_more.ini") 634 | .await?; 635 | 636 | assert_eq!(sync_content, async_content); 637 | 638 | Ok(()) 639 | } 640 | 641 | #[test] 642 | #[cfg(feature = "indexmap")] 643 | fn multiline_off() -> Result<(), Box> { 644 | let mut config = Ini::new_cs(); 645 | config.load("tests/test_multiline.ini")?; 646 | 647 | let map = config.get_map_ref(); 648 | 649 | let section = map.get("Section").unwrap(); 650 | 651 | assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); 652 | assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); 653 | assert_eq!(config.get("Section", "Key3").unwrap(), "this is a haiku"); 654 | assert!(section.contains_key("spread across separate lines")); 655 | assert!(section.contains_key("a single value")); 656 | 657 | assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); 658 | 659 | assert_eq!( 660 | config.writes(), 661 | "[Section] 662 | Key1=Value1 663 | Key2=Value Two 664 | Key3=this is a haiku 665 | spread across separate lines 666 | a single value 667 | Key4=Four 668 | " 669 | ); 670 | 671 | Ok(()) 672 | } 673 | 674 | #[test] 675 | #[cfg(feature = "indexmap")] 676 | fn multiline_on() -> Result<(), Box> { 677 | let mut config = Ini::new_cs(); 678 | config.set_multiline(true); 679 | config.load("tests/test_multiline.ini")?; 680 | 681 | assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); 682 | assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); 683 | assert_eq!( 684 | config.get("Section", "Key3").unwrap(), 685 | "this is a haiku\nspread across separate lines\n\na single value" 686 | ); 687 | assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); 688 | 689 | assert_eq!( 690 | config.writes(), 691 | "[Section] 692 | Key1=Value1 693 | Key2=Value Two 694 | Key3=this is a haiku 695 | spread across separate lines 696 | 697 | a single value 698 | Key4=Four 699 | " 700 | ); 701 | 702 | Ok(()) 703 | } 704 | 705 | #[test] 706 | #[cfg(feature = "serde")] 707 | fn serde_roundtrip() -> Result<(), Box> { 708 | // 1. Load original from file 709 | let mut original = Ini::new(); 710 | let map1 = original.load("tests/test.ini")?; 711 | 712 | // 2. Serialize to JSON 713 | let json = serde_json::to_string(&original)?; 714 | 715 | // 3. Deserialize back 716 | let deserialized: Ini = serde_json::from_str(&json)?; 717 | let map2 = deserialized 718 | .get_map() 719 | .expect("deserialized map should be non-empty"); 720 | 721 | // 4a. Quick equality check on the entire map 722 | assert_eq!(map1, map2, "entire maps must match"); 723 | 724 | // 4b. Cross-check every section, key, and value 725 | for (section, secmap) in &map1 { 726 | // Ensure section exists 727 | let sec2 = map2 728 | .get(section) 729 | .unwrap_or_else(|| panic!("section `{}` missing", section)); 730 | for (key, val1) in secmap { 731 | let val2 = sec2 732 | .get(key) 733 | .unwrap_or_else(|| panic!("key `{}` missing in section `{}`", key, section)); 734 | assert_eq!( 735 | val1, val2, 736 | "mismatch at [{}] {}: {:?} != {:?}", 737 | section, key, val1, val2 738 | ); 739 | } 740 | } 741 | 742 | Ok(()) 743 | } 744 | 745 | #[test] 746 | #[cfg(all(feature = "serde", feature = "indexmap"))] 747 | fn serde_indexmap_roundtrip() -> Result<(), Box> { 748 | // Same as above but with IndexMap ordering preserved 749 | let mut original = Ini::new(); 750 | let map1 = original.load("tests/test.ini")?; 751 | 752 | let json = serde_json::to_string(&original)?; 753 | let deserialized: Ini = serde_json::from_str(&json)?; 754 | let map2 = deserialized 755 | .get_map() 756 | .expect("deserialized map should be non-empty"); 757 | 758 | // Because IndexMap preserves insertion order, we still use equality 759 | assert_eq!( 760 | map1, map2, 761 | "IndexMap-backed maps must match in content and order" 762 | ); 763 | Ok(()) 764 | } 765 | 766 | #[test] 767 | #[cfg(feature = "serde")] 768 | fn serde_multiline_roundtrip() -> Result<(), Box> { 769 | // 1. Build a case-sensitive Ini with multiline enabled 770 | let mut orig = Ini::new(); 771 | orig.set_multiline(true); 772 | 773 | // 2. Load the multiline fixture 774 | let map1 = orig.load("tests/test_multiline.ini")?; 775 | // 3. Capture the Key3 value before Serde 776 | let before = orig.get("Section", "Key3").unwrap(); 777 | 778 | // 4. Serialize to JSON and back 779 | let json = serde_json::to_string(&orig)?; 780 | let mut deser: Ini = serde_json::from_str(&json)?; 781 | 782 | // 5. Re-enable multiline on the deserialized Ini 783 | deser.set_multiline(true); 784 | let after = deser.get("Section", "Key3").unwrap(); 785 | 786 | // 6. The entire map should match and Key3 must survive intact 787 | let map2 = deser.get_map().unwrap(); 788 | assert_eq!(map1, map2); 789 | assert_eq!(after, before); 790 | 791 | Ok(()) 792 | } 793 | 794 | #[test] 795 | #[cfg(feature = "serde")] 796 | fn serde_case_sensitive_roundtrip() -> Result<(), Box> { 797 | // 1. Load in case-sensitive mode 798 | let mut orig = Ini::new_cs(); 799 | let map1 = orig.load("tests/test.ini")?; 800 | // 2. Check that mixed-case keys work, lowercase doesn't 801 | let v1 = orig.get("default", "defaultvalues").unwrap(); 802 | assert!(orig.get("default", "DefaultValues").is_none()); 803 | 804 | // 3. Serde round-trip 805 | let json = serde_json::to_string(&orig)?; 806 | let deser_plain: Ini = serde_json::from_str(&json)?; 807 | let map2 = deser_plain.get_map().unwrap(); 808 | 809 | // 4. Reconstruct a case-sensitive Ini and inject the map 810 | let mut deser_cs = Ini::new_cs(); 811 | *deser_cs.get_mut_map() = map2; 812 | 813 | // 5. Assert the exact-case key still exists and lowercase still fails 814 | let v2 = deser_cs.get("default", "defaultvalues").unwrap(); 815 | assert_eq!(v2, v1); 816 | assert!(deser_cs.get("default", "DefaultValues").is_none()); 817 | 818 | Ok(()) 819 | } 820 | -------------------------------------------------------------------------------- /src/ini.rs: -------------------------------------------------------------------------------- 1 | //!The ini module provides all the things necessary to load and parse ini-syntax files. The most important of which is the `Ini` struct. 2 | //!See the [implementation](https://docs.rs/configparser/*/configparser/ini/struct.Ini.html) documentation for more details. 3 | #[cfg(feature = "indexmap")] 4 | use indexmap::IndexMap as Map; 5 | #[cfg(feature = "serde")] 6 | use serde::de::{Deserialize, Deserializer}; 7 | #[cfg(feature = "serde")] 8 | use serde::ser::{Serialize, Serializer}; 9 | #[cfg(not(feature = "indexmap"))] 10 | use std::collections::HashMap as Map; 11 | #[cfg(feature = "tokio")] 12 | use tokio::fs as async_fs; 13 | 14 | use std::collections::HashMap; 15 | use std::convert::AsRef; 16 | use std::fmt::Write; 17 | use std::fs; 18 | use std::path::Path; 19 | 20 | ///The `Ini` struct simply contains a nested hashmap of the loaded configuration, the default section header and comment symbols. 21 | ///## Example 22 | ///```rust 23 | ///use configparser::ini::Ini; 24 | /// 25 | ///let mut config = Ini::new(); 26 | ///``` 27 | #[derive(Debug, Clone, Eq, PartialEq, Default)] 28 | #[non_exhaustive] 29 | pub struct Ini { 30 | map: Map>>, 31 | default_section: std::string::String, 32 | comment_symbols: Vec, 33 | inline_comment_symbols: Option>, 34 | delimiters: Vec, 35 | boolean_values: HashMap>, 36 | case_sensitive: bool, 37 | multiline: bool, 38 | enable_inline_comments: bool, 39 | } 40 | 41 | #[cfg(all(feature = "serde", not(feature = "indexmap")))] 42 | impl Serialize for Ini { 43 | fn serialize(&self, serializer: S) -> Result 44 | where 45 | S: Serializer, 46 | { 47 | // Delegate serialization to the internal map only 48 | self.map.serialize(serializer) 49 | } 50 | } 51 | 52 | #[cfg(all(feature = "serde", not(feature = "indexmap")))] 53 | impl<'de> Deserialize<'de> for Ini { 54 | fn deserialize(deserializer: D) -> Result 55 | where 56 | D: Deserializer<'de>, 57 | { 58 | // First, deserialize the raw map 59 | let map = Map::>>::deserialize(deserializer) 60 | .map_err(serde::de::Error::custom)?; 61 | // Build an Ini with defaults, then replace its map 62 | let mut ini = Ini::new(); 63 | ini.map = map; 64 | Ok(ini) 65 | } 66 | } 67 | 68 | #[cfg(all(feature = "serde", feature = "indexmap"))] 69 | impl Serialize for Ini { 70 | fn serialize(&self, serializer: S) -> Result 71 | where 72 | S: Serializer, 73 | { 74 | // Delegate to IndexMap’s Serialize impl 75 | serde::Serialize::serialize(&self.map, serializer) 76 | } 77 | } 78 | 79 | #[cfg(all(feature = "serde", feature = "indexmap"))] 80 | impl<'de> Deserialize<'de> for Ini { 81 | fn deserialize(deserializer: D) -> Result 82 | where 83 | D: Deserializer<'de>, 84 | { 85 | let map = serde::Deserialize::deserialize(deserializer)?; 86 | let mut ini = Ini::new(); 87 | ini.map = map; 88 | Ok(ini) 89 | } 90 | } 91 | 92 | ///The `IniDefault` struct serves as a template to create other `Ini` objects from. It can be used to store and load 93 | ///default properties from different `Ini` objects. 94 | ///## Example 95 | ///```rust 96 | ///use configparser::ini::Ini; 97 | /// 98 | ///let mut config = Ini::new(); 99 | ///let default = config.defaults(); 100 | ///let mut config2 = Ini::new_from_defaults(default); // default gets consumed 101 | ///``` 102 | #[derive(Debug, Clone, Eq, PartialEq)] 103 | #[non_exhaustive] 104 | pub struct IniDefault { 105 | ///Denotes the default section header name. 106 | ///## Example 107 | ///```rust 108 | ///use configparser::ini::Ini; 109 | /// 110 | ///let mut config = Ini::new(); 111 | ///let default = config.defaults(); 112 | ///assert_eq!(default.default_section, "default"); 113 | ///``` 114 | pub default_section: std::string::String, 115 | ///Denotes the set comment symbols for the object. 116 | ///## Example 117 | ///```rust 118 | ///use configparser::ini::Ini; 119 | /// 120 | ///let mut config = Ini::new(); 121 | ///let default = config.defaults(); 122 | ///assert_eq!(default.comment_symbols, vec![';', '#']); 123 | ///``` 124 | pub comment_symbols: Vec, 125 | ///Denotes the set of inline comment symbols for the object. The default of 126 | ///`None` means to fall back to the normal comment symbols. 127 | ///## Example 128 | ///```rust 129 | ///use configparser::ini::Ini; 130 | /// 131 | ///let mut config = Ini::new(); 132 | ///let default = config.defaults(); 133 | ///assert_eq!(default.inline_comment_symbols, None); 134 | ///``` 135 | pub inline_comment_symbols: Option>, 136 | ///Denotes the set delimiters for the key-value pairs. 137 | ///## Example 138 | ///```rust 139 | ///use configparser::ini::Ini; 140 | /// 141 | ///let mut config = Ini::new(); 142 | ///let default = config.defaults(); 143 | ///assert_eq!(default.delimiters, vec!['=', ':']); 144 | ///``` 145 | pub delimiters: Vec, 146 | pub boolean_values: HashMap>, 147 | ///Denotes if the `Ini` object is case-sensitive. 148 | ///## Example 149 | ///```rust 150 | ///use configparser::ini::Ini; 151 | /// 152 | ///let mut config = Ini::new(); 153 | ///let default = config.defaults(); 154 | ///assert_eq!(default.case_sensitive, false); 155 | ///``` 156 | pub case_sensitive: bool, 157 | ///Denotes if the `Ini` object parses multiline strings. 158 | ///## Example 159 | ///```rust 160 | ///use configparser::ini::Ini; 161 | /// 162 | ///let mut config = Ini::new(); 163 | ///let default = config.defaults(); 164 | ///assert_eq!(default.multiline, false); 165 | ///``` 166 | pub multiline: bool, 167 | ///Denotes if the `Ini` object recognizes inline comments. 168 | ///## Example 169 | ///```rust 170 | ///use configparser::ini::Ini; 171 | /// 172 | ///let mut config = Ini::new(); 173 | ///let default = config.defaults(); 174 | ///assert_eq!(default.enable_inline_comments, true); 175 | ///``` 176 | pub enable_inline_comments: bool, 177 | } 178 | 179 | impl Default for IniDefault { 180 | fn default() -> Self { 181 | Self { 182 | default_section: "default".to_owned(), 183 | comment_symbols: vec![';', '#'], 184 | inline_comment_symbols: None, 185 | delimiters: vec!['=', ':'], 186 | multiline: false, 187 | boolean_values: [ 188 | ( 189 | true, 190 | ["true", "yes", "t", "y", "on", "1"] 191 | .iter() 192 | .map(|&s| s.to_owned()) 193 | .collect(), 194 | ), 195 | ( 196 | false, 197 | ["false", "no", "f", "n", "off", "0"] 198 | .iter() 199 | .map(|&s| s.to_owned()) 200 | .collect(), 201 | ), 202 | ] 203 | .iter() 204 | .cloned() 205 | .collect(), 206 | case_sensitive: false, 207 | enable_inline_comments: true, // retain compatibility with previous versions 208 | } 209 | } 210 | } 211 | 212 | /// Use this struct to define formatting options for the `pretty_write` functions. 213 | #[derive(Debug, Clone, Eq, PartialEq)] 214 | #[non_exhaustive] 215 | pub struct WriteOptions { 216 | ///If true then the keys and values will be separated by " = ". In the special case where the value is empty, the 217 | ///line ends with " =". 218 | ///If false then keys and values will be separated by "=". 219 | ///Default is `false`. 220 | ///## Example 221 | ///```rust 222 | ///use configparser::ini::WriteOptions; 223 | /// 224 | ///let mut write_options = WriteOptions::default(); 225 | ///assert_eq!(write_options.space_around_delimiters, false); 226 | ///``` 227 | pub space_around_delimiters: bool, 228 | 229 | ///Defines the number of spaces for indentation of for multiline values. 230 | ///Default is 4 spaces. 231 | ///## Example 232 | ///```rust 233 | ///use configparser::ini::WriteOptions; 234 | /// 235 | ///let mut write_options = WriteOptions::default(); 236 | ///assert_eq!(write_options.multiline_line_indentation, 4); 237 | ///``` 238 | pub multiline_line_indentation: usize, 239 | 240 | ///Defines the number of blank lines between sections. 241 | ///Default is 0. 242 | ///## Example 243 | ///```rust 244 | ///use configparser::ini::WriteOptions; 245 | /// 246 | ///let mut write_options = WriteOptions::default(); 247 | ///assert_eq!(write_options.blank_lines_between_sections, 0); 248 | ///``` 249 | pub blank_lines_between_sections: usize, 250 | } 251 | 252 | impl Default for WriteOptions { 253 | fn default() -> Self { 254 | Self { 255 | space_around_delimiters: false, 256 | multiline_line_indentation: 4, 257 | blank_lines_between_sections: 0, 258 | } 259 | } 260 | } 261 | 262 | impl WriteOptions { 263 | ///Creates a new `WriteOptions` object with the default values. 264 | ///## Example 265 | ///```rust 266 | ///use configparser::ini::WriteOptions; 267 | /// 268 | ///let write_options = WriteOptions::new(); 269 | ///assert_eq!(write_options.space_around_delimiters, false); 270 | ///assert_eq!(write_options.multiline_line_indentation, 4); 271 | ///assert_eq!(write_options.blank_lines_between_sections, 0); 272 | ///``` 273 | ///Returns the struct and stores it in the calling variable. 274 | pub fn new() -> WriteOptions { 275 | WriteOptions::default() 276 | } 277 | 278 | ///Creates a new `WriteOptions` object with the given parameters. 279 | ///## Example 280 | ///```rust 281 | ///use configparser::ini::WriteOptions; 282 | /// 283 | ///let write_options = WriteOptions::new_with_params(true, 2, 1); 284 | ///assert_eq!(write_options.space_around_delimiters, true); 285 | ///assert_eq!(write_options.multiline_line_indentation, 2); 286 | ///assert_eq!(write_options.blank_lines_between_sections, 1); 287 | ///``` 288 | ///Returns the struct and stores it in the calling variable. 289 | pub fn new_with_params( 290 | space_around_delimiters: bool, 291 | multiline_line_indentation: usize, 292 | blank_lines_between_sections: usize, 293 | ) -> WriteOptions { 294 | Self { 295 | space_around_delimiters, 296 | multiline_line_indentation, 297 | blank_lines_between_sections, 298 | } 299 | } 300 | } 301 | 302 | #[cfg(windows)] 303 | const LINE_ENDING: &str = "\r\n"; 304 | #[cfg(not(windows))] 305 | const LINE_ENDING: &str = "\n"; 306 | 307 | impl Ini { 308 | ///Creates a new `Map` of `Map>>` type for the struct. 309 | ///All values in the Map are stored in `String` type. 310 | /// 311 | ///By default, [`std::collections::HashMap`] is used for the Map object. 312 | ///The `indexmap` feature can be used to use an [`indexmap::map::IndexMap`] instead, which 313 | ///allows keeping the insertion order for sections and keys. 314 | /// 315 | ///## Example 316 | ///```rust 317 | ///use configparser::ini::Ini; 318 | /// 319 | ///let mut config = Ini::new(); 320 | ///``` 321 | ///Returns the struct and stores it in the calling variable. 322 | pub fn new() -> Ini { 323 | Ini::new_from_defaults(IniDefault::default()) 324 | } 325 | 326 | ///Creates a new **case-sensitive** `Map` of `Map>>` type for the struct. 327 | ///All values in the Map are stored in `String` type. 328 | ///## Example 329 | ///```rust 330 | ///use configparser::ini::Ini; 331 | /// 332 | ///let mut config = Ini::new_cs(); 333 | ///``` 334 | ///Returns the struct and stores it in the calling variable. 335 | pub fn new_cs() -> Ini { 336 | Ini::new_from_defaults(IniDefault { 337 | case_sensitive: true, 338 | ..Default::default() 339 | }) 340 | } 341 | 342 | ///Creates a new `Ini` with the given defaults from an existing `IniDefault` object. 343 | ///## Example 344 | ///```rust 345 | ///use configparser::ini::Ini; 346 | ///use configparser::ini::IniDefault; 347 | /// 348 | ///let mut default = IniDefault::default(); 349 | ///default.comment_symbols = vec![';']; 350 | ///default.delimiters = vec!['=']; 351 | ///let mut config = Ini::new_from_defaults(default.clone()); 352 | ///// Now, load as usual with new defaults: 353 | ///let map = config.load("tests/test.ini").unwrap(); 354 | ///assert_eq!(config.defaults(), default); 355 | /// 356 | ///``` 357 | pub fn new_from_defaults(defaults: IniDefault) -> Ini { 358 | Ini { 359 | map: Map::new(), 360 | default_section: defaults.default_section, 361 | comment_symbols: defaults.comment_symbols, 362 | inline_comment_symbols: defaults.inline_comment_symbols, 363 | delimiters: defaults.delimiters, 364 | boolean_values: defaults.boolean_values, 365 | case_sensitive: defaults.case_sensitive, 366 | multiline: defaults.multiline, 367 | enable_inline_comments: defaults.enable_inline_comments, 368 | } 369 | } 370 | 371 | ///Fetches the defaults from the current `Ini` object and stores it as a `IniDefault` struct for usage elsewhere. 372 | ///## Example 373 | ///```rust 374 | ///use configparser::ini::Ini; 375 | /// 376 | ///let mut config = Ini::new(); 377 | ///let default = config.defaults(); 378 | ///``` 379 | ///Returns an `IniDefault` object. Keep in mind that it will get borrowed since it has non-`Copy` types. 380 | pub fn defaults(&self) -> IniDefault { 381 | IniDefault { 382 | default_section: self.default_section.to_owned(), 383 | comment_symbols: self.comment_symbols.to_owned(), 384 | inline_comment_symbols: self.inline_comment_symbols.to_owned(), 385 | delimiters: self.delimiters.to_owned(), 386 | boolean_values: self.boolean_values.to_owned(), 387 | case_sensitive: self.case_sensitive, 388 | multiline: self.multiline, 389 | enable_inline_comments: self.enable_inline_comments, 390 | } 391 | } 392 | 393 | ///Takes an `IniDefault` object and stores its properties in the calling `Ini` object. This happens in-place and 394 | ///does not work retroactively, only future operations are affected. 395 | ///## Example 396 | ///```rust 397 | ///use configparser::ini::Ini; 398 | ///use configparser::ini::IniDefault; 399 | /// 400 | ///let mut config = Ini::new(); 401 | ///let mut default = IniDefault::default(); 402 | ///default.case_sensitive = true; 403 | ///// This is equivalent to ini_cs() defaults 404 | ///config.load_defaults(default.clone()); 405 | ///assert_eq!(config.defaults(), default); 406 | ///``` 407 | ///Returns nothing. 408 | pub fn load_defaults(&mut self, defaults: IniDefault) { 409 | self.default_section = defaults.default_section; 410 | self.comment_symbols = defaults.comment_symbols; 411 | self.inline_comment_symbols = defaults.inline_comment_symbols; 412 | self.delimiters = defaults.delimiters; 413 | self.boolean_values = defaults.boolean_values; 414 | self.case_sensitive = defaults.case_sensitive; 415 | } 416 | 417 | ///Sets the default section header to the defined string (the default is `default`). 418 | ///It must be set before `load()` or `read()` is called in order to take effect. 419 | ///## Example 420 | ///```rust 421 | ///use configparser::ini::Ini; 422 | /// 423 | ///let mut config = Ini::new(); 424 | /// 425 | ///config.set_default_section("topsecret"); 426 | ///let map = config.load("tests/test.ini").unwrap(); 427 | ///``` 428 | ///Returns nothing. 429 | pub fn set_default_section(&mut self, section: &str) { 430 | self.default_section = section.to_owned(); 431 | } 432 | 433 | ///Sets the default comment symbols to the defined character slice (the defaults are `;` and `#`). 434 | ///Keep in mind that this will remove the default symbols. It must be set before `load()` or `read()` is called in order to take effect. 435 | ///## Example 436 | ///```rust 437 | ///use configparser::ini::Ini; 438 | /// 439 | ///let mut config = Ini::new(); 440 | ///config.set_comment_symbols(&['!', '#']); 441 | ///let map = config.load("tests/test.ini").unwrap(); 442 | ///``` 443 | ///Returns nothing. 444 | pub fn set_comment_symbols(&mut self, symlist: &[char]) { 445 | self.comment_symbols = symlist.to_vec(); 446 | } 447 | 448 | ///Sets the default inline comment symbols to the defined character slice (the default is `None` which falls back to the normal comment symbols). 449 | ///Keep in mind that this will remove the default symbols. It must be set before `load()` or `read()` is called in order to take effect. 450 | ///## Example 451 | ///```rust 452 | ///use configparser::ini::Ini; 453 | /// 454 | ///let mut config = Ini::new(); 455 | ///config.set_inline_comment_symbols(Some(&['!', '#'])); 456 | ///let map = config.load("tests/test.ini").unwrap(); 457 | ///``` 458 | ///Returns nothing. 459 | pub fn set_inline_comment_symbols(&mut self, symlist: Option<&[char]>) { 460 | self.inline_comment_symbols = symlist.map(|val| val.to_vec()); 461 | } 462 | 463 | ///Sets multiline string support. 464 | ///It must be set before `load()` or `read()` is called in order to take effect. 465 | ///## Example 466 | ///```rust 467 | ///use configparser::ini::Ini; 468 | /// 469 | ///let mut config = Ini::new(); 470 | ///config.set_multiline(true); 471 | ///let map = config.load("tests/test.ini").unwrap(); 472 | ///``` 473 | ///Returns nothing. 474 | pub fn set_multiline(&mut self, multiline: bool) { 475 | self.multiline = multiline; 476 | } 477 | 478 | ///Gets all the sections of the currently-stored `Map` in a vector. 479 | ///## Example 480 | ///```rust 481 | ///use configparser::ini::Ini; 482 | /// 483 | ///let mut config = Ini::new(); 484 | ///config.load("tests/test.ini"); 485 | ///let sections = config.sections(); 486 | ///``` 487 | ///Returns `Vec`. 488 | pub fn sections(&self) -> Vec { 489 | self.map.keys().cloned().collect() 490 | } 491 | 492 | ///Loads a file from a defined path, parses it and puts the hashmap into our struct. 493 | ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. 494 | ///## Example 495 | ///```rust 496 | ///use configparser::ini::Ini; 497 | /// 498 | ///let mut config = Ini::new(); 499 | ///let map = config.load("tests/test.ini").unwrap(); // we can get a clone like this, or just store it 500 | /////Then, we can use standard hashmap functions like: 501 | ///let values = map.get("values").unwrap(); 502 | ///``` 503 | ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. 504 | ///Use `get_mut_map()` if you want a mutable reference. 505 | pub fn load>( 506 | &mut self, 507 | path: T, 508 | ) -> Result>>, String> { 509 | self.map = match self.parse(match fs::read_to_string(&path) { 510 | Err(why) => { 511 | return Err(format!( 512 | "couldn't read {}: {}", 513 | &path.as_ref().display(), 514 | why 515 | )); 516 | } 517 | Ok(s) => s, 518 | }) { 519 | Err(why) => { 520 | return Err(format!( 521 | "couldn't read {}: {}", 522 | &path.as_ref().display(), 523 | why 524 | )); 525 | } 526 | Ok(map) => map, 527 | }; 528 | Ok(self.map.clone()) 529 | } 530 | 531 | ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. 532 | ///While `load()` will clear the existing `Map`, `load_and_append()` applies the new values on top of 533 | ///the existing hashmap, preserving previous values. 534 | ///## Example 535 | ///```rust 536 | ///use configparser::ini::Ini; 537 | /// 538 | ///let mut config = Ini::new(); 539 | ///config.load("tests/test.ini").unwrap(); 540 | ///config.load_and_append("tests/sys_cfg.ini").ok(); // we don't have to worry if this doesn't succeed 541 | ///config.load_and_append("tests/user_cfg.ini").ok(); // we don't have to worry if this doesn't succeed 542 | ///let map = config.get_map().unwrap(); 543 | /////Then, we can use standard hashmap functions like: 544 | ///let values = map.get("values").unwrap(); 545 | ///``` 546 | ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. 547 | ///Use `get_mut_map()` if you want a mutable reference. 548 | pub fn load_and_append>( 549 | &mut self, 550 | path: T, 551 | ) -> Result>>, String> { 552 | let loaded = match self.parse(match fs::read_to_string(&path) { 553 | Err(why) => { 554 | return Err(format!( 555 | "couldn't read {}: {}", 556 | &path.as_ref().display(), 557 | why 558 | )); 559 | } 560 | Ok(s) => s, 561 | }) { 562 | Err(why) => { 563 | return Err(format!( 564 | "couldn't read {}: {}", 565 | &path.as_ref().display(), 566 | why 567 | )); 568 | } 569 | Ok(map) => map, 570 | }; 571 | 572 | for (section, section_map) in loaded.iter() { 573 | self.map 574 | .entry(section.clone()) 575 | .or_default() 576 | .extend(section_map.clone()); 577 | } 578 | 579 | Ok(self.map.clone()) 580 | } 581 | 582 | ///Reads an input string, parses it and puts the hashmap into our struct. 583 | ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. 584 | ///## Example 585 | ///```rust 586 | ///use configparser::ini::Ini; 587 | /// 588 | ///let mut config = Ini::new(); 589 | ///let map = match config.read(String::from( 590 | /// "[2000s] 591 | /// 2020 = bad")) { 592 | /// Err(why) => panic!("{}", why), 593 | /// Ok(inner) => inner 594 | ///}; 595 | ///let this_year = map["2000s"]["2020"].clone().unwrap(); 596 | ///assert_eq!(this_year, "bad"); // value accessible! 597 | ///``` 598 | ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. 599 | ///Use `get_mut_map()` if you want a mutable reference. 600 | pub fn read( 601 | &mut self, 602 | input: String, 603 | ) -> Result>>, String> { 604 | self.map = self.parse(input)?; 605 | Ok(self.map.clone()) 606 | } 607 | 608 | ///Reads an input string, parses it and applies it to the existing hashmap in our struct. 609 | ///While `read()` and `load()` will clear the existing `Map`, `read_and_append()` applies the new 610 | ///values on top of the existing hashmap, preserving previous values. 611 | ///## Example 612 | ///```rust 613 | ///use configparser::ini::Ini; 614 | /// 615 | ///let mut config = Ini::new(); 616 | ///if let Err(why) = config.read(String::from( 617 | /// "[2000s] 618 | /// 2020 = bad 619 | /// 2023 = better")) { 620 | /// panic!("{}", why); 621 | ///}; 622 | ///if let Err(why) = config.read_and_append(String::from( 623 | /// "[2000s] 624 | /// 2020 = terrible")) { 625 | /// panic!("{}", why); 626 | ///}; 627 | ///let map = config.get_map().unwrap(); 628 | ///let few_years_ago = map["2000s"]["2020"].clone().unwrap(); 629 | ///let this_year = map["2000s"]["2023"].clone().unwrap(); 630 | ///assert_eq!(few_years_ago, "terrible"); // value updated! 631 | ///assert_eq!(this_year, "better"); // keeps old values! 632 | ///``` 633 | ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. 634 | ///Use `get_mut_map()` if you want a mutable reference. 635 | pub fn read_and_append( 636 | &mut self, 637 | input: String, 638 | ) -> Result>>, String> { 639 | let loaded = self.parse(input)?; 640 | 641 | for (section, section_map) in loaded.iter() { 642 | self.map 643 | .entry(section.clone()) 644 | .or_default() 645 | .extend(section_map.clone()); 646 | } 647 | 648 | Ok(self.map.clone()) 649 | } 650 | 651 | ///Writes the current configuation to the specified path using default formatting. 652 | ///If a file is not present then it is automatically created for you. If a file already exists then it is overwritten. 653 | ///## Example 654 | ///```rust 655 | ///use configparser::ini::Ini; 656 | /// 657 | ///fn main() -> std::io::Result<()> { 658 | /// let mut config = Ini::new(); 659 | /// config.read(String::from( 660 | /// "[2000s] 661 | /// 2020 = bad")); 662 | /// config.write("output.ini") 663 | ///} 664 | ///``` 665 | ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. 666 | pub fn write>(&self, path: T) -> std::io::Result<()> { 667 | fs::write(path.as_ref(), self.unparse(&WriteOptions::default())) 668 | } 669 | 670 | ///Writes the current configuation to the specified path using the given formatting options. 671 | ///If a file is not present then it is automatically created for you. If a file already exists then it is overwritten. 672 | ///## Example 673 | ///```rust 674 | ///use configparser::ini::{Ini, WriteOptions}; 675 | /// 676 | ///fn main() -> std::io::Result<()> { 677 | /// let mut write_options = WriteOptions::default(); 678 | /// write_options.space_around_delimiters = true; 679 | /// write_options.multiline_line_indentation = 2; 680 | /// write_options.blank_lines_between_sections = 1; 681 | /// 682 | /// let mut config = Ini::new(); 683 | /// config.read(String::from( 684 | /// "[2000s] 685 | /// 2020 = bad")); 686 | /// config.pretty_write("output.ini", &write_options) 687 | ///} 688 | ///``` 689 | ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. 690 | pub fn pretty_write>( 691 | &self, 692 | path: T, 693 | write_options: &WriteOptions, 694 | ) -> std::io::Result<()> { 695 | fs::write(path.as_ref(), self.unparse(write_options)) 696 | } 697 | 698 | ///Returns a string with the current configuration formatted with valid ini-syntax using default formatting. 699 | ///This is always safe since the configuration is validated during parsing. 700 | ///## Example 701 | ///```rust 702 | ///use configparser::ini::Ini; 703 | /// 704 | ///let mut config = Ini::new(); 705 | ///config.read(String::from( 706 | /// "[2000s] 707 | /// 2020 = bad")); 708 | ///let outstring = config.writes(); 709 | ///``` 710 | ///Returns a `String` type contatining the ini-syntax file. 711 | pub fn writes(&self) -> String { 712 | self.unparse(&WriteOptions::default()) 713 | } 714 | 715 | ///Returns a string with the current configuration formatted with valid ini-syntax using the given formatting options. 716 | ///This is always safe since the configuration is validated during parsing. 717 | ///## Example 718 | ///```rust 719 | ///use configparser::ini::{Ini, WriteOptions}; 720 | /// 721 | ///let mut write_options = WriteOptions::default(); 722 | ///write_options.space_around_delimiters = true; 723 | ///write_options.multiline_line_indentation = 2; 724 | ///write_options.blank_lines_between_sections = 1; 725 | /// 726 | ///let mut config = Ini::new(); 727 | ///config.read(String::from( 728 | /// "[2000s] 729 | /// 2020 = bad")); 730 | ///let outstring = config.pretty_writes(&write_options); 731 | ///``` 732 | ///Returns a `String` type contatining the ini-syntax file. 733 | pub fn pretty_writes(&self, write_options: &WriteOptions) -> String { 734 | self.unparse(write_options) 735 | } 736 | 737 | ///Private function that converts the currently stored configuration into a valid ini-syntax string. 738 | fn unparse(&self, write_options: &WriteOptions) -> String { 739 | // push key/value pairs in outmap to out string. 740 | fn unparse_key_values( 741 | out: &mut String, 742 | outmap: &Map>, 743 | multiline: bool, 744 | space_around_delimiters: bool, 745 | indent: usize, 746 | ) { 747 | let delimiter = if space_around_delimiters { " = " } else { "=" }; 748 | for (key, val) in outmap.iter() { 749 | out.push_str(key); 750 | 751 | if let Some(value) = val { 752 | if value.is_empty() { 753 | out.push_str(delimiter.trim_end()); 754 | } else { 755 | out.push_str(delimiter); 756 | } 757 | 758 | if multiline { 759 | let mut lines = value.lines(); 760 | 761 | out.push_str(lines.next().unwrap_or_default()); 762 | 763 | for line in lines { 764 | out.push_str(LINE_ENDING); 765 | if !line.is_empty() { 766 | out.push_str(" ".repeat(indent).as_ref()); 767 | out.push_str(line); 768 | } 769 | } 770 | } else { 771 | out.push_str(value); 772 | } 773 | } 774 | 775 | out.push_str(LINE_ENDING); 776 | } 777 | } 778 | 779 | let line_endings = LINE_ENDING.repeat(write_options.blank_lines_between_sections); 780 | let mut out = String::new(); 781 | 782 | if let Some(defaultmap) = self.map.get(&self.default_section) { 783 | unparse_key_values( 784 | &mut out, 785 | defaultmap, 786 | self.multiline, 787 | write_options.space_around_delimiters, 788 | write_options.multiline_line_indentation, 789 | ); 790 | } 791 | 792 | let mut is_first = true; 793 | for (section, secmap) in self.map.iter() { 794 | if !is_first { 795 | out.push_str(line_endings.as_ref()); 796 | } 797 | if section != &self.default_section { 798 | write!(out, "[{}]", section).unwrap(); 799 | out.push_str(LINE_ENDING); 800 | unparse_key_values( 801 | &mut out, 802 | secmap, 803 | self.multiline, 804 | write_options.space_around_delimiters, 805 | write_options.multiline_line_indentation, 806 | ); 807 | } 808 | is_first = false; 809 | } 810 | out 811 | } 812 | 813 | ///Private function that parses ini-style syntax into a Map. 814 | fn parse(&self, input: String) -> Result>>, String> { 815 | let inline_comment_symbols: &[char] = self 816 | .inline_comment_symbols 817 | .as_deref() 818 | .unwrap_or_else(|| self.comment_symbols.as_ref()); 819 | let mut map: Map>> = Map::new(); 820 | let mut section = self.default_section.clone(); 821 | let mut current_key: Option = None; 822 | 823 | let caser = |val: &str| { 824 | if self.case_sensitive { 825 | val.to_owned() 826 | } else { 827 | val.to_lowercase() 828 | } 829 | }; 830 | 831 | // Track blank lines to preserve them in multiline values. 832 | let mut blank_lines = 0usize; 833 | 834 | for (num, raw_line) in input.lines().enumerate() { 835 | let line = raw_line.trim(); 836 | 837 | // If the line is _just_ a comment, skip it entirely. 838 | let line = match line.find(|c: char| self.comment_symbols.contains(&c)) { 839 | Some(0) => continue, 840 | Some(_) | None => line, 841 | }; 842 | 843 | let line = line.trim(); 844 | 845 | // Skip empty lines, but keep track of them for multiline values. 846 | if line.is_empty() { 847 | blank_lines += 1; 848 | continue; 849 | } 850 | 851 | let line = if self.enable_inline_comments { 852 | match line.find(|c: char| inline_comment_symbols.contains(&c)) { 853 | Some(idx) => &line[..idx], 854 | None => line, 855 | } 856 | } else { 857 | line 858 | }; 859 | 860 | let trimmed = line.trim(); 861 | 862 | match (trimmed.find('['), trimmed.rfind(']')) { 863 | (Some(0), Some(end)) => { 864 | section = caser(trimmed[1..end].trim()); 865 | 866 | map.entry(section.clone()).or_default(); 867 | 868 | continue; 869 | } 870 | (Some(0), None) => { 871 | return Err(format!( 872 | "line {}: Found opening bracket for section name but no closing bracket", 873 | num 874 | )); 875 | } 876 | _ => {} 877 | } 878 | 879 | if raw_line.starts_with(char::is_whitespace) && self.multiline { 880 | let key = match current_key.as_ref() { 881 | Some(x) => x, 882 | None => { 883 | return Err(format!( 884 | "line {}: Started with indentation but there is no current entry", 885 | num, 886 | )); 887 | } 888 | }; 889 | 890 | let valmap = map.entry(section.clone()).or_default(); 891 | 892 | let val = valmap 893 | .entry(key.clone()) 894 | .or_insert_with(|| Some(String::new())); 895 | 896 | match val { 897 | Some(s) => { 898 | for _ in 0..blank_lines { 899 | s.push_str(LINE_ENDING); 900 | } 901 | s.push_str(LINE_ENDING); 902 | s.push_str(trimmed); 903 | } 904 | None => { 905 | let mut s = String::with_capacity( 906 | (blank_lines + 1) * LINE_ENDING.len() + trimmed.len(), 907 | ); 908 | for _ in 0..blank_lines { 909 | s.push_str(LINE_ENDING); 910 | } 911 | s.push_str(LINE_ENDING); 912 | s.push_str(trimmed); 913 | *val = Some(s); 914 | } 915 | } 916 | } else { 917 | let valmap = map.entry(section.clone()).or_default(); 918 | 919 | match trimmed.find(&self.delimiters[..]) { 920 | Some(delimiter) => { 921 | let key = caser(trimmed[..delimiter].trim()); 922 | 923 | if key.is_empty() { 924 | return Err(format!("line {}:{}: Key cannot be empty", num, delimiter)); 925 | } else { 926 | current_key = Some(key.clone()); 927 | 928 | let value = trimmed[delimiter + 1..].trim().to_owned(); 929 | 930 | valmap.insert(key, Some(value)); 931 | } 932 | } 933 | None => { 934 | let key = caser(trimmed); 935 | current_key = Some(key.clone()); 936 | 937 | valmap.insert(key, None); 938 | } 939 | } 940 | } 941 | 942 | blank_lines = 0; 943 | } 944 | 945 | Ok(map) 946 | } 947 | 948 | ///Private function that cases things automatically depending on the set variable. 949 | fn autocase(&self, section: &str, key: &str) -> (String, String) { 950 | if self.case_sensitive { 951 | (section.to_owned(), key.to_owned()) 952 | } else { 953 | (section.to_lowercase(), key.to_lowercase()) 954 | } 955 | } 956 | 957 | ///Returns a clone of the stored value from the key stored in the defined section. 958 | ///Unlike accessing the map directly, `get()` can process your input to make case-insensitive access *if* the 959 | ///default constructor is used. 960 | ///All `get` functions will do this automatically under the hood. 961 | ///## Example 962 | ///```rust 963 | ///use configparser::ini::Ini; 964 | /// 965 | ///let mut config = Ini::new(); 966 | ///config.load("tests/test.ini"); 967 | ///let value = config.get("default", "defaultvalues").unwrap(); 968 | ///assert_eq!(value, String::from("defaultvalues")); 969 | ///``` 970 | ///Returns `Some(value)` of type `String` if value is found or else returns `None`. 971 | pub fn get(&self, section: &str, key: &str) -> Option { 972 | let (section, key) = self.autocase(section, key); 973 | self.map.get(§ion)?.get(&key)?.clone() 974 | } 975 | 976 | ///Parses the stored value from the key stored in the defined section to a `bool`. 977 | ///For ease of use, the function converts the type case-insensitively (`true` == `True`). 978 | ///## Example 979 | ///```rust 980 | ///use configparser::ini::Ini; 981 | /// 982 | ///let mut config = Ini::new(); 983 | ///config.load("tests/test.ini"); 984 | ///let value = config.getbool("values", "bool").unwrap().unwrap(); 985 | ///assert!(value); // value accessible! 986 | ///``` 987 | ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. 988 | ///If the parsing fails, it returns an `Err(string)`. 989 | pub fn getbool(&self, section: &str, key: &str) -> Result, String> { 990 | let (section, key) = self.autocase(section, key); 991 | match self.map.get(§ion) { 992 | Some(secmap) => match secmap.get(&key) { 993 | Some(val) => match val { 994 | Some(inner) => match inner.to_lowercase().parse::() { 995 | Err(why) => Err(why.to_string()), 996 | Ok(boolean) => Ok(Some(boolean)), 997 | }, 998 | None => Ok(None), 999 | }, 1000 | None => Ok(None), 1001 | }, 1002 | None => Ok(None), 1003 | } 1004 | } 1005 | 1006 | ///Parses the stored value from the key stored in the defined section to a `bool`. For ease of use, the function converts the type coerces a match. 1007 | ///It attempts to case-insenstively find `true`, `yes`, `t`, `y`, `1` and `on` to parse it as `True`. 1008 | ///Similarly it attempts to case-insensitvely find `false`, `no`, `f`, `n`, `0` and `off` to parse it as `False`. 1009 | ///## Example 1010 | ///```rust 1011 | ///use configparser::ini::Ini; 1012 | /// 1013 | ///let mut config = Ini::new(); 1014 | ///config.load("tests/test.ini"); 1015 | ///let value = config.getboolcoerce("values", "boolcoerce").unwrap().unwrap(); 1016 | ///assert!(!value); // value accessible! 1017 | ///``` 1018 | ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. 1019 | ///If the parsing fails, it returns an `Err(string)`. 1020 | pub fn getboolcoerce(&self, section: &str, key: &str) -> Result, String> { 1021 | let (section, key) = self.autocase(section, key); 1022 | match self.map.get(§ion) { 1023 | Some(secmap) => match secmap.get(&key) { 1024 | Some(val) => match val { 1025 | Some(inner) => { 1026 | let boolval = &inner.to_lowercase()[..]; 1027 | if self 1028 | .boolean_values 1029 | .get(&true) 1030 | .unwrap() 1031 | .iter() 1032 | .any(|elem| elem == boolval) 1033 | { 1034 | Ok(Some(true)) 1035 | } else if self 1036 | .boolean_values 1037 | .get(&false) 1038 | .unwrap() 1039 | .iter() 1040 | .any(|elem| elem == boolval) 1041 | { 1042 | Ok(Some(false)) 1043 | } else { 1044 | Err(format!( 1045 | "Unable to parse value into bool at {}:{}", 1046 | section, key 1047 | )) 1048 | } 1049 | } 1050 | None => Ok(None), 1051 | }, 1052 | None => Ok(None), 1053 | }, 1054 | None => Ok(None), 1055 | } 1056 | } 1057 | 1058 | ///Parses the stored value from the key stored in the defined section to an `i64`. 1059 | ///## Example 1060 | ///```rust 1061 | ///use configparser::ini::Ini; 1062 | /// 1063 | ///let mut config = Ini::new(); 1064 | ///config.load("tests/test.ini"); 1065 | ///let value = config.getint("values", "int").unwrap().unwrap(); 1066 | ///assert_eq!(value, -31415); // value accessible! 1067 | ///``` 1068 | ///Returns `Ok(Some(value))` of type `i64` if value is found or else returns `Ok(None)`. 1069 | ///If the parsing fails, it returns an `Err(string)`. 1070 | pub fn getint(&self, section: &str, key: &str) -> Result, String> { 1071 | let (section, key) = self.autocase(section, key); 1072 | match self.map.get(§ion) { 1073 | Some(secmap) => match secmap.get(&key) { 1074 | Some(val) => match val { 1075 | Some(inner) => match inner.parse::() { 1076 | Err(why) => Err(why.to_string()), 1077 | Ok(int) => Ok(Some(int)), 1078 | }, 1079 | None => Ok(None), 1080 | }, 1081 | None => Ok(None), 1082 | }, 1083 | None => Ok(None), 1084 | } 1085 | } 1086 | 1087 | ///Parses the stored value from the key stored in the defined section to a `u64`. 1088 | ///## Example 1089 | ///```rust 1090 | ///use configparser::ini::Ini; 1091 | /// 1092 | ///let mut config = Ini::new(); 1093 | ///config.load("tests/test.ini"); 1094 | ///let value = config.getint("values", "Uint").unwrap().unwrap(); 1095 | ///assert_eq!(value, 31415); // value accessible! 1096 | ///``` 1097 | ///Returns `Ok(Some(value))` of type `u64` if value is found or else returns `Ok(None)`. 1098 | ///If the parsing fails, it returns an `Err(string)`. 1099 | pub fn getuint(&self, section: &str, key: &str) -> Result, String> { 1100 | let (section, key) = self.autocase(section, key); 1101 | match self.map.get(§ion) { 1102 | Some(secmap) => match secmap.get(&key) { 1103 | Some(val) => match val { 1104 | Some(inner) => match inner.parse::() { 1105 | Err(why) => Err(why.to_string()), 1106 | Ok(uint) => Ok(Some(uint)), 1107 | }, 1108 | None => Ok(None), 1109 | }, 1110 | None => Ok(None), 1111 | }, 1112 | None => Ok(None), 1113 | } 1114 | } 1115 | 1116 | ///Parses the stored value from the key stored in the defined section to a `f64`. 1117 | ///## Example 1118 | ///```rust 1119 | ///use configparser::ini::Ini; 1120 | /// 1121 | ///let mut config = Ini::new(); 1122 | ///config.load("tests/test.ini"); 1123 | ///let value = config.getfloat("values", "float").unwrap().unwrap(); 1124 | ///assert_eq!(value, 3.1415); // value accessible! 1125 | ///``` 1126 | ///Returns `Ok(Some(value))` of type `f64` if value is found or else returns `Ok(None)`. 1127 | ///If the parsing fails, it returns an `Err(string)`. 1128 | pub fn getfloat(&self, section: &str, key: &str) -> Result, String> { 1129 | let (section, key) = self.autocase(section, key); 1130 | match self.map.get(§ion) { 1131 | Some(secmap) => match secmap.get(&key) { 1132 | Some(val) => match val { 1133 | Some(inner) => match inner.parse::() { 1134 | Err(why) => Err(why.to_string()), 1135 | Ok(float) => Ok(Some(float)), 1136 | }, 1137 | None => Ok(None), 1138 | }, 1139 | None => Ok(None), 1140 | }, 1141 | None => Ok(None), 1142 | } 1143 | } 1144 | 1145 | ///Returns a clone of the `Map` stored in our struct. 1146 | ///## Example 1147 | ///```rust 1148 | ///use configparser::ini::Ini; 1149 | /// 1150 | ///let mut config = Ini::new(); 1151 | ///config.read(String::from( 1152 | /// "[section] 1153 | /// key=values")); 1154 | ///let map = config.get_map().unwrap(); 1155 | ///assert_eq!(map, *config.get_map_ref()); // the cloned map is basically a snapshot that you own 1156 | ///``` 1157 | ///Returns `Some(map)` if map is non-empty or else returns `None`. 1158 | ///Similar to `load()` but returns an `Option` type with the currently stored `Map`. 1159 | pub fn get_map(&self) -> Option>>> { 1160 | if self.map.is_empty() { 1161 | None 1162 | } else { 1163 | Some(self.map.clone()) 1164 | } 1165 | } 1166 | 1167 | ///Returns an immutable reference to the `Map` stored in our struct. 1168 | ///## Example 1169 | ///```rust 1170 | ///use configparser::ini::Ini; 1171 | /// 1172 | ///let mut config = Ini::new(); 1173 | ///let mapclone = config.read(String::from 1174 | /// ("[topsecrets] 1175 | /// Valueless key")).unwrap(); 1176 | /////Think of the clone as being a snapshot at a point of time while the reference always points to the current configuration. 1177 | ///assert_eq!(*config.get_map_ref(), mapclone); // same as expected. 1178 | ///``` 1179 | ///If you just need to definitely mutate the map, use `get_mut_map()` instead. Alternatively, you can generate a snapshot by getting a clone 1180 | ///with `get_map()` and work with that. 1181 | pub fn get_map_ref(&self) -> &Map>> { 1182 | &self.map 1183 | } 1184 | 1185 | ///Returns a mutable reference to the `Map` stored in our struct. 1186 | ///## Example 1187 | ///```rust 1188 | ///use configparser::ini::Ini; 1189 | /// 1190 | ///let mut config = Ini::new(); 1191 | ///config.read(String::from 1192 | /// ("[topsecrets] 1193 | /// Valueless key")); 1194 | /////We can then get the mutable map and insert a value like: 1195 | ///config.get_mut_map().get_mut("topsecrets").unwrap().insert(String::from("nuclear launch codes"), None); 1196 | ///assert_eq!(config.get("topsecrets", "nuclear launch codes"), None); // inserted successfully! 1197 | ///``` 1198 | ///If you just need to access the map without mutating, use `get_map_ref()` or make a clone with `get_map()` instead. 1199 | pub fn get_mut_map(&mut self) -> &mut Map>> { 1200 | &mut self.map 1201 | } 1202 | 1203 | ///Sets an `Option` in the `Map` stored in our struct. If a particular section or key does not exist, it will be automatically created. 1204 | ///An existing value in the map will be overwritten. You can also set `None` safely. 1205 | ///## Example 1206 | ///```rust 1207 | ///use configparser::ini::Ini; 1208 | /// 1209 | ///let mut config = Ini::new(); 1210 | ///config.read(String::from( 1211 | /// "[section] 1212 | /// key=value")); 1213 | ///let key_value = String::from("value"); 1214 | ///config.set("section", "key", Some(key_value)); 1215 | ///config.set("section", "key", None); // also valid! 1216 | ///assert_eq!(config.get("section", "key"), None); // correct! 1217 | ///``` 1218 | ///Returns `None` if there is no existing value, else returns `Some(Option)`, with the existing value being the wrapped `Option`. 1219 | ///If you want to insert using a string literal, use `setstr()` instead. 1220 | pub fn set( 1221 | &mut self, 1222 | section: &str, 1223 | key: &str, 1224 | value: Option, 1225 | ) -> Option> { 1226 | let (section, key) = self.autocase(section, key); 1227 | match self.map.get_mut(§ion) { 1228 | Some(secmap) => secmap.insert(key, value), 1229 | None => { 1230 | let mut valmap: Map> = Map::new(); 1231 | valmap.insert(key, value); 1232 | self.map.insert(section, valmap); 1233 | None 1234 | } 1235 | } 1236 | } 1237 | 1238 | ///Sets an `Option<&str>` in the `Map` stored in our struct. If a particular section or key does not exist, it will be automatically created. 1239 | ///An existing value in the map will be overwritten. You can also set `None` safely. 1240 | ///## Example 1241 | ///```rust 1242 | ///use configparser::ini::Ini; 1243 | /// 1244 | ///let mut config = Ini::new(); 1245 | ///config.read(String::from( 1246 | /// "[section] 1247 | /// key=notvalue")); 1248 | ///config.setstr("section", "key", Some("value")); 1249 | ///config.setstr("section", "key", None); // also valid! 1250 | ///assert_eq!(config.get("section", "key"), None); // correct! 1251 | ///``` 1252 | ///Returns `None` if there is no existing value, else returns `Some(Option)`, with the existing value being the wrapped `Option`. 1253 | ///If you want to insert using a `String`, use `set()` instead. 1254 | pub fn setstr( 1255 | &mut self, 1256 | section: &str, 1257 | key: &str, 1258 | value: Option<&str>, 1259 | ) -> Option> { 1260 | let (section, key) = self.autocase(section, key); 1261 | self.set(§ion, &key, value.map(String::from)) 1262 | } 1263 | 1264 | ///Clears the map, removing all sections and properties from the hashmap. It keeps the allocated memory for reuse. 1265 | ///## Example 1266 | ///```rust 1267 | ///use configparser::ini::Ini; 1268 | /// 1269 | ///let mut config = Ini::new(); 1270 | ///config.read(String::from( 1271 | /// "[section] 1272 | /// key=somevalue")); 1273 | ///config.clear(); 1274 | ///assert!(config.get_map_ref().is_empty()); // our map is empty! 1275 | ///``` 1276 | ///Returns nothing. 1277 | pub fn clear(&mut self) { 1278 | self.map.clear(); 1279 | } 1280 | 1281 | ///Removes a section from the hashmap, returning the properties stored in the section if the section was previously in the map. 1282 | ///## Example 1283 | ///```rust 1284 | ///use configparser::ini::Ini; 1285 | /// 1286 | ///let mut config = Ini::new(); 1287 | ///config.read(String::from( 1288 | /// "[section] 1289 | /// updog=whatsupdog")); 1290 | ///config.remove_section("section"); // this will return a cloned hashmap of the stored property 1291 | ///assert!(config.get_map_ref().is_empty()); // with the last section removed, our map is now empty! 1292 | ///``` 1293 | ///Returns `Some(section_map)` if the section exists or else, `None`. 1294 | pub fn remove_section(&mut self, section: &str) -> Option>> { 1295 | let section = if self.case_sensitive { 1296 | section.to_owned() 1297 | } else { 1298 | section.to_lowercase() 1299 | }; 1300 | #[cfg(not(feature = "indexmap"))] 1301 | { 1302 | self.map.remove(§ion) 1303 | } 1304 | #[cfg(feature = "indexmap")] 1305 | { 1306 | self.map.swap_remove(§ion) 1307 | } 1308 | } 1309 | 1310 | ///Removes a key from a section in the hashmap, returning the value attached to the key if it was previously in the map. 1311 | ///## Example 1312 | ///```rust 1313 | ///use configparser::ini::Ini; 1314 | /// 1315 | ///let mut config = Ini::new(); 1316 | ///config.read(String::from( 1317 | /// "[section] 1318 | /// updog=whatsupdog 1319 | /// [anothersection] 1320 | /// updog=differentdog")); 1321 | ///let val = config.remove_key("anothersection", "updog").unwrap().unwrap(); 1322 | ///assert_eq!(val, String::from("differentdog")); // with the last section removed, our map is now empty! 1323 | ///``` 1324 | ///Returns `Some(Option)` if the value exists or else, `None`. 1325 | pub fn remove_key(&mut self, section: &str, key: &str) -> Option> { 1326 | let (section, key) = self.autocase(section, key); 1327 | #[cfg(not(feature = "indexmap"))] 1328 | { 1329 | self.map.get_mut(§ion)?.remove(&key) 1330 | } 1331 | #[cfg(feature = "indexmap")] 1332 | { 1333 | self.map.get_mut(§ion)?.swap_remove(&key) 1334 | } 1335 | } 1336 | } 1337 | 1338 | #[cfg(feature = "tokio")] 1339 | impl Ini { 1340 | ///Loads a file asynchronously from a defined path, parses it and puts the hashmap into our struct. 1341 | ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. 1342 | /// 1343 | ///Usage is similar to `load`, but `.await` must be called after along with the usual async rules. 1344 | /// 1345 | ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. 1346 | ///Use `get_mut_map()` if you want a mutable reference. 1347 | pub async fn load_async>( 1348 | &mut self, 1349 | path: T, 1350 | ) -> Result>>, String> { 1351 | self.map = match self.parse(match async_fs::read_to_string(&path).await { 1352 | Err(why) => { 1353 | return Err(format!( 1354 | "couldn't read {}: {}", 1355 | &path.as_ref().display(), 1356 | why 1357 | )); 1358 | } 1359 | Ok(s) => s, 1360 | }) { 1361 | Err(why) => { 1362 | return Err(format!( 1363 | "couldn't read {}: {}", 1364 | &path.as_ref().display(), 1365 | why 1366 | )); 1367 | } 1368 | Ok(map) => map, 1369 | }; 1370 | Ok(self.map.clone()) 1371 | } 1372 | 1373 | ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. 1374 | ///While `load_async()` will clear the existing `Map`, `load_and_append_async()` applies the new values on top 1375 | ///of the existing hashmap, preserving previous values. 1376 | /// 1377 | ///Usage is similar to `load_and_append`, but `.await` must be called after along with the usual async rules. 1378 | /// 1379 | ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. 1380 | ///Use `get_mut_map()` if you want a mutable reference. 1381 | pub async fn load_and_append_async>( 1382 | &mut self, 1383 | path: T, 1384 | ) -> Result>>, String> { 1385 | let loaded = match self.parse(match async_fs::read_to_string(&path).await { 1386 | Err(why) => { 1387 | return Err(format!( 1388 | "couldn't read {}: {}", 1389 | &path.as_ref().display(), 1390 | why 1391 | )); 1392 | } 1393 | Ok(s) => s, 1394 | }) { 1395 | Err(why) => { 1396 | return Err(format!( 1397 | "couldn't read {}: {}", 1398 | &path.as_ref().display(), 1399 | why 1400 | )); 1401 | } 1402 | Ok(map) => map, 1403 | }; 1404 | 1405 | for (section, section_map) in loaded.iter() { 1406 | self.map 1407 | .entry(section.clone()) 1408 | .or_insert_with(Map::new) 1409 | .extend(section_map.clone()); 1410 | } 1411 | 1412 | Ok(self.map.clone()) 1413 | } 1414 | 1415 | ///Writes the current configuation to the specified path asynchronously using default formatting. If a file is not present, it is automatically created for you, if a file already 1416 | ///exists, it is truncated and the configuration is written to it. 1417 | /// 1418 | ///Usage is the same as `write`, but `.await` must be called after along with the usual async rules. 1419 | /// 1420 | ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. 1421 | pub async fn write_async>(&self, path: T) -> std::io::Result<()> { 1422 | async_fs::write(path.as_ref(), self.unparse(&WriteOptions::default())).await 1423 | } 1424 | 1425 | ///Writes the current configuation to the specified path asynchronously using the given formatting options. If a file is not present, it is automatically created for you, if a file already 1426 | ///exists, it is truncated and the configuration is written to it. 1427 | /// 1428 | ///Usage is the same as `pretty_pretty_write`, but `.await` must be called after along with the usual async rules. 1429 | /// 1430 | ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. 1431 | pub async fn pretty_write_async>( 1432 | &self, 1433 | path: T, 1434 | write_options: &WriteOptions, 1435 | ) -> std::io::Result<()> { 1436 | async_fs::write(path.as_ref(), self.unparse(write_options)).await 1437 | } 1438 | } 1439 | --------------------------------------------------------------------------------