mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-11-04 10:35:27 +08:00 
			
		
		
		
	Add support for converting command parameters into FileInfo and DirectoryInfo (#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.
This commit is contained in:
		@@ -78,18 +78,26 @@ internal static class CommandValueResolver
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    else
 | 
					                    else
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        var converter = GetConverter(lookup, binder, resolver, mapped.Parameter);
 | 
					                        var (converter, stringConstructor) = GetConverter(lookup, binder, resolver, mapped.Parameter);
 | 
				
			||||||
                        if (converter == null)
 | 
					                        if (converter == null)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            throw CommandRuntimeException.NoConverterFound(mapped.Parameter);
 | 
					                            throw CommandRuntimeException.NoConverterFound(mapped.Parameter);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        object? value;
 | 
					                        object? value;
 | 
				
			||||||
 | 
					                        var mappedValue = mapped.Value ?? string.Empty;
 | 
				
			||||||
                        try
 | 
					                        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);
 | 
					                            throw CommandRuntimeException.ConversionFailed(mapped, converter, exception);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
@@ -122,7 +130,7 @@ internal static class CommandValueResolver
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        if (result != null && result.GetType() != parameter.ParameterType)
 | 
					        if (result != null && result.GetType() != parameter.ParameterType)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var converter = GetConverter(lookup, binder, resolver, parameter);
 | 
					            var (converter, _) = GetConverter(lookup, binder, resolver, parameter);
 | 
				
			||||||
            if (converter != null)
 | 
					            if (converter != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                result = converter.ConvertFrom(result);
 | 
					                result = converter.ConvertFrom(result);
 | 
				
			||||||
@@ -133,8 +141,14 @@ internal static class CommandValueResolver
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [SuppressMessage("Style", "IDE0019:Use pattern matching", Justification = "It's OK")]
 | 
					    [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.Converter == null)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (parameter.ParameterType.IsArray)
 | 
					            if (parameter.ParameterType.IsArray)
 | 
				
			||||||
@@ -146,12 +160,12 @@ internal static class CommandValueResolver
 | 
				
			|||||||
                    throw new InvalidOperationException("Could not get element type");
 | 
					                    throw new InvalidOperationException("Could not get element type");
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return TypeDescriptor.GetConverter(elementType);
 | 
					                return (TypeDescriptor.GetConverter(elementType), GetStringConstructor(elementType));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (parameter.IsFlagValue())
 | 
					            if (parameter.IsFlagValue())
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                // Is the optional value instanciated?
 | 
					                // Is the optional value instantiated?
 | 
				
			||||||
                var value = lookup.GetValue(parameter) as IFlagValue;
 | 
					                var value = lookup.GetValue(parameter) as IFlagValue;
 | 
				
			||||||
                if (value == null)
 | 
					                if (value == null)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -161,18 +175,18 @@ internal static class CommandValueResolver
 | 
				
			|||||||
                    value = lookup.GetValue(parameter) as IFlagValue;
 | 
					                    value = lookup.GetValue(parameter) as IFlagValue;
 | 
				
			||||||
                    if (value == null)
 | 
					                    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 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);
 | 
					        var type = Type.GetType(parameter.Converter.ConverterTypeName);
 | 
				
			||||||
        return resolver.Resolve(type) as TypeConverter;
 | 
					        return (resolver.Resolve(type) as TypeConverter, null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,15 @@
 | 
				
			|||||||
 | 
					using System.IO;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Spectre.Console.Tests.Data;
 | 
					namespace Spectre.Console.Tests.Data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class HorseSettings : MammalSettings
 | 
					public class HorseSettings : MammalSettings
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    [CommandOption("-d|--day")]
 | 
					    [CommandOption("-d|--day")]
 | 
				
			||||||
    public DayOfWeek Day { get; set; }
 | 
					    public DayOfWeek Day { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [CommandOption("--file")]
 | 
				
			||||||
 | 
					    public FileInfo File { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    [CommandOption("--directory")]
 | 
				
			||||||
 | 
					    public DirectoryInfo Directory { get; set; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -30,6 +30,8 @@
 | 
				
			|||||||
      <Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
 | 
					      <Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
 | 
				
			||||||
        <Parameters>
 | 
					        <Parameters>
 | 
				
			||||||
          <Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
 | 
					          <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>
 | 
					        </Parameters>
 | 
				
			||||||
      </Command>
 | 
					      </Command>
 | 
				
			||||||
    </Command>
 | 
					    </Command>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,6 +26,8 @@
 | 
				
			|||||||
    <Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
 | 
					    <Command Name="horse" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
 | 
				
			||||||
      <Parameters>
 | 
					      <Parameters>
 | 
				
			||||||
        <Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
 | 
					        <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" />
 | 
					        <Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
 | 
				
			||||||
      </Parameters>
 | 
					      </Parameters>
 | 
				
			||||||
    </Command>
 | 
					    </Command>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,8 @@
 | 
				
			|||||||
        <Description>Indicates whether or not the animal is alive.</Description>
 | 
					        <Description>Indicates whether or not the animal is alive.</Description>
 | 
				
			||||||
      </Option>
 | 
					      </Option>
 | 
				
			||||||
      <Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
 | 
					      <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" />
 | 
					      <Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
 | 
				
			||||||
    </Parameters>
 | 
					    </Parameters>
 | 
				
			||||||
  </Command>
 | 
					  </Command>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					using System.IO;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Spectre.Console.Tests.Unit.Cli;
 | 
					namespace Spectre.Console.Tests.Unit.Cli;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed partial class CommandAppTests
 | 
					public sealed partial class CommandAppTests
 | 
				
			||||||
@@ -76,5 +78,27 @@ public sealed partial class CommandAppTests
 | 
				
			|||||||
            result.Output.ShouldContain(nameof(DayOfWeek.Friday));
 | 
					            result.Output.ShouldContain(nameof(DayOfWeek.Friday));
 | 
				
			||||||
            result.Output.ShouldContain(nameof(DayOfWeek.Saturday));
 | 
					            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");
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user