mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Show default values in help (#54)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -38,5 +38,7 @@ namespace CliFx.Internal
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static bool OverridesToStringMethod(this object obj) => obj?.ToString() != obj?.GetType().ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user