diff --git a/CliFx.Tests/HelpTextSpecs.Commands.cs b/CliFx.Tests/HelpTextSpecs.Commands.cs index 4a3198c..620dff0 100644 --- a/CliFx.Tests/HelpTextSpecs.Commands.cs +++ b/CliFx.Tests/HelpTextSpecs.Commands.cs @@ -82,6 +82,20 @@ namespace CliFx.Tests public ValueTask ExecuteAsync(IConsole console) => default; } + [Command("cmd-with-enum-opts")] + private class EnumOptionsCommand : ICommand + { + public enum ValuesEnum { Value1, Value2, Value3 }; + + [CommandOption("value", Description = "Enum option.", IsRequired = true)] + public ValuesEnum Value { get; set; } = ValuesEnum.Value1; + + [CommandOption("nullable-value", Description = "Nullable enum option.")] + public ValuesEnum? NullableValue { get; set; } + + public ValueTask ExecuteAsync(IConsole console) => default; + } + [Command("cmd-with-env-vars")] private class EnvironmentVariableCommand : ICommand { diff --git a/CliFx.Tests/HelpTextSpecs.cs b/CliFx.Tests/HelpTextSpecs.cs index 1766b2e..d0e17db 100644 --- a/CliFx.Tests/HelpTextSpecs.cs +++ b/CliFx.Tests/HelpTextSpecs.cs @@ -238,6 +238,34 @@ namespace CliFx.Tests _output.WriteLine(stdOutData); } + [Fact] + public async Task Help_text_shows_usage_format_which_lists_all_valid_values_for_enum_options() + { + // Arrange + await using var stdOut = new MemoryStream(); + var console = new VirtualConsole(output: stdOut); + + var application = new CliApplicationBuilder() + .AddCommand(typeof(EnumOptionsCommand)) + .UseConsole(console) + .Build(); + + // Act + await application.RunAsync(new[] { "cmd-with-enum-opts", "--help" }); + var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); + + // Assert + stdOutData.Should().ContainAll( + "Usage", + "cmd-with-enum-opts", "[options]", + "Options", + "* --value", "Enum option.", "Valid values: Value1, Value2, Value3.", + "--nullable-value", "Nullable enum option.", "Valid values: Value1, Value2, Value3." + ); + + _output.WriteLine(stdOutData); + } + [Fact] public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined() { diff --git a/CliFx/Domain/CommandArgumentSchema.cs b/CliFx/Domain/CommandArgumentSchema.cs index f0852f9..9b219bf 100644 --- a/CliFx/Domain/CommandArgumentSchema.cs +++ b/CliFx/Domain/CommandArgumentSchema.cs @@ -10,6 +10,7 @@ namespace CliFx.Domain { internal abstract partial class CommandArgumentSchema { + private IReadOnlyList? _validValues; public PropertyInfo Property { get; } public string? Description { get; } @@ -18,12 +19,52 @@ namespace CliFx.Domain public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null; + public IReadOnlyList GetValidValues() => _validValues ?? + (_validValues = EnumerateValidValues().ToList().AsReadOnly()); + protected CommandArgumentSchema(PropertyInfo property, string? description) { Property = property; Description = description; } + private IEnumerable EnumerateValidValues() + { + var propertyType = Property?.PropertyType; + + // Property can actually be null here due to damn it operators + // in CommandOptionSchema lines 103 and 106, so we have to check + // for now. In such case that it is null, let's end early. + if (propertyType is null) + { + yield break; + } + + // If 'propertyType' is nullable, this will return a non-null value. + var underlyingType = propertyType.GetNullableUnderlyingType(); + + // If 'propertyType' is nullable, 'underlying' type will be not null. + if (underlyingType is object) + { + // Handle nullable num. + if (underlyingType.IsEnum) + { + // Reasign so we can do the 'foreach' over the enum values + // only once at the end of the method. + propertyType = underlyingType; + } + } + + // Handle non-nullable enums or nullable enums that were "unwrapped". + if (propertyType.IsEnum) + { + foreach (var value in Enum.GetValues(propertyType)) + { + yield return value.ToString(); + } + } + } + private Type? TryGetEnumerableArgumentUnderlyingType() => Property.PropertyType != typeof(string) ? Property.PropertyType.GetEnumerableUnderlyingType() @@ -115,7 +156,7 @@ namespace CliFx.Domain Property.SetValue(command, Convert(values)); public void Inject(ICommand command, params string[] values) => - Inject(command, (IReadOnlyList) values); + Inject(command, (IReadOnlyList)values); } internal partial class CommandArgumentSchema diff --git a/CliFx/Domain/HelpTextWriter.cs b/CliFx/Domain/HelpTextWriter.cs index f5d77ef..c764ef7 100644 --- a/CliFx/Domain/HelpTextWriter.cs +++ b/CliFx/Domain/HelpTextWriter.cs @@ -246,18 +246,25 @@ namespace CliFx.Domain RenderColumnIndent(); - // Description if (!string.IsNullOrWhiteSpace(option.Description)) { Render(option.Description); Render(" "); } + var validValues = option.GetValidValues(); + if (validValues.Any()) + { + Render($"Valid values: {string.Join(", ", validValues)}."); + Render(" "); + } + + // TODO: Render default value here. + // Environment variable if (!string.IsNullOrWhiteSpace(option.EnvironmentVariableName)) { - Render($"(Environment variable: {option.EnvironmentVariableName})."); - Render(" "); + Render($"(Environment variable: {option.EnvironmentVariableName})"); } RenderNewLine();