diff --git a/src/dotenv.net.Tests/ParserTests.cs b/src/dotenv.net.Tests/ParserTests.cs index ebad0a4..e570343 100644 --- a/src/dotenv.net.Tests/ParserTests.cs +++ b/src/dotenv.net.Tests/ParserTests.cs @@ -11,7 +11,7 @@ public class ParserTests 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(); result.Length.ShouldBe(1); result[0].ShouldBe(new KeyValuePair("KEY", "value")); } @@ -20,7 +20,7 @@ public void Parse_EmptyLines_ShouldBeIgnored() 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("KEY", "value")); } @@ -29,7 +29,7 @@ public void Parse_CommentLines_ShouldBeIgnored() 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("KEY", "value")); } @@ -38,7 +38,7 @@ public void Parse_LinesWithoutKey_ShouldBeIgnored() 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("TEST_KEY", "test_value")); } @@ -47,7 +47,7 @@ public void Parse_SimpleKeyValue_ShouldReturnPair() 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("KEY", " value ")); } @@ -56,7 +56,7 @@ public void Parse_UntrimmedValueWithTrimValuesFalse_ShouldPreserveWhitespace() 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("KEY", "value")); } @@ -65,7 +65,7 @@ public void Parse_UntrimmedValueWithTrimValuesTrue_ShouldTrimValue() 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("KEY", "value with ' quote")); } @@ -74,7 +74,7 @@ public void Parse_SingleQuotedValue_ShouldUnescapeQuotes() 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("KEY", "value with \" quote")); } @@ -83,7 +83,7 @@ public void Parse_DoubleQuotedValue_ShouldUnescapeQuotes() 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("KEY", "escaped \\ backslash")); } @@ -92,7 +92,7 @@ public void Parse_EscapedBackslashes_ShouldUnescape() 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("KEY", $"first line{Environment.NewLine}second line")); result[1].ShouldBe(new KeyValuePair("NEXT", "value")); @@ -102,7 +102,7 @@ public void Parse_MultiLineValue_ShouldCombineLines() 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() .Message.ShouldBe("Unable to parse environment variable: KEY. Missing closing quote."); } @@ -111,7 +111,16 @@ public void Parse_UnclosedQuote_ShouldThrowException() 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("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("KEY", "before'after")); } @@ -120,7 +129,7 @@ public void Parse_EscapedQuoteInMiddle_ShouldUnescape() 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("KEY", "before\\after")); } diff --git a/src/dotenv.net.Tests/ReaderTests.cs b/src/dotenv.net.Tests/ReaderTests.cs index fe76aef..b75b8d3 100644 --- a/src/dotenv.net.Tests/ReaderTests.cs +++ b/src/dotenv.net.Tests/ReaderTests.cs @@ -39,7 +39,7 @@ public void Dispose() [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); @@ -51,7 +51,7 @@ public void ReadFileLines_InvalidPathAndIgnoreExceptionsFalse_ShouldThrowArgumen [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(); @@ -95,7 +95,7 @@ public void ReadFileLines_WithCustomEncoding_ShouldReturnCorrectContent() [Fact] public void ExtractEnvKeyValues_EmptySpan_ShouldReturnEmptySpan() { - var result = Reader.ExtractEnvKeyValues(ReadOnlySpan.Empty, false).ToArray(); + var result = Reader.ExtractEnvKeyValues(ReadOnlySpan.Empty, false, supportExportSyntax: false).ToArray(); result.ShouldBeEmpty(); } @@ -103,7 +103,17 @@ public void ExtractEnvKeyValues_EmptySpan_ShouldReturnEmptySpan() 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("KEY1", "value1")); + result[1].ShouldBe(new KeyValuePair("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("KEY1", "value1")); result[1].ShouldBe(new KeyValuePair("KEY2", "value2")); diff --git a/src/dotenv.net/DotEnv.cs b/src/dotenv.net/DotEnv.cs index d6d3164..7c150fe 100644 --- a/src/dotenv.net/DotEnv.cs +++ b/src/dotenv.net/DotEnv.cs @@ -25,7 +25,8 @@ public static IDictionary 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(); @@ -43,4 +44,4 @@ public static void Load(DotEnvOptions? options = null) var envVars = Read(options); Writer.WriteToEnv(envVars, options.OverwriteExistingVars); } -} +} \ No newline at end of file diff --git a/src/dotenv.net/DotEnvOptions.cs b/src/dotenv.net/DotEnvOptions.cs index 641473d..a990a1a 100644 --- a/src/dotenv.net/DotEnvOptions.cs +++ b/src/dotenv.net/DotEnvOptions.cs @@ -47,6 +47,11 @@ public class DotEnvOptions /// public int? ProbeLevelsToSearch { get; private set; } + /// + /// Whether the optional dotenv export syntax should be supported. The default is false + /// + public bool SupportExportSyntax { get; private set; } + /// /// Default constructor for the dot env options /// @@ -59,7 +64,7 @@ public class DotEnvOptions /// The env file paths to load public DotEnvOptions(bool ignoreExceptions = true, IEnumerable? envFilePaths = null, 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(); @@ -83,6 +88,11 @@ public DotEnvOptions(bool ignoreExceptions = true, IEnumerable? envFileP WithProbeForEnv(probeLevelsToSearch ?? DefaultProbeAscendLimit); else WithoutProbeForEnv(); + + if (supportExportSyntax) + WithSupportExportSyntax(); + else + WithoutSupportExportSyntax(); } /// @@ -176,13 +186,33 @@ public DotEnvOptions WithoutTrimValues() /// configured dot env options public DotEnvOptions WithEncoding(Encoding encoding) { - if (encoding == null ) + if (encoding == null) throw new ArgumentNullException(nameof(encoding), "Encoding cannot be null"); Encoding = encoding; return this; } + /// + /// Support export syntax entries + /// + /// Configured DotEnvOptions + public DotEnvOptions WithSupportExportSyntax() + { + SupportExportSyntax = true; + return this; + } + + /// + /// Disallow support export syntax entries + /// + /// Configured DotEnvOptions + public DotEnvOptions WithoutSupportExportSyntax() + { + SupportExportSyntax = false; + return this; + } + /// /// Set the env files to be read, if none is provided, we revert to the default '.env' /// @@ -192,7 +222,7 @@ public DotEnvOptions WithEnvFiles(params string[] envFilePaths) 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; @@ -209,4 +239,4 @@ public DotEnvOptions WithEnvFiles(params string[] envFilePaths) /// ReadFileLines the env files and write to the system environment variables /// public void Load() => DotEnv.Load(this); -} +} \ No newline at end of file diff --git a/src/dotenv.net/Parser.cs b/src/dotenv.net/Parser.cs index bfc742c..eedb380 100644 --- a/src/dotenv.net/Parser.cs +++ b/src/dotenv.net/Parser.cs @@ -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> Parse(ReadOnlySpan rawEnvRows, - bool trimValues) + bool trimValues, bool supportExportSyntax) { var keyValuePairs = new List>(); @@ -28,7 +29,7 @@ internal static ReadOnlySpan> Parse(ReadOnlySpan 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); @@ -71,25 +72,25 @@ private static string ParseQuotedValue(string key, ReadOnlySpan 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; } @@ -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); } diff --git a/src/dotenv.net/Reader.cs b/src/dotenv.net/Reader.cs index eb0e356..3e53f1e 100644 --- a/src/dotenv.net/Reader.cs +++ b/src/dotenv.net/Reader.cs @@ -38,9 +38,9 @@ internal static ReadOnlySpan ReadFileLines(string envFilePath, bool igno } internal static ReadOnlySpan> ExtractEnvKeyValues(ReadOnlySpan rawEnvRows, - bool trimValues) => rawEnvRows == ReadOnlySpan.Empty + bool trimValues, bool supportExportSyntax) => rawEnvRows == ReadOnlySpan.Empty ? ReadOnlySpan>.Empty - : Parser.Parse(rawEnvRows, trimValues); + : Parser.Parse(rawEnvRows, trimValues, supportExportSyntax); internal static Dictionary MergeEnvKeyValues( IEnumerable[]> envFileKeyValues, bool overwriteExistingVars) @@ -48,11 +48,11 @@ internal static Dictionary MergeEnvKeyValues( var response = new Dictionary(); 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; } diff --git a/src/dotenv.net/Utilities/EnvReader.cs b/src/dotenv.net/Utilities/EnvReader.cs index 06f22ed..1d1fad0 100644 --- a/src/dotenv.net/Utilities/EnvReader.cs +++ b/src/dotenv.net/Utilities/EnvReader.cs @@ -15,7 +15,7 @@ public static class EnvReader /// When the value could not be found public static string GetStringValue(string key) { - if (TryGetStringValue(key, out var value)) + if (TryGetStringValue(key, out var value)) return value!; throw new Exception("Value could not be retrieved."); @@ -29,7 +29,7 @@ public static string GetStringValue(string key) /// When the value could not be found or is not an integer public static int GetIntValue(string key) { - if (TryGetIntValue(key, out var value)) + if (TryGetIntValue(key, out var value)) return value; throw new Exception("Value could not be retrieved."); @@ -43,7 +43,7 @@ public static int GetIntValue(string key) /// When the value could not be found or is not a valid double public static double GetDoubleValue(string key) { - if (TryGetDoubleValue(key, out var value)) + if (TryGetDoubleValue(key, out var value)) return value; throw new Exception("Value could not be retrieved."); @@ -57,7 +57,7 @@ public static double GetDoubleValue(string key) /// When the value could not be found or is not a valid decimal public static decimal GetDecimalValue(string key) { - if (TryGetDecimalValue(key, out var value)) + if (TryGetDecimalValue(key, out var value)) return value; throw new Exception("Value could not be retrieved."); @@ -71,7 +71,7 @@ public static decimal GetDecimalValue(string key) /// When the value could not be found or is not a valid bool public static bool GetBooleanValue(string key) { - if (TryGetBooleanValue(key, out var value)) + if (TryGetBooleanValue(key, out var value)) return value; throw new Exception("Value could not be retrieved."); @@ -107,7 +107,7 @@ public static bool TryGetIntValue(string key, out int value) { var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) + if (!string.IsNullOrEmpty(retrievedValue)) return int.TryParse(retrievedValue, out value); value = 0; @@ -124,7 +124,7 @@ public static bool TryGetDoubleValue(string key, out double value) { var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) + if (!string.IsNullOrEmpty(retrievedValue)) return double.TryParse(retrievedValue, out value); value = 0.0; @@ -141,7 +141,7 @@ public static bool TryGetDecimalValue(string key, out decimal value) { var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) + if (!string.IsNullOrEmpty(retrievedValue)) return decimal.TryParse(retrievedValue, out value); value = 0.0m; @@ -158,7 +158,7 @@ public static bool TryGetBooleanValue(string key, out bool value) { var retrievedValue = Environment.GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(retrievedValue)) + if (!string.IsNullOrEmpty(retrievedValue)) return bool.TryParse(retrievedValue, out value); value = false;