| File: EnvFile.cs | Web Access |
| Project: src\src\Aspire.Hosting.Docker\Aspire.Hosting.Docker.csproj (Aspire.Hosting.Docker) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Docker; internal sealed record EnvEntry(string Key, string? Value, string? Comment); internal sealed class EnvFile { private string? _path; private readonly ILogger? _logger; internal SortedDictionary<string, EnvEntry> Entries { get; } = []; private EnvFile(ILogger? logger = null) { _logger = logger; } public static EnvFile Create(string path, ILogger? logger = null) { return new EnvFile(logger) { _path = path }; } public static EnvFile Load(string path, ILogger? logger = null) { var envFile = new EnvFile(logger) { _path = path }; if (!File.Exists(path)) { return envFile; } string? currentComment = null; foreach (var line in File.ReadAllLines(path)) { var trimmed = line.TrimStart(); if (trimmed.StartsWith('#')) { // Extract comment text (remove # and trim) currentComment = trimmed.Length > 1 ? trimmed[1..].Trim() : string.Empty; } else if (TryParseKeyValue(line, out var key, out var value)) { envFile.Entries[key] = new EnvEntry(key, value, currentComment); currentComment = null; // Reset comment after associating it with a key } else { // Reset comment if we encounter a non-comment, non-key line currentComment = null; } } return envFile; } public void Add(string key, string? value, string? comment, bool onlyIfMissing = true) { if (Entries.ContainsKey(key) && onlyIfMissing) { return; } Entries[key] = new EnvEntry(key, value, comment); } private static bool TryParseKeyValue(string line, out string key, out string? value) { key = string.Empty; value = null; var trimmed = line.TrimStart(); if (!trimmed.StartsWith('#') && trimmed.Contains('=')) { var eqIndex = trimmed.IndexOf('='); if (eqIndex > 0) { key = trimmed[..eqIndex].Trim(); value = eqIndex < trimmed.Length - 1 ? trimmed[(eqIndex + 1)..] : string.Empty; return true; } } return false; } public void Save() { if (_path is null) { throw new InvalidOperationException("Cannot save EnvFile without a path. Use Load() to create an EnvFile with a path."); } // Log if we're about to overwrite an existing file if (File.Exists(_path)) { _logger?.LogInformation("Environment file '{EnvFilePath}' already exists and will be overwritten", _path); } var lines = new List<string>(); foreach (var entry in Entries.Values) { if (!string.IsNullOrWhiteSpace(entry.Comment)) { lines.Add($"# {entry.Comment}"); } lines.Add(entry.Value is not null ? $"{entry.Key}={entry.Value}" : $"{entry.Key}="); lines.Add(string.Empty); } File.WriteAllLines(_path, lines); } public void Save(bool includeValues) { if (includeValues) { Save(); } else { SaveKeysOnly(); } } private void SaveKeysOnly() { if (_path is null) { throw new InvalidOperationException("Cannot save EnvFile without a path. Use Load() to create an EnvFile with a path."); } var lines = new List<string>(); foreach (var entry in Entries.Values) { if (!string.IsNullOrWhiteSpace(entry.Comment)) { lines.Add($"# {entry.Comment}"); } // If the entry already has a non-empty value (loaded from disk), preserve it // This ensures user-modified values are not overwritten when we save keys only if (!string.IsNullOrEmpty(entry.Value)) { lines.Add($"{entry.Key}={entry.Value}"); } else { lines.Add($"{entry.Key}="); } lines.Add(string.Empty); } File.WriteAllLines(_path, lines); } }