Skip to content

Commit

Permalink
Merge pull request #46 from rogusdev/parent-dir-env-files
Browse files Browse the repository at this point in the history
Search for .env file higher than current/given dir
  • Loading branch information
rogusdev authored Dec 16, 2020
2 parents faf6518 + 9e21d1c commit a336773
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 50 deletions.
1 change: 1 addition & 0 deletions .env_much_higher
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TEST="See DotNetEnvTraverse.Tests for why this is here"
17 changes: 16 additions & 1 deletion DotNetEnv.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
Expand All @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DECBE6ED-4
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetEnv.Tests", "test\DotNetEnv.Tests\DotNetEnv.Tests.csproj", "{062FD47B-69A9-4808-97CA-CF24CD65B0F0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetEnvTraverse.Tests", "test\DotNetEnvTraverse.Tests\DotNetEnvTraverse.Tests.csproj", "{947252C9-700A-4A6E-A9EA-DFA6631916EF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -48,9 +50,22 @@ Global
{062FD47B-69A9-4808-97CA-CF24CD65B0F0}.Release|x64.Build.0 = Release|x64
{062FD47B-69A9-4808-97CA-CF24CD65B0F0}.Release|x86.ActiveCfg = Release|x86
{062FD47B-69A9-4808-97CA-CF24CD65B0F0}.Release|x86.Build.0 = Release|x86
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Debug|x64.ActiveCfg = Debug|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Debug|x64.Build.0 = Debug|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Debug|x86.ActiveCfg = Debug|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Debug|x86.Build.0 = Debug|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Release|Any CPU.Build.0 = Release|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Release|x64.ActiveCfg = Release|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Release|x64.Build.0 = Release|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Release|x86.ActiveCfg = Release|Any CPU
{947252C9-700A-4A6E-A9EA-DFA6631916EF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{56AE6D80-197D-426C-B4A3-B1F329263B71} = {E1CC46FC-4D7C-42C0-8F3A-BCB1B45CD459}
{062FD47B-69A9-4808-97CA-CF24CD65B0F0} = {DECBE6ED-4F14-424C-BEF1-0B9D45EEDB79}
{947252C9-700A-4A6E-A9EA-DFA6631916EF} = {DECBE6ED-4F14-424C-BEF1-0B9D45EEDB79}
EndGlobalSection
EndGlobal
57 changes: 48 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,32 @@ dotnet add package DotNetEnv

### Load env file

`Load()` will automatically look for a `.env` file in the current directory
`Load()` will automatically look for a `.env` file in the current directory by default,
or any higher parent/ancestor directory if given the option flag via TraversePath()
```csharp
DotNetEnv.Env.Load();
DotNetEnv.Env.TraversePath().Load();
```

Or you can specify the path to the `.env` file
Or you can specify the path directly to the `.env` file,
amd as above, with `TraversePath()`, it will start looking there
and then look in higher dirs from there if not found.
```csharp
DotNetEnv.Env.Load("./path/to/.env");
```

It's also possible to load the (text) file as a `Stream`
It's also possible to load the (text) file as a `Stream` or `string[]`

```csharp
using (var stream = File.OpenRead("./path/to/.env"))
{
DotNetEnv.Env.Load(stream);
}

DotNetEnv.Env.Load(new[] {
"OK=GOOD",
"TEST=\"more stuff\"",
});
```

### Accessing environment variables
Expand Down Expand Up @@ -72,29 +81,41 @@ DotNetEnv.Env.GetString("THIS_DOES_NOT_EXIST", "Variable not found");
You can also pass a `LoadOptions` object arg to all `DotNetEnv.Env.Load` variants to affect the Load/Parse behavior:

```csharp
new DotNetEnv.Env.LoadOptions(
new DotNetEnv.LoadOptions(
setEnvVars: true,
clobberExistingVars: true
clobberExistingVars: true,
onlyExactPath: true
)
```

However the recommended approach is with a fluent syntax for turning flags off such as:

```csharp
DotNetEnv.Env.NoEnvVars().NoClobber().TraversePath().Load();
```

All parameters default to true, which means:

1. `setEnvVars`, first arg: `true` in order to actually update env vars.
Setting it `false` allows consumers of this library to process the .env file
but use it for things other than updating env vars, as a generic configuration file.
The Load methods all return an `IEnumerable<KeyValuePair<string,string>> for this.
The Load methods all return an `IEnumerable<KeyValuePair<string,string>> for this, but
there is an extension method ToDictionary to get a dict with the last value for each key.

```env
KEY=value
```

```csharp
var kvps = DotNetEnv.Env.Load(
new DotNetEnv.Env.LoadOptions(
options: new DotNetEnv.Env.LoadOptions(
setEnvVars: false
)
)

// or the recommended, cleaner (fluent) approach:
var dict = DotNetEnv.Env.NoEnvVars().Load().ToDictionary();

// not "value" from the .env file
null == System.Environment.GetEnvironmentVariable("KEY")
"KEY" == kvps.First().Key
Expand All @@ -111,14 +132,31 @@ KEY=value
```csharp
System.Environment.SetEnvironmentVariable("KEY", "really important value, don't overwrite");
DotNetEnv.Env.Load(
new DotNetEnv.Env.LoadOptions(
options: new DotNetEnv.Env.LoadOptions(
clobberExistingVars: false
)
)

// or the recommended, cleaner (fluent) approach:
DotNetEnv.Env.NoClobber().Load();

// not "value" from the .env file
"really important value, don't overwrite" == System.Environment.GetEnvironmentVariable("KEY")
```

3. `exactPathOnly`, third arg: `true` to require .env to be
in the current directory if not specified, or to match the exact path passed in,
`false` would traverse the parent directories above the current or given path
to find the nearest `.env` file or whatever name was passed in.
This option only applies to Env.Load that takes a string path.

See `DotNetEnvTraverse.Tests` for examples.

```csharp
// the recommended, cleaner (fluent) approach:
DotNetEnv.Env.TraversePath().Load();
```

## .env file structure

All lines must be valid assignments or empty lines (with optional comments).
Expand Down Expand Up @@ -238,7 +276,7 @@ If you have found a bug or if you have a feature request, please report them at

## Contributing

Run `dotnet test test/DotNetEnv.Tests` to run all tests.
Run `dotnet test` to run all tests.

Or some more specific test examples:

Expand All @@ -249,6 +287,7 @@ Or some more specific test examples:

`src/DotNetEnvEnv/Parsers.cs` defines all the [Sprache](https://github.com/sprache/Sprache) parsers.

The `DotNetEnvTraverse.Tests` project tests loading `.env` files in parent (or higher) directories from the executable.

Open a PR on Github if you have some changes, or an issue if you want to discuss some proposed changes before creating a PR for them.

Expand Down
54 changes: 33 additions & 21 deletions src/DotNetEnv/Env.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,42 @@ public class Env
{
public const string DEFAULT_ENVFILENAME = ".env";

private static LoadOptions DEFAULT_OPTIONS = new LoadOptions();

public static IEnumerable<KeyValuePair<string, string>> Load (string[] lines, LoadOptions options = null)
{
return LoadContents(String.Join("\n", lines), options);
}

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

var file = Path.GetFileName(path);
if (file == null || file == string.Empty) file = DEFAULT_ENVFILENAME;
var dir = Path.GetDirectoryName(path);
if (dir == null || dir == string.Empty) dir = Directory.GetCurrentDirectory();
path = Path.Combine(dir, file);

if (options.OnlyExactPath)
{
if (!File.Exists(path)) path = null;
}
else
{
while (!File.Exists(path))
{
var parent = Directory.GetParent(dir);
if (parent == null)
{
path = null;
break;
}
dir = parent.FullName;
path = Path.Combine(dir, file);
}
}

// in production, there should be no .env file, so this should be the common code path
if (!File.Exists(path))
if (path == null)
{
return Enumerable.Empty<KeyValuePair<string, string>>();
}
Expand All @@ -38,7 +63,7 @@ public static IEnumerable<KeyValuePair<string, string>> Load (Stream file, LoadO

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

if (options.SetEnvVars)
{
Expand All @@ -57,9 +82,6 @@ public static IEnumerable<KeyValuePair<string, string>> LoadContents (string con
}
}

public static IEnumerable<KeyValuePair<string, string>> Load (LoadOptions options = null) =>
Load(Path.Combine(Directory.GetCurrentDirectory(), DEFAULT_ENVFILENAME), options);

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

Expand All @@ -72,19 +94,9 @@ public static IEnumerable<KeyValuePair<string, string>> Load (LoadOptions option
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 class LoadOptions
{
public bool SetEnvVars { get; }
public bool ClobberExistingVars { get; }

public LoadOptions(
bool setEnvVars = true,
bool clobberExistingVars = true
) {
SetEnvVars = setEnvVars;
ClobberExistingVars = clobberExistingVars;
}
}
public static LoadOptions NoEnvVars () => LoadOptions.NoEnvVars();
public static LoadOptions NoClobber () => LoadOptions.NoClobber();
public static LoadOptions TraversePath () => LoadOptions.TraversePath();
}

public static class Extensions
Expand Down
54 changes: 54 additions & 0 deletions src/DotNetEnv/LoadOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace DotNetEnv
{
public class LoadOptions
{
public static readonly LoadOptions DEFAULT = new LoadOptions();

public bool SetEnvVars { get; }
public bool ClobberExistingVars { get; }
public bool OnlyExactPath { get; }

public LoadOptions(
bool setEnvVars = true,
bool clobberExistingVars = true,
bool onlyExactPath = true
) {
SetEnvVars = setEnvVars;
ClobberExistingVars = clobberExistingVars;
OnlyExactPath = onlyExactPath;
}

public LoadOptions(
LoadOptions old,
bool? setEnvVars = null,
bool? clobberExistingVars = null,
bool? onlyExactPath = null
) {
SetEnvVars = setEnvVars ?? old.SetEnvVars;
ClobberExistingVars = clobberExistingVars ?? old.ClobberExistingVars;
OnlyExactPath = onlyExactPath ?? old.OnlyExactPath;
}

public static LoadOptions NoEnvVars (LoadOptions options = null) =>
options == null ? DEFAULT.NoEnvVars() : options.NoEnvVars();

public static LoadOptions NoClobber (LoadOptions options = null) =>
options == null ? DEFAULT.NoClobber() : options.NoClobber();

public static LoadOptions TraversePath (LoadOptions options = null) =>
options == null ? DEFAULT.TraversePath() : options.TraversePath();

public LoadOptions NoEnvVars () => new LoadOptions(this, setEnvVars: false);
public LoadOptions NoClobber () => new LoadOptions(this, clobberExistingVars: false);
public LoadOptions TraversePath () => new LoadOptions(this, onlyExactPath: false);

public IEnumerable<KeyValuePair<string, string>> Load (string path = null) => Env.Load(path, this);
}
}
1 change: 1 addition & 0 deletions test/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TEST=here
Loading

0 comments on commit a336773

Please sign in to comment.