Skip to content

Commit

Permalink
Add support for converting command parameters into FileInfo and Direc…
Browse files Browse the repository at this point in the history
…toryInfo (#1145)

Add support for converting command parameters that doesn't have a built-in TypeConverter but has a constructor that takes a string. For CLI apps, FileInfo and DirectoryInfo will likely be the most useful ones, but there may be others.
  • Loading branch information
0xced authored Mar 1, 2023
1 parent 6740f0b commit d3f4f5f
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 11 deletions.
36 changes: 25 additions & 11 deletions src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,26 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso
}
else
{
var converter = GetConverter(lookup, binder, resolver, mapped.Parameter);
var (converter, stringConstructor) = GetConverter(lookup, binder, resolver, mapped.Parameter);
if (converter == null)
{
throw CommandRuntimeException.NoConverterFound(mapped.Parameter);
}

object? value;
var mappedValue = mapped.Value ?? string.Empty;
try
{
value = converter.ConvertFromInvariantString(mapped.Value ?? string.Empty);
try
{
value = converter.ConvertFromInvariantString(mappedValue);
}
catch (NotSupportedException) when (stringConstructor != null)
{
value = stringConstructor.Invoke(new object[] { mappedValue });
}
}
catch (Exception exception)
catch (Exception exception) when (exception is not CommandRuntimeException)
{
throw CommandRuntimeException.ConversionFailed(mapped, converter, exception);
}
Expand Down Expand Up @@ -122,7 +130,7 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso
{
if (result != null && result.GetType() != parameter.ParameterType)
{
var converter = GetConverter(lookup, binder, resolver, parameter);
var (converter, _) = GetConverter(lookup, binder, resolver, parameter);
if (converter != null)
{
result = converter.ConvertFrom(result);
Expand All @@ -133,8 +141,14 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso
}

[SuppressMessage("Style", "IDE0019:Use pattern matching", Justification = "It's OK")]
private static TypeConverter? GetConverter(CommandValueLookup lookup, CommandValueBinder binder, ITypeResolver resolver, CommandParameter parameter)
private static (TypeConverter? Converter, ConstructorInfo? StringConstructor) GetConverter(CommandValueLookup lookup, CommandValueBinder binder, ITypeResolver resolver, CommandParameter parameter)
{
static ConstructorInfo? GetStringConstructor(Type type)
{
var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(string) }, null);
return constructor?.GetParameters()[0].ParameterType == typeof(string) ? constructor : null;
}

if (parameter.Converter == null)
{
if (parameter.ParameterType.IsArray)
Expand All @@ -146,12 +160,12 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso
throw new InvalidOperationException("Could not get element type");
}

return TypeDescriptor.GetConverter(elementType);
return (TypeDescriptor.GetConverter(elementType), GetStringConstructor(elementType));
}

if (parameter.IsFlagValue())
{
// Is the optional value instanciated?
// Is the optional value instantiated?
var value = lookup.GetValue(parameter) as IFlagValue;
if (value == null)
{
Expand All @@ -161,18 +175,18 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso
value = lookup.GetValue(parameter) as IFlagValue;
if (value == null)
{
throw new InvalidOperationException("Could not intialize optional value.");
throw new InvalidOperationException("Could not initialize optional value.");
}
}

// Return a converter for the flag element type.
return TypeDescriptor.GetConverter(value.Type);
return (TypeDescriptor.GetConverter(value.Type), GetStringConstructor(value.Type));
}

return TypeDescriptor.GetConverter(parameter.ParameterType);
return (TypeDescriptor.GetConverter(parameter.ParameterType), GetStringConstructor(parameter.ParameterType));
}

var type = Type.GetType(parameter.Converter.ConverterTypeName);
return resolver.Resolve(type) as TypeConverter;
return (resolver.Resolve(type) as TypeConverter, null);
}
}
8 changes: 8 additions & 0 deletions test/Spectre.Console.Cli.Tests/Data/Settings/HorseSettings.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
using System.IO;

namespace Spectre.Console.Tests.Data;

public class HorseSettings : MammalSettings
{
[CommandOption("-d|--day")]
public DayOfWeek Day { get; set; }

[CommandOption("--file")]
public FileInfo File { get; set; }

[CommandOption("--directory")]
public DirectoryInfo Directory { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
<Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
</Parameters>
</Command>
</Command>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
<Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
<Description>Indicates whether or not the animal is alive.</Description>
</Option>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.IO;

namespace Spectre.Console.Tests.Unit.Cli;

public sealed partial class CommandAppTests
Expand Down Expand Up @@ -76,5 +78,27 @@ public void Should_List_All_Valid_Enum_Values_On_Conversion_Error()
result.Output.ShouldContain(nameof(DayOfWeek.Friday));
result.Output.ShouldContain(nameof(DayOfWeek.Saturday));
}

[Fact]
public void Should_Convert_FileInfo_And_DirectoryInfo()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddCommand<HorseCommand>("horse");
});

// When
var result = app.Run(new[] { "horse", "--file", "ntp.conf", "--directory", "etc" });

// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<HorseSettings>().And(horse =>
{
horse.File.Name.ShouldBe("ntp.conf");
horse.Directory.Name.ShouldBe("etc");
});
}
}
}

0 comments on commit d3f4f5f

Please sign in to comment.