Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • libraries/envish
1 result
Show changes
Commits on Source (2)
hard_tabs = true
reorder_imports = true
max_width = 140
merge_derives = true
\ No newline at end of file
use crate::parser::{file, FileLine};
use nom_locate::LocatedSpan;
use std::env;
use std::env::VarError;
use std::fmt::Display;
use crate::parser::{file, FileLine};
use nom_locate::LocatedSpan;
use std::str::FromStr;
use std::string::ParseError;
......@@ -105,18 +105,35 @@ impl EnvironmentFile {
}
}
/// Varies the behaviour of applying an environment file.
///
/// ## Defaults
///
/// When not provided, `envish` will use the default values for this type:
/// - `prefix` is set to `None` (Keys in env files will be used as-written)
/// - `overwrite` is set to `false` (If a variable is already set, the value in the file will not be used)
#[derive(Clone, Debug, Default)]
pub struct ApplyOptions {
/// If present, this exact string will be prepended to the names of all environment variables
/// when applying.
///
/// ### Example
///
/// With the prefix "APP_", a variable with the key "DATABASE_URL" will be added to the environment as
/// "APP_DATABASE_URL".
pub prefix: Option<String>,
/// By default, `envish` will not update an existing environment variable, ignoring the value
/// in the environment file. When the `overwrite` option is set to `true`, `envish` will apply
/// any values from the file regardless of whether they are already set.
pub overwrite: bool,
}
impl ApplyOptions {
pub fn new(prefix: impl Display, overwrite: bool) -> Self {
Self {
prefix: Some(prefix.to_string()),
overwrite,
}
prefix: Some(prefix.to_string()),
overwrite,
}
}
pub fn with_prefix(prefix: impl Display) -> Self {
......@@ -124,10 +141,7 @@ impl ApplyOptions {
}
pub fn with_overwrite(overwrite: bool) -> Self {
Self {
prefix: None,
overwrite,
}
Self { prefix: None, overwrite }
}
}
......@@ -141,20 +155,20 @@ impl ApplyEnvironmentFile for EnvironmentFile {
}
}
impl <E> ApplyEnvironmentFile for Result<EnvironmentFile, E> {
impl<E> ApplyEnvironmentFile for Result<EnvironmentFile, E> {
fn apply(&self, options: ApplyOptions) {
if let Ok(file) = self {
file.apply(options);
}
}
if let Ok(file) = self {
file.apply(options);
}
}
}
impl ApplyEnvironmentFile for Option<EnvironmentFile> {
fn apply(&self, options: ApplyOptions) {
if let Some(file) = self {
file.apply(options);
}
}
if let Some(file) = self {
file.apply(options);
}
}
}
pub struct EnvFileIterator<'a> {
......@@ -251,7 +265,6 @@ fn set_from_file(file: &EnvironmentFile, options: ApplyOptions) -> Result<(), En
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
......
use crate::{EnvironmentFile};
use crate::env_file::{ApplyEnvironmentFile, ApplyOptions};
use crate::EnvironmentFile;
use std::fmt::Display;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use crate::env_file::{ApplyEnvironmentFile, ApplyOptions};
#[derive(Debug, thiserror::Error)]
#[allow(clippy::enum_variant_names)]
pub enum EnvFsError {
/// Error opening the file, or reading it into a buffer.
#[error(transparent)]
IoError(#[from] std::io::Error),
/// Error parsing the file from a string into an EnvironmentFile. Denotes syntax errors in the
/// file.
#[error(transparent)]
ParseError(#[from] crate::EnvironmentFileError),
/// An error thrown specifically when the given environment variable exists, but is cannot be
/// represented in Rust (e.g. not Unicode). This error does **not** occur when the environment
/// variable exists as valid Unicode.
#[error("The target environment variable exists, but is not Unicode")]
EnvironmentError,
}
/// Reads the file `.env` into memory, but does not apply it to the environment.
pub fn env_file() -> Result<EnvironmentFile, EnvFsError> {
env_file_from_path(".env")
}
/// Reads the file `.env.<environment>` into memory, but does not apply it to the environment.
///
/// ### Example
///
/// Calling `env_file_suffix("production")` will attempt to read the file `.env.production` in the
/// directory the server was launched from.
pub fn env_file_suffix(environment: impl Display) -> Result<EnvironmentFile, EnvFsError> {
env_file_from_path(format!(".env.{}", environment))
}
/// Reads the file at the specified path into memory, but does not apply it to the environment.
pub fn env_file_from_path(path: impl AsRef<Path>) -> Result<EnvironmentFile, EnvFsError> {
let mut file = File::open(path)?;
let mut buffer = String::new();
......@@ -31,22 +45,98 @@ pub fn env_file_from_path(path: impl AsRef<Path>) -> Result<EnvironmentFile, Env
Ok(buffer.parse()?)
}
/// Look up a `.env` file in the working directory and apply its contents to the current environment.
///
/// ### Errors
///
/// This method returns an error under the following circumstances:
///
/// - The target file does not exist
/// - There was an error when reading the target file
/// - The target was not a correctly formatted `.env` file
///
/// ### Example
///
/// This example will attempt to read the file `.env` in the directory the server was launched from.
/// ```rust
/// use envish::dotenv;
/// let _ = dotenv();
/// ```
pub fn dotenv() -> Result<(), EnvFsError> {
env_file()?.apply(Default::default());
Ok(())
}
/// Look up a `.env` file in the working directory and apply its contents to the current environment,
/// with the provided options, allowing for prefixing and overwriting existing values.
///
/// ### Errors
///
/// This method returns an error under the following circumstances:
///
/// - The target file does not exist
/// - There was an error when reading the target file
/// - The target was not a correctly formatted `.env` file
///
/// ### Example
///
/// This example will attempt to read the file `.env`, and will prepend the given prefix to all the
/// contained variables. For example, a variable in the file named "DATABASE_URL" will be added to
/// the environment as "APP_DATABASE_URL".
///
/// ```rust
/// use envish::{dotenv_opts, ApplyOptions};
/// let _ = dotenv_opts(ApplyOptions::with_prefix("APP_"));
/// ```
pub fn dotenv_opts(options: ApplyOptions) -> Result<(), EnvFsError> {
env_file()?.apply(options);
Ok(())
}
/// Look up a `.env` file with the provided suffix in the working directory and apply its contents
/// to the current environment.
///
/// ### Errors
///
/// This method returns an error under the following circumstances:
///
/// - The target file does not exist
/// - There was an error when reading the target file
/// - The target was not a correctly formatted `.env` file
///
/// ### Example
///
/// This example will attempt to read the file `.env.development`
///
/// ```rust
/// use envish::dotenv_suffix;
/// let _ = dotenv_suffix("development");
/// ```
pub fn dotenv_suffix(environment: impl Display) -> Result<(), EnvFsError> {
env_file_suffix(environment)?.apply(Default::default());
Ok(())
}
/// Look up an environment file at the given path and apply its contents to the current environment.
///
/// ### Errors
///
/// This method returns an error under the following circumstances:
///
/// - The target file does not exist
/// - There was an error when reading the target file
/// - The target was not a correctly formatted `.env` file
///
/// ### Example
///
/// This example will attempt to read the file `my_dotenv_file` at the specified path. The file must
/// be correctly formatted as any other .env file, but does not need to have a specific name.
///
/// ```rust
/// use envish::dotenv_from;
/// let _ = dotenv_from("/some/other/path/to/my_dotenv_file");
/// ```
pub fn dotenv_from(path: impl AsRef<Path>) -> Result<(), EnvFsError> {
env_file_from_path(path)?.apply(Default::default());
Ok(())
}
#![allow(unused_labels)]
//! This crate provides a simple interface for reading, parsing, and using `.env` style environment
//! files. The goal is to provide comprehensive compatibility with the variety of .env formats
//! and options found in popular tools across languages, both for reading and writing.
//!
//! ## Embedding
//!
//! This library is designed to be easy to use with both Rust and non-Rust projects. The core
//! functionality covers parsing and manipulating environment files. By default, `envish` also
//! includes filesystem support for reading and writing `.env` files, but this can be disabled by
//! turning off default features.
//!
//! It is recommended to disable filesystem support when embedding `envish`, and instead
//! using the platform-native filesystem operations in tandem with this crate's parsing module.
mod env_file;
#[cfg(feature = "fs")]
mod filesystem;
mod parser;
pub use env_file::{EnvironmentFile, EnvironmentFileError, ApplyEnvironmentFile, ApplyOptions};
pub use env_file::{ApplyEnvironmentFile, ApplyOptions, EnvironmentFile, EnvironmentFileError};
pub use parser::{FileLine, ValuePart};
#[cfg(feature = "fs")]
pub use filesystem::{dotenv, dotenv_from, dotenv_suffix, dotenv_opts};
pub use filesystem::{dotenv, dotenv_from, dotenv_opts, dotenv_suffix};
......@@ -13,10 +13,17 @@ use nom::{
};
use std::fmt::Display;
/// Holds one part of the value of an environment variable. Static parts will be used verbatim,
/// while variables will be looked up at render time when an environment file is applied.
/// Comments are always ignored by the renderer, but are preserved for serialization.
#[derive(Clone, Debug, PartialEq)]
pub enum ValuePart {
/// A static value that will be rendered verbatim.
Static(String),
/// The exact name of an environment variable that will be looked up at render time.
/// Case-sensitivity is dependent on the operating system.
Variable(String),
/// Arbitrary text that will not be added to the environment.
Comment(String),
}
......@@ -31,12 +38,15 @@ impl Display for ValuePart {
}
impl ValuePart {
/// Create a static value that will be used verbatim.
pub fn new(value: impl ToString) -> Self {
Self::Static(value.to_string())
}
/// Create a reference to another environment variable that will be interpolated into the value.
pub fn variable(value: impl ToString) -> Self {
Self::Variable(value.to_string())
}
/// Create a comment that will be ignored by the value renderer.
pub fn comment(value: impl ToString) -> Self {
Self::Comment(value.to_string())
}
......@@ -59,10 +69,18 @@ impl ValuePart {
}
}
/// A whole line of an environment file. May contain comments, static strings, or interpolation
/// variables.
#[derive(Clone, Debug, PartialEq)]
pub enum FileLine {
/// A blank lin in a file, containing only whitespace. Ignored for all purposes except 1-1
/// serialisation.
Empty,
/// A full line comment, starting with a '#' and containing arbitrary text for the rest of the
/// line.
Comment(String),
/// A key-value pair, where the key is a string and the value is a combination of static parts,
/// dynamic parts, and comments.
KeyValue { key: String, value: Vec<ValuePart> },
}
......@@ -79,14 +97,22 @@ impl Display for FileLine {
}
impl FileLine {
/// Create a new empty line.
pub fn empty() -> Self {
Self::Empty
}
/// Create a new full line comment.
pub fn comment(comment: impl ToString) -> Self {
Self::Comment(comment.to_string())
}
/// Create a new key-value line, where the value may be a combination of static strings,
/// variable references, and comments.
///
/// Lines constructed this way may contain comments in the middle of strings and references
/// without issue, but will not be able to correctly serialise if writing the non-compliant
/// line to a file.
pub fn key_value(key: impl ToString, value: impl ToOwned<Owned = Vec<ValuePart>>) -> Self {
Self::KeyValue {
key: key.to_string(),
......@@ -107,8 +133,8 @@ impl FileLine {
Self::Comment(span.fragment().to_string())
}
/// Whether this line is entirely self contained. If the line is a kv pair that requires
/// interpolation, it is not considered complete.
/// Returns true if this line can be evaluated by itself. A line cannot be evaluated by itself
/// if it references other environment variables.
pub fn is_complete(&self) -> bool {
match self {
Self::KeyValue { value, .. } => value.iter().all(|part| !matches!(part, &ValuePart::Variable(..))),
......@@ -116,6 +142,8 @@ impl FileLine {
}
}
/// Returns a copy of this line without comments. If the entire line is a comment, it is
/// converted to `Line::Empty`. Otherwise, comment parts of key-value lines are removed.
pub fn strip_comments(&self) -> Self {
match self {
Self::Empty => Self::Empty,
......@@ -131,7 +159,8 @@ impl FileLine {
}
}
/// Convert the line into a complete value string
/// Convert the line into a complete value string, interpolating dynamic variables and stripping
/// comments. Empty lines or whole line comments will return an empty string.
pub fn assemble_value(&self) -> String {
match self {
Self::Empty => String::new(),
......
......@@ -2,7 +2,7 @@ use envish::{ApplyEnvironmentFile, ApplyOptions, EnvironmentFile};
#[test]
fn it_parses_basic_dotenv_file() {
let file_contents = r#"
let file_contents = r#"
# This value won't be set in the test, and this comment will be ignored
MY_BEST_VARIABLE=some_value
# This variable is also not defined, and it'll still be a string,
......@@ -10,19 +10,19 @@ fn it_parses_basic_dotenv_file() {
SOME_OTHER_VARIABLE=1234
"#;
std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
file.apply(Default::default());
let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
file.apply(Default::default());
assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value");
assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234");
assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value");
assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234");
}
#[test]
fn it_parses_dotenv_file_with_interpolation() {
let file_contents = r#"
let file_contents = r#"
# This value won't be set in the test, and this comment will be ignored
MY_BEST_VARIABLE=some_value
# This variable is also not defined, and it'll still be a string,
......@@ -32,21 +32,21 @@ fn it_parses_dotenv_file_with_interpolation() {
INTERPOLATED_VARIABLE=${SOME_OTHER_VARIABLE}567
"#;
std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
std::env::var("INTERPOLATED_VARIABLE").expect_err("INTERPOLATED_VARIABLE should not be set");
std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
std::env::var("INTERPOLATED_VARIABLE").expect_err("INTERPOLATED_VARIABLE should not be set");
let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
file.apply(Default::default());
let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
file.apply(Default::default());
assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value");
assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234");
assert_eq!(std::env::var("INTERPOLATED_VARIABLE").unwrap(), "1234567");
assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value");
assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234");
assert_eq!(std::env::var("INTERPOLATED_VARIABLE").unwrap(), "1234567");
}
#[test]
fn it_parses_dotenv_file_with_interpolation_and_prefix_option() {
let file_contents = r#"
let file_contents = r#"
# This value won't be set in the test, and this comment will be ignored
MY_BEST_VARIABLE=some_value
# This variable is also not defined, and it'll still be a string,
......@@ -56,14 +56,14 @@ fn it_parses_dotenv_file_with_interpolation_and_prefix_option() {
INTERPOLATED_VARIABLE=${SOME_OTHER_VARIABLE}567
"#;
std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
std::env::var("INTERPOLATED_VARIABLE").expect_err("INTERPOLATED_VARIABLE should not be set");
std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
std::env::var("INTERPOLATED_VARIABLE").expect_err("INTERPOLATED_VARIABLE should not be set");
let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
file.apply(ApplyOptions::with_prefix("APP_"));
let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
file.apply(ApplyOptions::with_prefix("APP_"));
assert_eq!(std::env::var("APP_MY_BEST_VARIABLE").unwrap(), "some_value");
assert_eq!(std::env::var("APP_SOME_OTHER_VARIABLE").unwrap(), "1234");
assert_eq!(std::env::var("APP_INTERPOLATED_VARIABLE").unwrap(), "1234567");
}
\ No newline at end of file
assert_eq!(std::env::var("APP_MY_BEST_VARIABLE").unwrap(), "some_value");
assert_eq!(std::env::var("APP_SOME_OTHER_VARIABLE").unwrap(), "1234");
assert_eq!(std::env::var("APP_INTERPOLATED_VARIABLE").unwrap(), "1234567");
}