Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 22 additions & 13 deletions src/dotenv.net.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
public void Parse_EmptyLines_ShouldBeIgnored()
{
var lines = new[] { "", " ", null, "KEY=value" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();

Check warning on line 14 in src/dotenv.net.Tests/ParserTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Argument of type 'string?[]' cannot be used for parameter 'rawEnvRows' of type 'ReadOnlySpan<string>' in 'ReadOnlySpan<KeyValuePair<string, string>> Parser.Parse(ReadOnlySpan<string> rawEnvRows, bool trimValues, bool supportExportSyntax)' due to differences in the nullability of reference types.
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
}
Expand All @@ -20,7 +20,7 @@
public void Parse_CommentLines_ShouldBeIgnored()
{
var lines = new[] { "# Comment", " # Indented comment", "KEY=value" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
}
Expand All @@ -29,7 +29,7 @@
public void Parse_LinesWithoutKey_ShouldBeIgnored()
{
var lines = new[] { "=value", "NOKEY", "KEY=value" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
}
Expand All @@ -38,7 +38,7 @@
public void Parse_SimpleKeyValue_ShouldReturnPair()
{
var lines = new[] { "TEST_KEY=test_value" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("TEST_KEY", "test_value"));
}
Expand All @@ -47,7 +47,7 @@
public void Parse_UntrimmedValueWithTrimValuesFalse_ShouldPreserveWhitespace()
{
var lines = new[] { " KEY = value " };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", " value "));
}
Expand All @@ -56,7 +56,7 @@
public void Parse_UntrimmedValueWithTrimValuesTrue_ShouldTrimValue()
{
var lines = new[] { "KEY= value " };
var result = Parser.Parse(lines, trimValues: true).ToArray();
var result = Parser.Parse(lines, trimValues: true, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
}
Expand All @@ -65,7 +65,7 @@
public void Parse_SingleQuotedValue_ShouldUnescapeQuotes()
{
var lines = new[] { "KEY='value with \\' quote'" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value with ' quote"));
}
Expand All @@ -74,7 +74,7 @@
public void Parse_DoubleQuotedValue_ShouldUnescapeQuotes()
{
var lines = new[] { "KEY=\"value with \\\" quote\"" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value with \" quote"));
}
Expand All @@ -83,7 +83,7 @@
public void Parse_EscapedBackslashes_ShouldUnescape()
{
var lines = new[] { "KEY='escaped \\\\ backslash'" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result.ShouldContain(new KeyValuePair<string, string>("KEY", "escaped \\ backslash"));
}
Expand All @@ -92,7 +92,7 @@
public void Parse_MultiLineValue_ShouldCombineLines()
{
var lines = new[] { "KEY='first line", "second line'", "NEXT=value" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(2);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", $"first line{Environment.NewLine}second line"));
result[1].ShouldBe(new KeyValuePair<string, string>("NEXT", "value"));
Expand All @@ -102,7 +102,7 @@
public void Parse_UnclosedQuote_ShouldThrowException()
{
var lines = new[] { "KEY='unclosed quote" };
Action act = () => Parser.Parse(lines, trimValues: false).ToArray();
Action act = () => Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
act.ShouldThrow<ArgumentException>()
.Message.ShouldBe("Unable to parse environment variable: KEY. Missing closing quote.");
}
Expand All @@ -111,7 +111,16 @@
public void Parse_EscapedQuoteInMiddle_ShouldUnescape()
{
var lines = new[] { "KEY='before\\'after'" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result.ShouldContain(new KeyValuePair<string, string>("KEY", "before'after"));
}

[Fact]
public void Parse_ExportEscapedQuoteInMiddle_ShouldUnescape()
{
var lines = new[] { "export KEY='before\\'after'" };
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: true).ToArray();
result.Length.ShouldBe(1);
result.ShouldContain(new KeyValuePair<string, string>("KEY", "before'after"));
}
Expand All @@ -120,7 +129,7 @@
public void Parse_BackslashNotEscapingQuote_ShouldRemain()
{
var lines = new[] { "KEY='before\\after'" };
var result = Parser.Parse(lines, trimValues: false).ToArray();
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
result.Length.ShouldBe(1);
result.ShouldContain(new KeyValuePair<string, string>("KEY", "before\\after"));
}
Expand Down
18 changes: 14 additions & 4 deletions src/dotenv.net.Tests/ReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
[InlineData(null, false)]
[InlineData("", false)]
[InlineData(" ", false)]
public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumentException(string path,
public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumentException(string? path,
bool ignoreExceptions)
{
Action act = () => Reader.ReadFileLines(path, ignoreExceptions, null);

Check warning on line 45 in src/dotenv.net.Tests/ReaderTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Possible null reference argument for parameter 'envFilePath' in 'ReadOnlySpan<string> Reader.ReadFileLines(string envFilePath, bool ignoreExceptions, Encoding? encoding)'.
act.ShouldThrow<ArgumentException>().Message
.ShouldContain("The file path cannot be null, empty or whitespace.");
}
Expand All @@ -51,10 +51,10 @@
[InlineData(null, true)]
[InlineData("", true)]
[InlineData(" ", true)]
public void ReadFileLines_InvalidPathAndIgnoreExceptionsTrue_ShouldReturnEmptySpan(string path,
public void ReadFileLines_InvalidPathAndIgnoreExceptionsTrue_ShouldReturnEmptySpan(string? path,
bool ignoreExceptions)
{
var result = Reader.ReadFileLines(path, ignoreExceptions, null).ToArray();

Check warning on line 57 in src/dotenv.net.Tests/ReaderTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Possible null reference argument for parameter 'envFilePath' in 'ReadOnlySpan<string> Reader.ReadFileLines(string envFilePath, bool ignoreExceptions, Encoding? encoding)'.
result.ShouldBeEmpty();
}

Expand Down Expand Up @@ -95,15 +95,25 @@
[Fact]
public void ExtractEnvKeyValues_EmptySpan_ShouldReturnEmptySpan()
{
var result = Reader.ExtractEnvKeyValues(ReadOnlySpan<string>.Empty, false).ToArray();
var result = Reader.ExtractEnvKeyValues(ReadOnlySpan<string>.Empty, false, supportExportSyntax: false).ToArray();
result.ShouldBeEmpty();
}

[Fact]
public void ExtractEnvKeyValues_ValidLines_ShouldReturnKeyValuePairs()
{
var lines = new[] { "KEY1=value1", "KEY2=value2" };
var result = Reader.ExtractEnvKeyValues(lines, false);
var result = Reader.ExtractEnvKeyValues(lines, false, supportExportSyntax: false);
result.Length.ShouldBe(2);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY1", "value1"));
result[1].ShouldBe(new KeyValuePair<string, string>("KEY2", "value2"));
}

[Fact]
public void ExtractEnvKeyValues_ExportSyntaxValidLines_ShouldReturnKeyValuePairs()
{
var lines = new[] { "export KEY1=value1", " export KEY2 =value2" };
var result = Reader.ExtractEnvKeyValues(lines, false, supportExportSyntax: true);
result.Length.ShouldBe(2);
result[0].ShouldBe(new KeyValuePair<string, string>("KEY1", "value1"));
result[1].ShouldBe(new KeyValuePair<string, string>("KEY2", "value2"));
Expand Down
5 changes: 3 additions & 2 deletions src/dotenv.net/DotEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public static IDictionary<string, string> Read(DotEnvOptions? options = null)
.Select(envFilePath =>
{
var fileRows = Reader.ReadFileLines(envFilePath, options.IgnoreExceptions, options.Encoding);
var envKeyValues = Reader.ExtractEnvKeyValues(fileRows, options.TrimValues);
var envKeyValues =
Reader.ExtractEnvKeyValues(fileRows, options.TrimValues, options.SupportExportSyntax);
return envKeyValues.ToArray();
})
.ToList();
Expand All @@ -43,4 +44,4 @@ public static void Load(DotEnvOptions? options = null)
var envVars = Read(options);
Writer.WriteToEnv(envVars, options.OverwriteExistingVars);
}
}
}
38 changes: 34 additions & 4 deletions src/dotenv.net/DotEnvOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
/// </summary>
public int? ProbeLevelsToSearch { get; private set; }

/// <summary>
/// Whether the optional dotenv export syntax should be supported. The default is false
/// </summary>
public bool SupportExportSyntax { get; private set; }

/// <summary>
/// Default constructor for the dot env options
/// </summary>
Expand All @@ -57,9 +62,9 @@
/// <param name="probeForEnv">Whether to search up the directories looking for an env file</param>
/// <param name="probeLevelsToSearch">How high up the directory chain to search</param>
/// <param name="envFilePaths">The env file paths to load</param>
public DotEnvOptions(bool ignoreExceptions = true, IEnumerable<string>? envFilePaths = null,

Check warning on line 65 in src/dotenv.net/DotEnvOptions.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'EnvFilePaths' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 65 in src/dotenv.net/DotEnvOptions.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Encoding' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 65 in src/dotenv.net/DotEnvOptions.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'EnvFilePaths' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 65 in src/dotenv.net/DotEnvOptions.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Encoding' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 65 in src/dotenv.net/DotEnvOptions.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'EnvFilePaths' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
Encoding? encoding = null, bool trimValues = false, bool overwriteExistingVars = true,
bool probeForEnv = false, int? probeLevelsToSearch = null)
bool probeForEnv = false, int? probeLevelsToSearch = null, bool supportExportSyntax = false)
{
if (ignoreExceptions)
WithoutExceptions();
Expand All @@ -83,6 +88,11 @@
WithProbeForEnv(probeLevelsToSearch ?? DefaultProbeAscendLimit);
else
WithoutProbeForEnv();

if (supportExportSyntax)
WithSupportExportSyntax();
else
WithoutSupportExportSyntax();
}

/// <summary>
Expand Down Expand Up @@ -176,13 +186,33 @@
/// <returns>configured dot env options</returns>
public DotEnvOptions WithEncoding(Encoding encoding)
{
if (encoding == null )
if (encoding == null)
throw new ArgumentNullException(nameof(encoding), "Encoding cannot be null");

Encoding = encoding;
return this;
}

/// <summary>
/// Support export syntax entries
/// </summary>
/// <returns>Configured DotEnvOptions</returns>
public DotEnvOptions WithSupportExportSyntax()
{
SupportExportSyntax = true;
return this;
}

/// <summary>
/// Disallow support export syntax entries
/// </summary>
/// <returns>Configured DotEnvOptions</returns>
public DotEnvOptions WithoutSupportExportSyntax()
{
SupportExportSyntax = false;
return this;
}

/// <summary>
/// Set the env files to be read, if none is provided, we revert to the default '.env'
/// </summary>
Expand All @@ -192,7 +222,7 @@
if (ProbeForEnv)
throw new InvalidOperationException("EnvFiles paths cannot be set when ProbeForEnv is true");

if (envFilePaths == null )
if (envFilePaths == null)
throw new ArgumentNullException(nameof(envFilePaths), "EnvFilePaths cannot be null");

EnvFilePaths = envFilePaths.Any() != true ? DefaultEnvPath : envFilePaths;
Expand All @@ -209,4 +239,4 @@
/// ReadFileLines the env files and write to the system environment variables
/// </summary>
public void Load() => DotEnv.Load(this);
}
}
21 changes: 13 additions & 8 deletions src/dotenv.net/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ internal static class Parser
private const char SingleQuote = '\'';
private const char DoubleQuotes = '"';
private const char BackSlash = '\\';
private const string ExportPrefix = "export";

internal static ReadOnlySpan<KeyValuePair<string, string>> Parse(ReadOnlySpan<string> rawEnvRows,
bool trimValues)
bool trimValues, bool supportExportSyntax)
{
var keyValuePairs = new List<KeyValuePair<string, string>>();

Expand All @@ -28,7 +29,7 @@ internal static ReadOnlySpan<KeyValuePair<string, string>> Parse(ReadOnlySpan<st
if (!rawEnvRow.HasKey(out var equalsIndex))
continue;

var (key, rawValue) = rawEnvRow.SplitIntoKv(equalsIndex);
var (key, rawValue) = rawEnvRow.SplitIntoKv(equalsIndex, supportExportSyntax);

if (string.IsNullOrEmpty(key))
continue;
Expand Down Expand Up @@ -62,7 +63,7 @@ private static string ParseQuotedValue(string key, ReadOnlySpan<string> rawEnvRo
var endQuoteIndex = -1;
var searchFrom = 0;

// find the next unescaped quote on the current line.
// find the next unescaped quote on the current line
while (searchFrom < currentLineContent.Length)
{
var nextQuote = currentLineContent.IndexOf(quoteChar, searchFrom);
Expand All @@ -71,25 +72,25 @@ private static string ParseQuotedValue(string key, ReadOnlySpan<string> rawEnvRo
if (nextQuote == -1)
break;

// count preceding backslashes to see if the quote is escaped.
// count preceding backslashes to see if the quote is escaped
var backslashCount = 0;
for (var j = nextQuote - 1; j >= 0 && currentLineContent[j] == BackSlash; j--)
backslashCount++;

// an even number of backslashes means the quote is NOT escaped.
// an even number of backslashes means the quote is NOT escaped
if (backslashCount % 2 == 0)
{
endQuoteIndex = nextQuote;
break;
}

// odd number of backslashes means it's escaped, continue searching
// an odd number of backslashes means it is escaped, continue searching
searchFrom = nextQuote + 1;
}

if (endQuoteIndex != -1)
{
// closing quote found. Append the content before it and exit
// no closing quote found; append the content before it and exit
valueBuilder.Append(currentLineContent, 0, endQuoteIndex);
break;
}
Expand Down Expand Up @@ -119,10 +120,14 @@ private static bool HasKey(this string value, out int index)
return index > 0;
}

private static (string Key, string Value) SplitIntoKv(this string rawEnvRow, int index)
private static (string Key, string Value) SplitIntoKv(this string rawEnvRow, int index, bool supportExportSyntax)
{
var key = rawEnvRow.Substring(0, index).Trim();
var value = rawEnvRow.Substring(index + 1);

if (supportExportSyntax && key.StartsWith(ExportPrefix))
key = key.Replace(ExportPrefix, string.Empty).Trim();

return (key, value);
}

Expand Down
14 changes: 7 additions & 7 deletions src/dotenv.net/Reader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,21 @@ internal static ReadOnlySpan<string> ReadFileLines(string envFilePath, bool igno
}

internal static ReadOnlySpan<KeyValuePair<string, string>> ExtractEnvKeyValues(ReadOnlySpan<string> rawEnvRows,
bool trimValues) => rawEnvRows == ReadOnlySpan<string>.Empty
bool trimValues, bool supportExportSyntax) => rawEnvRows == ReadOnlySpan<string>.Empty
? ReadOnlySpan<KeyValuePair<string, string>>.Empty
: Parser.Parse(rawEnvRows, trimValues);
: Parser.Parse(rawEnvRows, trimValues, supportExportSyntax);

internal static Dictionary<string, string> MergeEnvKeyValues(
IEnumerable<KeyValuePair<string, string>[]> envFileKeyValues, bool overwriteExistingVars)
{
var response = new Dictionary<string, string>();

foreach (var envFileKeyValue in envFileKeyValues)
foreach (var envKeyValue in envFileKeyValue)
// if the key does not exist or if a previous env file has the same key, and we are allowed to overwrite it
if (!response.ContainsKey(envKeyValue.Key) ||
(response.ContainsKey(envKeyValue.Key) && overwriteExistingVars))
response[envKeyValue.Key] = envKeyValue.Value;
foreach (var envKeyValue in envFileKeyValue)
// if the key does not exist or if a previous env file has the same key, and we are allowed to overwrite it
if (!response.ContainsKey(envKeyValue.Key) ||
(response.ContainsKey(envKeyValue.Key) && overwriteExistingVars))
response[envKeyValue.Key] = envKeyValue.Value;

return response;
}
Expand Down
Loading
Loading