Skip to content

Feature/decouple env vars #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e576b66
include a green test showing that Interpolation works independent fro…
Philipp-Binder Sep 21, 2024
98f02d8
Introduce red test showing problems with multi-files with NoClobber
Philipp-Binder Sep 22, 2024
6f51d42
Introduce red test showing problems with multi-files with Interpolati…
Philipp-Binder Sep 22, 2024
44829a1
change FakeEnvVars to EnvVarSnapshot and use it instead of direct acc…
Philipp-Binder Sep 21, 2024
3e7de59
make red tests from previous commit green by removing EnvironmentVari…
Philipp-Binder Sep 21, 2024
daba766
move transformations from Parsers to Env (removes the dependency of P…
Philipp-Binder Sep 21, 2024
a9a5829
move EnvVarSnapshot to Parsers, take partial care of clobbering by pr…
Philipp-Binder Sep 21, 2024
78471ba
remove valueAction (former transform Function) from Parsers.ParseDote…
Philipp-Binder Sep 24, 2024
0b0c267
introduce "additionalValues" to solve the multi-file problem; makes a…
Philipp-Binder Sep 24, 2024
d3bc711
let Env return only the resulting keyValuePairs --> Env takes respons…
Philipp-Binder Sep 24, 2024
2f5f4c1
rename additionalValues to previousValues and hide them inside Env, m…
Philipp-Binder Sep 24, 2024
9dd5a9b
rename EnvVarSnapshot to ActualValuesSnapshot; move direct usages fro…
Philipp-Binder Sep 24, 2024
1498d20
fix test-declaration and move misplaced comment
Philipp-Binder Mar 2, 2025
9a060b0
introduce ValueProvider to remove the need for snapshots; it also sep…
Philipp-Binder Mar 28, 2025
d53f295
refactoring
Philipp-Binder Mar 29, 2025
70f8598
removed unnecessary override for GetValue in ChainedValueProvider; al…
Philipp-Binder Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 4 additions & 26 deletions src/DotNetEnv/Configuration/EnvConfigurationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,11 @@ public EnvConfigurationProvider(

public override void Load()
{
IEnumerable<KeyValuePair<string, string>> values;
if (paths == null)
{
values = Env.Load(options: options);
}
else
{
if (paths.Length == 1)
{
values = Env.Load(paths[0], options);
}
else
{
values = Env.LoadMulti(paths, options);
}
}
var values = paths == null
? Env.Load(options: options)
: Env.LoadMulti(paths, options);

// Since the Load method does not take care of clobberring, We have to check it here!
var dictionaryOption = options.ClobberExistingVars ? CreateDictionaryOption.TakeLast : CreateDictionaryOption.TakeFirst;
var dotEnvDictionary = values.ToDotEnvDictionary(dictionaryOption);

if (!options.ClobberExistingVars)
foreach (string key in Environment.GetEnvironmentVariables().Keys)
if (dotEnvDictionary.ContainsKey(key))
dotEnvDictionary[key] = Environment.GetEnvironmentVariable(key);

foreach (var value in dotEnvDictionary)
foreach (var value in values)
Data[NormalizeKey(value.Key)] = value.Value;
}

Expand Down
86 changes: 53 additions & 33 deletions src/DotNetEnv/Env.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using DotNetEnv.Extensions;

namespace DotNetEnv
{
public class Env
{
public const string DEFAULT_ENVFILENAME = ".env";

public static ConcurrentDictionary<string, string> FakeEnvVars = new ConcurrentDictionary<string, string>();

public static IEnumerable<KeyValuePair<string, string>> LoadMulti (string[] paths, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> LoadMulti(string[] paths, LoadOptions options = null)
{
return paths.Aggregate(
Enumerable.Empty<KeyValuePair<string, string>>(),
(kvps, path) => kvps.Concat(Load(path, options))
Array.Empty<KeyValuePair<string, string>>(),
(kvps, path) => kvps.Concat(Load(path, options, kvps)).ToArray()
);
}

public static IEnumerable<KeyValuePair<string, string>> Load (string path = null, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> Load(string path = null, LoadOptions options = null)
=> Load(path, options, null);

private static IEnumerable<KeyValuePair<string, string>> Load(string path, LoadOptions options,
IEnumerable<KeyValuePair<string, string>> actualValues)
{
if (options == null) options = LoadOptions.DEFAULT;

Expand All @@ -45,6 +47,7 @@ public static IEnumerable<KeyValuePair<string, string>> Load (string path = null
path = null;
break;
}

dir = parent.FullName;
path = Path.Combine(dir, file);
}
Expand All @@ -55,52 +58,69 @@ public static IEnumerable<KeyValuePair<string, string>> Load (string path = null
{
return Enumerable.Empty<KeyValuePair<string, string>>();
}
return LoadContents(File.ReadAllText(path), options);

return LoadContents(File.ReadAllText(path), options, actualValues);
}

public static IEnumerable<KeyValuePair<string, string>> Load (Stream file, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> Load(Stream file, LoadOptions options = null)
{
using (var reader = new StreamReader(file))
{
return LoadContents(reader.ReadToEnd(), options);
}
}

public static IEnumerable<KeyValuePair<string, string>> LoadContents (string contents, LoadOptions options = null)
public static IEnumerable<KeyValuePair<string, string>> LoadContents(string contents,
LoadOptions options = null)
=> LoadContents(contents, options, null);

private static IEnumerable<KeyValuePair<string, string>> LoadContents(string contents,
LoadOptions options, IEnumerable<KeyValuePair<string, string>> actualValues)
{
if (options == null) options = LoadOptions.DEFAULT;
if (options == null)
options = LoadOptions.DEFAULT;

var dictionaryOption = options.ClobberExistingVars
? CreateDictionaryOption.TakeLast
: CreateDictionaryOption.TakeFirst;

var actualValueProvider = actualValues == null
? (IValueProvider)new EnvironmentValueProvider()
: new ChainedValueProvider(options.ClobberExistingVars,
new EnvironmentValueProvider(),
new KeyValuePairValueProvider(options.ClobberExistingVars, actualValues.ToList()));

var parsedValues = Parsers.ParseDotenvFile(contents, options.ClobberExistingVars, actualValueProvider);

var unClobberedDictionary = (options.ClobberExistingVars
? parsedValues
: parsedValues.Where(p => !actualValueProvider.TryGetValue(p.Key, out _)))
.ToDotEnvDictionary(dictionaryOption);

if (options.SetEnvVars)
{
if (options.ClobberExistingVars)
{
return Parsers.ParseDotenvFile(contents, Parsers.SetEnvVar);
}
else
{
return Parsers.ParseDotenvFile(contents, Parsers.NoClobberSetEnvVar);
}
}
else
{
return Parsers.ParseDotenvFile(contents, Parsers.DoNotSetEnvVar);
}
foreach (var pair in unClobberedDictionary)
Environment.SetEnvironmentVariable(pair.Key, pair.Value);

return unClobberedDictionary;
}

public static string GetString (string key, string fallback = default(string)) =>
public static string GetString(string key, string fallback = default(string)) =>
Environment.GetEnvironmentVariable(key) ?? fallback;

public static bool GetBool (string key, bool fallback = default(bool)) =>
public static bool GetBool(string key, bool fallback = default(bool)) =>
bool.TryParse(Environment.GetEnvironmentVariable(key), out var value) ? value : fallback;

public static int GetInt (string key, int fallback = default(int)) =>
public static int GetInt(string key, int fallback = default(int)) =>
int.TryParse(Environment.GetEnvironmentVariable(key), out var value) ? value : fallback;

public static double GetDouble (string key, double fallback = default(double)) =>
double.TryParse(Environment.GetEnvironmentVariable(key), NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : fallback;
public static double GetDouble(string key, double fallback = default(double)) =>
double.TryParse(Environment.GetEnvironmentVariable(key), NumberStyles.Any, CultureInfo.InvariantCulture,
out var value)
? value
: fallback;

public static LoadOptions NoEnvVars () => LoadOptions.NoEnvVars();
public static LoadOptions NoClobber () => LoadOptions.NoClobber();
public static LoadOptions TraversePath () => LoadOptions.TraversePath();
public static LoadOptions NoEnvVars() => LoadOptions.NoEnvVars();
public static LoadOptions NoClobber() => LoadOptions.NoClobber();
public static LoadOptions TraversePath() => LoadOptions.TraversePath();
}
}
29 changes: 7 additions & 22 deletions src/DotNetEnv/IInterpolationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,6 @@

namespace DotNetEnv
{
static class LookupHelper
{
public static string GetEnvironmentVariable(string key)
{
var val = Environment.GetEnvironmentVariable(key);
if (val == null && Env.FakeEnvVars.TryGetValue(key, out var fakeVal))
{
return fakeVal;
}

return val;
}
}

public interface IInterpolationHandler
{
string Handle(string key);
Expand All @@ -25,13 +11,13 @@ public class DirectSubstitutionInterpolationHandler : IInterpolationHandler
{
public string Handle(string key)
{
return LookupHelper.GetEnvironmentVariable(key) ?? string.Empty;
return Parsers.CurrentValueProvider.TryGetValue(key, out var value) ? value : string.Empty;
}
}

public class DefaultInterpolationHandler : IInterpolationHandler
{
readonly string _defaultValue;
private readonly string _defaultValue;

public DefaultInterpolationHandler(string defaultValue)
{
Expand All @@ -40,17 +26,17 @@ public DefaultInterpolationHandler(string defaultValue)

public string Handle(string key)
{
var val = LookupHelper.GetEnvironmentVariable(key);
return val ?? _defaultValue;
return Parsers.CurrentValueProvider.TryGetValue(key, out var value) ? value : _defaultValue;
}
}

public class RequiredInterpolationHandler : IInterpolationHandler
{
public string Handle(string key)
{
var val = LookupHelper.GetEnvironmentVariable(key);
return val ?? throw new Exception($"Required environment variable '{key}' is not set.");
return Parsers.CurrentValueProvider.TryGetValue(key, out var value)
? value
: throw new Exception($"Required environment variable '{key}' is not set.");
}
}

Expand All @@ -65,8 +51,7 @@ public ReplacementInterpolationHandler(string replacementValue)

public string Handle(string key)
{
var val = LookupHelper.GetEnvironmentVariable(key);
return val != null
return Parsers.CurrentValueProvider.TryGetValue(key, out _)
? _replacementValue
: string.Empty;
}
Expand Down
66 changes: 38 additions & 28 deletions src/DotNetEnv/Parsers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
Expand All @@ -10,30 +11,19 @@

namespace DotNetEnv
{
class Parsers
internal static class Parsers
{
public static KeyValuePair<string, string> SetEnvVar (KeyValuePair<string, string> kvp)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
return kvp;
}

public static KeyValuePair<string, string> DoNotSetEnvVar (KeyValuePair<string, string> kvp)
{
Env.FakeEnvVars.AddOrUpdate(kvp.Key, kvp.Value, (_, v) => v);
return kvp;
}
/// <summary>
/// Returns the current value for a given key, while parsing is in progress.<br />
/// Clobber-setting is taken into account.
/// </summary>
public static IValueProvider CurrentValueProvider;

public static KeyValuePair<string, string> NoClobberSetEnvVar (KeyValuePair<string, string> kvp)
{
if (Environment.GetEnvironmentVariable(kvp.Key) == null)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
}
// not sure if maybe should return something different if avoided clobber... (current value?)
// probably not since the point is to return what the dotenv file reported, but it's arguable
return kvp;
}
/// <summary>
/// Contains already parsed variables while parsing.
/// </summary>
private static readonly IList<KeyValuePair<string, string>> ParsedValues =
new List<KeyValuePair<string, string>>();

// helpful blog I discovered only after digging through all the Sprache source myself:
// https://justinpealing.me.uk/post/2020-03-11-sprache1-chars/
Expand Down Expand Up @@ -327,13 +317,33 @@ from _c in Comment.OptionalOrDefault()
from _lt in LineTerminator
select new KeyValuePair<string, string>(null, null));

public static IEnumerable<KeyValuePair<string, string>> ParseDotenvFile (
string contents,
Func<KeyValuePair<string, string>, KeyValuePair<string, string>> tranform
)
/// <summary>
/// Returns all parsed entries in correct order.<br />
/// Duplicates are still contained, but interpolation takes clobber-setting into account.
/// </summary>
/// <param name="contents"></param>
/// <param name="clobberExistingVariables"></param>
/// <param name="actualValueProvider"></param>
/// <returns></returns>
public static IEnumerable<KeyValuePair<string, string>> ParseDotenvFile(string contents,
bool clobberExistingVariables = true, IValueProvider actualValueProvider = null)
{
return Assignment.Select(tranform).Or(Empty).Many().AtEnd()
.Parse(contents).Where(kvp => kvp.Key != null);
ParsedValues.Clear();
CurrentValueProvider = new ChainedValueProvider(clobberExistingVariables,
actualValueProvider ?? ValueProvider.Empty,
new KeyValuePairValueProvider(clobberExistingVariables, ParsedValues));

return Assignment.Select(UpdateParsedValues).Or(Empty)
.Many()
.AtEnd()
.Parse(contents)
.Where(kvp => kvp.Key != null);

KeyValuePair<string, string> UpdateParsedValues(KeyValuePair<string, string> pair)
{
ParsedValues.Add(pair);
return pair;
}
}
}
}
Loading