configparser-3.0.3/.cargo_vcs_info.json0000644000000001360000000000100135210ustar { "git": { "sha1": "3be365cf3a5bfd2898f3cb61eea65c6c6a0225c7" }, "path_in_vcs": "" }configparser-3.0.3/.github/workflows/rust.yaml000064400000000000000000000020101046102023000175600ustar 00000000000000name: Rust CI on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: ci: runs-on: ubuntu-latest continue-on-error: ${{ matrix.rust == 'nightly' }} strategy: fail-fast: false matrix: rust: - stable - beta - nightly steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true components: rustfmt - uses: actions-rs/cargo@v1 with: command: build args: --release --all-features -v - uses: actions-rs/cargo@v1 with: command: test args: --all-features -v - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - name: Security audit uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} configparser-3.0.3/.gitignore000075500000000000000000000001471046102023000143060ustar 00000000000000/target .DS_Store Cargo.lock output.ini output2.ini test2.ini .vscode output_async.ini output_sync.ini configparser-3.0.3/CHANGELOG.md000075500000000000000000000065561046102023000141410ustar 00000000000000## Changelog - 0.1.0 (yanked) - First experimental version with only a public-facing load() function. - 0.1.1 - `configparser` module renamed to `ini`. - 0.2.1 - `Ini` struct is added along with file-loading, parsing and hashmap functions. Documentation is added. - 0.2.2 - Fixed docs. - 0.3.0 - Added `get()` for getting values from the map directly. Docs expanded as well. - Mark `ini::load()` for deprecation. - 0.3.1 - Updated docs. - All parameters now trimmed before insertion. - Converted `ini::load()` into a wrapper around `Ini`. - 0.4.0 - Changed `Ini::load()` to return an `Ok(map)` with a clone of the stored `HashMap`. - 0.4.1 - Fixed and added docs. - 0.5.0 (**BETA**) (yanked) - Changelog added. - Support for value-less keys. - `HashMap` values are now `Option` instead of `String` to denote empty values vs. no values. - Documentation greatly improved. - Syntax docs provided. - `new()` and `get()` methods are simplified. - 0.5.1 - Fixed erroneous docs - 0.6.0 (**BETA 2**) - Tests added - `get_map_ref()` and `get_mut_map()` are now added to allow direct `HashMap` index access making things greatly easier. - 0.6.1 (yanked) - Fixed tests - 0.6.2 - Fixed accidental binary delivery increasing crate size by ~20x - 0.7.0 (**BETA 3**) - Handy getter functions introduced such as `getint()`, `getuint()`, `getfloat()`, `getbool()` - Fixed docs - Fixed tests - 0.7.1 - Enable `Eq` and `PartialEq` traits - Improve docs - 0.8.0 (**BETA 4**) - Added feature to set default headers. - Added feature to parse from a `String`. - 0.8.1 - Added support for comments - Improved docs - 0.9.0 (**BETA 5**) - Comment customization is here! (**note:** defaults are now changed to `#` and `;`) - Fixed some docs - Make more docs pass tests - 0.9.1 - Hotfix to change getters to return `Ok(None)` instead of failing parsing for `None` values - 0.9.2 - Added `getboolcoerce()` function to parse more `bool`-like values. - Convert some snippets to doctests. - 0.10.0 (**BETA 6**) - Added `set()` and `setstr()` methods to add section, key, values to the configuration. - Added more test, minor doc fixes. - 0.11.0 (**BETA 7**) - Writing to file is here! (`write()`). - More doctests and docs fixed, next release is planned to be stable. - 0.11.1 - Hotfix to remove hardcoded default section and use set default section. - Enabled auto-trait implementation of `Default` for empty inits. - Added the `sections()` method to get a vector of sections. - 0.12.0 (**BETA 8**) - New function added, `writes()` to support writing configuration to a string. - More doctests passed. - 0.13.0 (**BETA 9**) - New functions added, `clear()` and `remove_section()` to make handling similar to hashmaps. - Docs fixed. On track to stable. - 0.13.1 (yanked) - New function added, `remove_key()` to remove a key from a section - All doctests passing! - 0.13.2 (**FINAL BETA**) - Erroneous docs fixed. - Final release before stable. - 1.0.0 - Dropped support for `ini::load()` - Updated tests - 2.0.0 - **BREAKING** Added Python-esque support for `:` as a delimiter. - :new: Add support for case-sensitive maps with automatic handling under the hood. - :hammer: Fixed buggy setters which went uncaught, to preserve case-insensitive nature. Older changelogs are preserved here, current changelog is present in [README.md](README.md). configparser-3.0.3/CODE_OF_CONDUCT.md000064400000000000000000000121161046102023000151110ustar 00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement, All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. configparser-3.0.3/Cargo.toml0000644000000024720000000000100115240ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "configparser" version = "3.0.3" authors = ["QEDK "] 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." homepage = "https://github.com/QEDK/configparser-rs" documentation = "https://docs.rs/configparser" readme = "README.md" keywords = [ "config", "ini", "settings", "configuration", "parser", ] categories = [ "config", "encoding", "parser-implementations", ] license = "MIT OR LGPL-3.0-or-later" repository = "https://github.com/QEDK/configparser-rs" [dependencies.async-std] version = "1.12.0" optional = true [dependencies.indexmap] version = "2.1.0" optional = true [badges.maintenance] status = "actively-developed" configparser-3.0.3/Cargo.toml.orig000075500000000000000000000016471046102023000152130ustar 00000000000000[package] name = "configparser" version = "3.0.3" authors = ["QEDK "] edition = "2021" 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." homepage = "https://github.com/QEDK/configparser-rs" repository = "https://github.com/QEDK/configparser-rs" documentation = "https://docs.rs/configparser" readme = "README.md" license = "MIT OR LGPL-3.0-or-later" keywords = ["config", "ini", "settings", "configuration", "parser"] categories = ["config", "encoding", "parser-implementations"] [badges] maintenance = { status = "actively-developed" } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] async-std = { version = "1.12.0", optional = true } indexmap = { version = "2.1.0", optional = true } configparser-3.0.3/LICENSE-LGPL000064400000000000000000000167741046102023000140710ustar 00000000000000Copyright (c) 2023 QEDK GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. configparser-3.0.3/LICENSE-MIT000064400000000000000000000020451046102023000137460ustar 00000000000000MIT License Copyright (c) 2023 QEDK Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. configparser-3.0.3/README.md000075500000000000000000000252631046102023000136030ustar 00000000000000# configparser [![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/2023)](https://github.com/QEDK/configparser-rs) 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. You can use this to write Rust programs which can be customized by end users easily. This is a simple configuration parsing utility with no dependencies built on Rust. It is inspired by Python's `configparser`. 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. ## πŸš€ Quick Start A basic `ini`-syntax file (we say ini-syntax files because the files don't need to be necessarily `*.ini`) looks like this: ```INI [DEFAULT] key1 = value1 pizzatime = yes cost = 9 [topsecrets] nuclear launch codes = topsecret [github.com] User = QEDK ``` 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 strings as well as files. ### 🧰 Installation You can install this easily via `cargo` by including it in your `Cargo.toml` file like: ```TOML [dependencies] configparser = "3.0.3" ``` ## βž• Supported datatypes `configparser` does not guess the datatype of values in configuration files and stores everything as strings. However, some datatypes are so common 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()`, `getfloat()` and `getbool()`. The only bit of extra magic involved is that the `getbool()` function will treat boolean values case-insensitively (so `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. ```rust use configparser::ini::Ini; let mut config = Ini::new(); config.read(String::from( "[somesection] someintvalue = 5")); let my_value = config.getint("somesection", "someintvalue").unwrap().unwrap(); assert_eq!(my_value, 5); // value accessible! //You can ofcourse just choose to parse the values yourself: let my_string = String::from("1984"); let my_int = my_string.parse::().unwrap(); ``` ## πŸ“ Supported `ini` file structure 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. Key values can be omitted, in which case the key-value delimiter may also be left out (but this is different from putting a delimiter, we'll explain it later). You can use comment symbols (`;` and `#` to denote comments). This can be configured with the `set_comment_symbols()` method in the API. Keep in mind that key-value pairs or section headers cannot span multiple lines. Owing to how ini files usually are, this means that `[`, `]`, `=`, `:`, `;` and `#` are special symbols by default (this crate will allow you to use `]` sparingly). Let's take for example: ```INI [section headers are case-insensitive by default] [ section headers are case-insensitive by default ] are the section headers above same? = yes sectionheaders_and_keysarestored_in_lowercase? = yes keys_are_also_case_insensitive = Values are case sensitive Case-sensitive_keys_and_sections = using a special constructor you can also use colons : instead of the equal symbol ;anything after a comment symbol is ignored #this is also a comment spaces in keys=allowed ;and everything before this is still valid! spaces in values=allowed as well spaces around the delimiter = also OK [All values are strings] values like this= 0000 or this= 0.999 are they treated as numbers? = no integers, floats and booleans are held as= strings [value-less?] a_valueless_key_has_None this key has an empty string value has Some("") = [indented sections] can_values_be_as_well = True purpose = formatting for readability is_this_same = yes is_this_same=yes ``` 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 or property key) is the one that remains in the `HashMap`. 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. Keep in mind that a section named "default" is also treated as sectionless so the output files remains consistent with no section header. ## πŸ›  Usage Let's take another simple `ini` file and talk about working with it: ```INI [topsecret] KFC = the secret herb is orega- [values] Uint = 31415 ``` 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 manner and 3) we can use `getuint()` to parse the `Uint` value into an `u64`. Let's see that in action. ```rust use configparser::ini::Ini; use std::error::Error; fn main() -> Result<(), Box> { let mut config = Ini::new(); // You can easily load a file to get a clone of the map: let map = config.load("tests/test.ini")?; println!("{:?}", map); // You can also safely not store the reference and access it later with get_map_ref() or get a clone with get_map() // If you want to access the value, then you can simply do: let val = config.get("TOPSECRET", "KFC").unwrap(); // Notice how get() can access indexes case-insensitively. assert_eq!(val, "the secret herb is orega-"); // value accessible! // What if you want remove KFC's secret recipe? Just use set(): config.set("topsecret", "kfc", None); assert_eq!(config.get("TOPSECRET", "KFC"), None); // as expected! // What if you want to get an unsigned integer? let my_number = config.getuint("values", "Uint")?.unwrap(); assert_eq!(my_number, 31415); // and we got it! // The Ini struct provides more getters for primitive datatypes. // You can also access it like a normal hashmap: let innermap = map["topsecret"].clone(); // Remember that all indexes are stored in lowercase! // You can easily write the currently stored configuration to a file like: config.write("output.ini"); // If you want to simply mutate the stored hashmap, you can use get_mut_map() let map = config.get_mut_map(); // You can then use normal HashMap functions on this map at your convenience. // Remember that functions which rely on standard formatting might stop working // if it's mutated differently. // If you want a case-sensitive map, just do: let mut config = Ini::new_cs(); // This automatically changes the behaviour of every function and parses the file as case-sensitive. Ok(()) } ``` 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. ## πŸ“–Features - *indexmap*: Activating the `indexmap` feature allows using [indexmap](https://crates.io/crates/indexmap) in place of `HashMap` to store the sections and keys. This ensures that insertion order is preserved when iterating on or serializing the Ini object. Due to the nature of indexmap, it offers mostly similar performance to stdlib HashMaps but with [slower lookup times](https://github.com/bluss/indexmap#performance). You can activate it by adding it as a feature like this: ```TOML [dependencies] configparser = { version = "3.0.2", features = ["indexmap"] } ``` - *async-std*: Activating the `async-std` feature adds asynchronous functions for reading from (`load_async()`) and writing to (`write_async()`) files using [async-std](https://crates.io/crates/async-std). You can activate it by adding it as a feature like this: ```TOML [dependencies] configparser = { version = "3.0.2", features = ["async-std"] } ``` ## πŸ“œ License Licensed under either of * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) * Lesser General Public license v3.0 or later ([LICENSE-LGPL](LICENSE-LGPL) or https://www.gnu.org/licenses/lgpl-3.0.html) at your option. ### ✏ Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the LGPL-3.0 license, shall be dual licensed as above, without any additional terms or conditions. ## πŸ†• Changelog Old changelogs are in [CHANGELOG.md](CHANGELOG.md). - 2.0.1 - Add first-class support for setting, loading and reading defaults - New available struct `IniDefault` for fast templating - 2.1.0 - 😯 **BREAKING** Parse keys with higher priority, both brackets `[` and `]` can be part of values now. - β„Ή Only affects current behaviour **iff** your section headers had comments in front of them like, `comment[HEADER]`, you can fix it by adding the comment after the header like `[HEADER]#comment` or otherwise. - πŸš€ `load()` and `write()` work with `Path`-like arguments now. - πŸ“œ Add docs for new struct - 3.0.0 - πŸ˜… **BREAKING** `IniDefault` is now a non-exhaustive struct, this will make future upgrades easier and non-breaking in nature. This change might also have a few implications in updating your existing codebase, please read the [official docs](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute) for more guidance. - `IniDefault` is now internally used for generating defaults, reducing crate size. - πŸš€ There is now a new optional `indexmap` feature that preserves insertion order of your loaded configurations. - 3.0.1 - Uses `CRLF` line endings for Windows files. - Bumps crate to 2021 edition. - Adds features to CI pipeline. - 3.0.2 - Adds support for multi-line key-value pairs. - Adds `async-std` feature for asynchronous file operations. - Some performance optimizations. - 3.0.3 (**STABLE**) - Add default empty line on empty strings. - Feature to append to existing `Ini` objects. - Minor lint fixes. ### πŸ”œ Future plans - Support for appending sections, coercing them as well. - Benchmarking against similar packages. configparser-3.0.3/src/ini.rs000075500000000000000000001205641046102023000142400ustar 00000000000000//!The ini module provides all the things necessary to load and parse ini-syntax files. The most important of which is the `Ini` struct. //!See the [implementation](https://docs.rs/configparser/*/configparser/ini/struct.Ini.html) documentation for more details. #[cfg(feature = "indexmap")] use indexmap::IndexMap as Map; #[cfg(not(feature = "indexmap"))] use std::collections::HashMap as Map; #[cfg(feature = "async-std")] use async_std::{fs as async_fs, path::Path as AsyncPath}; use std::collections::HashMap; use std::convert::AsRef; use std::fmt::Write; use std::fs; use std::path::Path; ///The `Ini` struct simply contains a nested hashmap of the loaded configuration, the default section header and comment symbols. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///``` #[derive(Debug, Clone, Eq, PartialEq, Default)] #[non_exhaustive] pub struct Ini { map: Map>>, default_section: std::string::String, comment_symbols: Vec, delimiters: Vec, boolean_values: HashMap>, case_sensitive: bool, multiline: bool, } ///The `IniDefault` struct serves as a template to create other `Ini` objects from. It can be used to store and load ///default properties from different `Ini` objects. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let default = config.defaults(); ///let mut config2 = Ini::new_from_defaults(default); // default gets consumed ///``` #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] pub struct IniDefault { ///Denotes the default section header name. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let default = config.defaults(); ///assert_eq!(default.default_section, "default"); ///``` pub default_section: std::string::String, ///Denotes the set comment symbols for the object. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let default = config.defaults(); ///assert_eq!(default.comment_symbols, vec![';', '#']); ///``` pub comment_symbols: Vec, ///Denotes the set delimiters for the key-value pairs. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let default = config.defaults(); ///assert_eq!(default.delimiters, vec!['=', ':']); ///``` pub delimiters: Vec, pub boolean_values: HashMap>, ///Denotes if the `Ini` object is case-sensitive. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let default = config.defaults(); ///assert_eq!(default.case_sensitive, false); ///``` pub case_sensitive: bool, ///Denotes if the `Ini` object parses multiline strings. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let default = config.defaults(); ///assert_eq!(default.multiline, false); ///``` pub multiline: bool, } impl Default for IniDefault { fn default() -> Self { Self { default_section: "default".to_owned(), comment_symbols: vec![';', '#'], delimiters: vec!['=', ':'], multiline: false, boolean_values: [ ( true, vec!["true", "yes", "t", "y", "on", "1"] .iter() .map(|&s| s.to_owned()) .collect(), ), ( false, vec!["false", "no", "f", "n", "off", "0"] .iter() .map(|&s| s.to_owned()) .collect(), ), ] .iter() .cloned() .collect(), case_sensitive: false, } } } #[cfg(windows)] const LINE_ENDING: &str = "\r\n"; #[cfg(not(windows))] const LINE_ENDING: &str = "\n"; impl Ini { ///Creates a new `Map` of `Map>>` type for the struct. ///All values in the Map are stored in `String` type. /// ///By default, [`std::collections::HashMap`] is used for the Map object. ///The `indexmap` feature can be used to use an [`indexmap::map::IndexMap`] instead, which ///allows keeping the insertion order for sections and keys. /// ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///``` ///Returns the struct and stores it in the calling variable. pub fn new() -> Ini { Ini::new_from_defaults(IniDefault::default()) } ///Creates a new **case-sensitive** `Map` of `Map>>` type for the struct. ///All values in the Map are stored in `String` type. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new_cs(); ///``` ///Returns the struct and stores it in the calling variable. pub fn new_cs() -> Ini { Ini::new_from_defaults(IniDefault { case_sensitive: true, ..Default::default() }) } ///Creates a new `Ini` with the given defaults from an existing `IniDefault` object. ///## Example ///```rust ///use configparser::ini::Ini; ///use configparser::ini::IniDefault; /// ///let mut default = IniDefault::default(); ///default.comment_symbols = vec![';']; ///default.delimiters = vec!['=']; ///let mut config = Ini::new_from_defaults(default.clone()); ///// Now, load as usual with new defaults: ///let map = config.load("tests/test.ini").unwrap(); ///assert_eq!(config.defaults(), default); /// ///``` pub fn new_from_defaults(defaults: IniDefault) -> Ini { Ini { map: Map::new(), default_section: defaults.default_section, comment_symbols: defaults.comment_symbols, delimiters: defaults.delimiters, boolean_values: defaults.boolean_values, case_sensitive: defaults.case_sensitive, multiline: defaults.multiline, } } ///Fetches the defaults from the current `Ini` object and stores it as a `IniDefault` struct for usage elsewhere. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let default = config.defaults(); ///``` ///Returns an `IniDefault` object. Keep in mind that it will get borrowed since it has non-`Copy` types. pub fn defaults(&self) -> IniDefault { IniDefault { default_section: self.default_section.to_owned(), comment_symbols: self.comment_symbols.to_owned(), delimiters: self.delimiters.to_owned(), boolean_values: self.boolean_values.to_owned(), case_sensitive: self.case_sensitive, multiline: self.multiline, } } ///Takes an `IniDefault` object and stores its properties in the calling `Ini` object. This happens in-place and ///does not work retroactively, only future operations are affected. ///## Example ///```rust ///use configparser::ini::Ini; ///use configparser::ini::IniDefault; /// ///let mut config = Ini::new(); ///let mut default = IniDefault::default(); ///default.case_sensitive = true; ///// This is equivalent to ini_cs() defaults ///config.load_defaults(default.clone()); ///assert_eq!(config.defaults(), default); ///``` ///Returns nothing. pub fn load_defaults(&mut self, defaults: IniDefault) { self.default_section = defaults.default_section; self.comment_symbols = defaults.comment_symbols; self.delimiters = defaults.delimiters; self.boolean_values = defaults.boolean_values; self.case_sensitive = defaults.case_sensitive; } ///Sets the default section header to the defined string (the default is `default`). ///It must be set before `load()` or `read()` is called in order to take effect. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); /// ///config.set_default_section("topsecret"); ///let map = config.load("tests/test.ini").unwrap(); ///``` ///Returns nothing. pub fn set_default_section(&mut self, section: &str) { self.default_section = section.to_owned(); } ///Sets the default comment symbols to the defined character slice (the defaults are `;` and `#`). ///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. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.set_comment_symbols(&['!', '#']); ///let map = config.load("tests/test.ini").unwrap(); ///``` ///Returns nothing. pub fn set_comment_symbols(&mut self, symlist: &[char]) { self.comment_symbols = symlist.to_vec(); } ///Sets multiline string support. ///It must be set before `load()` or `read()` is called in order to take effect. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.set_multiline(true); ///let map = config.load("tests/test.ini").unwrap(); ///``` ///Returns nothing. pub fn set_multiline(&mut self, multiline: bool) { self.multiline = multiline; } ///Gets all the sections of the currently-stored `Map` in a vector. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini"); ///let sections = config.sections(); ///``` ///Returns `Vec`. pub fn sections(&self) -> Vec { self.map.keys().cloned().collect() } ///Loads a file from a defined path, parses it and puts the hashmap into our struct. ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let map = config.load("tests/test.ini").unwrap(); // we can get a clone like this, or just store it /////Then, we can use standard hashmap functions like: ///let values = map.get("values").unwrap(); ///``` ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. ///Use `get_mut_map()` if you want a mutable reference. pub fn load>( &mut self, path: T, ) -> Result>>, String> { self.map = match self.parse(match fs::read_to_string(&path) { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(s) => s, }) { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(map) => map, }; Ok(self.map.clone()) } ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. ///While `load()` will clear the existing `Map`, `load_and_append()` applies the new values on top of ///the existing hashmap, preserving previous values. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini").unwrap(); ///config.load_and_append("tests/sys_cfg.ini").ok(); // we don't have to worry if this doesn't succeed ///config.load_and_append("tests/user_cfg.ini").ok(); // we don't have to worry if this doesn't succeed ///let map = config.get_map().unwrap(); /////Then, we can use standard hashmap functions like: ///let values = map.get("values").unwrap(); ///``` ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. ///Use `get_mut_map()` if you want a mutable reference. pub fn load_and_append>( &mut self, path: T, ) -> Result>>, String> { let loaded = match self.parse(match fs::read_to_string(&path) { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(s) => s, }) { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(map) => map, }; for (section, section_map) in loaded.iter() { self.map .entry(section.clone()) .or_insert_with(Map::new) .extend(section_map.clone()); } Ok(self.map.clone()) } ///Reads an input string, parses it and puts the hashmap into our struct. ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let map = match config.read(String::from( /// "[2000s] /// 2020 = bad")) { /// Err(why) => panic!("{}", why), /// Ok(inner) => inner ///}; ///let this_year = map["2000s"]["2020"].clone().unwrap(); ///assert_eq!(this_year, "bad"); // value accessible! ///``` ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. ///Use `get_mut_map()` if you want a mutable reference. pub fn read( &mut self, input: String, ) -> Result>>, String> { self.map = match self.parse(input) { Err(why) => return Err(why), Ok(map) => map, }; Ok(self.map.clone()) } ///Reads an input string, parses it and applies it to the existing hashmap in our struct. ///While `read()` and `load()` will clear the existing `Map`, `read_and_append()` applies the new ///values on top of the existing hashmap, preserving previous values. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///if let Err(why) = config.read(String::from( /// "[2000s] /// 2020 = bad /// 2023 = better")) { /// panic!("{}", why); ///}; ///if let Err(why) = config.read_and_append(String::from( /// "[2000s] /// 2020 = terrible")) { /// panic!("{}", why); ///}; ///let map = config.get_map().unwrap(); ///let few_years_ago = map["2000s"]["2020"].clone().unwrap(); ///let this_year = map["2000s"]["2023"].clone().unwrap(); ///assert_eq!(few_years_ago, "terrible"); // value updated! ///assert_eq!(this_year, "better"); // keeps old values! ///``` ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. ///Use `get_mut_map()` if you want a mutable reference. pub fn read_and_append( &mut self, input: String, ) -> Result>>, String> { let loaded = match self.parse(input) { Err(why) => return Err(why), Ok(map) => map, }; for (section, section_map) in loaded.iter() { self.map .entry(section.clone()) .or_insert_with(Map::new) .extend(section_map.clone()); } Ok(self.map.clone()) } ///Writes the current configuation to the specified path. If a file is not present, it is automatically created for you, if a file already ///exists, it is truncated and the configuration is written to it. ///## Example ///```rust ///use configparser::ini::Ini; /// ///fn main() -> std::io::Result<()> { /// let mut config = Ini::new(); /// config.read(String::from( /// "[2000s] /// 2020 = bad")); /// config.write("output.ini") ///} ///``` ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. pub fn write>(&self, path: T) -> std::io::Result<()> { fs::write(path.as_ref(), self.unparse()) } ///Returns a string with the current configuration formatted with valid ini-syntax. This is always safe since the configuration is validated during ///parsing. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from( /// "[2000s] /// 2020 = bad")); ///let outstring = config.writes(); ///``` ///Returns a `String` type contatining the ini-syntax file. pub fn writes(&self) -> String { self.unparse() } ///Private function that converts the currently stored configuration into a valid ini-syntax string. fn unparse(&self) -> String { // push key/value pairs in outmap to out string. fn unparse_key_values( out: &mut String, outmap: &Map>, multiline: bool, ) { for (key, val) in outmap.iter() { out.push_str(key); if let Some(value) = val { out.push('='); if multiline { let mut lines = value.lines(); out.push_str(lines.next().unwrap_or_default()); for line in lines { out.push_str(LINE_ENDING); out.push_str(" "); out.push_str(line); } } else { out.push_str(value); } } out.push_str(LINE_ENDING); } } let mut out = String::new(); if let Some(defaultmap) = self.map.get(&self.default_section) { unparse_key_values(&mut out, defaultmap, self.multiline); } for (section, secmap) in self.map.iter() { if section != &self.default_section { write!(out, "[{}]", section).unwrap(); out.push_str(LINE_ENDING); unparse_key_values(&mut out, secmap, self.multiline); } } out } ///Private function that parses ini-style syntax into a Map. fn parse(&self, input: String) -> Result>>, String> { let mut map: Map>> = Map::new(); let mut section = self.default_section.clone(); let mut current_key: Option = None; let caser = |val: &str| { if self.case_sensitive { val.to_owned() } else { val.to_lowercase() } }; for (num, raw_line) in input.lines().enumerate() { let line = match raw_line.find(|c: char| self.comment_symbols.contains(&c)) { Some(idx) => &raw_line[..idx], None => raw_line, }; let trimmed = line.trim(); if trimmed.is_empty() { continue; } match (trimmed.find('['), trimmed.rfind(']')) { (Some(0), Some(end)) => { section = caser(trimmed[1..end].trim()); continue; } (Some(0), None) => { return Err(format!( "line {}: Found opening bracket for section name but no closing bracket", num )); } _ => {} } if line.starts_with(char::is_whitespace) && self.multiline { let key = match current_key.as_ref() { Some(x) => x, None => { return Err(format!( "line {}: Started with indentation but there is no current entry", num, )) } }; let valmap = map.entry(section.clone()).or_insert_with(Map::new); let val = valmap .entry(key.clone()) .or_insert_with(|| Some(String::new())); match val { Some(x) => { x.push_str(LINE_ENDING); x.push_str(trimmed); } None => { *val = Some(format!("{}{}", LINE_ENDING, trimmed)); } } continue; } let valmap = map.entry(section.clone()).or_insert_with(Map::new); match trimmed.find(&self.delimiters[..]) { Some(delimiter) => { let key = caser(trimmed[..delimiter].trim()); if key.is_empty() { return Err(format!("line {}:{}: Key cannot be empty", num, delimiter)); } else { current_key = Some(key.clone()); let value = trimmed[delimiter + 1..].trim().to_owned(); valmap.insert(key, Some(value)); } } None => { let key = caser(trimmed); current_key = Some(key.clone()); valmap.insert(key, None); } } } Ok(map) } ///Private function that cases things automatically depending on the set variable. fn autocase(&self, section: &str, key: &str) -> (String, String) { if self.case_sensitive { (section.to_owned(), key.to_owned()) } else { (section.to_lowercase(), key.to_lowercase()) } } ///Returns a clone of the stored value from the key stored in the defined section. ///Unlike accessing the map directly, `get()` can process your input to make case-insensitive access *if* the ///default constructor is used. ///All `get` functions will do this automatically under the hood. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini"); ///let value = config.get("default", "defaultvalues").unwrap(); ///assert_eq!(value, String::from("defaultvalues")); ///``` ///Returns `Some(value)` of type `String` if value is found or else returns `None`. pub fn get(&self, section: &str, key: &str) -> Option { let (section, key) = self.autocase(section, key); self.map.get(§ion)?.get(&key)?.clone() } ///Parses the stored value from the key stored in the defined section to a `bool`. ///For ease of use, the function converts the type case-insensitively (`true` == `True`). ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini"); ///let value = config.getbool("values", "bool").unwrap().unwrap(); ///assert!(value); // value accessible! ///``` ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. ///If the parsing fails, it returns an `Err(string)`. pub fn getbool(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); match self.map.get(§ion) { Some(secmap) => match secmap.get(&key) { Some(val) => match val { Some(inner) => match inner.to_lowercase().parse::() { Err(why) => Err(why.to_string()), Ok(boolean) => Ok(Some(boolean)), }, None => Ok(None), }, None => Ok(None), }, None => Ok(None), } } ///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. ///It attempts to case-insenstively find `true`, `yes`, `t`, `y`, `1` and `on` to parse it as `True`. ///Similarly it attempts to case-insensitvely find `false`, `no`, `f`, `n`, `0` and `off` to parse it as `False`. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini"); ///let value = config.getboolcoerce("values", "boolcoerce").unwrap().unwrap(); ///assert!(!value); // value accessible! ///``` ///Returns `Ok(Some(value))` of type `bool` if value is found or else returns `Ok(None)`. ///If the parsing fails, it returns an `Err(string)`. pub fn getboolcoerce(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); match self.map.get(§ion) { Some(secmap) => match secmap.get(&key) { Some(val) => match val { Some(inner) => { let boolval = &inner.to_lowercase()[..]; if self .boolean_values .get(&true) .unwrap() .iter() .any(|elem| elem == boolval) { Ok(Some(true)) } else if self .boolean_values .get(&false) .unwrap() .iter() .any(|elem| elem == boolval) { Ok(Some(false)) } else { Err(format!( "Unable to parse value into bool at {}:{}", section, key )) } } None => Ok(None), }, None => Ok(None), }, None => Ok(None), } } ///Parses the stored value from the key stored in the defined section to an `i64`. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini"); ///let value = config.getint("values", "int").unwrap().unwrap(); ///assert_eq!(value, -31415); // value accessible! ///``` ///Returns `Ok(Some(value))` of type `i64` if value is found or else returns `Ok(None)`. ///If the parsing fails, it returns an `Err(string)`. pub fn getint(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); match self.map.get(§ion) { Some(secmap) => match secmap.get(&key) { Some(val) => match val { Some(inner) => match inner.parse::() { Err(why) => Err(why.to_string()), Ok(int) => Ok(Some(int)), }, None => Ok(None), }, None => Ok(None), }, None => Ok(None), } } ///Parses the stored value from the key stored in the defined section to a `u64`. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini"); ///let value = config.getint("values", "Uint").unwrap().unwrap(); ///assert_eq!(value, 31415); // value accessible! ///``` ///Returns `Ok(Some(value))` of type `u64` if value is found or else returns `Ok(None)`. ///If the parsing fails, it returns an `Err(string)`. pub fn getuint(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); match self.map.get(§ion) { Some(secmap) => match secmap.get(&key) { Some(val) => match val { Some(inner) => match inner.parse::() { Err(why) => Err(why.to_string()), Ok(uint) => Ok(Some(uint)), }, None => Ok(None), }, None => Ok(None), }, None => Ok(None), } } ///Parses the stored value from the key stored in the defined section to a `f64`. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.load("tests/test.ini"); ///let value = config.getfloat("values", "float").unwrap().unwrap(); ///assert_eq!(value, 3.1415); // value accessible! ///``` ///Returns `Ok(Some(value))` of type `f64` if value is found or else returns `Ok(None)`. ///If the parsing fails, it returns an `Err(string)`. pub fn getfloat(&self, section: &str, key: &str) -> Result, String> { let (section, key) = self.autocase(section, key); match self.map.get(§ion) { Some(secmap) => match secmap.get(&key) { Some(val) => match val { Some(inner) => match inner.parse::() { Err(why) => Err(why.to_string()), Ok(float) => Ok(Some(float)), }, None => Ok(None), }, None => Ok(None), }, None => Ok(None), } } ///Returns a clone of the `Map` stored in our struct. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from( /// "[section] /// key=values")); ///let map = config.get_map().unwrap(); ///assert_eq!(map, *config.get_map_ref()); // the cloned map is basically a snapshot that you own ///``` ///Returns `Some(map)` if map is non-empty or else returns `None`. ///Similar to `load()` but returns an `Option` type with the currently stored `Map`. pub fn get_map(&self) -> Option>>> { if self.map.is_empty() { None } else { Some(self.map.clone()) } } ///Returns an immutable reference to the `Map` stored in our struct. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///let mapclone = config.read(String::from /// ("[topsecrets] /// Valueless key")).unwrap(); /////Think of the clone as being a snapshot at a point of time while the reference always points to the current configuration. ///assert_eq!(*config.get_map_ref(), mapclone); // same as expected. ///``` ///If you just need to definitely mutate the map, use `get_mut_map()` instead. Alternatively, you can generate a snapshot by getting a clone ///with `get_map()` and work with that. pub fn get_map_ref(&self) -> &Map>> { &self.map } ///Returns a mutable reference to the `Map` stored in our struct. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from /// ("[topsecrets] /// Valueless key")); /////We can then get the mutable map and insert a value like: ///config.get_mut_map().get_mut("topsecrets").unwrap().insert(String::from("nuclear launch codes"), None); ///assert_eq!(config.get("topsecrets", "nuclear launch codes"), None); // inserted successfully! ///``` ///If you just need to access the map without mutating, use `get_map_ref()` or make a clone with `get_map()` instead. pub fn get_mut_map(&mut self) -> &mut Map>> { &mut self.map } ///Sets an `Option` in the `Map` stored in our struct. If a particular section or key does not exist, it will be automatically created. ///An existing value in the map will be overwritten. You can also set `None` safely. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from( /// "[section] /// key=value")); ///let key_value = String::from("value"); ///config.set("section", "key", Some(key_value)); ///config.set("section", "key", None); // also valid! ///assert_eq!(config.get("section", "key"), None); // correct! ///``` ///Returns `None` if there is no existing value, else returns `Some(Option)`, with the existing value being the wrapped `Option`. ///If you want to insert using a string literal, use `setstr()` instead. pub fn set( &mut self, section: &str, key: &str, value: Option, ) -> Option> { let (section, key) = self.autocase(section, key); match self.map.get_mut(§ion) { Some(secmap) => secmap.insert(key, value), None => { let mut valmap: Map> = Map::new(); valmap.insert(key, value); self.map.insert(section, valmap); None } } } ///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. ///An existing value in the map will be overwritten. You can also set `None` safely. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from( /// "[section] /// key=notvalue")); ///config.setstr("section", "key", Some("value")); ///config.setstr("section", "key", None); // also valid! ///assert_eq!(config.get("section", "key"), None); // correct! ///``` ///Returns `None` if there is no existing value, else returns `Some(Option)`, with the existing value being the wrapped `Option`. ///If you want to insert using a `String`, use `set()` instead. pub fn setstr( &mut self, section: &str, key: &str, value: Option<&str>, ) -> Option> { let (section, key) = self.autocase(section, key); self.set(§ion, &key, value.map(String::from)) } ///Clears the map, removing all sections and properties from the hashmap. It keeps the allocated memory for reuse. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from( /// "[section] /// key=somevalue")); ///config.clear(); ///assert!(config.get_map_ref().is_empty()); // our map is empty! ///``` ///Returns nothing. pub fn clear(&mut self) { self.map.clear(); } ///Removes a section from the hashmap, returning the properties stored in the section if the section was previously in the map. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from( /// "[section] /// updog=whatsupdog")); ///config.remove_section("section"); // this will return a cloned hashmap of the stored property ///assert!(config.get_map_ref().is_empty()); // with the last section removed, our map is now empty! ///``` ///Returns `Some(section_map)` if the section exists or else, `None`. pub fn remove_section(&mut self, section: &str) -> Option>> { let section = if self.case_sensitive { section.to_owned() } else { section.to_lowercase() }; self.map.remove(§ion) } ///Removes a key from a section in the hashmap, returning the value attached to the key if it was previously in the map. ///## Example ///```rust ///use configparser::ini::Ini; /// ///let mut config = Ini::new(); ///config.read(String::from( /// "[section] /// updog=whatsupdog /// [anothersection] /// updog=differentdog")); ///let val = config.remove_key("anothersection", "updog").unwrap().unwrap(); ///assert_eq!(val, String::from("differentdog")); // with the last section removed, our map is now empty! ///``` ///Returns `Some(Option)` if the value exists or else, `None`. pub fn remove_key(&mut self, section: &str, key: &str) -> Option> { let (section, key) = self.autocase(section, key); self.map.get_mut(§ion)?.remove(&key) } } #[cfg(feature = "async-std")] impl Ini { ///Loads a file asynchronously from a defined path, parses it and puts the hashmap into our struct. ///At one time, it only stores one configuration, so each call to `load()` or `read()` will clear the existing `Map`, if present. /// ///Usage is similar to `load`, but `.await` must be called after along with the usual async rules. /// ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. ///Use `get_mut_map()` if you want a mutable reference. pub async fn load_async>( &mut self, path: T, ) -> Result>>, String> { self.map = match self.parse(match async_fs::read_to_string(&path).await { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(s) => s, }) { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(map) => map, }; Ok(self.map.clone()) } ///Loads a file from a defined path, parses it and applies it to the existing hashmap in our struct. ///While `load_async()` will clear the existing `Map`, `load_and_append_async()` applies the new values on top ///of the existing hashmap, preserving previous values. /// ///Usage is similar to `load_and_append`, but `.await` must be called after along with the usual async rules. /// ///Returns `Ok(map)` with a clone of the stored `Map` if no errors are thrown or else `Err(error_string)`. ///Use `get_mut_map()` if you want a mutable reference. pub async fn load_and_append_async>( &mut self, path: T, ) -> Result>>, String> { let loaded = match self.parse(match async_fs::read_to_string(&path).await { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(s) => s, }) { Err(why) => { return Err(format!( "couldn't read {}: {}", &path.as_ref().display(), why )) } Ok(map) => map, }; for (section, section_map) in loaded.iter() { self.map .entry(section.clone()) .or_insert_with(Map::new) .extend(section_map.clone()); } Ok(self.map.clone()) } ///Writes the current configuation to the specified path asynchronously. If a file is not present, it is automatically created for you, if a file already ///exists, it is truncated and the configuration is written to it. /// ///Usage is the same as `write`, but `.await` must be called after along with the usual async rules. /// ///Returns a `std::io::Result<()>` type dependent on whether the write was successful or not. pub async fn write_async>(&self, path: T) -> std::io::Result<()> { async_fs::write(path.as_ref(), self.unparse()).await } } configparser-3.0.3/src/lib.rs000075500000000000000000000152561046102023000142300ustar 00000000000000/*! 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. You can use this to write Rust programs which can be customized by end users easily. This is a simple configuration parsing utility with no dependencies built on Rust. It is inspired by Python's `configparser`. 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. ## πŸš€ Quick Start A basic `ini`-syntax file (we say ini-syntax files because the files don't need to be necessarily `*.ini`) looks like this: ```INI [DEFAULT] key1 = value1 pizzatime = yes cost = 9 [topsecrets] nuclear launch codes = topsecret [github.com] User = QEDK ``` 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 strings as well as files. ## βž• Supported datatypes `configparser` does not guess the datatype of values in configuration files and stores everything as strings. However, some datatypes are so common 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()`, `getfloat()` and `getbool()`. The only bit of extra magic involved is that the `getbool()` function will treat boolean values case-insensitively (so `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. ```rust use configparser::ini::Ini; let mut config = Ini::new(); config.read(String::from( "[somesection] someintvalue = 5")); let my_value = config.getint("somesection", "someintvalue").unwrap().unwrap(); assert_eq!(my_value, 5); // value accessible! //You can ofcourse just choose to parse the values yourself: let my_string = String::from("1984"); let my_int = my_string.parse::().unwrap(); ``` ## πŸ“ Supported `ini` file structure 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. Key values can be omitted, in which case the key-value delimiter may also be left out (but this is different from putting a delimiter, we'll explain it later). You can use comment symbols (`;` and `#` to denote comments). This can be configured with the `set_comment_symbols()` method in the API. Keep in mind that key-value pairs or section headers cannot span multiple lines. Owing to how ini files usually are, this means that `[`, `]`, `=`, `:`, `;` and `#` are special symbols by default (this crate will allow you to use `]` sparingly). Let's take for example: ```INI [section headers are case-insensitive by default] [ section headers are case-insensitive by default ] are the section headers above same? = yes sectionheaders_and_keysarestored_in_lowercase? = yes keys_are_also_case_insensitive = Values are case sensitive Case-sensitive_keys_and_sections = using a special constructor you can also use colons : instead of the equal symbol ;anything after a comment symbol is ignored #this is also a comment spaces in keys=allowed ;and everything before this is still valid! spaces in values=allowed as well spaces around the delimiter = also OK [All values are strings] values like this= 0000 or this= 0.999 are they treated as numbers? = no integers, floats and booleans are held as= strings [value-less?] a_valueless_key_has_None this key has an empty string value has Some("") = [indented sections] can_values_be_as_well = True purpose = formatting for readability is_this_same = yes is_this_same=yes ``` 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 or property key) is the one that remains in the `HashMap`. 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. Keep in mind that a section named "default" is also treated as sectionless so the output files remains consistent with no section header. ## Usage Let's take another simple `ini` file and talk about working with it: ```INI [topsecret] KFC = the secret herb is orega- [values] Uint = 31415 ``` 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 manner and 3) we can use `getint()` to parse the `Int` value into an `i64`. Let's see that in action. ```rust use configparser::ini::Ini; use std::error::Error; fn main() -> Result<(), Box> { let mut config = Ini::new(); // You can easily load a file to get a clone of the map: let map = config.load("tests/test.ini")?; println!("{:?}", map); // You can also safely not store the reference and access it later with get_map_ref() or get a clone with get_map() // If you want to access the value, then you can simply do: let val = config.get("TOPSECRET", "KFC").unwrap(); // Notice how get() can access indexes case-insensitively. assert_eq!(val, "the secret herb is orega-"); // value accessible! // What if you want remove KFC's secret recipe? Just use set(): config.set("topsecret", "kfc", None); assert_eq!(config.get("TOPSECRET", "KFC"), None); // as expected! // What if you want to get an unsigned integer? let my_number = config.getuint("values", "Uint")?.unwrap(); assert_eq!(my_number, 31415); // and we got it! // The Ini struct provides more getters for primitive datatypes. // You can also access it like a normal hashmap: let innermap = map["topsecret"].clone(); // Remember that all indexes are stored in lowercase! // You can easily write the currently stored configuration to a file like: config.write("output.ini"); // If you want to simply mutate the stored hashmap, you can use get_mut_map() let map = config.get_mut_map(); // You can then use normal HashMap functions on this map at your convenience. // Remember that functions which rely on standard formatting might stop working // if it's mutated differently. // If you want a case-sensitive map, just do: let mut config = Ini::new_cs(); // This automatically changes the behaviour of every function and parses the file as case-sensitive. Ok(()) } ``` */ pub mod ini; configparser-3.0.3/tests/test.ini000075500000000000000000000004371046102023000151420ustar 00000000000000defaultvalues=defaultvalues [topsecret] KFC = the secret herb is orega- colon:value after colon Empty string = None string Password=[in-brackets] [ spacing ] indented=indented not indented = not indented [values] Bool = True Boolcoerce = 0 Int = -31415 Uint = 31415 Float = 3.1415 configparser-3.0.3/tests/test.rs000075500000000000000000000276451046102023000150210ustar 00000000000000use configparser::ini::Ini; use std::error::Error; #[test] #[allow(clippy::approx_constant)] fn non_cs() -> Result<(), Box> { let mut config = Ini::new(); let map = config.load("tests/test.ini")?; config.set_comment_symbols(&[';', '#', '!']); let inpstring = config.read( "defaultvalues=defaultvalues [topsecret] KFC = the secret herb is orega- colon:value after colon Empty string = None string Password=[in-brackets] [ spacing ] indented=indented not indented = not indented ;testcomment !modified comment [values]#another comment Bool = True Boolcoerce = 0 Int = -31415 Uint = 31415 Float = 3.1415" .to_owned(), )?; assert_eq!(map, inpstring); config.set("DEFAULT", "defaultvalues", Some("notdefault".to_owned())); assert_eq!( config.get("DEFAULT", "defaultvalues").unwrap(), "notdefault" ); config.setstr("DEFAULT", "defaultvalues", Some("defaultvalues")); assert_eq!( config.get("DEFAULT", "defaultvalues").unwrap(), "defaultvalues" ); config.setstr("DEFAULT", "defaultvalues", None); config.write("output.ini")?; let map2 = config.clone().load("output.ini")?; assert_eq!(map2, *config.get_map_ref()); let map3 = config.clone().read(config.writes())?; assert_eq!(map2, map3); assert_eq!(config.sections().len(), 4); assert_eq!(config.get("DEFAULT", "defaultvalues"), None); assert_eq!( config.get("topsecret", "KFC").unwrap(), "the secret herb is orega-" ); assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); assert_eq!(config.get("topsecret", "None string"), None); assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); assert_eq!( config.get("spacing", "not indented").unwrap(), "not indented" ); assert_eq!( config.get("topsecret", "colon").unwrap(), "value after colon" ); assert!(config.getbool("values", "Bool")?.unwrap()); assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); assert_eq!( map["default"]["defaultvalues"].clone().unwrap(), "defaultvalues" ); assert_eq!( map["topsecret"]["kfc"].clone().unwrap(), "the secret herb is orega-" ); assert_eq!(map["topsecret"]["empty string"].clone().unwrap(), ""); assert_eq!(map["topsecret"]["none string"], None); assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); assert_eq!( map["spacing"]["not indented"].clone().unwrap(), "not indented" ); let mut config2 = config.clone(); let val = config2.remove_key("default", "defaultvalues"); assert_eq!(val, Some(None)); assert_eq!(config2.get("default", "defaultvalues"), None); config2.remove_section("default"); assert_eq!(config2.get("default", "nope"), None); let mut_map = config.get_mut_map(); mut_map.get_mut("topsecret").unwrap().insert( String::from("none string"), Some(String::from("None string")), ); assert_eq!( mut_map["topsecret"]["none string"].clone().unwrap(), "None string" ); mut_map.clear(); config2.clear(); assert_eq!(config.get_map_ref(), config2.get_map_ref()); config.load("tests/test.ini")?; config.read_and_append("defaultvalues=somenewvalue".to_owned())?; assert_eq!( config.get("default", "defaultvalues").unwrap(), "somenewvalue" ); assert_eq!( config.get("topsecret", "KFC").unwrap(), "the secret herb is orega-" ); let mut config3 = config.clone(); let mut_map = config3.get_mut_map(); mut_map.clear(); config3.load("tests/test.ini")?; config3.load_and_append("tests/test_more.ini")?; assert_eq!( config3.get("default", "defaultvalues").unwrap(), "overwritten" ); assert_eq!(config3.get("topsecret", "KFC").unwrap(), "redacted"); // spacing -> indented exists in tests/test.ini, but not tests/test_more.ini assert_eq!(config3.get("spacing", "indented").unwrap(), "indented"); assert_eq!(config3.getbool("values", "Bool")?.unwrap(), false); Ok(()) } #[test] #[allow(clippy::approx_constant)] fn cs() -> Result<(), Box> { let mut config = Ini::new_cs(); let map = config.load("tests/test.ini")?; config.set_comment_symbols(&[';', '#', '!']); let inpstring = config.read( "defaultvalues=defaultvalues [topsecret] KFC = the secret herb is orega- colon:value after colon Empty string = None string Password=[in-brackets] [ spacing ] indented=indented not indented = not indented ;testcomment !modified comment [values]#another comment Bool = True Boolcoerce = 0 Int = -31415 Uint = 31415 Float = 3.1415" .to_owned(), )?; assert_eq!(map, inpstring); config.set("default", "defaultvalues", Some("notdefault".to_owned())); assert_eq!( config.get("default", "defaultvalues").unwrap(), "notdefault" ); config.setstr("default", "defaultvalues", Some("defaultvalues")); assert_eq!( config.get("default", "defaultvalues").unwrap(), "defaultvalues" ); config.setstr("default", "defaultvalues", None); config.write("output2.ini")?; let map2 = config.clone().load("output2.ini")?; assert_eq!(map2, *config.get_map_ref()); let map3 = config.clone().read(config.writes())?; assert_eq!(map2, map3); assert_eq!(config.sections().len(), 4); assert_eq!(config.get("default", "defaultvalues"), None); assert_eq!( config.get("topsecret", "KFC").unwrap(), "the secret herb is orega-" ); assert_eq!(config.get("topsecret", "Empty string").unwrap(), ""); assert_eq!(config.get("topsecret", "None string"), None); assert_eq!(config.get("spacing", "indented").unwrap(), "indented"); assert_eq!( config.get("spacing", "not indented").unwrap(), "not indented" ); assert_eq!( config.get("topsecret", "colon").unwrap(), "value after colon" ); assert!(config.getbool("values", "Bool")?.unwrap()); assert!(!config.getboolcoerce("values", "Boolcoerce")?.unwrap()); assert_eq!(config.getint("values", "Int")?.unwrap(), -31415); assert_eq!(config.getuint("values", "Uint")?.unwrap(), 31415); assert_eq!(config.getfloat("values", "Float")?.unwrap(), 3.1415); assert_eq!(config.getfloat("topsecret", "None string"), Ok(None)); assert_eq!( map["default"]["defaultvalues"].clone().unwrap(), "defaultvalues" ); assert_eq!( map["topsecret"]["KFC"].clone().unwrap(), "the secret herb is orega-" ); assert_eq!(map["topsecret"]["Empty string"].clone().unwrap(), ""); assert_eq!(map["topsecret"]["None string"], None); assert_eq!(map["spacing"]["indented"].clone().unwrap(), "indented"); assert_eq!( map["spacing"]["not indented"].clone().unwrap(), "not indented" ); let mut config2 = config.clone(); let val = config2.remove_key("default", "defaultvalues"); assert_eq!(val, Some(None)); assert_eq!(config2.get("default", "defaultvalues"), None); config2.remove_section("default"); assert_eq!(config2.get("default", "nope"), None); let mut_map = config.get_mut_map(); mut_map.get_mut("topsecret").unwrap().insert( String::from("none string"), Some(String::from("None string")), ); assert_eq!( mut_map["topsecret"]["none string"].clone().unwrap(), "None string" ); mut_map.clear(); config2.clear(); assert_eq!(config.get_map_ref(), config2.get_map_ref()); Ok(()) } #[test] #[cfg(feature = "indexmap")] fn sort_on_write() -> Result<(), Box> { let mut config = Ini::new_cs(); config.load("tests/test.ini")?; assert_eq!( config.writes(), "defaultvalues=defaultvalues [topsecret] KFC=the secret herb is orega- colon=value after colon Empty string= None string Password=[in-brackets] [spacing] indented=indented not indented=not indented [values] Bool=True Boolcoerce=0 Int=-31415 Uint=31415 Float=3.1415 " ); Ok(()) } #[test] #[cfg(feature = "async-std")] fn async_load_write() -> Result<(), Box> { const OUT_FILE_CONTENTS: &str = "defaultvalues=defaultvalues [topsecret] KFC = the secret herb is orega- colon:value after colon Empty string = None string Password=[in-brackets] [ spacing ] indented=indented not indented = not indented ;testcomment !modified comment [values]#another comment Bool = True Boolcoerce = 0 Int = -31415 Uint = 31415 Float = 3.1415"; let mut config = Ini::new(); config.read(OUT_FILE_CONTENTS.to_owned())?; config.write("output_sync.ini")?; async_std::task::block_on::<_, Result<_, String>>(async { let mut config_async = Ini::new(); config_async.read(OUT_FILE_CONTENTS.to_owned())?; config_async .write_async("output_async.ini") .await .map_err(|e| e.to_string())?; Ok(()) })?; let mut sync_content = Ini::new(); sync_content.load("output_sync.ini")?; let async_content = async_std::task::block_on::<_, Result<_, String>>(async { let mut async_content = Ini::new(); async_content.load_async("output_async.ini").await?; Ok(async_content) })?; assert_eq!(sync_content, async_content); Ok(()) } #[test] #[cfg(feature = "async-std")] fn async_load_and_append() -> Result<(), Box> { let mut sync_content = Ini::new(); sync_content.load("tests/test.ini")?; sync_content.load_and_append("tests/test_more.ini")?; let async_content = async_std::task::block_on::<_, Result<_, String>>(async { let mut async_content = Ini::new(); async_content.load_async("tests/test.ini").await?; async_content .load_and_append_async("tests/test_more.ini") .await?; Ok(async_content) })?; assert_eq!(sync_content, async_content); Ok(()) } #[test] #[cfg(feature = "indexmap")] fn multiline_off() -> Result<(), Box> { let mut config = Ini::new_cs(); config.load("tests/test_multiline.ini")?; let map = config.get_map_ref(); let section = map.get("Section").unwrap(); assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); assert_eq!(config.get("Section", "Key3").unwrap(), "this is a haiku"); assert!(section.contains_key("spread across separate lines")); assert!(section.contains_key("a single value")); assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); assert_eq!( config.writes(), "[Section] Key1=Value1 Key2=Value Two Key3=this is a haiku spread across separate lines a single value Key4=Four " ); Ok(()) } #[test] #[cfg(feature = "indexmap")] fn multiline_on() -> Result<(), Box> { let mut config = Ini::new_cs(); config.set_multiline(true); config.load("tests/test_multiline.ini")?; assert_eq!(config.get("Section", "Key1").unwrap(), "Value1"); assert_eq!(config.get("Section", "Key2").unwrap(), "Value Two"); assert_eq!( config.get("Section", "Key3").unwrap(), "this is a haiku\nspread across separate lines\na single value" ); assert_eq!(config.get("Section", "Key4").unwrap(), "Four"); assert_eq!( config.writes(), "[Section] Key1=Value1 Key2=Value Two Key3=this is a haiku spread across separate lines a single value Key4=Four " ); Ok(()) } configparser-3.0.3/tests/test_more.ini000075500000000000000000000001161046102023000161560ustar 00000000000000defaultvalues=overwritten [topsecret] KFC = redacted [values] Bool = False configparser-3.0.3/tests/test_multiline.ini000064400000000000000000000001731046102023000172160ustar 00000000000000[Section] Key1: Value1 Key2: Value Two Key3: this is a haiku spread across separate lines a single value Key4: Four