Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"6a2aae5186dcf36c1ec13775d37e4ed0e509a4dfda914dc12934c64672cdf9cb","Cargo.lock":"781560302de048989b52b2ea45f008500aa146f48e86e49b3680bd4f5105e1fa","Cargo.toml":"237b211c582309ad12001edee734b3aa7a52ac96aba4f022e58df57ea9d5aaf3","Cargo.toml.orig":"029f9cb735ce1c05abca94c8071b5ec80124ed3cf79c57aa271870b758ba5052","LICENSE":"37653ea50ac84bb5e93bf59d801df1ecbe17f0042a30bfe335e3b20f4a4465fa","README.md":"d80daa7717f02da17c1c220aa92e5efeaeaf985bdffea0efcffe331ce1c12b23","examples/list_variables.rs":"d09b0ec6a3ef8b51e69c38b979c73d8e6989d0f4631abbe1ef71dae50809b99f","src/bin/dotenvy.rs":"bfe42c2f6c541a977e05e2282c13f4da7e144e452dc2fe92660585b849d10f8d","src/errors.rs":"c69341545adce1f0f25961304d7670cfc325150377e781145bd963bbb9613185","src/find.rs":"c36921f93ec4496fbbc81e867d89ec681f95d33a9b62a40cd62fd3996cb642cb","src/iter.rs":"62c445da40d79d806fc5f61ddfbb844a903eddcea0b8baf4bd695ef27b24efa9","src/lib.rs":"833308eb43075850f3f2a91850326d5aa39f3aefcab790b486c3ccc7976f38b8","src/parse.rs":"0d671881bd3066f941705b90f78c79042a8cf0c137e2ef93dc175a8c097ca5dc","tests/common/mod.rs":"27b04b76f3e994d1a53e5b508bbfdfc0db4364623ff04638682ceb57562aa2bd","tests/integration/main.rs":"6e6bf9ecadc52ddc29309b561a1a3e813de75839aa6ad3b7e9ebc31e13b9ed8e","tests/integration/util/mod.rs":"d0ba8fce24cdbc97f5e1a19c26654ab7651e6803411fa8c069ba69aacb0727e9","tests/integration/util/testenv.rs":"bf8f4a81733f77ef79725f24b88f9f41d538ac1e4395c3098d867047c68e80bf","tests/test-child-dir.rs":"49fff1b504a227669cee06c58855c81426a6f80fe0c0b50ee2c49a488c05ff06","tests/test-default-location-override.rs":"90ca25bf419dea245237aeaa02119045ef3fa60a648cc05565a83701b920f74b","tests/test-default-location.rs":"12e3199589f2ce116e06a9ffc9e799801d0e23370bd766c2bc01ce7d933d2a3c","tests/test-dotenv-iter.rs":"9e1b8357056e057ebf1de4be5c0fead26d928504f666e5bda390a91b876135e9","tests/test-from-filename-iter.rs":"f186253f071e2e431be23ea420cfaaedc2a5eedbcf52012e821d467114e491f5","tests/test-from-filename-override.rs":"3b0b5c418f976da142e2701dd260d577f18377a542bd8f8292ce258ee726eda4","tests/test-from-filename.rs":"5f7ad787cf9303b3b0d47874a75753453e65adbe49f030f8d49366286193b197","tests/test-from-path-iter.rs":"b004b8fc933fe456e497d6681bde8091363f7f4f411cd9e21b99a84a7f0b7b9f","tests/test-from-path-override.rs":"8fcb3030a25c4ee31905d976ac0c0eb32946a7430637db21372d642f7c29f2c7","tests/test-from-path.rs":"4d73d770b817083a0f759eefd4fee23f033272f707bca69197acb9f075d4e1e3","tests/test-from-read-override.rs":"6e7ff07f52f7c515fb8818a78ba09576fb95c7e4543d84064a9bf5683bd81f54","tests/test-from-read.rs":"4ac33b20e31fb7a250a2070318ea7fe2232f509755ea5ec7c5a4b979f3399d81","tests/test-ignore-bom.rs":"bb1b404d9e18a168a85a4bd0de1b90957fdd5e209f7079d0f21c903fd2883091","tests/test-multiline-comment.rs":"0c6438b06a84cd4bdd61c3bd6775289baf008a9c3e408865b5750cdd1d2154ee","tests/test-multiline.rs":"4fd9502974216a982ed7dfcc5d6cd2f57b15fa764f35cd2fadee79b0414756b1","tests/test-var.rs":"c11750dfd8ddbaa8edf7491b766fbdc92a3826230799d4246652b96153e035bc","tests/test-variable-substitution.rs":"6a8ed0121661fd5d8d792cc499bd70c47346021d938e76319797e3335f503e70","tests/test-vars.rs":"189827a23362c54d1619107334216969abca5ec840ba385976b26761a673e227"},"package":"1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "e330110a7cd9064ec913e554c9fc325c9d7cdb88"
},
"path_in_vcs": "dotenv"
}
+345
View File
@@ -0,0 +1,345 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
dependencies = [
"atty",
"bitflags",
"clap_lex",
"indexmap",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
dependencies = [
"clap",
"once_cell",
"tempfile",
]
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "io-lifetimes"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.45.0",
]
[[package]]
name = "libc"
version = "0.2.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "once_cell"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "os_str_bytes"
version = "6.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
[[package]]
name = "rustix"
version = "0.36.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.45.0",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "tempfile"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"windows-sys 0.42.0",
]
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+55
View File
@@ -0,0 +1,55 @@
# 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 = "2018"
rust-version = "1.56.1"
name = "dotenvy"
version = "0.15.7"
authors = [
"Noemi Lapresta <noemi.lapresta@gmail.com>",
"Craig Hills <chills@gmail.com>",
"Mike Piccolo <mfpiccolo@gmail.com>",
"Alice Maz <alice@alicemaz.com>",
"Sean Griffin <sean@seantheprogrammer.com>",
"Adam Sharp <adam@sharplet.me>",
"Arpad Borsos <arpad.borsos@googlemail.com>",
"Allan Zhang <al@ayz.ai>",
]
description = "A well-maintained fork of the dotenv crate"
homepage = "https://github.com/allan2/dotenvy"
readme = "README.md"
keywords = [
"dotenv",
"env",
"environment",
"settings",
"config",
]
license = "MIT"
repository = "https://github.com/allan2/dotenvy"
[[bin]]
name = "dotenvy"
required-features = ["cli"]
[dependencies.clap]
version = "3.2"
optional = true
[dev-dependencies.once_cell]
version = "1.16.0"
[dev-dependencies.tempfile]
version = "3.3.0"
[features]
cli = ["clap"]
+35
View File
@@ -0,0 +1,35 @@
[package]
name = "dotenvy"
version = "0.15.7"
authors = [
"Noemi Lapresta <noemi.lapresta@gmail.com>",
"Craig Hills <chills@gmail.com>",
"Mike Piccolo <mfpiccolo@gmail.com>",
"Alice Maz <alice@alicemaz.com>",
"Sean Griffin <sean@seantheprogrammer.com>",
"Adam Sharp <adam@sharplet.me>",
"Arpad Borsos <arpad.borsos@googlemail.com>",
"Allan Zhang <al@ayz.ai>",
]
description = "A well-maintained fork of the dotenv crate"
homepage = "https://github.com/allan2/dotenvy"
readme = "README.md"
keywords = ["dotenv", "env", "environment", "settings", "config"]
license = "MIT"
repository = "https://github.com/allan2/dotenvy"
edition = "2018"
rust-version = "1.56.1"
[[bin]]
name = "dotenvy"
required-features = ["cli"]
[dependencies]
clap = { version = "3.2", optional = true }
[dev-dependencies]
tempfile = "3.3.0"
once_cell = "1.16.0"
[features]
cli = ["clap"]
+21
View File
@@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2014 Santiago Lapresta and contributors
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.
+80
View File
@@ -0,0 +1,80 @@
# dotenvy
[![Crates.io](https://img.shields.io/crates/v/dotenvy.svg)](https://crates.io/crates/dotenvy)
[![msrv
1.56.1](https://img.shields.io/badge/msrv-1.56.1-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.56.1)
[![ci](https://github.com/allan2/dotenvy/actions/workflows/ci.yml/badge.svg)](https://github.com/allan2/dotenvy/actions/workflows/ci.yml)
[![docs](https://img.shields.io/docsrs/dotenvy?logo=docs.rs)](https://docs.rs/dotenvy/)
A well-maintained fork of the [dotenv](https://github.com/dotenv-rs/dotenv) crate.
This crate is the suggested alternative for `dotenv` in security advisory [RUSTSEC-2021-0141](https://rustsec.org/advisories/RUSTSEC-2021-0141.html).
This library loads environment variables from a _.env_ file. This is convenient for dev environments.
## Components
1. [`dotenvy`](https://crates.io/crates/dotenvy) crate - A well-maintained fork of the `dotenv` crate.
2. [`dotenvy_macro`](https://crates.io/crates/dotenvy_macro) crate - A macro for compile time dotenv inspection. This is a fork of `dotenv_codegen`.
3. `dotenvy` CLI tool for running a command using the environment from a _.env_ file (currently Unix only)
## Usage
### Loading at runtime
```rust
use std::env;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// Load environment variables from .env file.
// Fails if .env file not found, not readable or invalid.
dotenvy::dotenv()?;
for (key, value) in env::vars() {
println!("{key}: {value}");
}
Ok(())
}
```
### Loading at compile time
The `dotenv!` macro provided by `dotenvy_macro` crate can be used.
Warning: there is an outstanding issue with rust-analyzer ([rust-analyzer #9606](https://github.com/rust-analyzer/rust-analyzer/issues/9606)) related to the `dotenv!` macro
## Minimum supported Rust version
Currently: **1.56.1**
We aim to support the latest 8 rustc versions - approximately 1 year. Increasing
MSRV is _not_ considered a semver-breaking change.
## Why does this fork exist?
The original dotenv crate has not been updated since June 26, 2020. Attempts to reach the authors and present maintainer were not successful ([dotenv-rs/dotenv #74](https://github.com/dotenv-rs/dotenv/issues/74)).
This fork intends to serve as the development home for the dotenv implementation in Rust.
## What are the differences from the original?
This repo fixes:
- more helpful errors for `dotenv!` ([dotenv-rs/dotenv #57](https://github.com/dotenv-rs/dotenv/pull/57))
It also adds:
- multiline support for environment variable values
- `io::Read` support via [`from_read`](https://docs.rs/dotenvy/latest/dotenvy/fn.from_read.html) and [`from_read_iter`](https://docs.rs/dotenvy/latest/dotenvy/fn.from_read_iter.html)
- override support via [`dotenv_override`], [`from_filename_override`], [`from_path_override`] and [`from_read_override`]
- improved docs
For a full list of changes, refer to the [changelog](./CHANGELOG.md).
## The legend
Legend has it that the Lost Maintainer will return, merging changes from `dotenvy` into `dotenv` with such thrust that all `Cargo.toml`s will lose one keystroke. Only then shall the Rust dotenv crateverse be united in true harmony.
Until then, this repo dutifully carries on the dotenv torch. It is actively maintained. Contributions and PRs are very welcome!
+10
View File
@@ -0,0 +1,10 @@
use dotenvy::{dotenv_iter, Error};
fn main() -> Result<(), Error> {
dotenvy::dotenv()?;
for item in dotenv_iter()? {
let (key, val) = item?;
println!("{}={}", key, val);
}
Ok(())
}
+68
View File
@@ -0,0 +1,68 @@
use clap::Arg;
use std::os::unix::process::CommandExt;
use std::process;
macro_rules! die {
($fmt:expr) => ({
eprintln!($fmt);
process::exit(1);
});
($fmt:expr, $($arg:tt)*) => ({
eprintln!($fmt, $($arg)*);
process::exit(1);
});
}
fn make_command(name: &str, args: Vec<&str>) -> process::Command {
let mut command = process::Command::new(name);
for arg in args {
command.arg(arg);
}
return command;
}
fn main() {
let matches = clap::Command::new("dotenvy")
.about("Run a command using the environment in a .env file")
.override_usage("dotenvy <COMMAND> [ARGS]...")
.allow_external_subcommands(true)
.arg_required_else_help(true)
.arg(
Arg::new("FILE")
.short('f')
.long("file")
.takes_value(true)
.help("Use a specific .env file (defaults to .env)"),
)
.get_matches();
match matches.value_of("FILE") {
None => dotenvy::dotenv(),
Some(file) => dotenvy::from_filename(file),
}
.unwrap_or_else(|e| die!("error: failed to load environment: {}", e));
let mut command = match matches.subcommand() {
Some((name, matches)) => {
let args = matches
.values_of("")
.map(|v| v.collect())
.unwrap_or(Vec::new());
make_command(name, args)
}
None => die!("error: missing required argument <COMMAND>"),
};
if cfg!(target_os = "windows") {
match command.spawn().and_then(|mut child| child.wait()) {
Ok(status) => process::exit(status.code().unwrap_or(1)),
Err(error) => die!("fatal: {}", error),
};
} else {
let error = command.exec();
die!("fatal: {}", error);
};
}
+122
View File
@@ -0,0 +1,122 @@
use std::env;
use std::error;
use std::fmt;
use std::io;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
LineParse(String, usize),
Io(io::Error),
EnvVar(env::VarError),
}
impl Error {
pub fn not_found(&self) -> bool {
if let Error::Io(ref io_error) = *self {
return io_error.kind() == io::ErrorKind::NotFound;
}
false
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Error::Io(err) => Some(err),
Error::EnvVar(err) => Some(err),
_ => None,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Io(err) => write!(fmt, "{}", err),
Error::EnvVar(err) => write!(fmt, "{}", err),
Error::LineParse(line, error_index) => write!(
fmt,
"Error parsing line: '{}', error at line index: {}",
line, error_index
),
}
}
}
#[cfg(test)]
mod test {
use std::env;
use std::error::Error as StdError;
use std::io;
use super::*;
#[test]
fn test_io_error_source() {
let err = Error::Io(io::ErrorKind::PermissionDenied.into());
let io_err = err.source().unwrap().downcast_ref::<io::Error>().unwrap();
assert_eq!(io::ErrorKind::PermissionDenied, io_err.kind());
}
#[test]
fn test_envvar_error_source() {
let err = Error::EnvVar(env::VarError::NotPresent);
let var_err = err
.source()
.unwrap()
.downcast_ref::<env::VarError>()
.unwrap();
assert_eq!(&env::VarError::NotPresent, var_err);
}
#[test]
fn test_lineparse_error_source() {
let err = Error::LineParse("test line".to_string(), 2);
assert!(err.source().is_none());
}
#[test]
fn test_error_not_found_true() {
let err = Error::Io(io::ErrorKind::NotFound.into());
assert!(err.not_found());
}
#[test]
fn test_error_not_found_false() {
let err = Error::Io(io::ErrorKind::PermissionDenied.into());
assert!(!err.not_found());
}
#[test]
fn test_io_error_display() {
let err = Error::Io(io::ErrorKind::PermissionDenied.into());
let io_err: io::Error = io::ErrorKind::PermissionDenied.into();
let err_desc = format!("{}", err);
let io_err_desc = format!("{}", io_err);
assert_eq!(io_err_desc, err_desc);
}
#[test]
fn test_envvar_error_display() {
let err = Error::EnvVar(env::VarError::NotPresent);
let var_err = env::VarError::NotPresent;
let err_desc = format!("{}", err);
let var_err_desc = format!("{}", var_err);
assert_eq!(var_err_desc, err_desc);
}
#[test]
fn test_lineparse_error_display() {
let err = Error::LineParse("test line".to_string(), 2);
let err_desc = format!("{}", err);
assert_eq!(
"Error parsing line: 'test line', error at line index: 2",
err_desc
);
}
}
+57
View File
@@ -0,0 +1,57 @@
use std::fs::File;
use std::path::{Path, PathBuf};
use std::{env, fs, io};
use crate::errors::*;
use crate::iter::Iter;
pub struct Finder<'a> {
filename: &'a Path,
}
impl<'a> Finder<'a> {
pub fn new() -> Self {
Finder {
filename: Path::new(".env"),
}
}
pub fn filename(mut self, filename: &'a Path) -> Self {
self.filename = filename;
self
}
pub fn find(self) -> Result<(PathBuf, Iter<File>)> {
let path = find(&env::current_dir().map_err(Error::Io)?, self.filename)?;
let file = File::open(&path).map_err(Error::Io)?;
let iter = Iter::new(file);
Ok((path, iter))
}
}
/// Searches for `filename` in `directory` and parent directories until found or root is reached.
pub fn find(directory: &Path, filename: &Path) -> Result<PathBuf> {
let candidate = directory.join(filename);
match fs::metadata(&candidate) {
Ok(metadata) => {
if metadata.is_file() {
return Ok(candidate);
}
}
Err(error) => {
if error.kind() != io::ErrorKind::NotFound {
return Err(Error::Io(error));
}
}
}
if let Some(parent) = directory.parent() {
find(parent, filename)
} else {
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"path not found",
)))
}
}
+199
View File
@@ -0,0 +1,199 @@
use std::collections::HashMap;
use std::env;
use std::io::prelude::*;
use std::io::BufReader;
use crate::errors::*;
use crate::parse;
pub struct Iter<R> {
lines: QuotedLines<BufReader<R>>,
substitution_data: HashMap<String, Option<String>>,
}
impl<R: Read> Iter<R> {
pub fn new(reader: R) -> Iter<R> {
Iter {
lines: QuotedLines {
buf: BufReader::new(reader),
},
substitution_data: HashMap::new(),
}
}
/// Loads all variables found in the `reader` into the environment,
/// preserving any existing environment variables of the same name.
///
/// If a variable is specified multiple times within the reader's data,
/// then the first occurrence is applied.
pub fn load(mut self) -> Result<()> {
self.remove_bom()?;
for item in self {
let (key, value) = item?;
if env::var(&key).is_err() {
env::set_var(&key, value);
}
}
Ok(())
}
/// Loads all variables found in the `reader` into the environment,
/// overriding any existing environment variables of the same name.
///
/// If a variable is specified multiple times within the reader's data,
/// then the last occurrence is applied.
pub fn load_override(mut self) -> Result<()> {
self.remove_bom()?;
for item in self {
let (key, value) = item?;
env::set_var(key, value);
}
Ok(())
}
fn remove_bom(&mut self) -> Result<()> {
let buffer = self.lines.buf.fill_buf().map_err(Error::Io)?;
// https://www.compart.com/en/unicode/U+FEFF
if buffer.starts_with(&[0xEF, 0xBB, 0xBF]) {
// remove the BOM from the bufreader
self.lines.buf.consume(3);
}
Ok(())
}
}
struct QuotedLines<B> {
buf: B,
}
enum ParseState {
Complete,
Escape,
StrongOpen,
StrongOpenEscape,
WeakOpen,
WeakOpenEscape,
Comment,
WhiteSpace,
}
fn eval_end_state(prev_state: ParseState, buf: &str) -> (usize, ParseState) {
let mut cur_state = prev_state;
let mut cur_pos: usize = 0;
for (pos, c) in buf.char_indices() {
cur_pos = pos;
cur_state = match cur_state {
ParseState::WhiteSpace => match c {
'#' => return (cur_pos, ParseState::Comment),
'\\' => ParseState::Escape,
'"' => ParseState::WeakOpen,
'\'' => ParseState::StrongOpen,
_ => ParseState::Complete,
},
ParseState::Escape => ParseState::Complete,
ParseState::Complete => match c {
c if c.is_whitespace() && c != '\n' && c != '\r' => ParseState::WhiteSpace,
'\\' => ParseState::Escape,
'"' => ParseState::WeakOpen,
'\'' => ParseState::StrongOpen,
_ => ParseState::Complete,
},
ParseState::WeakOpen => match c {
'\\' => ParseState::WeakOpenEscape,
'"' => ParseState::Complete,
_ => ParseState::WeakOpen,
},
ParseState::WeakOpenEscape => ParseState::WeakOpen,
ParseState::StrongOpen => match c {
'\\' => ParseState::StrongOpenEscape,
'\'' => ParseState::Complete,
_ => ParseState::StrongOpen,
},
ParseState::StrongOpenEscape => ParseState::StrongOpen,
// Comments last the entire line.
ParseState::Comment => panic!("should have returned early"),
};
}
(cur_pos, cur_state)
}
impl<B: BufRead> Iterator for QuotedLines<B> {
type Item = Result<String>;
fn next(&mut self) -> Option<Result<String>> {
let mut buf = String::new();
let mut cur_state = ParseState::Complete;
let mut buf_pos;
let mut cur_pos;
loop {
buf_pos = buf.len();
match self.buf.read_line(&mut buf) {
Ok(0) => match cur_state {
ParseState::Complete => return None,
_ => {
let len = buf.len();
return Some(Err(Error::LineParse(buf, len)));
}
},
Ok(_n) => {
// Skip lines which start with a # before iteration
// This optimizes parsing a bit.
if buf.trim_start().starts_with('#') {
return Some(Ok(String::with_capacity(0)));
}
let result = eval_end_state(cur_state, &buf[buf_pos..]);
cur_pos = result.0;
cur_state = result.1;
match cur_state {
ParseState::Complete => {
if buf.ends_with('\n') {
buf.pop();
if buf.ends_with('\r') {
buf.pop();
}
}
return Some(Ok(buf));
}
ParseState::Escape
| ParseState::StrongOpen
| ParseState::StrongOpenEscape
| ParseState::WeakOpen
| ParseState::WeakOpenEscape
| ParseState::WhiteSpace => {}
ParseState::Comment => {
buf.truncate(buf_pos + cur_pos);
return Some(Ok(buf));
}
}
}
Err(e) => return Some(Err(Error::Io(e))),
}
}
}
}
impl<R: Read> Iterator for Iter<R> {
type Item = Result<(String, String)>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let line = match self.lines.next() {
Some(Ok(line)) => line,
Some(Err(err)) => return Some(Err(err)),
None => return None,
};
match parse::parse_line(&line, &mut self.substitution_data) {
Ok(Some(result)) => return Some(Ok(result)),
Ok(None) => {}
Err(err) => return Some(Err(err)),
}
}
}
}
+373
View File
@@ -0,0 +1,373 @@
//! [`dotenv`]: https://crates.io/crates/dotenv
//! A well-maintained fork of the [`dotenv`] crate
//!
//! This library loads environment variables from a *.env* file. This is convenient for dev environments.
mod errors;
mod find;
mod iter;
mod parse;
use std::env::{self, Vars};
use std::ffi::OsStr;
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Once;
pub use crate::errors::*;
use crate::find::Finder;
pub use crate::iter::Iter;
static START: Once = Once::new();
/// Gets the value for an environment variable.
///
/// The value is `Ok(s)` if the environment variable is present and valid unicode.
///
/// Note: this function gets values from any visible environment variable key,
/// regardless of whether a *.env* file was loaded.
///
/// # Examples:
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let value = dotenvy::var("HOME")?;
/// println!("{}", value); // prints `/home/foo`
/// # Ok(())
/// # }
/// ```
pub fn var<K: AsRef<OsStr>>(key: K) -> Result<String> {
START.call_once(|| {
dotenv().ok();
});
env::var(key).map_err(Error::EnvVar)
}
/// Returns an iterator of `(key, value)` pairs for all environment variables of the current process.
/// The returned iterator contains a snapshot of the process's environment variables at the time of invocation. Modifications to environment variables afterwards will not be reflected.
///
/// # Examples:
///
/// ```no_run
/// use std::io;
///
/// let result: Vec<(String, String)> = dotenvy::vars().collect();
/// ```
pub fn vars() -> Vars {
START.call_once(|| {
dotenv().ok();
});
env::vars()
}
/// Loads environment variables from the specified path.
///
/// If variables with the same names already exist in the environment, then their values will be
/// preserved.
///
/// Where multiple declarations for the same environment variable exist in your *.env*
/// file, the *first one* is applied.
///
/// If you wish to ensure all variables are loaded from your *.env* file, ignoring variables
/// already existing in the environment, then use [`from_path_override`] instead.
///
/// # Examples
///
/// ```no_run
/// use std::path::Path;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::from_path(Path::new("path/to/.env"))?;
/// # Ok(())
/// # }
/// ```
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<()> {
let iter = Iter::new(File::open(path).map_err(Error::Io)?);
iter.load()
}
/// Loads environment variables from the specified path,
/// overriding existing environment variables.
///
/// Where multiple declarations for the same environment variable exist in your *.env* file, the
/// *last one* is applied.
///
/// If you want the existing environment to take precedence,
/// or if you want to be able to override environment variables on the command line,
/// then use [`from_path`] instead.
///
/// # Examples
///
/// ```no_run
/// use std::path::Path;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::from_path_override(Path::new("path/to/.env"))?;
/// # Ok(())
/// # }
/// ```
pub fn from_path_override<P: AsRef<Path>>(path: P) -> Result<()> {
let iter = Iter::new(File::open(path).map_err(Error::Io)?);
iter.load_override()
}
/// Returns an iterator over environment variables from the specified path.
///
/// # Examples
///
/// ```no_run
/// use std::path::Path;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// for item in dotenvy::from_path_iter(Path::new("path/to/.env"))? {
/// let (key, val) = item?;
/// println!("{}={}", key, val);
/// }
/// # Ok(())
/// # }
/// ```
pub fn from_path_iter<P: AsRef<Path>>(path: P) -> Result<Iter<File>> {
Ok(Iter::new(File::open(path).map_err(Error::Io)?))
}
/// Loads environment variables from the specified file.
///
/// If variables with the same names already exist in the environment, then their values will be
/// preserved.
///
/// Where multiple declarations for the same environment variable exist in your *.env*
/// file, the *first one* is applied.
///
/// If you wish to ensure all variables are loaded from your *.env* file, ignoring variables
/// already existing in the environment, then use [`from_filename_override`] instead.
///
/// # Examples
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::from_filename("custom.env")?;
/// # Ok(())
/// # }
/// ```
///
/// It is also possible to load from a typical *.env* file like so. However, using [`dotenv`] is preferred.
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::from_filename(".env")?;
/// # Ok(())
/// # }
/// ```
pub fn from_filename<P: AsRef<Path>>(filename: P) -> Result<PathBuf> {
let (path, iter) = Finder::new().filename(filename.as_ref()).find()?;
iter.load()?;
Ok(path)
}
/// Loads environment variables from the specified file,
/// overriding existing environment variables.
///
/// Where multiple declarations for the same environment variable exist in your *.env* file, the
/// *last one* is applied.
///
/// If you want the existing environment to take precedence,
/// or if you want to be able to override environment variables on the command line,
/// then use [`from_filename`] instead.
///
/// # Examples
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::from_filename_override("custom.env")?;
/// # Ok(())
/// # }
/// ```
///
/// It is also possible to load from a typical *.env* file like so. However, using [`dotenv_override`] is preferred.
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::from_filename_override(".env")?;
/// # Ok(())
/// # }
/// ```
pub fn from_filename_override<P: AsRef<Path>>(filename: P) -> Result<PathBuf> {
let (path, iter) = Finder::new().filename(filename.as_ref()).find()?;
iter.load_override()?;
Ok(path)
}
/// Returns an iterator over environment variables from the specified file.
///
/// # Examples
///
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// for item in dotenvy::from_filename_iter("custom.env")? {
/// let (key, val) = item?;
/// println!("{}={}", key, val);
/// }
/// # Ok(())
/// # }
/// ```
pub fn from_filename_iter<P: AsRef<Path>>(filename: P) -> Result<Iter<File>> {
let (_, iter) = Finder::new().filename(filename.as_ref()).find()?;
Ok(iter)
}
/// Loads environment variables from [`io::Read`](std::io::Read).
///
/// This is useful for loading environment variables from IPC or the network.
///
/// If variables with the same names already exist in the environment, then their values will be
/// preserved.
///
/// Where multiple declarations for the same environment variable exist in your `reader`,
/// the *first one* is applied.
///
/// If you wish to ensure all variables are loaded from your `reader`, ignoring variables
/// already existing in the environment, then use [`from_read_override`] instead.
///
/// For regular files, use [`from_path`] or [`from_filename`].
///
/// # Examples
///
/// ```no_run
/// # #![cfg(unix)]
/// use std::io::Read;
/// use std::os::unix::net::UnixStream;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut stream = UnixStream::connect("/some/socket")?;
/// dotenvy::from_read(stream)?;
/// # Ok(())
/// # }
/// ```
pub fn from_read<R: io::Read>(reader: R) -> Result<()> {
let iter = Iter::new(reader);
iter.load()?;
Ok(())
}
/// Loads environment variables from [`io::Read`](std::io::Read),
/// overriding existing environment variables.
///
/// This is useful for loading environment variables from IPC or the network.
///
/// Where multiple declarations for the same environment variable exist in your `reader`, the
/// *last one* is applied.
///
/// If you want the existing environment to take precedence,
/// or if you want to be able to override environment variables on the command line,
/// then use [`from_read`] instead.
///
/// For regular files, use [`from_path_override`] or [`from_filename_override`].
///
/// # Examples
/// ```no_run
/// # #![cfg(unix)]
/// use std::io::Read;
/// use std::os::unix::net::UnixStream;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut stream = UnixStream::connect("/some/socket")?;
/// dotenvy::from_read_override(stream)?;
/// # Ok(())
/// # }
/// ```
pub fn from_read_override<R: io::Read>(reader: R) -> Result<()> {
let iter = Iter::new(reader);
iter.load_override()?;
Ok(())
}
/// Returns an iterator over environment variables from [`io::Read`](std::io::Read).
///
/// # Examples
///
/// ```no_run
/// # #![cfg(unix)]
/// use std::io::Read;
/// use std::os::unix::net::UnixStream;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut stream = UnixStream::connect("/some/socket")?;
///
/// for item in dotenvy::from_read_iter(stream) {
/// let (key, val) = item?;
/// println!("{}={}", key, val);
/// }
/// # Ok(())
/// # }
/// ```
pub fn from_read_iter<R: io::Read>(reader: R) -> Iter<R> {
Iter::new(reader)
}
/// Loads the *.env* file from the current directory or parents. This is typically what you want.
///
/// If variables with the same names already exist in the environment, then their values will be
/// preserved.
///
/// Where multiple declarations for the same environment variable exist in your *.env*
/// file, the *first one* is applied.
///
/// If you wish to ensure all variables are loaded from your *.env* file, ignoring variables
/// already existing in the environment, then use [`dotenv_override`] instead.
///
/// An error will be returned if the file is not found.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::dotenv()?;
/// # Ok(())
/// # }
/// ```
pub fn dotenv() -> Result<PathBuf> {
let (path, iter) = Finder::new().find()?;
iter.load()?;
Ok(path)
}
/// Loads all variables found in the `reader` into the environment,
/// overriding any existing environment variables of the same name.
///
/// Where multiple declarations for the same environment variable exist in your *.env* file, the
/// *last one* is applied.
///
/// If you want the existing environment to take precedence,
/// or if you want to be able to override environment variables on the command line,
/// then use [`dotenv`] instead.
///
/// # Examples
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// dotenvy::dotenv_override()?;
/// # Ok(())
/// # }
/// ```
pub fn dotenv_override() -> Result<PathBuf> {
let (path, iter) = Finder::new().find()?;
iter.load_override()?;
Ok(path)
}
/// Returns an iterator over environment variables.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// for item in dotenvy::dotenv_iter()? {
/// let (key, val) = item?;
/// println!("{}={}", key, val);
/// }
/// # Ok(())
/// # }
/// ```
pub fn dotenv_iter() -> Result<iter::Iter<File>> {
let (_, iter) = Finder::new().find()?;
Ok(iter)
}
+653
View File
@@ -0,0 +1,653 @@
use std::collections::HashMap;
use std::env;
use crate::errors::*;
// for readability's sake
pub type ParsedLine = Result<Option<(String, String)>>;
pub fn parse_line(
line: &str,
substitution_data: &mut HashMap<String, Option<String>>,
) -> ParsedLine {
let mut parser = LineParser::new(line, substitution_data);
parser.parse_line()
}
struct LineParser<'a> {
original_line: &'a str,
substitution_data: &'a mut HashMap<String, Option<String>>,
line: &'a str,
pos: usize,
}
impl<'a> LineParser<'a> {
fn new(
line: &'a str,
substitution_data: &'a mut HashMap<String, Option<String>>,
) -> LineParser<'a> {
LineParser {
original_line: line,
substitution_data,
line: line.trim_end(), // we don’t want trailing whitespace
pos: 0,
}
}
fn err(&self) -> Error {
Error::LineParse(self.original_line.into(), self.pos)
}
fn parse_line(&mut self) -> ParsedLine {
self.skip_whitespace();
// if its an empty line or a comment, skip it
if self.line.is_empty() || self.line.starts_with('#') {
return Ok(None);
}
let mut key = self.parse_key()?;
self.skip_whitespace();
// export can be either an optional prefix or a key itself
if key == "export" {
// here we check for an optional `=`, below we throw directly when it’s not found.
if self.expect_equal().is_err() {
key = self.parse_key()?;
self.skip_whitespace();
self.expect_equal()?;
}
} else {
self.expect_equal()?;
}
self.skip_whitespace();
if self.line.is_empty() || self.line.starts_with('#') {
self.substitution_data.insert(key.clone(), None);
return Ok(Some((key, String::new())));
}
let parsed_value = parse_value(self.line, self.substitution_data)?;
self.substitution_data
.insert(key.clone(), Some(parsed_value.clone()));
Ok(Some((key, parsed_value)))
}
fn parse_key(&mut self) -> Result<String> {
if !self
.line
.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
{
return Err(self.err());
}
let index = match self
.line
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.'))
{
Some(index) => index,
None => self.line.len(),
};
self.pos += index;
let key = String::from(&self.line[..index]);
self.line = &self.line[index..];
Ok(key)
}
fn expect_equal(&mut self) -> Result<()> {
if !self.line.starts_with('=') {
return Err(self.err());
}
self.line = &self.line[1..];
self.pos += 1;
Ok(())
}
fn skip_whitespace(&mut self) {
if let Some(index) = self.line.find(|c: char| !c.is_whitespace()) {
self.pos += index;
self.line = &self.line[index..];
} else {
self.pos += self.line.len();
self.line = "";
}
}
}
#[derive(Eq, PartialEq)]
enum SubstitutionMode {
None,
Block,
EscapedBlock,
}
fn parse_value(
input: &str,
substitution_data: &mut HashMap<String, Option<String>>,
) -> Result<String> {
let mut strong_quote = false; // '
let mut weak_quote = false; // "
let mut escaped = false;
let mut expecting_end = false;
//FIXME can this be done without yet another allocation per line?
let mut output = String::new();
let mut substitution_mode = SubstitutionMode::None;
let mut substitution_name = String::new();
for (index, c) in input.chars().enumerate() {
//the regex _should_ already trim whitespace off the end
//expecting_end is meant to permit: k=v #comment
//without affecting: k=v#comment
//and throwing on: k=v w
if expecting_end {
if c == ' ' || c == '\t' {
continue;
} else if c == '#' {
break;
} else {
return Err(Error::LineParse(input.to_owned(), index));
}
} else if escaped {
//TODO I tried handling literal \r but various issues
//imo not worth worrying about until there's a use case
//(actually handling backslash 0x10 would be a whole other matter)
//then there's \v \f bell hex... etc
match c {
'\\' | '\'' | '"' | '$' | ' ' => output.push(c),
'n' => output.push('\n'), // handle \n case
_ => {
return Err(Error::LineParse(input.to_owned(), index));
}
}
escaped = false;
} else if strong_quote {
if c == '\'' {
strong_quote = false;
} else {
output.push(c);
}
} else if substitution_mode != SubstitutionMode::None {
if c.is_alphanumeric() {
substitution_name.push(c);
} else {
match substitution_mode {
SubstitutionMode::None => unreachable!(),
SubstitutionMode::Block => {
if c == '{' && substitution_name.is_empty() {
substitution_mode = SubstitutionMode::EscapedBlock;
} else {
apply_substitution(
substitution_data,
&substitution_name.drain(..).collect::<String>(),
&mut output,
);
if c == '$' {
substitution_mode = if !strong_quote && !escaped {
SubstitutionMode::Block
} else {
SubstitutionMode::None
}
} else {
substitution_mode = SubstitutionMode::None;
output.push(c);
}
}
}
SubstitutionMode::EscapedBlock => {
if c == '}' {
substitution_mode = SubstitutionMode::None;
apply_substitution(
substitution_data,
&substitution_name.drain(..).collect::<String>(),
&mut output,
);
} else {
substitution_name.push(c);
}
}
}
}
} else if c == '$' {
substitution_mode = if !strong_quote && !escaped {
SubstitutionMode::Block
} else {
SubstitutionMode::None
}
} else if weak_quote {
if c == '"' {
weak_quote = false;
} else if c == '\\' {
escaped = true;
} else {
output.push(c);
}
} else if c == '\'' {
strong_quote = true;
} else if c == '"' {
weak_quote = true;
} else if c == '\\' {
escaped = true;
} else if c == ' ' || c == '\t' {
expecting_end = true;
} else {
output.push(c);
}
}
//XXX also fail if escaped? or...
if substitution_mode == SubstitutionMode::EscapedBlock || strong_quote || weak_quote {
let value_length = input.len();
Err(Error::LineParse(
input.to_owned(),
if value_length == 0 {
0
} else {
value_length - 1
},
))
} else {
apply_substitution(
substitution_data,
&substitution_name.drain(..).collect::<String>(),
&mut output,
);
Ok(output)
}
}
fn apply_substitution(
substitution_data: &mut HashMap<String, Option<String>>,
substitution_name: &str,
output: &mut String,
) {
if let Ok(environment_value) = env::var(substitution_name) {
output.push_str(&environment_value);
} else {
let stored_value = substitution_data
.get(substitution_name)
.unwrap_or(&None)
.to_owned();
output.push_str(&stored_value.unwrap_or_default());
};
}
#[cfg(test)]
mod test {
use crate::iter::Iter;
use super::*;
#[test]
fn test_parse_line_env() {
// Note 5 spaces after 'KEY8=' below
let actual_iter = Iter::new(
r#"
KEY=1
KEY2="2"
KEY3='3'
KEY4='fo ur'
KEY5="fi ve"
KEY6=s\ ix
KEY7=
KEY8=
KEY9= # foo
KEY10 ="whitespace before ="
KEY11= "whitespace after ="
export="export as key"
export SHELL_LOVER=1
"#
.as_bytes(),
);
let expected_iter = vec![
("KEY", "1"),
("KEY2", "2"),
("KEY3", "3"),
("KEY4", "fo ur"),
("KEY5", "fi ve"),
("KEY6", "s ix"),
("KEY7", ""),
("KEY8", ""),
("KEY9", ""),
("KEY10", "whitespace before ="),
("KEY11", "whitespace after ="),
("export", "export as key"),
("SHELL_LOVER", "1"),
]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()));
let mut count = 0;
for (expected, actual) in expected_iter.zip(actual_iter) {
assert!(actual.is_ok());
assert_eq!(expected, actual.unwrap());
count += 1;
}
assert_eq!(count, 13);
}
#[test]
fn test_parse_line_comment() {
let result: Result<Vec<(String, String)>> = Iter::new(
r#"
# foo=bar
# "#
.as_bytes(),
)
.collect();
assert!(result.unwrap().is_empty());
}
#[test]
fn test_parse_line_invalid() {
// Note 4 spaces after 'invalid' below
let actual_iter = Iter::new(
r#"
invalid
very bacon = yes indeed
=value"#
.as_bytes(),
);
let mut count = 0;
for actual in actual_iter {
assert!(actual.is_err());
count += 1;
}
assert_eq!(count, 3);
}
#[test]
fn test_parse_value_escapes() {
let actual_iter = Iter::new(
r#"
KEY=my\ cool\ value
KEY2=\$sweet
KEY3="awesome stuff \"mang\""
KEY4='sweet $\fgs'\''fds'
KEY5="'\"yay\\"\ "stuff"
KEY6="lol" #well you see when I say lol wh
KEY7="line 1\nline 2"
"#
.as_bytes(),
);
let expected_iter = vec![
("KEY", r#"my cool value"#),
("KEY2", r#"$sweet"#),
("KEY3", r#"awesome stuff "mang""#),
("KEY4", r#"sweet $\fgs'fds"#),
("KEY5", r#"'"yay\ stuff"#),
("KEY6", "lol"),
("KEY7", "line 1\nline 2"),
]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()));
for (expected, actual) in expected_iter.zip(actual_iter) {
assert!(actual.is_ok());
assert_eq!(expected, actual.unwrap());
}
}
#[test]
fn test_parse_value_escapes_invalid() {
let actual_iter = Iter::new(
r#"
KEY=my uncool value
KEY2="why
KEY3='please stop''
KEY4=h\8u
"#
.as_bytes(),
);
for actual in actual_iter {
assert!(actual.is_err());
}
}
}
#[cfg(test)]
mod variable_substitution_tests {
use crate::iter::Iter;
use std::env;
fn assert_parsed_string(input_string: &str, expected_parse_result: Vec<(&str, &str)>) {
let actual_iter = Iter::new(input_string.as_bytes());
let expected_count = &expected_parse_result.len();
let expected_iter = expected_parse_result
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()));
let mut count = 0;
for (expected, actual) in expected_iter.zip(actual_iter) {
assert!(actual.is_ok());
assert_eq!(expected, actual.unwrap());
count += 1;
}
assert_eq!(count, *expected_count);
}
#[test]
fn variable_in_parenthesis_surrounded_by_quotes() {
assert_parsed_string(
r#"
KEY=test
KEY1="${KEY}"
"#,
vec![("KEY", "test"), ("KEY1", "test")],
);
}
#[test]
fn substitute_undefined_variables_to_empty_string() {
assert_parsed_string(r#"KEY=">$KEY1<>${KEY2}<""#, vec![("KEY", "><><")]);
}
#[test]
fn do_not_substitute_variables_with_dollar_escaped() {
assert_parsed_string(
"KEY=>\\$KEY1<>\\${KEY2}<",
vec![("KEY", ">$KEY1<>${KEY2}<")],
);
}
#[test]
fn do_not_substitute_variables_in_weak_quotes_with_dollar_escaped() {
assert_parsed_string(
r#"KEY=">\$KEY1<>\${KEY2}<""#,
vec![("KEY", ">$KEY1<>${KEY2}<")],
);
}
#[test]
fn do_not_substitute_variables_in_strong_quotes() {
assert_parsed_string("KEY='>${KEY1}<>$KEY2<'", vec![("KEY", ">${KEY1}<>$KEY2<")]);
}
#[test]
fn same_variable_reused() {
assert_parsed_string(
r#"
KEY=VALUE
KEY1=$KEY$KEY
"#,
vec![("KEY", "VALUE"), ("KEY1", "VALUEVALUE")],
);
}
#[test]
fn with_dot() {
assert_parsed_string(
r#"
KEY.Value=VALUE
"#,
vec![("KEY.Value", "VALUE")],
);
}
#[test]
fn recursive_substitution() {
assert_parsed_string(
r#"
KEY=${KEY1}+KEY_VALUE
KEY1=${KEY}+KEY1_VALUE
"#,
vec![("KEY", "+KEY_VALUE"), ("KEY1", "+KEY_VALUE+KEY1_VALUE")],
);
}
#[test]
fn variable_without_parenthesis_is_substituted_before_separators() {
assert_parsed_string(
r#"
KEY1=test_user
KEY1_1=test_user_with_separator
KEY=">$KEY1_1<>$KEY1}<>$KEY1{<"
"#,
vec![
("KEY1", "test_user"),
("KEY1_1", "test_user_with_separator"),
("KEY", ">test_user_1<>test_user}<>test_user{<"),
],
);
}
#[test]
fn substitute_variable_from_env_variable() {
env::set_var("KEY11", "test_user_env");
assert_parsed_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]);
}
#[test]
fn substitute_variable_env_variable_overrides_dotenv_in_substitution() {
env::set_var("KEY11", "test_user_env");
assert_parsed_string(
r#"
KEY11=test_user
KEY=">${KEY11}<"
"#,
vec![("KEY11", "test_user"), ("KEY", ">test_user_env<")],
);
}
#[test]
fn consequent_substitutions() {
assert_parsed_string(
r#"
KEY1=test_user
KEY2=$KEY1_2
KEY=>${KEY1}<>${KEY2}<
"#,
vec![
("KEY1", "test_user"),
("KEY2", "test_user_2"),
("KEY", ">test_user<>test_user_2<"),
],
);
}
#[test]
fn consequent_substitutions_with_one_missing() {
assert_parsed_string(
r#"
KEY2=$KEY1_2
KEY=>${KEY1}<>${KEY2}<
"#,
vec![("KEY2", "_2"), ("KEY", "><>_2<")],
);
}
}
#[cfg(test)]
mod error_tests {
use crate::errors::Error::LineParse;
use crate::iter::Iter;
#[test]
fn should_not_parse_unfinished_substitutions() {
let wrong_value = ">${KEY{<";
let parsed_values: Vec<_> = Iter::new(
format!(
r#"
KEY=VALUE
KEY1={}
"#,
wrong_value
)
.as_bytes(),
)
.collect();
assert_eq!(parsed_values.len(), 2);
if let Ok(first_line) = &parsed_values[0] {
assert_eq!(first_line, &(String::from("KEY"), String::from("VALUE")))
} else {
panic!("Expected the first value to be parsed")
}
if let Err(LineParse(second_value, index)) = &parsed_values[1] {
assert_eq!(second_value, wrong_value);
assert_eq!(*index, wrong_value.len() - 1)
} else {
panic!("Expected the second value not to be parsed")
}
}
#[test]
fn should_not_allow_dot_as_first_character_of_key() {
let wrong_key_value = ".Key=VALUE";
let parsed_values: Vec<_> = Iter::new(wrong_key_value.as_bytes()).collect();
assert_eq!(parsed_values.len(), 1);
if let Err(LineParse(second_value, index)) = &parsed_values[0] {
assert_eq!(second_value, wrong_key_value);
assert_eq!(*index, 0)
} else {
panic!("Expected the second value not to be parsed")
}
}
#[test]
fn should_not_parse_illegal_format() {
let wrong_format = r"<><><>";
let parsed_values: Vec<_> = Iter::new(wrong_format.as_bytes()).collect();
assert_eq!(parsed_values.len(), 1);
if let Err(LineParse(wrong_value, index)) = &parsed_values[0] {
assert_eq!(wrong_value, wrong_format);
assert_eq!(*index, 0)
} else {
panic!("Expected the second value not to be parsed")
}
}
#[test]
fn should_not_parse_illegal_escape() {
let wrong_escape = r">\f<";
let parsed_values: Vec<_> =
Iter::new(format!("VALUE={}", wrong_escape).as_bytes()).collect();
assert_eq!(parsed_values.len(), 1);
if let Err(LineParse(wrong_value, index)) = &parsed_values[0] {
assert_eq!(wrong_value, wrong_escape);
assert_eq!(*index, wrong_escape.find('\\').unwrap() + 1)
} else {
panic!("Expected the second value not to be parsed")
}
}
}
+20
View File
@@ -0,0 +1,20 @@
use std::fs::File;
use std::io::prelude::*;
use std::{env, io};
use tempfile::{tempdir, TempDir};
pub fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result<TempDir> {
env::set_var("EXISTING", "from_env");
let dir = tempdir()?;
env::set_current_dir(dir.path())?;
let dotenv_path = dir.path().join(".env");
let mut dotenv_file = File::create(dotenv_path)?;
dotenv_file.write_all(dotenv_text.as_bytes())?;
dotenv_file.sync_all()?;
Ok(dir)
}
#[allow(dead_code)]
pub fn make_test_dotenv() -> io::Result<TempDir> {
tempdir_with_dotenv("TESTKEY=test_val\nTESTKEY=test_val_overridden\nEXISTING=from_file")
}
@@ -0,0 +1 @@
mod util;
@@ -0,0 +1,69 @@
#![allow(dead_code)]
mod testenv;
use std::env::{self, VarError};
pub use testenv::*;
/// Default key used in envfile
pub const TEST_KEY: &str = "TESTKEY";
/// Default value used in envfile
pub const TEST_VALUE: &str = "test_val";
/// Default existing key set before test is run
pub const TEST_EXISTING_KEY: &str = "TEST_EXISTING_KEY";
/// Default existing value set before test is run
pub const TEST_EXISTING_VALUE: &str = "from_env";
/// Default overriding value in envfile
pub const TEST_OVERRIDING_VALUE: &str = "from_file";
#[inline(always)]
pub fn create_default_envfile() -> String {
format!(
"{}={}\n{}={}",
TEST_KEY, TEST_VALUE, TEST_EXISTING_KEY, TEST_OVERRIDING_VALUE
)
}
/// missing equals
#[inline(always)]
pub fn create_invalid_envfile() -> String {
format!(
"{}{}\n{}{}",
TEST_KEY, TEST_VALUE, TEST_EXISTING_KEY, TEST_OVERRIDING_VALUE
)
}
/// Assert that an environment variable is set and has the expected value.
pub fn assert_env_var(key: &str, expected: &str) {
match env::var(key) {
Ok(actual) => assert_eq!(
expected, actual,
"\n\nFor Environment Variable `{}`:\n EXPECTED: `{}`\n ACTUAL: `{}`\n",
key, expected, actual
),
Err(VarError::NotPresent) => panic!("env var `{}` not found", key),
Err(VarError::NotUnicode(val)) => panic!(
"env var `{}` currently has invalid unicode: `{}`",
key,
val.to_string_lossy()
),
}
}
/// Assert that an environment variable is not currently set.
pub fn assert_env_var_unset(key: &str) {
match env::var(key) {
Ok(actual) => panic!(
"env var `{}` should not be set, currently it is: `{}`",
key, actual
),
Err(VarError::NotUnicode(val)) => panic!(
"env var `{}` should not be set, currently has invalid unicode: `{}`",
key,
val.to_string_lossy()
),
_ => (),
}
}
@@ -0,0 +1,336 @@
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, PoisonError};
use std::{env, fs, io};
use tempfile::{tempdir, TempDir};
use super::*;
/// Env var convenience type.
type EnvMap = HashMap<String, String>;
/// Initialized in [`get_env_locker`]
static ENV_LOCKER: OnceCell<Arc<Mutex<EnvMap>>> = OnceCell::new();
/// A test environment.
///
/// Will create a new temporary directory. Use its builder methods to configure
/// the directory structure, preset variables, envfile name and contents, and
/// the working directory to run the test from.
///
/// Creation methods:
/// - [`TestEnv::init`]: blank environment (no envfile)
/// - [`TestEnv::init_with_envfile`]: blank environment with an envfile
/// - [`TestEnv::default`]: default testing environment (1 existing var and 2
/// set in a `.env` file)
#[derive(Debug)]
pub struct TestEnv {
temp_dir: TempDir,
work_dir: PathBuf,
env_vars: Vec<KeyVal>,
envfile_contents: Option<String>,
envfile_path: PathBuf,
}
/// Simple key value struct for representing environment variables
#[derive(Debug, Clone)]
pub struct KeyVal {
key: String,
value: String,
}
/// Run a test closure within a test environment.
///
/// Resets the environment variables, loads the [`TestEnv`], then runs the test
/// closure. Ensures only one thread has access to the process environment.
pub fn test_in_env<F>(test_env: TestEnv, test: F)
where
F: FnOnce(),
{
let locker = get_env_locker();
// ignore a poisoned mutex
// we expect some tests may panic to indicate a failure
let original_env = locker.lock().unwrap_or_else(PoisonError::into_inner);
// we reset the environment anyway upon acquiring the lock
reset_env(&original_env);
create_env(&test_env);
test();
// drop the lock and the `TestEnv` - should delete the tempdir
}
/// Run a test closure within the default test environment.
///
/// Resets the environment variables, creates the default [`TestEnv`], then runs
/// the test closure. Ensures only one thread has access to the process
/// environment.
///
/// The default testing environment sets an existing environment variable
/// `TEST_EXISTING_KEY`, which is set to `from_env`. It also creates a `.env`
/// file with the two lines:
///
/// ```ini
/// TESTKEY=test_val
/// TEST_EXISTING_KEY=from_file
/// ```
///
/// Notice that file has the potential to override `TEST_EXISTING_KEY` depending
/// on the what's being tested.
pub fn test_in_default_env<F>(test: F)
where
F: FnOnce(),
{
let test_env = TestEnv::default();
test_in_env(test_env, test);
}
impl TestEnv {
/// Blank testing environment in a new temporary directory.
///
/// No envfile_contents or pre-existing variables to set. The envfile_name
/// is set to `.env` but wont be written until its contents is set. The
/// working directory is the created temporary directory.
pub fn init() -> Self {
let tempdir = tempdir().expect("create tempdir");
let work_dir = tempdir.path().to_owned();
let envfile_path = work_dir.join(".env");
Self {
temp_dir: tempdir,
work_dir,
env_vars: Default::default(),
envfile_contents: None,
envfile_path,
}
}
/// Testing environment with custom envfile_contents.
///
/// No pre-existing env_vars set. The envfile_name is set to `.env`. The
/// working directory is the created temporary directory.
pub fn init_with_envfile(contents: impl ToString) -> Self {
let mut test_env = Self::init();
test_env.set_envfile_contents(contents);
test_env
}
/// Change the name of the default `.env` file.
///
/// It will still be placed in the root temporary directory. If you need to
/// put the envfile in a different directory, use
/// [`set_envfile_path`](TestEnv::set_envfile_path) instead.
pub fn set_envfile_name(&mut self, name: impl AsRef<Path>) -> &mut Self {
self.envfile_path = self.temp_path().join(name);
self
}
/// Change the absolute path to the envfile.
pub fn set_envfile_path(&mut self, path: PathBuf) -> &mut Self {
self.envfile_path = path;
self
}
/// Specify the contents of the envfile.
///
/// If this is the only change to the [`TestEnv`] being made, use
/// [`new_with_envfile`](TestEnv::new_with_envfile).
///
/// Setting it to an empty string will cause an empty envfile to be created
pub fn set_envfile_contents(&mut self, contents: impl ToString) -> &mut Self {
self.envfile_contents = Some(contents.to_string());
self
}
/// Set the working directory the test will run from.
///
/// The default is the created temporary directory. This method is useful if
/// you wish to run a test from a subdirectory or somewhere else.
pub fn set_work_dir(&mut self, path: PathBuf) -> &mut Self {
self.work_dir = path;
self
}
/// Add an individual environment variable.
///
/// This adds more pre-existing environment variables to the process before
/// any tests are run.
pub fn add_env_var(&mut self, key: impl ToString, value: impl ToString) -> &mut Self {
self.env_vars.push(KeyVal {
key: key.to_string(),
value: value.to_string(),
});
self
}
/// Set the pre-existing environment variables.
///
/// These variables will get added to the process' environment before the
/// test is run. This overrides any previous env vars added to the
/// [`TestEnv`].
///
/// If you wish to just use a slice of tuples, use
/// [`set_env_vars_tuple`](TestEnv::set_env_vars_tuple) instead.
pub fn set_env_vars(&mut self, env_vars: Vec<KeyVal>) -> &mut Self {
self.env_vars = env_vars;
self
}
/// Set the pre-existing environment variables using [`str`] tuples.
///
/// These variables will get added to the process' environment before the
/// test is run. This overrides any previous env vars added to the
/// [`TestEnv`].
///
/// If you wish to add an owned `Vec<KeyVal>` instead of `str` tuples, use
/// [`set_env_vars`](TestEnv::set_env_vars) instead.
pub fn set_env_vars_tuple(&mut self, env_vars: &[(&str, &str)]) -> &mut Self {
self.env_vars = env_vars
.iter()
.map(|(key, value)| KeyVal {
key: key.to_string(),
value: value.to_string(),
})
.collect();
self
}
/// Create a child folder within the temporary directory.
///
/// This will not change the working directory the test is run in, or where
/// the envfile is created.
///
/// Will create parent directories if they are missing.
pub fn add_child_dir_all(&self, rel_path: impl AsRef<Path>) -> PathBuf {
let rel_path = rel_path.as_ref();
let child_dir = self.temp_path().join(rel_path);
if let Err(err) = fs::create_dir_all(&child_dir) {
panic!(
"unable to create child directory: `{}` in `{}`: {}",
self.temp_path().display(),
rel_path.display(),
err
);
}
child_dir
}
/// Get a reference to the path of the temporary directory.
pub fn temp_path(&self) -> &Path {
self.temp_dir.path()
}
/// Get a reference to the working directory the test will be run from.
pub fn work_dir(&self) -> &Path {
&self.work_dir
}
/// Get a reference to environnement variables that will be set **before**
/// the test.
pub fn env_vars(&self) -> &[KeyVal] {
&self.env_vars
}
/// Get a reference to the string that will be placed in the envfile.
///
/// If `None` is returned, an envfile will not be created
pub fn envfile_contents(&self) -> Option<&str> {
self.envfile_contents.as_deref()
}
/// Get a reference to the path of the envfile.
pub fn envfile_path(&self) -> &Path {
&self.envfile_path
}
}
impl Default for TestEnv {
fn default() -> Self {
let temp_dir = tempdir().expect("create tempdir");
let work_dir = temp_dir.path().to_owned();
let env_vars = vec![KeyVal {
key: TEST_EXISTING_KEY.into(),
value: TEST_EXISTING_VALUE.into(),
}];
let envfile_contents = Some(create_default_envfile());
let envfile_path = work_dir.join(".env");
Self {
temp_dir,
work_dir,
env_vars,
envfile_contents,
envfile_path,
}
}
}
impl From<(&str, &str)> for KeyVal {
fn from(kv: (&str, &str)) -> Self {
let (key, value) = kv;
Self {
key: key.to_string(),
value: value.to_string(),
}
}
}
impl From<(String, String)> for KeyVal {
fn from(kv: (String, String)) -> Self {
let (key, value) = kv;
Self { key, value }
}
}
/// Get a guarded copy of the original process' env vars.
fn get_env_locker() -> Arc<Mutex<EnvMap>> {
Arc::clone(ENV_LOCKER.get_or_init(|| {
let map: EnvMap = env::vars().collect();
Arc::new(Mutex::new(map))
}))
}
/// Reset the process' env vars back to what was in `original_env`.
fn reset_env(original_env: &EnvMap) {
// remove keys if they weren't in the original environment
env::vars()
.filter(|(key, _)| !original_env.contains_key(key))
.for_each(|(key, _)| env::remove_var(key));
// ensure original keys have their original values
original_env
.iter()
.for_each(|(key, value)| env::set_var(key, value));
}
/// Create an environment to run tests in.
///
/// Writes the envfile, sets the working directory, and sets environment vars.
fn create_env(test_env: &TestEnv) {
// only create the envfile if its contents has been set
if let Some(contents) = test_env.envfile_contents() {
create_envfile(&test_env.envfile_path, contents);
}
env::set_current_dir(&test_env.work_dir).expect("setting working directory");
for KeyVal { key, value } in &test_env.env_vars {
env::set_var(key, value)
}
}
/// Create an envfile for use in tests.
fn create_envfile(path: &Path, contents: &str) {
if path.exists() {
panic!("envfile `{}` already exists", path.display())
}
// inner function to group together io::Results
fn create_env_file_inner(path: &Path, contents: &str) -> io::Result<()> {
let mut file = fs::File::create(path)?;
file.write_all(contents.as_bytes())?;
file.sync_all()
}
// call inner function
if let Err(err) = create_env_file_inner(path, contents) {
// handle any io::Result::Err
panic!("error creating envfile `{}`: {}", path.display(), err);
}
}
+22
View File
@@ -0,0 +1,22 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, fs, result::Result};
use crate::common::*;
#[test]
fn test_child_dir() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
fs::create_dir("child")?;
env::set_current_dir("child")?;
dotenv()?;
assert_eq!(env::var("TESTKEY")?, "test_val");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,21 @@
mod common;
use std::{env, error::Error, result::Result};
use dotenvy::*;
use crate::common::*;
#[test]
fn test_default_location_override() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
dotenv_override()?;
assert_eq!(env::var("TESTKEY")?, "test_val_overridden");
assert_eq!(env::var("EXISTING")?, "from_file");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,21 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_default_location() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
dotenv()?;
assert_eq!(env::var("TESTKEY")?, "test_val");
assert_eq!(env::var("EXISTING")?, "from_env");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
+21
View File
@@ -0,0 +1,21 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_dotenv_iter() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
let iter = dotenv_iter()?;
assert!(env::var("TESTKEY").is_err());
iter.load()?;
assert_eq!(env::var("TESTKEY")?, "test_val");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,23 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_from_filename_iter() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
let iter = from_filename_iter(".env")?;
assert!(env::var("TESTKEY").is_err());
iter.load()?;
assert_eq!(env::var("TESTKEY").unwrap(), "test_val");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,20 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_from_filename_override() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
from_filename_override(".env")?;
assert_eq!(env::var("TESTKEY")?, "test_val_overridden");
assert_eq!(env::var("EXISTING")?, "from_file");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,20 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_from_filename() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
from_filename(".env")?;
assert_eq!(env::var("TESTKEY")?, "test_val");
assert_eq!(env::var("EXISTING")?, "from_env");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,26 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_from_path_iter() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
let mut path = env::current_dir()?;
path.push(".env");
let iter = from_path_iter(&path)?;
assert!(env::var("TESTKEY").is_err());
iter.load()?;
assert_eq!(env::var("TESTKEY")?, "test_val");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,22 @@
mod common;
use crate::common::*;
use dotenvy::*;
use std::{env, error::Error, result::Result};
#[test]
fn test_from_path_override() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
let mut path = env::current_dir()?;
path.push(".env");
from_path_override(&path)?;
assert_eq!(env::var("TESTKEY")?, "test_val_overridden");
assert_eq!(env::var("EXISTING")?, "from_file");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
+22
View File
@@ -0,0 +1,22 @@
mod common;
use crate::common::*;
use dotenvy::*;
use std::{env, error::Error, result::Result};
#[test]
fn test_from_path() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
let mut path = env::current_dir()?;
path.push(".env");
from_path(&path)?;
assert_eq!(env::var("TESTKEY")?, "test_val");
assert_eq!(env::var("EXISTING")?, "from_env");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,20 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, fs::File, result::Result};
use crate::common::*;
#[test]
fn test_from_read_override() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
from_read_override(File::open(".env")?)?;
assert_eq!(env::var("TESTKEY")?, "test_val_overridden");
assert_eq!(env::var("EXISTING")?, "from_file");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
+20
View File
@@ -0,0 +1,20 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, fs::File, result::Result};
use crate::common::*;
#[test]
fn test_from_read() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
from_read(File::open(".env")?)?;
assert_eq!(env::var("TESTKEY")?, "test_val");
assert_eq!(env::var("EXISTING")?, "from_env");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
+22
View File
@@ -0,0 +1,22 @@
mod common;
use crate::common::*;
use dotenvy::*;
use std::{env, error::Error, result::Result};
#[test]
fn test_ignore_bom() -> Result<(), Box<dyn Error>> {
let bom = "\u{feff}";
let dir = tempdir_with_dotenv(&format!("{}TESTKEY=test_val", bom))?;
let mut path = env::current_dir()?;
path.push(".env");
from_path(&path)?;
assert_eq!(env::var("TESTKEY")?, "test_val");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,56 @@
mod common;
use std::env;
use common::tempdir_with_dotenv;
use dotenvy::dotenv;
#[test]
fn test_issue_12() {
let _f = tempdir_with_dotenv(
r#"
# Start of .env file
# Comment line with single ' quote
# Comment line with double " quote
# Comment line with double " quote and starts with a space
TESTKEY1=test_val # 1 '" comment
TESTKEY2=test_val_with_#_hash # 2 '" comment
TESTKEY3="test_val quoted with # hash" # 3 '" comment
TESTKEY4="Line 1
# Line 2
Line 3" # 4 Multiline "' comment
TESTKEY5="Line 4
# Line 5
Line 6
" # 5 Multiline "' comment
# End of .env file
"#,
)
.expect("should write test env");
dotenv().expect("should succeed");
assert_eq!(
env::var("TESTKEY1").expect("testkey1 env key not set"),
"test_val"
);
assert_eq!(
env::var("TESTKEY2").expect("testkey2 env key not set"),
"test_val_with_#_hash"
);
assert_eq!(
env::var("TESTKEY3").expect("testkey3 env key not set"),
"test_val quoted with # hash"
);
assert_eq!(
env::var("TESTKEY4").expect("testkey4 env key not set"),
r#"Line 1
# Line 2
Line 3"#
);
assert_eq!(
env::var("TESTKEY5").expect("testkey5 env key not set"),
r#"Line 4
# Line 5
Line 6
"#
);
}
+46
View File
@@ -0,0 +1,46 @@
mod common;
use crate::common::*;
use dotenvy::*;
use std::{env, error::Error, result::Result};
#[test]
fn test_multiline() -> Result<(), Box<dyn Error>> {
let value = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\\n\\\"QUOTED\\\"";
let weak = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n\"QUOTED\"";
let dir = tempdir_with_dotenv(&format!(
r#"
KEY=my\ cool\ value
KEY3="awesome \"stuff\"
more
on other
lines"
KEY4='hello '\''world'"
good ' \'morning"
WEAK="{}"
STRONG='{}'
"#,
value, value
))?;
dotenv()?;
assert_eq!(var("KEY")?, r#"my cool value"#);
assert_eq!(
var("KEY3")?,
r#"awesome "stuff"
more
on other
lines"#
);
assert_eq!(
var("KEY4")?,
r#"hello 'world
good ' 'morning"#
);
assert_eq!(var("WEAK")?, weak);
assert_eq!(var("STRONG")?, value);
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
+17
View File
@@ -0,0 +1,17 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_var() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
assert_eq!(var("TESTKEY")?, "test_val");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
@@ -0,0 +1,64 @@
mod common;
use dotenvy::*;
use std::{env, error::Error, result::Result};
use crate::common::*;
#[test]
fn test_variable_substitutions() -> Result<(), Box<dyn Error>> {
std::env::set_var("KEY", "value");
std::env::set_var("KEY1", "value1");
let substitutions_to_test = [
"$ZZZ", "$KEY", "$KEY1", "${KEY}1", "$KEY_U", "${KEY_U}", "\\$KEY",
];
let common_string = substitutions_to_test.join(">>");
let dir = tempdir_with_dotenv(&format!(
r#"
KEY1=new_value1
KEY_U=$KEY+valueU
SUBSTITUTION_FOR_STRONG_QUOTES='{}'
SUBSTITUTION_FOR_WEAK_QUOTES="{}"
SUBSTITUTION_WITHOUT_QUOTES={}
"#,
common_string, common_string, common_string
))?;
assert_eq!(var("KEY")?, "value");
assert_eq!(var("KEY1")?, "value1");
assert_eq!(var("KEY_U")?, "value+valueU");
assert_eq!(var("SUBSTITUTION_FOR_STRONG_QUOTES")?, common_string);
assert_eq!(
var("SUBSTITUTION_FOR_WEAK_QUOTES")?,
[
"",
"value",
"value1",
"value1",
"value_U",
"value+valueU",
"$KEY"
]
.join(">>")
);
assert_eq!(
var("SUBSTITUTION_WITHOUT_QUOTES")?,
[
"",
"value",
"value1",
"value1",
"value_U",
"value+valueU",
"$KEY"
]
.join(">>")
);
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}
+20
View File
@@ -0,0 +1,20 @@
mod common;
use std::{collections::HashMap, env, error::Error, result::Result};
use dotenvy::*;
use crate::common::*;
#[test]
fn test_vars() -> Result<(), Box<dyn Error>> {
let dir = make_test_dotenv()?;
let vars: HashMap<String, String> = vars().collect();
assert_eq!(vars["TESTKEY"], "test_val");
env::set_current_dir(dir.path().parent().unwrap())?;
dir.close()?;
Ok(())
}