Skip to content

Commit 49ae1cf

Browse files
Merge pull request #65 from bolorundurowb/ft/add-export-syntax-support
ft/add export syntax support
2 parents a5d374e + 1a2a396 commit 49ae1cf

7 files changed

Lines changed: 102 additions & 47 deletions

File tree

src/dotenv.net.Tests/ParserTests.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class ParserTests
1111
public void Parse_EmptyLines_ShouldBeIgnored()
1212
{
1313
var lines = new[] { "", " ", null, "KEY=value" };
14-
var result = Parser.Parse(lines, trimValues: false).ToArray();
14+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
1515
result.Length.ShouldBe(1);
1616
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
1717
}
@@ -20,7 +20,7 @@ public void Parse_EmptyLines_ShouldBeIgnored()
2020
public void Parse_CommentLines_ShouldBeIgnored()
2121
{
2222
var lines = new[] { "# Comment", " # Indented comment", "KEY=value" };
23-
var result = Parser.Parse(lines, trimValues: false).ToArray();
23+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
2424
result.Length.ShouldBe(1);
2525
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
2626
}
@@ -29,7 +29,7 @@ public void Parse_CommentLines_ShouldBeIgnored()
2929
public void Parse_LinesWithoutKey_ShouldBeIgnored()
3030
{
3131
var lines = new[] { "=value", "NOKEY", "KEY=value" };
32-
var result = Parser.Parse(lines, trimValues: false).ToArray();
32+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
3333
result.Length.ShouldBe(1);
3434
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
3535
}
@@ -38,7 +38,7 @@ public void Parse_LinesWithoutKey_ShouldBeIgnored()
3838
public void Parse_SimpleKeyValue_ShouldReturnPair()
3939
{
4040
var lines = new[] { "TEST_KEY=test_value" };
41-
var result = Parser.Parse(lines, trimValues: false).ToArray();
41+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
4242
result.Length.ShouldBe(1);
4343
result[0].ShouldBe(new KeyValuePair<string, string>("TEST_KEY", "test_value"));
4444
}
@@ -47,7 +47,7 @@ public void Parse_SimpleKeyValue_ShouldReturnPair()
4747
public void Parse_UntrimmedValueWithTrimValuesFalse_ShouldPreserveWhitespace()
4848
{
4949
var lines = new[] { " KEY = value " };
50-
var result = Parser.Parse(lines, trimValues: false).ToArray();
50+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
5151
result.Length.ShouldBe(1);
5252
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", " value "));
5353
}
@@ -56,7 +56,7 @@ public void Parse_UntrimmedValueWithTrimValuesFalse_ShouldPreserveWhitespace()
5656
public void Parse_UntrimmedValueWithTrimValuesTrue_ShouldTrimValue()
5757
{
5858
var lines = new[] { "KEY= value " };
59-
var result = Parser.Parse(lines, trimValues: true).ToArray();
59+
var result = Parser.Parse(lines, trimValues: true, supportExportSyntax: false).ToArray();
6060
result.Length.ShouldBe(1);
6161
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value"));
6262
}
@@ -65,7 +65,7 @@ public void Parse_UntrimmedValueWithTrimValuesTrue_ShouldTrimValue()
6565
public void Parse_SingleQuotedValue_ShouldUnescapeQuotes()
6666
{
6767
var lines = new[] { "KEY='value with \\' quote'" };
68-
var result = Parser.Parse(lines, trimValues: false).ToArray();
68+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
6969
result.Length.ShouldBe(1);
7070
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value with ' quote"));
7171
}
@@ -74,7 +74,7 @@ public void Parse_SingleQuotedValue_ShouldUnescapeQuotes()
7474
public void Parse_DoubleQuotedValue_ShouldUnescapeQuotes()
7575
{
7676
var lines = new[] { "KEY=\"value with \\\" quote\"" };
77-
var result = Parser.Parse(lines, trimValues: false).ToArray();
77+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
7878
result.Length.ShouldBe(1);
7979
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", "value with \" quote"));
8080
}
@@ -83,7 +83,7 @@ public void Parse_DoubleQuotedValue_ShouldUnescapeQuotes()
8383
public void Parse_EscapedBackslashes_ShouldUnescape()
8484
{
8585
var lines = new[] { "KEY='escaped \\\\ backslash'" };
86-
var result = Parser.Parse(lines, trimValues: false).ToArray();
86+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
8787
result.Length.ShouldBe(1);
8888
result.ShouldContain(new KeyValuePair<string, string>("KEY", "escaped \\ backslash"));
8989
}
@@ -92,7 +92,7 @@ public void Parse_EscapedBackslashes_ShouldUnescape()
9292
public void Parse_MultiLineValue_ShouldCombineLines()
9393
{
9494
var lines = new[] { "KEY='first line", "second line'", "NEXT=value" };
95-
var result = Parser.Parse(lines, trimValues: false).ToArray();
95+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
9696
result.Length.ShouldBe(2);
9797
result[0].ShouldBe(new KeyValuePair<string, string>("KEY", $"first line{Environment.NewLine}second line"));
9898
result[1].ShouldBe(new KeyValuePair<string, string>("NEXT", "value"));
@@ -102,7 +102,7 @@ public void Parse_MultiLineValue_ShouldCombineLines()
102102
public void Parse_UnclosedQuote_ShouldThrowException()
103103
{
104104
var lines = new[] { "KEY='unclosed quote" };
105-
Action act = () => Parser.Parse(lines, trimValues: false).ToArray();
105+
Action act = () => Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
106106
act.ShouldThrow<ArgumentException>()
107107
.Message.ShouldBe("Unable to parse environment variable: KEY. Missing closing quote.");
108108
}
@@ -111,7 +111,16 @@ public void Parse_UnclosedQuote_ShouldThrowException()
111111
public void Parse_EscapedQuoteInMiddle_ShouldUnescape()
112112
{
113113
var lines = new[] { "KEY='before\\'after'" };
114-
var result = Parser.Parse(lines, trimValues: false).ToArray();
114+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
115+
result.Length.ShouldBe(1);
116+
result.ShouldContain(new KeyValuePair<string, string>("KEY", "before'after"));
117+
}
118+
119+
[Fact]
120+
public void Parse_ExportEscapedQuoteInMiddle_ShouldUnescape()
121+
{
122+
var lines = new[] { "export KEY='before\\'after'" };
123+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: true).ToArray();
115124
result.Length.ShouldBe(1);
116125
result.ShouldContain(new KeyValuePair<string, string>("KEY", "before'after"));
117126
}
@@ -120,7 +129,7 @@ public void Parse_EscapedQuoteInMiddle_ShouldUnescape()
120129
public void Parse_BackslashNotEscapingQuote_ShouldRemain()
121130
{
122131
var lines = new[] { "KEY='before\\after'" };
123-
var result = Parser.Parse(lines, trimValues: false).ToArray();
132+
var result = Parser.Parse(lines, trimValues: false, supportExportSyntax: false).ToArray();
124133
result.Length.ShouldBe(1);
125134
result.ShouldContain(new KeyValuePair<string, string>("KEY", "before\\after"));
126135
}

src/dotenv.net.Tests/ReaderTests.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void Dispose()
3939
[InlineData(null, false)]
4040
[InlineData("", false)]
4141
[InlineData(" ", false)]
42-
public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumentException(string path,
42+
public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumentException(string? path,
4343
bool ignoreExceptions)
4444
{
4545
Action act = () => Reader.ReadFileLines(path, ignoreExceptions, null);
@@ -51,7 +51,7 @@ public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumen
5151
[InlineData(null, true)]
5252
[InlineData("", true)]
5353
[InlineData(" ", true)]
54-
public void ReadFileLines_InvalidPathAndIgnoreExceptionsTrue_ShouldReturnEmptySpan(string path,
54+
public void ReadFileLines_InvalidPathAndIgnoreExceptionsTrue_ShouldReturnEmptySpan(string? path,
5555
bool ignoreExceptions)
5656
{
5757
var result = Reader.ReadFileLines(path, ignoreExceptions, null).ToArray();
@@ -95,15 +95,25 @@ public void ReadFileLines_WithCustomEncoding_ShouldReturnCorrectContent()
9595
[Fact]
9696
public void ExtractEnvKeyValues_EmptySpan_ShouldReturnEmptySpan()
9797
{
98-
var result = Reader.ExtractEnvKeyValues(ReadOnlySpan<string>.Empty, false).ToArray();
98+
var result = Reader.ExtractEnvKeyValues(ReadOnlySpan<string>.Empty, false, supportExportSyntax: false).ToArray();
9999
result.ShouldBeEmpty();
100100
}
101101

102102
[Fact]
103103
public void ExtractEnvKeyValues_ValidLines_ShouldReturnKeyValuePairs()
104104
{
105105
var lines = new[] { "KEY1=value1", "KEY2=value2" };
106-
var result = Reader.ExtractEnvKeyValues(lines, false);
106+
var result = Reader.ExtractEnvKeyValues(lines, false, supportExportSyntax: false);
107+
result.Length.ShouldBe(2);
108+
result[0].ShouldBe(new KeyValuePair<string, string>("KEY1", "value1"));
109+
result[1].ShouldBe(new KeyValuePair<string, string>("KEY2", "value2"));
110+
}
111+
112+
[Fact]
113+
public void ExtractEnvKeyValues_ExportSyntaxValidLines_ShouldReturnKeyValuePairs()
114+
{
115+
var lines = new[] { "export KEY1=value1", " export KEY2 =value2" };
116+
var result = Reader.ExtractEnvKeyValues(lines, false, supportExportSyntax: true);
107117
result.Length.ShouldBe(2);
108118
result[0].ShouldBe(new KeyValuePair<string, string>("KEY1", "value1"));
109119
result[1].ShouldBe(new KeyValuePair<string, string>("KEY2", "value2"));

src/dotenv.net/DotEnv.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public static IDictionary<string, string> Read(DotEnvOptions? options = null)
2525
.Select(envFilePath =>
2626
{
2727
var fileRows = Reader.ReadFileLines(envFilePath, options.IgnoreExceptions, options.Encoding);
28-
var envKeyValues = Reader.ExtractEnvKeyValues(fileRows, options.TrimValues);
28+
var envKeyValues =
29+
Reader.ExtractEnvKeyValues(fileRows, options.TrimValues, options.SupportExportSyntax);
2930
return envKeyValues.ToArray();
3031
})
3132
.ToList();
@@ -43,4 +44,4 @@ public static void Load(DotEnvOptions? options = null)
4344
var envVars = Read(options);
4445
Writer.WriteToEnv(envVars, options.OverwriteExistingVars);
4546
}
46-
}
47+
}

src/dotenv.net/DotEnvOptions.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public class DotEnvOptions
4747
/// </summary>
4848
public int? ProbeLevelsToSearch { get; private set; }
4949

50+
/// <summary>
51+
/// Whether the optional dotenv export syntax should be supported. The default is false
52+
/// </summary>
53+
public bool SupportExportSyntax { get; private set; }
54+
5055
/// <summary>
5156
/// Default constructor for the dot env options
5257
/// </summary>
@@ -59,7 +64,7 @@ public class DotEnvOptions
5964
/// <param name="envFilePaths">The env file paths to load</param>
6065
public DotEnvOptions(bool ignoreExceptions = true, IEnumerable<string>? envFilePaths = null,
6166
Encoding? encoding = null, bool trimValues = false, bool overwriteExistingVars = true,
62-
bool probeForEnv = false, int? probeLevelsToSearch = null)
67+
bool probeForEnv = false, int? probeLevelsToSearch = null, bool supportExportSyntax = false)
6368
{
6469
if (ignoreExceptions)
6570
WithoutExceptions();
@@ -83,6 +88,11 @@ public DotEnvOptions(bool ignoreExceptions = true, IEnumerable<string>? envFileP
8388
WithProbeForEnv(probeLevelsToSearch ?? DefaultProbeAscendLimit);
8489
else
8590
WithoutProbeForEnv();
91+
92+
if (supportExportSyntax)
93+
WithSupportExportSyntax();
94+
else
95+
WithoutSupportExportSyntax();
8696
}
8797

8898
/// <summary>
@@ -176,13 +186,33 @@ public DotEnvOptions WithoutTrimValues()
176186
/// <returns>configured dot env options</returns>
177187
public DotEnvOptions WithEncoding(Encoding encoding)
178188
{
179-
if (encoding == null )
189+
if (encoding == null)
180190
throw new ArgumentNullException(nameof(encoding), "Encoding cannot be null");
181191

182192
Encoding = encoding;
183193
return this;
184194
}
185195

196+
/// <summary>
197+
/// Support export syntax entries
198+
/// </summary>
199+
/// <returns>Configured DotEnvOptions</returns>
200+
public DotEnvOptions WithSupportExportSyntax()
201+
{
202+
SupportExportSyntax = true;
203+
return this;
204+
}
205+
206+
/// <summary>
207+
/// Disallow support export syntax entries
208+
/// </summary>
209+
/// <returns>Configured DotEnvOptions</returns>
210+
public DotEnvOptions WithoutSupportExportSyntax()
211+
{
212+
SupportExportSyntax = false;
213+
return this;
214+
}
215+
186216
/// <summary>
187217
/// Set the env files to be read, if none is provided, we revert to the default '.env'
188218
/// </summary>
@@ -192,7 +222,7 @@ public DotEnvOptions WithEnvFiles(params string[] envFilePaths)
192222
if (ProbeForEnv)
193223
throw new InvalidOperationException("EnvFiles paths cannot be set when ProbeForEnv is true");
194224

195-
if (envFilePaths == null )
225+
if (envFilePaths == null)
196226
throw new ArgumentNullException(nameof(envFilePaths), "EnvFilePaths cannot be null");
197227

198228
EnvFilePaths = envFilePaths.Any() != true ? DefaultEnvPath : envFilePaths;
@@ -209,4 +239,4 @@ public DotEnvOptions WithEnvFiles(params string[] envFilePaths)
209239
/// ReadFileLines the env files and write to the system environment variables
210240
/// </summary>
211241
public void Load() => DotEnv.Load(this);
212-
}
242+
}

src/dotenv.net/Parser.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ internal static class Parser
99
private const char SingleQuote = '\'';
1010
private const char DoubleQuotes = '"';
1111
private const char BackSlash = '\\';
12+
private const string ExportPrefix = "export";
1213

1314
internal static ReadOnlySpan<KeyValuePair<string, string>> Parse(ReadOnlySpan<string> rawEnvRows,
14-
bool trimValues)
15+
bool trimValues, bool supportExportSyntax)
1516
{
1617
var keyValuePairs = new List<KeyValuePair<string, string>>();
1718

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

31-
var (key, rawValue) = rawEnvRow.SplitIntoKv(equalsIndex);
32+
var (key, rawValue) = rawEnvRow.SplitIntoKv(equalsIndex, supportExportSyntax);
3233

3334
if (string.IsNullOrEmpty(key))
3435
continue;
@@ -62,7 +63,7 @@ private static string ParseQuotedValue(string key, ReadOnlySpan<string> rawEnvRo
6263
var endQuoteIndex = -1;
6364
var searchFrom = 0;
6465

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

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

79-
// an even number of backslashes means the quote is NOT escaped.
80+
// an even number of backslashes means the quote is NOT escaped
8081
if (backslashCount % 2 == 0)
8182
{
8283
endQuoteIndex = nextQuote;
8384
break;
8485
}
8586

86-
// odd number of backslashes means it's escaped, continue searching
87+
// an odd number of backslashes means it is escaped, continue searching
8788
searchFrom = nextQuote + 1;
8889
}
8990

9091
if (endQuoteIndex != -1)
9192
{
92-
// closing quote found. Append the content before it and exit
93+
// no closing quote found; append the content before it and exit
9394
valueBuilder.Append(currentLineContent, 0, endQuoteIndex);
9495
break;
9596
}
@@ -119,10 +120,14 @@ private static bool HasKey(this string value, out int index)
119120
return index > 0;
120121
}
121122

122-
private static (string Key, string Value) SplitIntoKv(this string rawEnvRow, int index)
123+
private static (string Key, string Value) SplitIntoKv(this string rawEnvRow, int index, bool supportExportSyntax)
123124
{
124125
var key = rawEnvRow.Substring(0, index).Trim();
125126
var value = rawEnvRow.Substring(index + 1);
127+
128+
if (supportExportSyntax && key.StartsWith(ExportPrefix))
129+
key = key.Replace(ExportPrefix, string.Empty).Trim();
130+
126131
return (key, value);
127132
}
128133

src/dotenv.net/Reader.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ internal static ReadOnlySpan<string> ReadFileLines(string envFilePath, bool igno
3838
}
3939

4040
internal static ReadOnlySpan<KeyValuePair<string, string>> ExtractEnvKeyValues(ReadOnlySpan<string> rawEnvRows,
41-
bool trimValues) => rawEnvRows == ReadOnlySpan<string>.Empty
41+
bool trimValues, bool supportExportSyntax) => rawEnvRows == ReadOnlySpan<string>.Empty
4242
? ReadOnlySpan<KeyValuePair<string, string>>.Empty
43-
: Parser.Parse(rawEnvRows, trimValues);
43+
: Parser.Parse(rawEnvRows, trimValues, supportExportSyntax);
4444

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

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

5757
return response;
5858
}

0 commit comments

Comments
 (0)