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.
 
namespace Aspire.Hosting.Docker;
 
internal sealed record EnvEntry(string Key, string? Value, string? Comment);
 
internal sealed class EnvFile
{
    private readonly SortedDictionary<string, EnvEntry> _entries = [];
 
    public static EnvFile Load(string path)
    {
        var envFile = new EnvFile();
        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(string 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(string path, bool includeValues)
    {
        if (includeValues)
        {
            Save(path);
        }
        else
        {
            SaveKeysOnly(path);
        }
    }
 
    private void SaveKeysOnly(string path)
    {
        var lines = new List<string>();
 
        foreach (var entry in _entries.Values)
        {
            if (!string.IsNullOrWhiteSpace(entry.Comment))
            {
                lines.Add($"# {entry.Comment}");
            }
            lines.Add($"{entry.Key}=");
            lines.Add(string.Empty);
        }
 
        File.WriteAllLines(path, lines);
    }
}