Show default values in help (#54)

This commit is contained in:
Domn Werner
2020-05-16 04:11:23 -07:00
committed by GitHub
parent 19b87717c1
commit 4cef596fe8
6 changed files with 284 additions and 25 deletions

View File

@@ -1,4 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
@@ -110,5 +114,113 @@ namespace CliFx.Tests
public ValueTask ExecuteAsync(IConsole console) => default;
}
[Command("cmd-with-defaults")]
private class DefaultArgumentsCommand : ICommand
{
[CommandOption(nameof(Object))]
public object? Object { get; set; } = 42;
[CommandOption(nameof(String))]
public string? String { get; set; } = "foo";
[CommandOption(nameof(EmptyString))]
public string EmptyString { get; set; } = "";
[CommandOption(nameof(WhiteSpaceString))]
public string WhiteSpaceString { get; set; } = " ";
[CommandOption(nameof(Bool))]
public bool Bool { get; set; } = true;
[CommandOption(nameof(Char))]
public char Char { get; set; } = 't';
[CommandOption(nameof(Sbyte))]
public sbyte Sbyte { get; set; } = -0b11;
[CommandOption(nameof(Byte))]
public byte Byte { get; set; } = 0b11;
[CommandOption(nameof(Short))]
public short Short { get; set; } = -1234;
[CommandOption(nameof(Ushort))]
public short Ushort { get; set; } = 1234;
[CommandOption(nameof(Int))]
public int Int { get; set; } = 1337;
[CommandOption(nameof(Uint))]
public uint Uint { get; set; } = 2345;
[CommandOption(nameof(Long))]
public long Long { get; set; } = -1234567;
[CommandOption(nameof(Ulong))]
public ulong Ulong { get; set; } = 12345678;
[CommandOption(nameof(Float))]
public float Float { get; set; } = 123.4567F;
[CommandOption(nameof(Double))]
public double Double { get; set; } = 420.1337;
[CommandOption(nameof(Decimal))]
public decimal Decimal { get; set; } = 1337.420M;
[CommandOption(nameof(DateTime))]
public DateTime DateTime { get; set; } = new DateTime(2020, 4, 20);
[CommandOption(nameof(DateTimeOffset))]
public DateTimeOffset DateTimeOffset { get; set; } =
new DateTimeOffset(2008, 5, 1, 0, 0, 0, new TimeSpan(0, 1, 0, 0, 0));
[CommandOption(nameof(TimeSpan))]
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
public enum TestEnum { Value1, Value2, Value3 };
[CommandOption(nameof(CustomEnum))]
public TestEnum CustomEnum { get; set; } = TestEnum.Value2;
[CommandOption(nameof(IntNullable))]
public int? IntNullable { get; set; } = 1337;
[CommandOption(nameof(CustomEnumNullable))]
public TestEnum? CustomEnumNullable { get; set; } = TestEnum.Value2;
[CommandOption(nameof(TimeSpanNullable))]
public TimeSpan? TimeSpanNullable { get; set; } = TimeSpan.FromMinutes(234);
[CommandOption(nameof(ObjectArray))]
public object[]? ObjectArray { get; set; } = new object[] { "123", 4, 3.14 };
[CommandOption(nameof(StringArray))]
public string[]? StringArray { get; set; } = new[] { "foo", "bar", "baz" };
[CommandOption(nameof(IntArray))]
public int[]? IntArray { get; set; } = new[] { 1, 2, 3 };
[CommandOption(nameof(CustomEnumArray))]
public TestEnum[]? CustomEnumArray { get; set; } = new[] { TestEnum.Value1, TestEnum.Value3 };
[CommandOption(nameof(IntNullableArray))]
public int?[]? IntNullableArray { get; set; } = new int?[] { 2, 3, 4, null, 5 };
[CommandOption(nameof(EnumerableNullable))]
public IEnumerable? EnumerableNullable { get; set; } = Enumerable.Repeat("foo", 3);
[CommandOption(nameof(StringEnumerable))]
public IEnumerable<string>? StringEnumerable { get; set; } = Enumerable.Repeat("bar", 3);
[CommandOption(nameof(StringReadOnlyList))]
public IReadOnlyList<string>? StringReadOnlyList { get; set; } = new[] { "foo", "bar", "baz" };
[CommandOption(nameof(StringList))]
public List<string>? StringList { get; set; } = new List<string>() { "foo", "bar", "baz" };
public ValueTask ExecuteAsync(IConsole console) => default;
}
}
}

View File

@@ -1,4 +1,6 @@
using System.IO;
using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
@@ -267,7 +269,7 @@ namespace CliFx.Tests
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_lists_environment_variable_names_for_options_that_have_them_defined()
{
@@ -293,5 +295,63 @@ namespace CliFx.Tests
_output.WriteLine(stdOutData);
}
[Fact]
public async Task Help_text_shows_usage_format_which_lists_all_default_values_for_non_required_options()
{
// Arrange
await using var stdOut = new MemoryStream();
var console = new VirtualConsole(output: stdOut);
var application = new CliApplicationBuilder()
.AddCommand(typeof(DefaultArgumentsCommand))
.UseConsole(console)
.Build();
// Act
await application.RunAsync(new[] { "cmd-with-defaults", "--help" });
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
// Assert
stdOutData.Should().ContainAll(
"Usage",
"cmd-with-defaults", "[options]",
"Options",
"--Object", "(Default: 42)",
"--String", "(Default: foo)",
"--EmptyString", "(Default: \"\"",
"--WhiteSpaceString", "(Default: \" \"",
"--Bool", "(Default: True)",
"--Char", "(Default: t)",
"--Sbyte", "(Default: -3)",
"--Byte", "(Default: 3)",
"--Short", "(Default: -1234)",
"--Ushort", "(Default: 1234)",
"--Int", "(Default: 1337)",
"--Uint", "(Default: 2345)",
"--Long", "(Default: -1234567)",
"--Ulong", "(Default: 12345678)",
"--Float", "(Default: 123.4567)",
"--Double", "(Default: 420.1337)",
"--Decimal", "(Default: 1337.420)",
"--DateTime", $"(Default: {new DateTime(2020, 4, 20)}",
"--DateTimeOffset", $"(Default: {new DateTimeOffset(2008, 5, 1, 0, 0, 0, new TimeSpan(0, 1, 0, 0, 0))}",
"--TimeSpan", "(Default: 02:03:00)",
"--IntNullable", "(Default: 1337)",
"--CustomEnumNullable", "(Default: Value2)",
"--TimeSpanNullable", "(Default: 03:54:00)",
"--ObjectArray", "(Default: 123 4 3.14)",
"--StringArray", "(Default: foo bar baz)",
"--IntArray", "(Default: 1 2 3)",
"--CustomEnumArray", "(Default: Value1 Value3)",
"--IntNullableArray", "(Default: 2 3 4 5)",
"--EnumerableNullable", "(Default: foo foo foo)",
"--StringEnumerable", "(Default: bar bar bar)",
"--StringReadOnlyList", "(Default: foo bar baz)",
"--StringList", "(Default: foo bar baz)"
);
_output.WriteLine(stdOutData);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -116,24 +117,6 @@ namespace CliFx.Domain
public void Inject(ICommand command, params string[] values) =>
Inject(command, (IReadOnlyList<string>) values);
public IReadOnlyList<string> GetValidValues()
{
var result = new List<string>();
// Some arguments may have this as null due to a hack that enables built-in options
if (Property == null)
return result;
var underlyingPropertyType =
Property.PropertyType.GetNullableUnderlyingType() ?? Property.PropertyType;
// Enum
if (underlyingPropertyType.IsEnum)
result.AddRange(Enum.GetNames(underlyingPropertyType));
return result;
}
}
internal partial class CommandArgumentSchema
@@ -176,4 +159,83 @@ namespace CliFx.Domain
BindingFlags.Public | BindingFlags.Static,
null, new[] {typeof(string), typeof(IFormatProvider)}, null);
}
// Default and valid value handling.
internal partial class CommandArgumentSchema
{
/// <summary>
/// Retrieves the valid values of this command argument.
/// </summary>
/// <returns>A string collection of this command's valid values.</returns>
public IReadOnlyList<string> GetValidValues()
{
var result = new List<string>();
// Some arguments may have this as null due to a hack that enables built-in options
if (Property == null)
return result;
var underlyingPropertyType =
Property.PropertyType.GetNullableUnderlyingType() ?? Property.PropertyType;
// Enum
if (underlyingPropertyType.IsEnum)
result.AddRange(Enum.GetNames(underlyingPropertyType));
return result;
}
/// <summary>
/// Gets the default value of this command argument.
/// Returns null if there's no default value.
/// </summary>
/// <param name="instance">A dummy instance of the command
/// this command argument belongs to.</param>
/// <returns>The string representation of the default value.
/// If there's no default value, it returns null.</returns>
/// <remarks>
/// We need a dummy instance in order to implement this because
/// we cannot retrieve it from a PropertyInfo.
/// </remarks>
public string? GetDefaultValue(ICommand? instance)
{
if (Property is null || instance is null)
{
return null;
}
var propertyName = Property?.Name;
string? defaultValue = null;
// Get the current culture so that the default value string
// matches the user's culture for cultured information like
// DateTimes and TimeSpans.
var culture = CultureInfo.CurrentCulture;
if (!string.IsNullOrWhiteSpace(propertyName))
{
var instanceProperty = instance.GetType().GetProperty(propertyName);
var value = instanceProperty.GetValue(instance);
if (value.OverridesToStringMethod())
{
// Wrap empty or whitespace strings in quotes so that they're not
// just an ugly blank in the output.
defaultValue = value.ToCulturedString(culture)
.WrapWithQuotesIfEmptyOrWhiteSpace();
}
else if (value is IEnumerable values)
{
// Cast 'values' to IEnumerable<object> so we can use LINQ on it.
defaultValue =
string.Join(" ",
values.Cast<object>()
.Where(v => v != null)
.Select(v => v.ToCulturedString(culture)
.WrapWithQuotesIfEmptyOrWhiteSpace()));
}
}
return defaultValue;
}
}
}

View File

@@ -219,6 +219,9 @@ namespace CliFx.Domain
RenderMargin();
RenderHeader("Options");
// Instantiate a temporary instance of the command so we can get default values from it.
ICommand? tempInstance = command.Type is null ? null : Activator.CreateInstance(command.Type) as ICommand;
var options = command.Options
.OrderByDescending(o => o.IsRequired)
.Concat(command.GetBuiltInOptions())
@@ -270,12 +273,23 @@ namespace CliFx.Domain
Render(" ");
}
// TODO: Render default value here.
// Environment variable
if (!string.IsNullOrWhiteSpace(option.EnvironmentVariableName))
{
Render($"Environment variable: {option.EnvironmentVariableName}");
Render($"(Environment variable: {option.EnvironmentVariableName})");
Render(" ");
}
// Default value
if (!option.IsRequired)
{
var defaultValue = option.GetDefaultValue(tempInstance);
// If 'defaultValue' is null, it means there's no default value.
if (defaultValue is object)
{
Render($"(Default: {defaultValue})");
Render(" ");
}
}
RenderNewLine();

View File

@@ -1,4 +1,6 @@
using System.Text;
using System;
using System.Globalization;
using System.Text;
namespace CliFx.Internal
{
@@ -10,5 +12,12 @@ namespace CliFx.Internal
public static StringBuilder AppendIfNotEmpty(this StringBuilder builder, char value) =>
builder.Length > 0 ? builder.Append(value) : builder;
public static bool IsEmptyOrWhiteSpace(this string s) => s is object && string.IsNullOrWhiteSpace(s);
public static string WrapWithQuotesIfEmptyOrWhiteSpace(this string s) =>
s.IsEmptyOrWhiteSpace() ? $"\"{s}\"" : s;
public static string ToCulturedString(this object obj, CultureInfo culture) => Convert.ToString(obj, culture);
}
}

View File

@@ -38,5 +38,7 @@ namespace CliFx.Internal
return array;
}
public static bool OverridesToStringMethod(this object obj) => obj?.ToString() != obj?.GetType().ToString();
}
}