diff --git a/src/Config.Tests/ConfigTests.cs b/src/Config.Tests/ConfigTests.cs index 24f4fec..4d511b3 100644 --- a/src/Config.Tests/ConfigTests.cs +++ b/src/Config.Tests/ConfigTests.cs @@ -9,10 +9,12 @@ namespace DotNetConfig.Tests { public class ConfigTests { + static readonly string currentDir = Directory.GetCurrentDirectory(); readonly ITestOutputHelper output; public ConfigTests(ITestOutputHelper output) { + Directory.SetCurrentDirectory(currentDir); Config.GlobalLocation = Path.Combine(Directory.GetCurrentDirectory(), "Content", "global.netconfig"); Config.SystemLocation = Path.Combine(Directory.GetCurrentDirectory(), "Content", "system.netconfig"); CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; @@ -385,5 +387,23 @@ public void when_resolving_path_then_resolves_relative_to_config() Assert.Equal(Path.Combine(parentDir, "file.txt"), section.GetNormalizedPath("parent")); Assert.Equal(Path.Combine(localDir, "file.txt"), section.GetNormalizedPath("local")); } + + [Fact] + public void when_setting_variable_in_global_dir_then_writes_global_file() + { + Config.GlobalLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), ".netconfig"); + Config.SystemLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), ".netconfig"); + + // Simulates opening a cli app from the user's profile dir + Directory.CreateDirectory(Path.GetDirectoryName(Config.GlobalLocation)); + Directory.SetCurrentDirectory(Path.GetDirectoryName(Config.GlobalLocation)); + var config = Config.Build(); + + config.SetString("foo", "bar", "baz"); + + var global = Config.Build(ConfigLevel.Global); + + Assert.Equal("baz", global.GetString("foo", "bar")); + } } } diff --git a/src/Config/AggregateConfig.cs b/src/Config/AggregateConfig.cs index 331f8b5..5c83773 100644 --- a/src/Config/AggregateConfig.cs +++ b/src/Config/AggregateConfig.cs @@ -13,16 +13,16 @@ public AggregateConfig(params Config[] configs) public List Files { get; } public override void AddBoolean(string section, string? subsection, string variable, bool value) - => Files.First(x => x.Level == null).AddBoolean(section, subsection, variable, value); + => GetConfig().AddBoolean(section, subsection, variable, value); public override void AddDateTime(string section, string? subsection, string variable, DateTime value) - => Files.First(x => x.Level == null).AddDateTime(section, subsection, variable, value); + => GetConfig().AddDateTime(section, subsection, variable, value); public override void AddNumber(string section, string? subsection, string variable, long value) - => Files.First(x => x.Level == null).AddNumber(section, subsection, variable, value); + => GetConfig().AddNumber(section, subsection, variable, value); public override void AddString(string section, string? subsection, string variable, string value) - => Files.First(x => x.Level == null).AddString(section, subsection, variable, value); + => GetConfig().AddString(section, subsection, variable, value); public override IEnumerable GetAll(string section, string? subsection, string variable, string? valueRegex) => Files.SelectMany(x => x.GetAll(section, subsection, variable, valueRegex)); @@ -34,34 +34,34 @@ public override IEnumerable GetRegex(string nameRegex, string? valu => Files.Select(config => config.GetNormalizedPath(section, subsection, variable)).Where(x => x != null).FirstOrDefault(); public override void RemoveSection(string section, string? subsection = null) - => Files.First(x => x.Level == null).RemoveSection(section, subsection); + => GetConfig().RemoveSection(section, subsection); public override void RenameSection(string oldSection, string? oldSubsection, string newSection, string? newSubsection) - => Files.First(x => x.Level == null).RenameSection(oldSection, oldSubsection, newSection, newSubsection); + => GetConfig().RenameSection(oldSection, oldSubsection, newSection, newSubsection); public override void SetAllBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllBoolean(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllBoolean(section, subsection, variable, value, valueRegex); public override void SetAllDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllDateTime(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllDateTime(section, subsection, variable, value, valueRegex); public override void SetAllNumber(string section, string? subsection, string variable, long value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllNumber(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllNumber(section, subsection, variable, value, valueRegex); public override void SetAllString(string section, string? subsection, string variable, string value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllString(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllString(section, subsection, variable, value, valueRegex); public override void SetBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) - => Files.First(x => x.Level == null).SetBoolean(section, subsection, variable, value, valueRegex); + => GetConfig().SetBoolean(section, subsection, variable, value, valueRegex); public override void SetDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) - => Files.First(x => x.Level == null).SetDateTime(section, subsection, variable, value, valueRegex); + => GetConfig().SetDateTime(section, subsection, variable, value, valueRegex); public override void SetNumber(string section, string? subsection, string variable, long value, string? valueRegex) - => Files.First(x => x.Level == null).SetNumber(section, subsection, variable, value, valueRegex); + => GetConfig().SetNumber(section, subsection, variable, value, valueRegex); public override void SetString(string section, string? subsection, string variable, string value, string? valueRegex) - => Files.First(x => x.Level == null).SetString(section, subsection, variable, value, valueRegex); + => GetConfig().SetString(section, subsection, variable, value, valueRegex); public override bool TryGetBoolean(string section, string? subsection, string variable, out bool value) { @@ -112,10 +112,18 @@ public override bool TryGetString(string section, string? subsection, string var } public override void Unset(string section, string? subsection, string variable) - => Files.First(x => x.Level == null).Unset(section, subsection, variable); + => GetConfig().Unset(section, subsection, variable); public override void UnsetAll(string section, string? subsection, string variable, string? valueRegex) - => Files.First(x => x.Level == null).UnsetAll(section, subsection, variable, valueRegex); + => GetConfig().UnsetAll(section, subsection, variable, valueRegex); + + // When writing via the aggregate config without specifying a ConfigLevel, we first try the default + // .netconfig location, followed by a non-local one (i.e. global/system if that's the first we find), + // followed by whatever is first last. + Config GetConfig() + => Files.FirstOrDefault(x => x.Level == null) ?? + Files.FirstOrDefault(x => x.Level != ConfigLevel.Local) ?? + Files.First(); protected override IEnumerable GetEntries() => Files.SelectMany(x => x); diff --git a/src/Config/Config.cs b/src/Config/Config.cs index 01bd3ba..b156108 100644 --- a/src/Config/Config.cs +++ b/src/Config/Config.cs @@ -91,20 +91,31 @@ public static Config Build(string? path = null) dir = new DirectoryInfo(Path.GetDirectoryName(path)).Parent; } - // [config] root = true stops the directory walking - while (configs.GetBoolean("config", "root") != true && dir != null && dir.Exists) + // If the path is not Global or System, we start walking the directory + // tree up to build the config hierarchy + if (!GlobalLocation.Equals(path, StringComparison.OrdinalIgnoreCase) && + !SystemLocation.Equals(path, StringComparison.OrdinalIgnoreCase)) { - var file = Path.Combine(dir.FullName, FileName + UserExtension); - if (File.Exists(file)) - configs.Files.Add(new FileConfig(file)); - - file = Path.Combine(dir.FullName, FileName); - if (File.Exists(file) && - !GlobalLocation.Equals(file, StringComparison.OrdinalIgnoreCase) && - !SystemLocation.Equals(file, StringComparison.OrdinalIgnoreCase)) - configs.Files.Add(new FileConfig(file)); - - dir = dir.Parent; + // [config] root = true stops the directory walking + while (configs.GetBoolean("config", "root") != true && dir != null && dir.Exists) + { + var file = Path.Combine(dir.FullName, FileName + UserExtension); + if (File.Exists(file)) + configs.Files.Add(new FileConfig(file)); + + file = Path.Combine(dir.FullName, FileName); + // Never add global or system locations here, this loop is just for directory hierarchies + // NOTE: we did add the .user file at the global/system location, since it might be useful + // to allow local writing there too. + if (GlobalLocation.Equals(file, StringComparison.OrdinalIgnoreCase) || + SystemLocation.Equals(file, StringComparison.OrdinalIgnoreCase)) + break; + + if (File.Exists(file)) + configs.Files.Add(new FileConfig(file)); + + dir = dir.Parent; + } } // Don't read the global location if we're building the system location or it's been opted out explicitly diff --git a/src/Config/ConfigLevel.cs b/src/Config/ConfigLevel.cs index 730cc4a..8ba9aee 100644 --- a/src/Config/ConfigLevel.cs +++ b/src/Config/ConfigLevel.cs @@ -5,6 +5,11 @@ namespace DotNetConfig /// /// Specifies the level of configuration to use. /// + /// + /// If not provided, the default .netconfig location will be used, + /// which is typically the current directory unless building configuration + /// from a specific file. + /// public enum ConfigLevel { ///