aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeongik Cha <jeongik@google.com>2023-09-27 09:17:56 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-09-27 09:17:56 +0000
commita74c53cb2bff4e7491602c78fa740ae5cb7149cd (patch)
tree3fd0f5f31a4a09f2e40d5a1e38db14fdc37e661f
parent26b5fe5d180a32c3f9390d236ae96b527c7b7478 (diff)
parent019f4830c7d44bfbb917980e893cb5c5bb00524f (diff)
downloadconfigparser-a74c53cb2bff4e7491602c78fa740ae5cb7149cd.tar.gz
Import configparser am: 7f2bc65135 am: 019f4830c7
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/configparser/+/2754250 Change-Id: I2359472928a0c74f5a5cf57c6f1a5faeb9497d9b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--.github/workflows/rust.yaml52
-rwxr-xr-x.gitignore9
-rw-r--r--Android.bp13
-rwxr-xr-xCHANGELOG.md86
-rw-r--r--CODE_OF_CONDUCT.md127
-rw-r--r--Cargo.toml46
-rwxr-xr-xCargo.toml.orig22
l---------LICENSE1
-rw-r--r--LICENSE-MIT27
-rw-r--r--METADATA19
-rw-r--r--MODULE_LICENSE_MIT0
-rw-r--r--OWNERS1
-rwxr-xr-xREADME.md232
-rwxr-xr-xsrc/ini.rs972
-rwxr-xr-xsrc/lib.rs154
-rwxr-xr-xtests/test.ini19
-rwxr-xr-xtests/test.rs348
-rw-r--r--tests/test_multiline.ini7
19 files changed, 2141 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..be07c69
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "aca329bc1872624d98cc3486b2cbe643e6641278"
+ },
+ "path_in_vcs": ""
+} \ No newline at end of file
diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml
new file mode 100644
index 0000000..a4a28ae
--- /dev/null
+++ b/.github/workflows/rust.yaml
@@ -0,0 +1,52 @@
+name: 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 }}
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..ee52156
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+/target
+.DS_Store
+Cargo.lock
+output.ini
+output2.ini
+test2.ini
+.vscode
+output_async.ini
+output_sync.ini
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..667c165
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,13 @@
+// This file is generated by cargo2android.py --run.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library_host {
+ name: "libconfigparser",
+ crate_name: "configparser",
+ cargo_env_compat: true,
+ cargo_pkg_version: "3.0.2",
+ srcs: ["src/lib.rs"],
+ edition: "2021",
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100755
index 0000000..e251ec4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,86 @@
+## 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<String>` 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
+
+Older changelogs are preserved here, current changelog is present in [README.md](README.md).
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..f45978c
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,127 @@
+# 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.
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..6d9376a
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,46 @@
+# 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.2"
+authors = ["QEDK <qedk.en@gmail.com>"]
+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"
+resolver = "2"
+
+[dependencies.async-std]
+version = "1.12.0"
+optional = true
+
+[dependencies.indexmap]
+version = "1.9.1"
+optional = true
+
+[badges.maintenance]
+status = "actively-developed"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100755
index 0000000..f65b357
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,22 @@
+[package]
+name = "configparser"
+version = "3.0.2"
+authors = ["QEDK <qedk.en@gmail.com>"]
+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 = "1.9.1", optional = true }
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 0000000..7f9a88e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+LICENSE-MIT \ No newline at end of file
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..fbe1dcb
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,27 @@
+MIT License
+
+Copyright (c) 2020 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. \ No newline at end of file
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..6d01048
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,19 @@
+name: "configparser"
+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."
+third_party {
+ identifier {
+ type: "crates.io"
+ value: "https://crates.io/crates/configparser"
+ }
+ identifier {
+ type: "Archive"
+ value: "https://static.crates.io/crates/configparser/configparser-3.0.2.crate"
+ }
+ version: "3.0.2"
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2023
+ month: 8
+ day: 23
+ }
+}
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..45dc4dd
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:master:/OWNERS
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..0481162
--- /dev/null
+++ b/README.md
@@ -0,0 +1,232 @@
+# 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/2022)](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.2"
+```
+
+## βž• 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::<i32>().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<dyn Error>> {
+ 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.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.
+- 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 (**STABLE**)
+ - Adds support for multi-line key-value pairs.
+ - Adds `async-std` feature for asynchronous file operations.
+ - Some performance optimizations.
+
+### πŸ”œ Future plans
+
+- Support for appending sections, coercing them as well.
+- Benchmarking against similar packages.
diff --git a/src/ini.rs b/src/ini.rs
new file mode 100755
index 0000000..d966e78
--- /dev/null
+++ b/src/ini.rs
@@ -0,0 +1,972 @@
+//!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::fs;
+use std::path::Path;
+use std::fmt::Write;
+
+///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<String, Map<String, Option<String>>>,
+ default_section: std::string::String,
+ comment_symbols: Vec<char>,
+ delimiters: Vec<char>,
+ boolean_values: HashMap<bool, Vec<String>>,
+ 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<char>,
+ ///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<char>,
+ pub boolean_values: HashMap<bool, Vec<String>>,
+ ///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<String, Map<String, Option<String>>>` 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<String, Map<String, Option<String>>>` 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<String>`.
+ pub fn sections(&self) -> Vec<String> {
+ 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<T: AsRef<Path>>(
+ &mut self,
+ path: T,
+ ) -> Result<Map<String, Map<String, Option<String>>>, 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())
+ }
+
+ ///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<Map<String, Map<String, Option<String>>>, String> {
+ self.map = match self.parse(input) {
+ Err(why) => return Err(why),
+ Ok(map) => map,
+ };
+ 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<T: AsRef<Path>>(&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<String, Option<String>>,
+ 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());
+
+ 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<Map<String, Map<String, Option<String>>>, String> {
+ let mut map: Map<String, Map<String, Option<String>>> = Map::new();
+ let mut section = self.default_section.clone();
+ let mut current_key: Option<String> = 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<String> {
+ let (section, key) = self.autocase(section, key);
+ self.map.get(&section)?.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<Option<bool>, String> {
+ let (section, key) = self.autocase(section, key);
+ match self.map.get(&section) {
+ Some(secmap) => match secmap.get(&key) {
+ Some(val) => match val {
+ Some(inner) => match inner.to_lowercase().parse::<bool>() {
+ 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<Option<bool>, String> {
+ let (section, key) = self.autocase(section, key);
+ match self.map.get(&section) {
+ 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<Option<i64>, String> {
+ let (section, key) = self.autocase(section, key);
+ match self.map.get(&section) {
+ Some(secmap) => match secmap.get(&key) {
+ Some(val) => match val {
+ Some(inner) => match inner.parse::<i64>() {
+ 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<Option<u64>, String> {
+ let (section, key) = self.autocase(section, key);
+ match self.map.get(&section) {
+ Some(secmap) => match secmap.get(&key) {
+ Some(val) => match val {
+ Some(inner) => match inner.parse::<u64>() {
+ 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<Option<f64>, String> {
+ let (section, key) = self.autocase(section, key);
+ match self.map.get(&section) {
+ Some(secmap) => match secmap.get(&key) {
+ Some(val) => match val {
+ Some(inner) => match inner.parse::<f64>() {
+ 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<Map<String, Map<String, Option<String>>>> {
+ 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<String, Map<String, Option<String>>> {
+ &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<String, Map<String, Option<String>>> {
+ &mut self.map
+ }
+
+ ///Sets an `Option<String>` 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<String>)`, with the existing value being the wrapped `Option<String>`.
+ ///If you want to insert using a string literal, use `setstr()` instead.
+ pub fn set(
+ &mut self,
+ section: &str,
+ key: &str,
+ value: Option<String>,
+ ) -> Option<Option<String>> {
+ let (section, key) = self.autocase(section, key);
+ match self.map.get_mut(&section) {
+ Some(secmap) => secmap.insert(key, value),
+ None => {
+ let mut valmap: Map<String, Option<String>> = 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<String>)`, with the existing value being the wrapped `Option<String>`.
+ ///If you want to insert using a `String`, use `set()` instead.
+ pub fn setstr(
+ &mut self,
+ section: &str,
+ key: &str,
+ value: Option<&str>,
+ ) -> Option<Option<String>> {
+ let (section, key) = self.autocase(section, key);
+ self.set(&section, &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<Map<String, Option<String>>> {
+ let section = if self.case_sensitive {
+ section.to_owned()
+ } else {
+ section.to_lowercase()
+ };
+ self.map.remove(&section)
+ }
+
+ ///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<String>)` if the value exists or else, `None`.
+ pub fn remove_key(&mut self, section: &str, key: &str) -> Option<Option<String>> {
+ let (section, key) = self.autocase(section, key);
+ self.map.get_mut(&section)?.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<T: AsRef<AsyncPath>>(
+ &mut self,
+ path: T,
+ ) -> Result<Map<String, Map<String, Option<String>>>, 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())
+ }
+
+ ///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<T: AsRef<Path>>(&self, path: T) -> std::io::Result<()> {
+ async_fs::write(path.as_ref(), self.unparse()).await
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100755
index 0000000..b313a56
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,154 @@
+/*!
+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::<i32>().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<dyn Error>> {
+ 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;
diff --git a/tests/test.ini b/tests/test.ini
new file mode 100755
index 0000000..7da5603
--- /dev/null
+++ b/tests/test.ini
@@ -0,0 +1,19 @@
+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
diff --git a/tests/test.rs b/tests/test.rs
new file mode 100755
index 0000000..b0f473c
--- /dev/null
+++ b/tests/test.rs
@@ -0,0 +1,348 @@
+use configparser::ini::Ini;
+use std::error::Error;
+
+#[test]
+#[allow(clippy::approx_constant)]
+fn non_cs() -> Result<(), Box<dyn Error>> {
+ 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());
+ Ok(())
+}
+
+#[test]
+#[allow(clippy::approx_constant)]
+fn cs() -> Result<(), Box<dyn Error>> {
+ 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<dyn Error>> {
+ 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<dyn Error>> {
+ 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 = "indexmap")]
+fn multiline_off() -> Result<(), Box<dyn Error>> {
+ 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<dyn Error>> {
+ 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(())
+}
diff --git a/tests/test_multiline.ini b/tests/test_multiline.ini
new file mode 100644
index 0000000..d980e99
--- /dev/null
+++ b/tests/test_multiline.ini
@@ -0,0 +1,7 @@
+[Section]
+Key1: Value1
+Key2: Value Two
+Key3: this is a haiku
+ spread across separate lines
+ a single value
+Key4: Four \ No newline at end of file