From f9c9eacfdedf27d6184628652b88096dea86c3bd Mon Sep 17 00:00:00 2001 From: "Per Christian B. Viken" Date: Fri, 19 Sep 2025 21:44:25 +0200 Subject: [PATCH 1/2] refactor(tool): Update System.CommandLine --- CHANGELOG.md | 1 + TypeContractor.Tool/Generator.cs | 2 +- TypeContractor.Tool/Program.cs | 177 ++++++++++++------ .../TypeContractor.Tool.csproj | 4 +- .../vendor/CommandLineExtensions.cs | 98 +++++----- 5 files changed, 168 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe1db9..2750f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Generate Zod v4 schemas (#168) - Use `globalThis.Response` instead of `Response` to avoid conflicts (#147) - Set kebab-case as the default casing (#166) +- Update System.CommandLine to latest release candidate (#163) ## [0.17.4.1] - 2025-09-19 diff --git a/TypeContractor.Tool/Generator.cs b/TypeContractor.Tool/Generator.cs index 586deca..b8b68b3 100644 --- a/TypeContractor.Tool/Generator.cs +++ b/TypeContractor.Tool/Generator.cs @@ -51,7 +51,7 @@ public Generator(string assemblyPath, _casing = casing; } - public Task Execute() + public Task Execute(CancellationToken cancellationToken) { var returnCode = 0; diff --git a/TypeContractor.Tool/Program.cs b/TypeContractor.Tool/Program.cs index 7535040..d60383c 100644 --- a/TypeContractor.Tool/Program.cs +++ b/TypeContractor.Tool/Program.cs @@ -1,6 +1,5 @@ using DotNetConfig; using System.CommandLine; -using System.CommandLine.Parsing; using TypeContractor; using TypeContractor.Logger; using TypeContractor.Tool; @@ -8,54 +7,117 @@ var config = Config.Build("typecontractor.config"); var rootCommand = new RootCommand("Tool for generating TypeScript definitions from C# code"); -var assemblyOption = new Option("--assembly", "Path to the assembly to start with. Will be relative to the current directory"); -var outputOption = new Option("--output", "Output path to write to. Will be relative to the current directory"); -var relativeRootOption = new Option("--root", "Relative root for generating cleaner imports. For example '~/api'"); -var cleanOption = new Option("--clean", () => CleanMethod.Smart, "Choose how to clean up no longer relevant type files in output directory. Danger!"); -var replaceOptions = new Option("--replace", "Provide one replacement in the form ':'. Can be repeated"); -var stripOptions = new Option("--strip", "Provide a prefix to strip out of types. Can be repeated"); -var mapOptions = new Option("--custom-map", "Provide a custom type map in the form ':'. Can be repeated"); -var packsOptions = new Option("--packs-path", () => @"C:\Program Files\dotnet\packs\", "Path where dotnet is installed and reference assemblies can be found."); -var dotnetVersionOptions = new Option("--dotnet-version", () => 8, "Major version of dotnet to look for"); -var logLevelOptions = new Option("--log-level", () => LogLevel.Info); -var buildZodSchemasOptions = new Option("--build-zod-schemas", () => false, "Enable experimental support for Zod schemas alongside generated types."); -var generateApiClientsOptions = new Option("--generate-api-clients", () => false, "Enable experimental support for auto-generating API clients for each endpoint."); -var apiClientsTemplateOptions = new Option("--api-client-template", () => "aurelia", "Template to use for API clients. Either 'aurelia', 'react-axios' (built-in) or a path to a Handlebars file, including extension"); -var casingOptions = new Option("--casing", () => Casing.Kebab, "Casing to use for generated file names"); -assemblyOption.IsRequired = true; -outputOption.IsRequired = true; - -rootCommand.AddOption(assemblyOption); -rootCommand.AddOption(outputOption); -rootCommand.AddOption(relativeRootOption); -rootCommand.AddOption(cleanOption); -rootCommand.AddOption(replaceOptions); -rootCommand.AddOption(stripOptions); -rootCommand.AddOption(mapOptions); -rootCommand.AddOption(packsOptions); -rootCommand.AddOption(dotnetVersionOptions); -rootCommand.AddOption(logLevelOptions); -rootCommand.AddOption(buildZodSchemasOptions); -rootCommand.AddOption(generateApiClientsOptions); -rootCommand.AddOption(apiClientsTemplateOptions); -rootCommand.AddOption(casingOptions); - -apiClientsTemplateOptions.AddValidator(result => -{ - var value = result.GetValueForOption(apiClientsTemplateOptions)!; + +var assemblyOption = new Option("--assembly") +{ + Description = "Path to the assembly to start with. Will be relative to the current directory", + Required = true, +}; + +var outputOption = new Option("--output") +{ + Description = "Output path to write to. Will be relative to the current directory", + Required = true, +}; + +var relativeRootOption = new Option("--root") +{ + Description = "Relative root for generating cleaner imports. For example '~/api'", +}; + +var cleanOption = new Option("--clean") +{ + DefaultValueFactory = (arg) => CleanMethod.Smart, + Description = "Choose how to clean up no longer relevant type files in output directory. Danger!", +}; + +var replaceOptions = new Option("--replace") +{ + Description = "Provide one replacement in the form ':'. Can be repeated", +}; + +var stripOptions = new Option("--strip") +{ + Description = "Provide a prefix to strip out of types. Can be repeated", +}; + +var mapOptions = new Option("--custom-map") +{ + Description = "Provide a custom type map in the form ':'. Can be repeated", +}; + +var packsOptions = new Option("--packs-path") +{ + DefaultValueFactory = (arg) => @"C:\Program Files\dotnet\packs\", + Description = "Path where dotnet is installed and reference assemblies can be found.", +}; + +var dotnetVersionOptions = new Option("--dotnet-version") +{ + DefaultValueFactory = (arg) => 8, + Description = "Major version of dotnet to look for", +}; + +var logLevelOptions = new Option("--log-level") +{ + DefaultValueFactory = (arg) => LogLevel.Info, +}; + +var buildZodSchemasOptions = new Option("--build-zod-schemas") +{ + DefaultValueFactory = (arg) => false, + Description = "Enable experimental support for Zod schemas alongside generated types.", +}; + +var generateApiClientsOptions = new Option("--generate-api-clients") +{ + DefaultValueFactory = (arg) => false, + Description = "Enable experimental support for auto-generating API clients for each endpoint.", +}; + +var apiClientsTemplateOptions = new Option("--api-client-template") +{ + DefaultValueFactory = (arg) => "aurelia", + Description = "Template to use for API clients. Either 'aurelia', 'react-axios' (built-in) or a path to a Handlebars file, including extension", +}; + +var casingOptions = new Option("--casing") +{ + DefaultValueFactory = (arg) => Casing.Kebab, + Description = "Casing to use for generated file names", +}; + +rootCommand.Options.Add(assemblyOption); +rootCommand.Options.Add(outputOption); +rootCommand.Options.Add(relativeRootOption); +rootCommand.Options.Add(cleanOption); +rootCommand.Options.Add(replaceOptions); +rootCommand.Options.Add(stripOptions); +rootCommand.Options.Add(mapOptions); +rootCommand.Options.Add(packsOptions); +rootCommand.Options.Add(dotnetVersionOptions); +rootCommand.Options.Add(logLevelOptions); +rootCommand.Options.Add(buildZodSchemasOptions); +rootCommand.Options.Add(generateApiClientsOptions); +rootCommand.Options.Add(apiClientsTemplateOptions); +rootCommand.Options.Add(casingOptions); + +apiClientsTemplateOptions.Validators.Add(result => +{ + var value = result.GetValue(apiClientsTemplateOptions)!; if (value.Equals("aurelia", StringComparison.CurrentCultureIgnoreCase) || value.Equals("react-axios", StringComparison.CurrentCultureIgnoreCase)) return; - var generateClients = result.GetValueForOption(generateApiClientsOptions); + var generateClients = result.GetValue(generateApiClientsOptions); if (!generateClients) { - result.ErrorMessage = $"Must generate API clients for --{apiClientsTemplateOptions.Name} to have any effect."; + result.AddError($"Must generate API clients for --{apiClientsTemplateOptions.Name} to have any effect."); return; } if (!File.Exists(value)) { - result.ErrorMessage = $"The template specified does not exist or is not readable. Searched for {Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), value))}."; + result.AddError($"The template specified does not exist or is not readable. Searched for {Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), value))}."); return; } }); @@ -63,22 +125,22 @@ // Apply configuration from file, if any rootCommand = rootCommand.WithConfigurableDefaults("typecontractor", config); -rootCommand.SetHandler(async (context) => -{ - var assemblyOptionValue = context.ParseResult.GetValueForOption(assemblyOption)!; - var outputValue = context.ParseResult.GetValueForOption(outputOption)!; - var relativeRootValue = context.ParseResult.GetValueForOption(relativeRootOption); - var cleanValue = context.ParseResult.GetValueForOption(cleanOption); - var replacementsValue = context.ParseResult.GetValueForOption(replaceOptions) ?? []; - var stripValue = context.ParseResult.GetValueForOption(stripOptions) ?? []; - var customMapsValue = context.ParseResult.GetValueForOption(mapOptions) ?? []; - var packsPathValue = context.ParseResult.GetValueForOption(packsOptions)!; - var dotnetVersionValue = context.ParseResult.GetValueForOption(dotnetVersionOptions); - var logLevelValue = context.ParseResult.GetValueForOption(logLevelOptions); - var buildZodSchemasValue = context.ParseResult.GetValueForOption(buildZodSchemasOptions); - var generateApiClientsValue = context.ParseResult.GetValueForOption(generateApiClientsOptions); - var apiClientsTemplateValue = context.ParseResult.GetValueForOption(apiClientsTemplateOptions)!; - var casingValue = context.ParseResult.GetValueForOption(casingOptions); +rootCommand.SetAction(async (parseResult, cancellationToken) => +{ + var assemblyOptionValue = parseResult.GetValue(assemblyOption)!; + var outputValue = parseResult.GetValue(outputOption)!; + var relativeRootValue = parseResult.GetValue(relativeRootOption); + var cleanValue = parseResult.GetValue(cleanOption); + var replacementsValue = parseResult.GetValue(replaceOptions) ?? []; + var stripValue = parseResult.GetValue(stripOptions) ?? []; + var customMapsValue = parseResult.GetValue(mapOptions) ?? []; + var packsPathValue = parseResult.GetValue(packsOptions)!; + var dotnetVersionValue = parseResult.GetValue(dotnetVersionOptions); + var logLevelValue = parseResult.GetValue(logLevelOptions); + var buildZodSchemasValue = parseResult.GetValue(buildZodSchemasOptions); + var generateApiClientsValue = parseResult.GetValue(generateApiClientsOptions); + var apiClientsTemplateValue = parseResult.GetValue(apiClientsTemplateOptions)!; + var casingValue = parseResult.GetValue(casingOptions); Log.Instance = new ConsoleLogger(logLevelValue); var generator = new Generator(assemblyOptionValue, @@ -95,7 +157,8 @@ apiClientsTemplateValue, casingValue); - context.ExitCode = await generator.Execute(); + return await generator.Execute(cancellationToken); }); -return await rootCommand.InvokeAsync(args); +var parsedResult = rootCommand.Parse(args); +return await parsedResult.InvokeAsync(); diff --git a/TypeContractor.Tool/TypeContractor.Tool.csproj b/TypeContractor.Tool/TypeContractor.Tool.csproj index fbb7883..32fb2da 100644 --- a/TypeContractor.Tool/TypeContractor.Tool.csproj +++ b/TypeContractor.Tool/TypeContractor.Tool.csproj @@ -1,4 +1,4 @@ - + Exe @@ -26,7 +26,7 @@ - + diff --git a/TypeContractor.Tool/vendor/CommandLineExtensions.cs b/TypeContractor.Tool/vendor/CommandLineExtensions.cs index be8f797..f497ced 100644 --- a/TypeContractor.Tool/vendor/CommandLineExtensions.cs +++ b/TypeContractor.Tool/vendor/CommandLineExtensions.cs @@ -1,6 +1,8 @@ using DotNetConfig; using HandlebarsDotNet; using System.Collections.Concurrent; +using System.CommandLine.Parsing; +using System.Reflection; namespace System.CommandLine { @@ -192,6 +194,7 @@ public DefaultValueFactory(Argument argument, Config config, string section) public void SetDefaultValue() { _orderedSubsections = [.. _subsections.OrderByDescending(x => x?.Length ?? 0)]; + var field = _argument.GetType().GetField("_defaultValueFactory", BindingFlags.Instance | BindingFlags.NonPublic)!; if (_argument.Arity.MaximumNumberOfValues > 1) { @@ -203,19 +206,19 @@ public void SetDefaultValue() switch (elementType) { case Type type when type.IsAssignableFrom(typeof(string)): - SetDefaultStrings(); + SetDefaultStrings(field); break; case Type type when type.IsAssignableFrom(typeof(long)): - SetDefaultNumbers(); + SetDefaultNumbers(field); break; case Type type when type.IsAssignableFrom(typeof(int)): - SetDefaultIntegers(); + SetDefaultIntegers(field); break; case Type type when type.IsAssignableFrom(typeof(bool)): - SetDefaultBooleans(); + SetDefaultBooleans(field); break; case Type type when type.IsAssignableFrom(typeof(DateTime)): - SetDefaultDateTimes(); + SetDefaultDateTimes(field); break; default: break; @@ -226,16 +229,16 @@ public void SetDefaultValue() switch (_argument.ValueType) { case Type type when type.IsAssignableFrom(typeof(string)): - SetDefaultString(); + SetDefaultString(field); break; case Type type when type.IsAssignableFrom(typeof(long)) || type.IsAssignableFrom(typeof(int)): - SetDefaultNumber(); + SetDefaultNumber(field); break; case Type type when type.IsAssignableFrom(typeof(bool)): - SetDefaultBoolean(); + SetDefaultBoolean(field); break; case Type type when type.IsAssignableFrom(typeof(DateTime)): - SetDefaultDateTime(); + SetDefaultDateTime(field); break; default: break; @@ -243,85 +246,70 @@ public void SetDefaultValue() } } - void SetDefaultStrings() + void SetDefaultStrings(FieldInfo field) { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { var values = new List(); foreach (var subsection in _orderedSubsections) values.AddRange(_config.GetAll(_section, subsection, _argument.Name).Select(x => x.GetString())); - if (_argument.ValueType.IsArray) - return values.ToArray(); - - return values; + return values.ToArray(); }); } - void SetDefaultNumbers() + void SetDefaultNumbers(FieldInfo field) { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { var values = new List(); foreach (var subsection in _orderedSubsections) values.AddRange(_config.GetAll(_section, subsection, _argument.Name).Select(x => x.GetNumber())); - if (_argument.ValueType.IsArray) - return values.ToArray(); - - return values; + return values.ToArray(); }); } - void SetDefaultIntegers() + void SetDefaultIntegers(FieldInfo field) { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { var values = new List(); foreach (var subsection in _orderedSubsections) values.AddRange(_config.GetAll(_section, subsection, _argument.Name).Select(x => (int)x.GetNumber())); - if (_argument.ValueType.IsArray) - return values.ToArray(); - - return values; + return values.ToArray(); }); } - void SetDefaultBooleans() + void SetDefaultBooleans(FieldInfo field) { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { var values = new List(); foreach (var subsection in _orderedSubsections) values.AddRange(_config.GetAll(_section, subsection, _argument.Name).Select(x => x.GetBoolean())); - if (_argument.ValueType.IsArray) - return values.ToArray(); - - return values; + return values.ToArray(); }); } - void SetDefaultDateTimes() + void SetDefaultDateTimes(FieldInfo field) { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { var values = new List(); foreach (var subsection in _orderedSubsections) values.AddRange(_config.GetAll(_section, subsection, _argument.Name).Select(x => x.GetDateTime())); - if (_argument.ValueType.IsArray) - return values.ToArray(); - - return values; + return values.ToArray(); }); } - void SetDefaultString() + void SetDefaultString(FieldInfo field) { - var originalDefault = _argument.HasDefaultValue ? _argument.GetDefaultValue() : null; - _argument.SetDefaultValueFactory(() => + string? originalDefault = _argument.HasDefaultValue ? (string?)_argument.GetDefaultValue() : null; + field.SetValue(_argument, (ArgumentResult arg) => { foreach (var subsection in _orderedSubsections) { @@ -332,58 +320,60 @@ void SetDefaultString() }); } - void SetDefaultNumber() + void SetDefaultNumber(FieldInfo field) { - var originalDefault = _argument.HasDefaultValue ? _argument.GetDefaultValue() : null; + int? originalDefault = _argument.HasDefaultValue ? (int?)_argument.GetDefaultValue() : null; if (_argument.ValueType == typeof(int)) { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { foreach (var subsection in _orderedSubsections) { if (_config.TryGetNumber(_section, subsection, _argument.Name, out var value)) return (int)value; } - return originalDefault ?? default; + return originalDefault ?? 0; }); } else { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { foreach (var subsection in _orderedSubsections) { if (_config.TryGetNumber(_section, subsection, _argument.Name, out var value)) return value; } - return originalDefault ?? default; + return (long?)originalDefault ?? 0L; }); } } - void SetDefaultBoolean() + void SetDefaultBoolean(FieldInfo field) { - var originalDefault = _argument.HasDefaultValue ? _argument.GetDefaultValue() : null; - _argument.SetDefaultValueFactory(() => + bool? originalDefault = _argument.HasDefaultValue ? (bool?)_argument.GetDefaultValue() : null; + field.SetValue(_argument, (ArgumentResult arg) => { foreach (var subsection in _orderedSubsections) { if (_config.TryGetBoolean(_section, subsection, _argument.Name, out var value)) return value; } - return originalDefault ?? default; + + return originalDefault ?? false; }); } - void SetDefaultDateTime() + void SetDefaultDateTime(FieldInfo field) { - _argument.SetDefaultValueFactory(() => + field.SetValue(_argument, (ArgumentResult arg) => { foreach (var subsection in _orderedSubsections) { if (_config.TryGetDateTime(_section, subsection, _argument.Name, out var value)) return value; } + return default; }); } From 3256f679230d01b60de034d06b3287e0bfe769dc Mon Sep 17 00:00:00 2001 From: "Per Christian B. Viken" Date: Fri, 3 Oct 2025 20:16:15 +0200 Subject: [PATCH 2/2] refactor(tool): Simplify reading configuration --- TypeContractor.Tool/Program.cs | 26 +- .../vendor/CommandLineExtensions.cs | 382 ------------------ .../vendor/ConfigurationExtensions.cs | 49 +++ 3 files changed, 64 insertions(+), 393 deletions(-) delete mode 100644 TypeContractor.Tool/vendor/CommandLineExtensions.cs create mode 100644 TypeContractor.Tool/vendor/ConfigurationExtensions.cs diff --git a/TypeContractor.Tool/Program.cs b/TypeContractor.Tool/Program.cs index d60383c..76a5694 100644 --- a/TypeContractor.Tool/Program.cs +++ b/TypeContractor.Tool/Program.cs @@ -3,6 +3,7 @@ using TypeContractor; using TypeContractor.Logger; using TypeContractor.Tool; +using TypeContractor.Tool.vendor; var config = Config.Build("typecontractor.config"); @@ -10,80 +11,86 @@ var assemblyOption = new Option("--assembly") { + DefaultValueFactory = (arg) => config.TryGetString("assembly") ?? "", Description = "Path to the assembly to start with. Will be relative to the current directory", Required = true, }; var outputOption = new Option("--output") { + DefaultValueFactory = (arg) => config.TryGetString("output") ?? "", Description = "Output path to write to. Will be relative to the current directory", Required = true, }; var relativeRootOption = new Option("--root") { + DefaultValueFactory = (arg) => config.TryGetString("root") ?? "", Description = "Relative root for generating cleaner imports. For example '~/api'", }; var cleanOption = new Option("--clean") { - DefaultValueFactory = (arg) => CleanMethod.Smart, + DefaultValueFactory = (arg) => config.GetEnum("clean", CleanMethod.Smart), Description = "Choose how to clean up no longer relevant type files in output directory. Danger!", }; var replaceOptions = new Option("--replace") { + DefaultValueFactory = (arg) => config.GetStrings("replace"), Description = "Provide one replacement in the form ':'. Can be repeated", }; var stripOptions = new Option("--strip") { + DefaultValueFactory = (arg) => config.GetStrings("strip"), Description = "Provide a prefix to strip out of types. Can be repeated", }; var mapOptions = new Option("--custom-map") { + DefaultValueFactory = (arg) => config.GetStrings("custom-map"), Description = "Provide a custom type map in the form ':'. Can be repeated", }; var packsOptions = new Option("--packs-path") { - DefaultValueFactory = (arg) => @"C:\Program Files\dotnet\packs\", + DefaultValueFactory = (arg) => config.GetStringWithFallback("packs-path", @"C:\Program Files\dotnet\packs\"), Description = "Path where dotnet is installed and reference assemblies can be found.", }; var dotnetVersionOptions = new Option("--dotnet-version") { - DefaultValueFactory = (arg) => 8, + DefaultValueFactory = (arg) => config.GetNumberWithFallback("dotnet-version", 8), Description = "Major version of dotnet to look for", }; var logLevelOptions = new Option("--log-level") { - DefaultValueFactory = (arg) => LogLevel.Info, + DefaultValueFactory = (arg) => config.GetEnum("log-level", LogLevel.Info), }; var buildZodSchemasOptions = new Option("--build-zod-schemas") { - DefaultValueFactory = (arg) => false, + DefaultValueFactory = (arg) => config.GetBoolean("build-zod-schemas", false), Description = "Enable experimental support for Zod schemas alongside generated types.", }; var generateApiClientsOptions = new Option("--generate-api-clients") { - DefaultValueFactory = (arg) => false, + DefaultValueFactory = (arg) => config.GetBoolean("generate-api-clients", false), Description = "Enable experimental support for auto-generating API clients for each endpoint.", }; var apiClientsTemplateOptions = new Option("--api-client-template") { - DefaultValueFactory = (arg) => "aurelia", + DefaultValueFactory = (arg) => config.GetStringWithFallback("api-client-template", "aurelia"), Description = "Template to use for API clients. Either 'aurelia', 'react-axios' (built-in) or a path to a Handlebars file, including extension", }; var casingOptions = new Option("--casing") { - DefaultValueFactory = (arg) => Casing.Kebab, + DefaultValueFactory = (arg) => config.GetEnum("casing", Casing.Kebab), Description = "Casing to use for generated file names", }; @@ -122,9 +129,6 @@ } }); -// Apply configuration from file, if any -rootCommand = rootCommand.WithConfigurableDefaults("typecontractor", config); - rootCommand.SetAction(async (parseResult, cancellationToken) => { var assemblyOptionValue = parseResult.GetValue(assemblyOption)!; diff --git a/TypeContractor.Tool/vendor/CommandLineExtensions.cs b/TypeContractor.Tool/vendor/CommandLineExtensions.cs deleted file mode 100644 index f497ced..0000000 --- a/TypeContractor.Tool/vendor/CommandLineExtensions.cs +++ /dev/null @@ -1,382 +0,0 @@ -using DotNetConfig; -using HandlebarsDotNet; -using System.Collections.Concurrent; -using System.CommandLine.Parsing; -using System.Reflection; - -namespace System.CommandLine -{ - /// - /// Extension methods to automatically read default values for arguments and - /// options from .netconfig. - /// - /// - /// After invoking on a command, all its - /// arguments and options in the entire command tree are processed according to - /// these heuristics: - /// - /// - /// - /// Only arguments/options without a default value are processed - /// - /// - /// - /// - /// Section matches root command name, subsection (dot - separated) for each additional nested - /// command level(i.e. `[mytool "mycommand.myverb"]`) - /// - /// - /// - /// Compatible arguments/options(same name/type) can be placed in ancestor section/subsection to affect - /// default value of entire subtree - /// - /// - /// - /// All the types supported by System.CommandLine for multiple artity arguments and options are - /// automatically populated: arrays, `IEnumerable{T}`, `ICollection{T}`, `IList{T}` and `List{T}`: - /// .netconfig can provide multi-valued variables for those - /// - /// - /// - /// - /// Numbers can be either `int` or `long`. - /// - /// - /// - /// - public static class CommandLineExtensions - { - static readonly HashSet _configurableTypes = new( - [ - typeof(string), - typeof(long), - typeof(long?), - typeof(int), - typeof(int?), - typeof(bool), - typeof(bool?), - typeof(DateTime), - typeof(DateTime?), - ]); - - /// - /// Register default value factories for all arguments and options in the command tree - /// which don't have a default value already. - /// - /// Type of command, inferred from usage. - /// The command to set default values for. - /// Optional root section for this command arguments and its tree. If not provided, the 's Name - /// property will be used. For a , this is the app's assembly name. - /// Optional pre-built configuration. If not provided, will - /// be used to build a default configuration. - /// The command where defaults have been applied. - public static T WithConfigurableDefaults(this T command, string? section = default, Config? configuration = default) where T : Command - { - configuration ??= Config.Build(); - section ??= command.Name; - - var arguments = new HashSet(); - CollectArguments(command, arguments); - - var sections = new ConcurrentDictionary<(string? subsection, string variable), HashSet>(); - PopulateSections(command, sections, arguments, null); - - // Clear sections with multiple variables but with incompatible types - foreach (var entry in sections.Where(x => x.Value.GroupBy(arg => arg.ValueType).Skip(1).Any()).ToArray()) - sections.TryRemove(entry.Key, out _); - - // Remaining sections are all the final arguments for defaulting. - var factories = new ConcurrentDictionary(); - foreach (var sharedSection in sections) - { - foreach (var argument in sharedSection.Value) - { - factories.GetOrAdd(argument, arg => new DefaultValueFactory(arg, configuration, section)) - .AddSubsection(sharedSection.Key.subsection); - } - } - - foreach (var factory in factories.Values) - factory.SetDefaultValue(); - - return command; - } - - static void PopulateSections(Command command, - ConcurrentDictionary<(string? subsection, string variable), HashSet> sections, - HashSet arguments, - string? subsection = null) - { - void Add(IEnumerable args) - { - foreach (var arg in args) - { - if (subsection != null) - { - var parts = subsection.Split('.'); - for (var i = 1; i <= parts.Length; i++) - sections.GetOrAdd((string.Join(".", parts[0..i]), arg.Name), _ => []).Add(arg); - - sections.GetOrAdd((null, arg.Name), _ => []).Add(arg); - } - else - { - sections.GetOrAdd((subsection, arg.Name), _ => []).Add(arg); - } - } - } - - Add(command.Arguments.Where(arguments.Contains)); - Add(command.Options.OfType