Add positional arguments (#32)

This commit is contained in:
Thorkil Holm-Jacobsen
2020-01-13 12:31:05 +01:00
committed by Alexey Golub
parent ed87373dc3
commit e48839b938
32 changed files with 1150 additions and 211 deletions

View File

@@ -14,7 +14,7 @@ namespace CliFx.Demo.Commands
{
private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
[CommandArgument(0, Name = "title", IsRequired = true, Description = "Book title.")]
public string Title { get; set; }
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]

View File

@@ -32,7 +32,7 @@ namespace CliFx.Tests
.UseDescription("test")
.UseConsole(new VirtualConsole(TextWriter.Null))
.UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type!)!)
.UseCommandOptionInputConverter(new CommandOptionInputConverter())
.UseCommandOptionInputConverter(new CommandInputConverter())
.UseEnvironmentVariablesProvider(new EnvironmentVariablesProviderStub())
.Build();
}

View File

@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandArgumentSchemasValidatorTests
{
private static CommandArgumentSchema GetValidArgumentSchema(string propertyName, string name, bool isRequired, int order, string? description = null)
{
return new CommandArgumentSchema(typeof(TestCommand).GetProperty(propertyName)!, name, isRequired, description, order);
}
private static IEnumerable<TestCaseData> GetTestCases_ValidatorTest()
{
// Validation should succeed when no arguments are supplied
yield return new TestCaseData(new ValidatorTest(new List<CommandArgumentSchema>(), true));
// Multiple sequence arguments
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "B", false, 1)
}, false));
// Argument after sequence
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1)
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 0),
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 1)
}, true));
// Required arguments must appear before optional arguments
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", true, 1)
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", true, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2),
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", true, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", true, 2),
}, false));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", true, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2),
}, true));
// Argument order must be unique
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 2)
}, true));
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "B", false, 1),
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "C", false, 1)
}, false));
// No arguments with the same name
yield return new TestCaseData(new ValidatorTest(
new []
{
GetValidArgumentSchema(nameof(TestCommand.StringProperty), "A", false, 0),
GetValidArgumentSchema(nameof(TestCommand.EnumerableProperty), "A", false, 1)
}, false));
}
private class TestCommand
{
public IEnumerable<int> EnumerableProperty { get; set; }
public string StringProperty { get; set; }
}
public class ValidatorTest
{
public ValidatorTest(IReadOnlyCollection<CommandArgumentSchema> schemas, bool succeedsValidation)
{
Schemas = schemas;
SucceedsValidation = succeedsValidation;
}
public IReadOnlyCollection<CommandArgumentSchema> Schemas { get; }
public bool SucceedsValidation { get; }
}
[Test]
[TestCaseSource(nameof(GetTestCases_ValidatorTest))]
public void Validation_Test(ValidatorTest testCase)
{
// Arrange
var validator = new CommandArgumentSchemasValidator();
// Act
var result = validator.ValidateArgumentSchemas(testCase.Schemas);
// Assert
result.Any().Should().Be(!testCase.SucceedsValidation);
}
}
}

View File

@@ -13,7 +13,7 @@ namespace CliFx.Tests.Services
public class CommandFactoryTests
{
private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] {commandType}).Single();
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{

View File

@@ -16,144 +16,215 @@ namespace CliFx.Tests.Services
public class CommandInitializerTests
{
private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver().GetCommandSchemas(new[] { commandType }).Single();
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] { commandType }).Single();
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
{
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8")
}),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("divisor", "8")
})),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8")
}),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("dividend", "13"),
new CommandOptionInput("d", "8")
})),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8")
}),
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("D", "13"),
new CommandOptionInput("d", "8")
})),
new DivideCommand { Dividend = 13, Divisor = 8 }
);
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("concat", new[]
{
new CommandOptionInput("i", new[] {"foo", " ", "bar"})
}),
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" }, new[]
{
new CommandOptionInput("i", new[] { "foo", " ", "bar" })
})),
new ConcatCommand { Inputs = new[] { "foo", " ", "bar" } }
);
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("concat", new[]
{
new CommandOptionInput("i", new[] {"foo", "bar"}),
new CommandOptionInput("s", " ")
}),
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" }, new[]
{
new CommandOptionInput("i", new[] { "foo", "bar" }),
new CommandOptionInput("s", " ")
})),
new ConcatCommand { Inputs = new[] { "foo", "bar" }, Separator = " " }
);
//Will read a value from environment variables because none is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableCommand(),
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new string[0],
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableCommand { Option = "A" }
);
//Will read multiple values from environment variables because none is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableWithMultipleValuesCommand(),
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
new string[0],
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableWithMultipleValuesCommand { Option = new[] { "A", "B", "C" } }
);
//Will not read a value from environment variables because one is supplied via CommandInput
yield return new TestCaseData(
new EnvironmentVariableCommand(),
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new CommandInput(null, new[]
{
new CommandOptionInput("opt", new[] { "X" })
},
EnvironmentVariablesProviderStub.EnvironmentVariables),
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableCommand)),
new string[0],
new CommandInput(new string[0], new[]
{
new CommandOptionInput("opt", new[] { "X" })
},
EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableCommand { Option = "X" }
);
//Will not split environment variable values because underlying property is not a collection
yield return new TestCaseData(
new EnvironmentVariableWithoutCollectionPropertyCommand(),
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
);
new CommandCandidate(
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
new string[0],
new CommandInput(new string[0], new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables)),
new EnvironmentVariableWithoutCollectionPropertyCommand { Option = $"A{Path.PathSeparator}B{Path.PathSeparator}C{Path.PathSeparator}" }
);
// Positional arguments
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new [] { "abc", "123", "1", "2" },
new CommandInput(new [] { "arg", "cmd", "abc", "123", "1", "2" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>())),
new ArgumentCommand { FirstArgument = "abc", SecondArgument = 123, ThirdArguments = new List<int>{1, 2}, Option = "option value" }
);
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new [] { "abc" },
new CommandInput(new [] { "arg", "cmd", "abc" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>())),
new ArgumentCommand { FirstArgument = "abc", Option = "option value" }
);
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
{
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div")
);
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" })
));
yield return new TestCaseData(
new DivideCommand(),
GetCommandSchema(typeof(DivideCommand)),
new CommandInput("div", new[]
{
new CommandOptionInput("D", "13")
})
);
new CommandCandidate(
GetCommandSchema(typeof(DivideCommand)),
new string[0],
new CommandInput(new[] { "div" }, new[]
{
new CommandOptionInput("D", "13")
})
));
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("concat")
);
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" })
));
yield return new TestCaseData(
new ConcatCommand(),
GetCommandSchema(typeof(ConcatCommand)),
new CommandInput("concat", new[]
{
new CommandOptionInput("s", "_")
})
);
new CommandCandidate(
GetCommandSchema(typeof(ConcatCommand)),
new string[0],
new CommandInput(new[] { "concat" }, new[]
{
new CommandOptionInput("s", "_")
})
));
// Missing required positional argument
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new string[0],
new CommandInput(new string[0], new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
);
// Incorrect data type in list
yield return new TestCaseData(
new ArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(ArgumentCommand)),
new []{ "abc", "123", "invalid" },
new CommandInput(new [] { "arg", "cmd", "abc", "123", "invalid" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
);
// Extraneous unused arguments
yield return new TestCaseData(
new SimpleArgumentCommand(),
new CommandCandidate(
GetCommandSchema(typeof(SimpleArgumentCommand)),
new []{ "abc", "123", "unused" },
new CommandInput(new [] { "arg", "cmd2", "abc", "123", "unused" }, new []{ new CommandOptionInput("o", "option value") }, new Dictionary<string, string>()))
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
public void InitializeCommand_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput,
public void InitializeCommand_Test(ICommand command, CommandCandidate commandCandidate,
ICommand expectedCommand)
{
// Arrange
var initializer = new CommandInitializer();
// Act
initializer.InitializeCommand(command, commandSchema, commandInput);
initializer.InitializeCommand(command, commandCandidate);
// Assert
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
@@ -161,13 +232,13 @@ namespace CliFx.Tests.Services
[Test]
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
public void InitializeCommand_Negative_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
public void InitializeCommand_Negative_Test(ICommand command, CommandCandidate commandCandidate)
{
// Arrange
var initializer = new CommandInitializer();
// Act & Assert
initializer.Invoking(i => i.InitializeCommand(command, commandSchema, commandInput))
initializer.Invoking(i => i.InitializeCommand(command, commandCandidate))
.Should().ThrowExactly<CliFxException>();
}
}

View File

@@ -12,7 +12,7 @@ using NUnit.Framework;
namespace CliFx.Tests.Services
{
[TestFixture]
public class CommandOptionInputConverterTests
public class CommandInputConverterTests
{
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput()
{
@@ -298,7 +298,7 @@ namespace CliFx.Tests.Services
object expectedConvertedValue)
{
// Arrange
var converter = new CommandOptionInputConverter();
var converter = new CommandInputConverter();
// Act
var convertedValue = converter.ConvertOptionInput(optionInput, targetType);
@@ -313,7 +313,7 @@ namespace CliFx.Tests.Services
public void ConvertOptionInput_Negative_Test(CommandOptionInput optionInput, Type targetType)
{
// Arrange
var converter = new CommandOptionInputConverter();
var converter = new CommandInputConverter();
// Act & Assert
converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType))

View File

@@ -158,13 +158,13 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(
new[] { "command" },
new CommandInput("command"),
new CommandInput(new []{ "command" }),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "command", "--option", "value" },
new CommandInput("command", new[]
new CommandInput(new []{ "command" }, new[]
{
new CommandOptionInput("option", "value")
}),
@@ -173,13 +173,13 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(
new[] { "long", "command", "name" },
new CommandInput("long command name"),
new CommandInput(new []{ "long", "command", "name"}),
new EmptyEnvironmentVariablesProviderStub()
);
yield return new TestCaseData(
new[] { "long", "command", "name", "--option", "value" },
new CommandInput("long command name", new[]
new CommandInput(new []{ "long", "command", "name" }, new[]
{
new CommandOptionInput("option", "value")
}),
@@ -188,7 +188,7 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(
new[] { "[debug]" },
new CommandInput(null,
new CommandInput(new string[0],
new[] { "debug" },
new CommandOptionInput[0]),
new EmptyEnvironmentVariablesProviderStub()
@@ -196,7 +196,7 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(
new[] { "[debug]", "[preview]" },
new CommandInput(null,
new CommandInput(new string[0],
new[] { "debug", "preview" },
new CommandOptionInput[0]),
new EmptyEnvironmentVariablesProviderStub()
@@ -204,7 +204,7 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(
new[] { "[debug]", "[preview]", "-o", "value" },
new CommandInput(null,
new CommandInput(new string[0],
new[] { "debug", "preview" },
new[]
{
@@ -215,7 +215,7 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(
new[] { "command", "[debug]", "[preview]", "-o", "value" },
new CommandInput("command",
new CommandInput(new []{"command"},
new[] { "debug", "preview" },
new[]
{
@@ -226,7 +226,7 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(
new[] { "command", "[debug]", "[preview]", "-o", "value" },
new CommandInput("command",
new CommandInput(new []{ "command"},
new[] { "debug", "preview" },
new[]
{

View File

@@ -19,7 +19,7 @@ namespace CliFx.Tests.Services
new[]
{
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
new[]
new CommandArgumentSchema[0], new[]
{
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
"dividend", 'D', true, "The number to divide.", null),
@@ -27,6 +27,7 @@ namespace CliFx.Tests.Services
"divisor", 'd', true, "The number to divide by.", null)
}),
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
new CommandArgumentSchema[0],
new[]
{
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
@@ -35,6 +36,7 @@ namespace CliFx.Tests.Services
null, 's', false, "String separator.", null)
}),
new CommandSchema(typeof(EnvironmentVariableCommand), null, "Reads option values from environment variables.",
new CommandArgumentSchema[0],
new[]
{
new CommandOptionSchema(typeof(EnvironmentVariableCommand).GetProperty(nameof(EnvironmentVariableCommand.Option)),
@@ -48,7 +50,7 @@ namespace CliFx.Tests.Services
new[] { typeof(HelloWorldDefaultCommand) },
new[]
{
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0])
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandArgumentSchema[0], new CommandOptionSchema[0])
}
);
}
@@ -62,37 +64,192 @@ namespace CliFx.Tests.Services
yield return new TestCaseData(new object[]
{
new[] {typeof(NonImplementedCommand)}
new[] { typeof(NonImplementedCommand) }
});
yield return new TestCaseData(new object[]
{
new[] {typeof(NonAnnotatedCommand)}
new[] { typeof(NonAnnotatedCommand) }
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionNamesCommand)}
new[] { typeof(DuplicateOptionNamesCommand) }
});
yield return new TestCaseData(new object[]
{
new[] {typeof(DuplicateOptionShortNamesCommand)}
new[] { typeof(DuplicateOptionShortNamesCommand) }
});
yield return new TestCaseData(new object[]
{
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}
new[] { typeof(ExceptionCommand), typeof(CommandExceptionCommand) }
});
}
private static IEnumerable<TestCaseData> GetTestCases_GetTargetCommandSchema_Positive()
{
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null)
},
new CommandInput(new[] { "command1", "argument1", "argument2" }),
new[] { "argument1", "argument2" },
"command1"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null)
},
new CommandInput(new[] { "argument1", "argument2" }),
new[] { "argument1", "argument2" },
""
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1 subcommand1", null, null, null),
},
new CommandInput(new[] { "command1", "subcommand1", "argument1" }),
new[] { "argument1" },
"command1 subcommand1"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a", "b", "d" }),
new[] { "d" },
"a b"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a", "b", "c", "d" }),
new[] { "d" },
"a b c"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "b", "c" }),
new string[0],
"b c"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "d", "a", "b"}),
new[] { "d", "a", "b" },
""
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a", "b c", "d" }),
new[] { "b c", "d" },
"a"
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "", null, null, null),
new CommandSchema(null, "a", null, null, null),
new CommandSchema(null, "a b", null, null, null),
new CommandSchema(null, "a b c", null, null, null),
new CommandSchema(null, "b", null, null, null),
new CommandSchema(null, "b c", null, null, null),
new CommandSchema(null, "c", null, null, null),
},
new CommandInput(new[] { "a b", "c", "d" }),
new[] { "a b", "c", "d" },
""
);
}
private static IEnumerable<TestCaseData> GetTestCases_GetTargetCommandSchema_Negative()
{
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null),
},
new CommandInput(new[] { "command4", "argument1" })
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1", null, null, null),
new CommandSchema(null, "command2", null, null, null),
new CommandSchema(null, "command3", null, null, null),
},
new CommandInput(new[] { "argument1" })
);
yield return new TestCaseData(
new []
{
new CommandSchema(null, "command1 subcommand1", null, null, null),
},
new CommandInput(new[] { "command1", "argument1" })
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes,
IReadOnlyList<CommandSchema> expectedCommandSchemas)
{
// Arrange
var commandSchemaResolver = new CommandSchemaResolver();
var commandSchemaResolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act
var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes);
@@ -106,11 +263,44 @@ namespace CliFx.Tests.Services
public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes)
{
// Arrange
var resolver = new CommandSchemaResolver();
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act & Assert
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
.Should().ThrowExactly<CliFxException>();
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetTargetCommandSchema_Positive))]
public void GetTargetCommandSchema_Positive_Test(IReadOnlyList<CommandSchema> availableCommandSchemas,
CommandInput commandInput,
IReadOnlyList<string> expectedPositionalArguments,
string expectedCommandSchemaName)
{
// Arrange
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act
var commandCandidate = resolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
// Assert
commandCandidate.Should().NotBeNull();
commandCandidate.PositionalArgumentsInput.Should().BeEquivalentTo(expectedPositionalArguments);
commandCandidate.Schema.Name.Should().Be(expectedCommandSchemaName);
}
[Test]
[TestCaseSource(nameof(GetTestCases_GetTargetCommandSchema_Negative))]
public void GetTargetCommandSchema_Negative_Test(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandInput commandInput)
{
// Arrange
var resolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
// Act
var commandCandidate = resolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
// Assert
commandCandidate.Should().BeNull();
}
}
}

View File

@@ -13,7 +13,7 @@ namespace CliFx.Tests.Services
public class DelegateCommandFactoryTests
{
private static CommandSchema GetCommandSchema(Type commandType) =>
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
new CommandSchemaResolver(new CommandArgumentSchemasValidator()).GetCommandSchemas(new[] {commandType}).Single();
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
{

View File

@@ -15,7 +15,7 @@ namespace CliFx.Tests.Services
{
private static HelpTextSource CreateHelpTextSource(IReadOnlyList<Type> availableCommandTypes, Type targetCommandType)
{
var commandSchemaResolver = new CommandSchemaResolver();
var commandSchemaResolver = new CommandSchemaResolver(new CommandArgumentSchemasValidator());
var applicationMetadata = new ApplicationMetadata("TestApp", "testapp", "1.0", null);
var availableCommandSchemas = commandSchemaResolver.GetCommandSchemas(availableCommandTypes);
@@ -85,6 +85,27 @@ namespace CliFx.Tests.Services
"-h|--help", "Shows help text."
}
);
yield return new TestCaseData(
CreateHelpTextSource(
new[] {typeof(ArgumentCommand)},
typeof(ArgumentCommand)),
new[]
{
"Description",
"Command using positional arguments",
"Usage",
"arg cmd", "<first>", "[<secondargument>]", "[<third list>]", "[options]",
"Arguments",
"* first",
"secondargument",
"third list", "A list of numbers",
"Options",
"-o|--option",
"-h|--help", "Shows help text."
}
);
}
[Test]

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command("arg cmd", Description = "Command using positional arguments")]
public class ArgumentCommand : ICommand
{
[CommandArgument(0, IsRequired = true, Name = "first")]
public string? FirstArgument { get; set; }
[CommandArgument(10)]
public int? SecondArgument { get; set; }
[CommandArgument(20, Description = "A list of numbers", Name = "third list")]
public IEnumerable<int> ThirdArguments { get; set; }
[CommandOption("option", 'o')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.TestCommands
{
[Command("arg cmd2", Description = "Command using positional arguments")]
public class SimpleArgumentCommand : ICommand
{
[CommandArgument(0, IsRequired = true, Name = "first")]
public string? FirstArgument { get; set; }
[CommandArgument(10)]
public int? SecondArgument { get; set; }
[CommandOption("option", 'o')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,42 @@
using System;
namespace CliFx.Attributes
{
/// <summary>
/// Annotates a property that defines a command argument.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CommandArgumentAttribute : Attribute
{
/// <summary>
/// The name of the argument, which is used in help text.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Whether the argument is required.
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Argument description, which is used in help text.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The ordering of the argument. Lower values will appear before higher values.
/// <remarks>
/// Two arguments of the same command cannot have the same <see cref="Order"/>.
/// </remarks>
/// </summary>
public int Order { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandArgumentAttribute"/> with a given order.
/// </summary>
public CommandArgumentAttribute(int order)
{
Order = order;
}
}
}

View File

@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
using CliFx.Services;
@@ -74,7 +72,7 @@ namespace CliFx
return null;
// Render command name
_console.Output.WriteLine($"Command name: {commandInput.CommandName}");
_console.Output.WriteLine($"Arguments: {string.Join(" ", commandInput.Arguments)}");
_console.Output.WriteLine();
// Render directives
@@ -103,7 +101,7 @@ namespace CliFx
private int? HandleVersionOption(CommandInput commandInput)
{
// Version should be rendered if it was requested on a default command
var shouldRenderVersion = !commandInput.IsCommandSpecified() && commandInput.IsVersionOptionSpecified();
var shouldRenderVersion = !commandInput.HasArguments() && commandInput.IsVersionOptionSpecified();
// If shouldn't render version, pass execution to the next handler
if (!shouldRenderVersion)
@@ -117,10 +115,10 @@ namespace CliFx
}
private int? HandleHelpOption(CommandInput commandInput,
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema? targetCommandSchema)
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandCandidate? commandCandidate)
{
// Help should be rendered if it was requested, or when executing a command which isn't defined
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || targetCommandSchema == null;
var shouldRenderHelp = commandInput.IsHelpOptionSpecified() || commandCandidate == null;
// If shouldn't render help, pass execution to the next handler
if (!shouldRenderHelp)
@@ -129,31 +127,22 @@ namespace CliFx
// Keep track whether there was an error in the input
var isError = false;
// If target command isn't defined, find its contextual replacement
if (targetCommandSchema == null)
// Report error if no command matched the arguments
if (commandCandidate is null)
{
// If command was specified, inform the user that it's not defined
if (commandInput.IsCommandSpecified())
// If a command was specified, inform the user that the command is not defined
if (commandInput.HasArguments())
{
_console.WithForegroundColor(ConsoleColor.Red,
() => _console.Error.WriteLine($"Specified command [{commandInput.CommandName}] is not defined."));
() => _console.Error.WriteLine($"No command could be matched for input [{string.Join(" ", commandInput.Arguments)}]"));
isError = true;
}
// Replace target command with closest parent of specified command
targetCommandSchema = availableCommandSchemas.FindParent(commandInput.CommandName);
// If there's no parent, replace with stub default command
if (targetCommandSchema == null)
{
targetCommandSchema = CommandSchema.StubDefaultCommand;
availableCommandSchemas = availableCommandSchemas.Concat(CommandSchema.StubDefaultCommand).ToArray();
}
commandCandidate = new CommandCandidate(CommandSchema.StubDefaultCommand, new string[0], commandInput);
}
// Build help text source
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, targetCommandSchema);
var helpTextSource = new HelpTextSource(_metadata, availableCommandSchemas, commandCandidate.Schema);
// Render help text
_helpTextRenderer.RenderHelpText(_console, helpTextSource);
@@ -162,13 +151,18 @@ namespace CliFx
return isError ? -1 : 0;
}
private async ValueTask<int> HandleCommandExecutionAsync(CommandInput commandInput, CommandSchema targetCommandSchema)
private async ValueTask<int> HandleCommandExecutionAsync(CommandCandidate? commandCandidate)
{
// Create an instance of the command
var command = _commandFactory.CreateCommand(targetCommandSchema);
if (commandCandidate is null)
{
throw new ArgumentException("Cannot execute command because it was not found.");
}
// Populate command with options according to its schema
_commandInitializer.InitializeCommand(command, targetCommandSchema, commandInput);
// Create an instance of the command
var command = _commandFactory.CreateCommand(commandCandidate.Schema);
// Populate command with options and arguments according to its schema
_commandInitializer.InitializeCommand(command, commandCandidate);
// Execute command
await command.ExecuteAsync(_console);
@@ -189,15 +183,15 @@ namespace CliFx
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
// Find command schema matching the name specified in the input
var targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName);
var commandCandidate = _commandSchemaResolver.GetTargetCommandSchema(availableCommandSchemas, commandInput);
// Chain handlers until the first one that produces an exit code
return
await HandleDebugDirectiveAsync(commandInput) ??
HandlePreviewDirective(commandInput) ??
HandleVersionOption(commandInput) ??
HandleHelpOption(commandInput, availableCommandSchemas, targetCommandSchema) ??
await HandleCommandExecutionAsync(commandInput, targetCommandSchema!);
HandleHelpOption(commandInput, availableCommandSchemas, commandCandidate) ??
await HandleCommandExecutionAsync(commandCandidate);
}
catch (Exception ex)
{

View File

@@ -25,7 +25,7 @@ namespace CliFx
private string? _description;
private IConsole? _console;
private ICommandFactory? _commandFactory;
private ICommandOptionInputConverter? _commandOptionInputConverter;
private ICommandInputConverter? _commandInputConverter;
private IEnvironmentVariablesProvider? _environmentVariablesProvider;
/// <inheritdoc />
@@ -107,9 +107,9 @@ namespace CliFx
}
/// <inheritdoc />
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter)
public ICliApplicationBuilder UseCommandOptionInputConverter(ICommandInputConverter converter)
{
_commandOptionInputConverter = converter;
_commandInputConverter = converter;
return this;
}
@@ -129,7 +129,7 @@ namespace CliFx
_versionText ??= GetDefaultVersionText() ?? "v1.0";
_console ??= new SystemConsole();
_commandFactory ??= new CommandFactory();
_commandOptionInputConverter ??= new CommandOptionInputConverter();
_commandInputConverter ??= new CommandInputConverter();
_environmentVariablesProvider ??= new EnvironmentVariablesProvider();
// Project parameters to expected types
@@ -137,8 +137,8 @@ namespace CliFx
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
return new CliApplication(metadata, configuration,
_console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(),
_commandFactory, new CommandInitializer(_commandOptionInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
_console, new CommandInputParser(_environmentVariablesProvider), new CommandSchemaResolver(new CommandArgumentSchemasValidator()),
_commandFactory, new CommandInitializer(_commandInputConverter, new EnvironmentVariablesParser()), new HelpTextRenderer());
}
}

View File

@@ -60,9 +60,9 @@ namespace CliFx
ICliApplicationBuilder UseCommandFactory(ICommandFactory factory);
/// <summary>
/// Configures application to use specified implementation of <see cref="ICommandOptionInputConverter"/>.
/// Configures application to use specified implementation of <see cref="ICommandInputConverter"/>.
/// </summary>
ICliApplicationBuilder UseCommandOptionInputConverter(ICommandOptionInputConverter converter);
ICliApplicationBuilder UseCommandOptionInputConverter(ICommandInputConverter converter);
/// <summary>
/// Configures application to use specified implementation of <see cref="IEnvironmentVariablesProvider"/>.

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CliFx.Models;
namespace CliFx.Internal
{
@@ -66,5 +67,11 @@ namespace CliFx.Internal
public static bool IsCollection(this Type type) =>
type != typeof(string) && type.GetEnumerableUnderlyingType() != null;
public static IOrderedEnumerable<CommandArgumentSchema> Ordered(this IEnumerable<CommandArgumentSchema> source)
{
return source
.OrderBy(a => a.Order);
}
}
}

View File

@@ -0,0 +1,78 @@
using System.Globalization;
using System.Reflection;
using System.Text;
namespace CliFx.Models
{
/// <summary>
/// Schema of a defined command argument.
/// </summary>
public class CommandArgumentSchema
{
/// <summary>
/// Underlying property.
/// </summary>
public PropertyInfo Property { get; }
/// <summary>
/// Argument name used for help text.
/// </summary>
public string? Name { get; }
/// <summary>
/// Whether the argument is required.
/// </summary>
public bool IsRequired { get; }
/// <summary>
/// Argument description.
/// </summary>
public string? Description { get; }
/// <summary>
/// Order of the argument.
/// </summary>
public int Order { get; }
/// <summary>
/// The display name of the argument. Returns <see cref="Name"/> if specified, otherwise the name of the underlying property.
/// </summary>
public string DisplayName => !string.IsNullOrWhiteSpace(Name) ? Name! : Property.Name.ToLower(CultureInfo.InvariantCulture);
/// <summary>
/// Initializes an instance of <see cref="CommandArgumentSchema"/>.
/// </summary>
public CommandArgumentSchema(PropertyInfo property, string? name, bool isRequired, string? description, int order)
{
Property = property;
Name = name;
IsRequired = isRequired;
Description = description;
Order = order;
}
/// <summary>
/// Returns the string representation of the argument schema.
/// </summary>
/// <returns></returns>
public override string ToString()
{
var sb = new StringBuilder();
if (!IsRequired)
{
sb.Append("[");
}
sb.Append("<");
sb.Append($"{DisplayName}");
sb.Append(">");
if (!IsRequired)
{
sb.Append("]");
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace CliFx.Models
{
/// <summary>
/// Defines the target command and the input required for initializing the command.
/// </summary>
public class CommandCandidate
{
/// <summary>
/// The command schema of the target command.
/// </summary>
public CommandSchema Schema { get; }
/// <summary>
/// The positional arguments input for the command.
/// </summary>
public IReadOnlyList<string> PositionalArgumentsInput { get; }
/// <summary>
/// The command input for the command.
/// </summary>
public CommandInput CommandInput { get; }
/// <summary>
/// Initializes and instance of <see cref="CommandCandidate"/>
/// </summary>
public CommandCandidate(CommandSchema schema, IReadOnlyList<string> positionalArgumentsInput, CommandInput commandInput)
{
Schema = schema;
PositionalArgumentsInput = positionalArgumentsInput;
CommandInput = commandInput;
}
}
}

View File

@@ -10,10 +10,9 @@ namespace CliFx.Models
public partial class CommandInput
{
/// <summary>
/// Specified command name.
/// Can be null if command was not specified.
/// Specified arguments.
/// </summary>
public string? CommandName { get; }
public IReadOnlyList<string> Arguments { get; }
/// <summary>
/// Specified directives.
@@ -33,10 +32,10 @@ namespace CliFx.Models
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options,
IReadOnlyDictionary<string, string> environmentVariables)
{
CommandName = commandName;
Arguments = arguments;
Directives = directives;
Options = options;
EnvironmentVariables = environmentVariables;
@@ -45,24 +44,24 @@ namespace CliFx.Models
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
: this(commandName, directives, options, EmptyEnvironmentVariables)
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
: this(arguments, directives, options, EmptyEnvironmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
: this(commandName, EmptyDirectives, options, environmentVariables)
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<CommandOptionInput> options, IReadOnlyDictionary<string, string> environmentVariables)
: this(arguments, EmptyDirectives, options, environmentVariables)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName, IReadOnlyList<CommandOptionInput> options)
: this(commandName, EmptyDirectives, options)
public CommandInput(IReadOnlyList<string> arguments, IReadOnlyList<CommandOptionInput> options)
: this(arguments, EmptyDirectives, options)
{
}
@@ -70,15 +69,15 @@ namespace CliFx.Models
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(IReadOnlyList<CommandOptionInput> options)
: this(null, options)
: this(new string[0], options)
{
}
/// <summary>
/// Initializes an instance of <see cref="CommandInput"/>.
/// </summary>
public CommandInput(string? commandName)
: this(commandName, EmptyOptions)
public CommandInput(IReadOnlyList<string> arguments)
: this(arguments, EmptyOptions)
{
}
@@ -87,8 +86,11 @@ namespace CliFx.Models
{
var buffer = new StringBuilder();
if (!string.IsNullOrWhiteSpace(CommandName))
buffer.Append(CommandName);
foreach (var argument in Arguments)
{
buffer.AppendIfNotEmpty(' ');
buffer.Append(argument);
}
foreach (var directive in Directives)
{

View File

@@ -30,15 +30,21 @@ namespace CliFx.Models
/// </summary>
public IReadOnlyList<CommandOptionSchema> Options { get; }
/// <summary>
/// Command arguments.
/// </summary>
public IReadOnlyList<CommandArgumentSchema> Arguments { get; }
/// <summary>
/// Initializes an instance of <see cref="CommandSchema"/>.
/// </summary>
public CommandSchema(Type? type, string? name, string? description, IReadOnlyList<CommandOptionSchema> options)
public CommandSchema(Type type, string name, string description, IReadOnlyList<CommandArgumentSchema> arguments, IReadOnlyList<CommandOptionSchema> options)
{
Type = type;
Name = name;
Description = description;
Options = options;
Arguments = arguments;
}
/// <inheritdoc />
@@ -64,6 +70,6 @@ namespace CliFx.Models
public partial class CommandSchema
{
internal static CommandSchema StubDefaultCommand { get; } =
new CommandSchema(null, null, null, new CommandOptionSchema[0]);
new CommandSchema(null, null, null, new CommandArgumentSchema[0], new CommandOptionSchema[0]);
}
}

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace CliFx.Models
{
@@ -90,7 +91,7 @@ namespace CliFx.Models
/// <summary>
/// Gets whether a command was specified in the input.
/// </summary>
public static bool IsCommandSpecified(this CommandInput commandInput) => !string.IsNullOrWhiteSpace(commandInput.CommandName);
public static bool HasArguments(this CommandInput commandInput) => commandInput.Arguments.Any();
/// <summary>
/// Gets whether debug directive was specified in the input.

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
namespace CliFx.Services
{
/// <inheritdoc />
public class CommandArgumentSchemasValidator : ICommandArgumentSchemasValidator
{
private bool IsEnumerableArgument(CommandArgumentSchema schema)
{
return schema.Property.PropertyType != typeof(string) && schema.Property.PropertyType.GetEnumerableUnderlyingType() != null;
}
/// <inheritdoc />
public IEnumerable<ValidationError> ValidateArgumentSchemas(IReadOnlyCollection<CommandArgumentSchema> commandArgumentSchemas)
{
if (commandArgumentSchemas.Count == 0)
{
// No validation needed
yield break;
}
// Make sure there are no arguments with the same name
var duplicateNameGroups = commandArgumentSchemas
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
.GroupBy(x => x.Name)
.Where(x => x.Count() > 1);
foreach (var schema in duplicateNameGroups)
{
yield return new ValidationError($"Multiple arguments with same name: \"{schema.Key}\".");
}
// Make sure that the order of all properties are distinct
var duplicateOrderGroups = commandArgumentSchemas
.GroupBy(x => x.Order)
.Where(x => x.Count() > 1);
foreach (var schema in duplicateOrderGroups)
{
yield return new ValidationError($"Multiple arguments with the same order: \"{schema.Key}\".");
}
var enumerableArguments = commandArgumentSchemas
.Where(IsEnumerableArgument)
.ToList();
// Verify that no more than one enumerable argument exists
if (enumerableArguments.Count > 1)
{
yield return new ValidationError($"Multiple sequence arguments found; only one is supported.");
}
// If an enumerable argument exists, ensure that it has the highest order
if (enumerableArguments.Count == 1)
{
if (enumerableArguments.Single().Order != commandArgumentSchemas.Max(x => x.Order))
{
yield return new ValidationError($"A sequence argument was defined with a lower order than another argument; the sequence argument must have the highest order (appear last).");
}
}
// Verify that all required arguments appear before optional arguments
if (commandArgumentSchemas.Any(x => x.IsRequired) && commandArgumentSchemas.Any(x => !x.IsRequired) &&
commandArgumentSchemas.Where(x => x.IsRequired).Max(x => x.Order) > commandArgumentSchemas.Where(x => !x.IsRequired).Min(x => x.Order))
{
yield return new ValidationError("One or more required arguments appear after optional arguments. Required arguments must appear before (i.e. have lower order than) optional arguments.");
}
}
}
/// <summary>
/// Represents a failed validation.
/// </summary>
public class ValidationError
{
/// <summary>
/// Creates an instance of <see cref="ValidationError"/> with a message.
/// </summary>
public ValidationError(string message)
{
Message = message;
}
/// <summary>
/// The error message for the failed validation.
/// </summary>
public string Message { get; }
}
}

View File

@@ -1,4 +1,6 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Internal;
using CliFx.Models;
@@ -10,15 +12,15 @@ namespace CliFx.Services
/// </summary>
public class CommandInitializer : ICommandInitializer
{
private readonly ICommandOptionInputConverter _commandOptionInputConverter;
private readonly ICommandInputConverter _commandInputConverter;
private readonly IEnvironmentVariablesParser _environmentVariablesParser;
/// <summary>
/// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary>
public CommandInitializer(ICommandOptionInputConverter commandOptionInputConverter, IEnvironmentVariablesParser environmentVariablesParser)
public CommandInitializer(ICommandInputConverter commandInputConverter, IEnvironmentVariablesParser environmentVariablesParser)
{
_commandOptionInputConverter = commandOptionInputConverter;
_commandInputConverter = commandInputConverter;
_environmentVariablesParser = environmentVariablesParser;
}
@@ -26,7 +28,7 @@ namespace CliFx.Services
/// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary>
public CommandInitializer(IEnvironmentVariablesParser environmentVariablesParser)
: this(new CommandOptionInputConverter(), environmentVariablesParser)
: this(new CommandInputConverter(), environmentVariablesParser)
{
}
@@ -34,43 +36,47 @@ namespace CliFx.Services
/// Initializes an instance of <see cref="CommandInitializer"/>.
/// </summary>
public CommandInitializer()
: this(new CommandOptionInputConverter(), new EnvironmentVariablesParser())
: this(new CommandInputConverter(), new EnvironmentVariablesParser())
{
}
/// <inheritdoc />
public void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
private void InitializeCommandOptions(ICommand command, CommandCandidate commandCandidate)
{
if (commandCandidate.Schema is null)
{
throw new ArgumentException("Cannot initialize command without a schema.");
}
// Keep track of unset required options to report an error at a later stage
var unsetRequiredOptions = commandSchema.Options.Where(o => o.IsRequired).ToList();
var unsetRequiredOptions = commandCandidate.Schema.Options.Where(o => o.IsRequired).ToList();
//Set command options
foreach (var optionSchema in commandSchema.Options)
foreach (var optionSchema in commandCandidate.Schema.Options)
{
// Ignore special options that are not backed by a property
if (optionSchema.Property == null)
continue;
//Find matching option input
var optionInput = commandInput.Options.FindByOptionSchema(optionSchema);
var optionInput = commandCandidate.CommandInput.Options.FindByOptionSchema(optionSchema);
//If no option input is available fall back to environment variable values
if (optionInput == null && !string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariableName))
{
var fallbackEnvironmentVariableExists = commandInput.EnvironmentVariables.ContainsKey(optionSchema.EnvironmentVariableName!);
var fallbackEnvironmentVariableExists = commandCandidate.CommandInput.EnvironmentVariables.ContainsKey(optionSchema.EnvironmentVariableName!);
//If no environment variable is found or there is no valid value for this option skip it
if (!fallbackEnvironmentVariableExists || string.IsNullOrWhiteSpace(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!]))
if (!fallbackEnvironmentVariableExists || string.IsNullOrWhiteSpace(commandCandidate.CommandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!]))
continue;
optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!], optionSchema);
optionInput = _environmentVariablesParser.GetCommandOptionInputFromEnvironmentVariable(commandCandidate.CommandInput.EnvironmentVariables[optionSchema.EnvironmentVariableName!], optionSchema);
}
//No fallback available and no option input was specified, skip option
if (optionInput == null)
continue;
var convertedValue = _commandOptionInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType);
var convertedValue = _commandInputConverter.ConvertOptionInput(optionInput, optionSchema.Property.PropertyType);
// Set value of the underlying property
optionSchema.Property.SetValue(command, convertedValue);
@@ -87,5 +93,57 @@ namespace CliFx.Services
throw new CliFxException($"Some of the required options were not provided: {unsetRequiredOptionNames}.");
}
}
private void InitializeCommandArguments(ICommand command, CommandCandidate commandCandidate)
{
if (commandCandidate.Schema is null)
{
throw new ArgumentException("Cannot initialize command without a schema.");
}
// Keep track of unset required options to report an error at a later stage
var unsetRequiredArguments = commandCandidate.Schema.Arguments
.Where(o => o.IsRequired)
.ToList();
var orderedArgumentSchemas = commandCandidate.Schema.Arguments.Ordered();
var argumentIndex = 0;
foreach (var argumentSchema in orderedArgumentSchemas)
{
if (argumentIndex >= commandCandidate.PositionalArgumentsInput.Count)
{
// No more positional arguments left - remaining argument properties stay unset
break;
}
var convertedValue = _commandInputConverter.ConvertArgumentInput(commandCandidate.PositionalArgumentsInput, ref argumentIndex, argumentSchema.Property.PropertyType);
// Set value of underlying property
argumentSchema.Property.SetValue(command, convertedValue);
// Mark this required argument as set
if (argumentSchema.IsRequired)
unsetRequiredArguments.Remove(argumentSchema);
}
// Throw if there are remaining input arguments
if (argumentIndex < commandCandidate.PositionalArgumentsInput.Count)
{
throw new CliFxException($"Could not map the following arguments to command name or positional arguments: {commandCandidate.PositionalArgumentsInput.Skip(argumentIndex).JoinToString(", ")}");
}
// Throw if any of the required arguments were not set
if (unsetRequiredArguments.Any())
{
throw new CliFxException($"One or more required arguments were not set: {unsetRequiredArguments.JoinToString(", ")}.");
}
}
/// <inheritdoc />
public void InitializeCommand(ICommand command, CommandCandidate commandCandidate)
{
InitializeCommandOptions(command, commandCandidate);
InitializeCommandArguments(command, commandCandidate);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
@@ -9,28 +10,53 @@ using CliFx.Models;
namespace CliFx.Services
{
/// <summary>
/// Default implementation of <see cref="ICommandOptionInputConverter"/>.
/// Default implementation of <see cref="ICommandInputConverter"/>.
/// </summary>
public partial class CommandOptionInputConverter : ICommandOptionInputConverter
public partial class CommandInputConverter : ICommandInputConverter
{
private readonly IFormatProvider _formatProvider;
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInputConverter"/>.
/// Initializes an instance of <see cref="CommandInputConverter"/>.
/// </summary>
public CommandOptionInputConverter(IFormatProvider formatProvider)
public CommandInputConverter(IFormatProvider formatProvider)
{
_formatProvider = formatProvider;
}
/// <summary>
/// Initializes an instance of <see cref="CommandOptionInputConverter"/>.
/// Initializes an instance of <see cref="CommandInputConverter"/>.
/// </summary>
public CommandOptionInputConverter()
public CommandInputConverter()
: this(CultureInfo.InvariantCulture)
{
}
private object? ConvertEnumerableValue(IReadOnlyList<string> values, Type enumerableUnderlyingType, Type targetType)
{
// Convert values to the underlying enumerable type and cast it to dynamic array
var convertedValues = values
.Select(v => ConvertValue(v, enumerableUnderlyingType))
.ToNonGenericArray(enumerableUnderlyingType);
// Get the type of produced array
var convertedValuesType = convertedValues.GetType();
// Try to assign the array (works for T[], IReadOnlyList<T>, IEnumerable<T>, etc)
if (targetType.IsAssignableFrom(convertedValuesType))
return convertedValues;
// Try to inject the array into the constructor (works for HashSet<T>, List<T>, etc)
var arrayConstructor = targetType.GetConstructor(new[] { convertedValuesType });
if (arrayConstructor != null)
return arrayConstructor.Invoke(new object[] { convertedValues });
// Throw if we can't find a way to convert the values
throw new CliFxException(
$"Can't convert a sequence of values [{values.JoinToString(", ")}] " +
$"to type [{targetType}].");
}
/// <summary>
/// Converts a single string value to specified target type.
/// </summary>
@@ -118,17 +144,17 @@ namespace CliFx.Services
// Has a constructor that accepts a single string
var stringConstructor = GetStringConstructor(targetType);
if (stringConstructor != null)
return stringConstructor.Invoke(new object[] {value});
return stringConstructor.Invoke(new object[] { value });
// Has a static parse method that accepts a single string and a format provider
var parseMethodWithFormatProvider = GetStaticParseMethodWithFormatProvider(targetType);
if (parseMethodWithFormatProvider != null)
return parseMethodWithFormatProvider.Invoke(null, new object[] {value, _formatProvider});
return parseMethodWithFormatProvider.Invoke(null, new object[] { value, _formatProvider });
// Has a static parse method that accepts a single string
var parseMethod = GetStaticParseMethod(targetType);
if (parseMethod != null)
return parseMethod.Invoke(null, new object[] {value});
return parseMethod.Invoke(null, new object[] { value });
}
catch (Exception ex)
{
@@ -145,6 +171,24 @@ namespace CliFx.Services
"This type is not among the list of types supported by this library.");
}
/// <inheritdoc />
public virtual object? ConvertArgumentInput(IReadOnlyList<string> arguments, ref int currentIndex, Type targetType)
{
var enumerableUnderlyingType = targetType != typeof(string) ? targetType.GetEnumerableUnderlyingType() : null;
if (enumerableUnderlyingType is null)
{
var argument = arguments[currentIndex];
currentIndex += 1;
return ConvertValue(argument, targetType);
}
//
var argumentSequence = arguments.Skip(currentIndex).ToList();
currentIndex = arguments.Count;
return ConvertEnumerableValue(argumentSequence, enumerableUnderlyingType, targetType);
}
/// <inheritdoc />
public virtual object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType)
{
@@ -170,32 +214,12 @@ namespace CliFx.Services
// Convert to an enumerable type
else
{
// Convert values to the underlying enumerable type and cast it to dynamic array
var convertedValues = optionInput.Values
.Select(v => ConvertValue(v, enumerableUnderlyingType))
.ToNonGenericArray(enumerableUnderlyingType);
// Get the type of produced array
var convertedValuesType = convertedValues.GetType();
// Try to assign the array (works for T[], IReadOnlyList<T>, IEnumerable<T>, etc)
if (targetType.IsAssignableFrom(convertedValuesType))
return convertedValues;
// Try to inject the array into the constructor (works for HashSet<T>, List<T>, etc)
var arrayConstructor = targetType.GetConstructor(new[] {convertedValuesType});
if (arrayConstructor != null)
return arrayConstructor.Invoke(new object[] {convertedValues});
// Throw if we can't find a way to convert the values
throw new CliFxException(
$"Can't convert a sequence of values [{optionInput.Values.JoinToString(", ")}] " +
$"to type [{targetType}].");
return ConvertEnumerableValue(optionInput.Values, enumerableUnderlyingType, targetType);
}
}
}
public partial class CommandOptionInputConverter
public partial class CommandInputConverter
{
private static ConstructorInfo? GetStringConstructor(Type type) => type.GetConstructor(new[] {typeof(string)});

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CliFx.Internal;
using CliFx.Models;
@@ -33,7 +32,7 @@ namespace CliFx.Services
/// <inheritdoc />
public CommandInput ParseCommandInput(IReadOnlyList<string> commandLineArguments)
{
var commandNameBuilder = new StringBuilder();
var arguments = new List<string>();
var directives = new List<string>();
var optionsDic = new Dictionary<string, List<string>>();
@@ -79,8 +78,7 @@ namespace CliFx.Services
}
else
{
commandNameBuilder.AppendIfNotEmpty(' ');
commandNameBuilder.Append(commandLineArgument);
arguments.Add(commandLineArgument);
}
}
@@ -91,12 +89,11 @@ namespace CliFx.Services
}
}
var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null;
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
var environmentVariables = _environmentVariablesProvider.GetEnvironmentVariables();
return new CommandInput(commandName, directives, options, environmentVariables);
return new CommandInput(arguments, directives, options, environmentVariables);
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Internal;
@@ -14,6 +15,16 @@ namespace CliFx.Services
/// </summary>
public class CommandSchemaResolver : ICommandSchemaResolver
{
private readonly ICommandArgumentSchemasValidator _commandArgumentSchemasValidator;
/// <summary>
/// Creates an instance of <see cref="CommandSchemaResolver"/>.
/// </summary>
public CommandSchemaResolver(ICommandArgumentSchemasValidator commandArgumentSchemasValidator)
{
_commandArgumentSchemasValidator = commandArgumentSchemasValidator;
}
private IReadOnlyList<CommandOptionSchema> GetCommandOptionSchemas(Type commandType)
{
var result = new List<CommandOptionSchema>();
@@ -67,6 +78,24 @@ namespace CliFx.Services
return result;
}
private IReadOnlyList<CommandArgumentSchema> GetCommandArgumentSchemas(Type commandType)
{
var argumentSchemas = commandType.GetProperties()
.Select(p => new { Property = p, Attribute = p.GetCustomAttribute<CommandArgumentAttribute>() })
.Where(a => a.Attribute != null)
.Select(a => new CommandArgumentSchema(a.Property, a.Attribute.Name, a.Attribute.IsRequired, a.Attribute.Description, a.Attribute.Order))
.ToList();
var validationErrors = _commandArgumentSchemasValidator.ValidateArgumentSchemas(argumentSchemas).ToList();
if (validationErrors.Any())
{
throw new CliFxException($"Command type [{commandType}] has invalid argument configuration:\n" +
$"{string.Join("\n", validationErrors.Select(v => v.Message))}");
}
return argumentSchemas;
}
/// <inheritdoc />
public IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes)
{
@@ -108,11 +137,14 @@ namespace CliFx.Services
// Get option schemas
var optionSchemas = GetCommandOptionSchemas(commandType);
// Get argument schemas
var argumentSchemas = GetCommandArgumentSchemas(commandType);
// Build command schema
var commandSchema = new CommandSchema(commandType,
attribute.Name,
attribute.Description,
optionSchemas);
argumentSchemas, optionSchemas);
// Make sure there are no other commands with the same name
var existingCommandWithSameName = result
@@ -131,5 +163,31 @@ namespace CliFx.Services
return result;
}
/// <inheritdoc />
public CommandCandidate? GetTargetCommandSchema(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandInput commandInput)
{
// If no arguments are given, use the default command
CommandSchema targetSchema;
if (!commandInput.Arguments.Any())
{
targetSchema = availableCommandSchemas.FirstOrDefault(c => c.IsDefault());
return targetSchema is null ? null : new CommandCandidate(targetSchema, new string[0], commandInput);
}
// Arguments can be part of the a command name as long as they are single words, i.e. no whitespace characters
var longestPossibleCommandName = string.Join(" ", commandInput.Arguments.TakeWhile(arg => !Regex.IsMatch(arg, @"\s")));
// Find the longest matching schema
var orderedSchemas = availableCommandSchemas.OrderByDescending(x => x.Name?.Length);
targetSchema = orderedSchemas.FirstOrDefault(c => longestPossibleCommandName.StartsWith(c.Name ?? string.Empty, StringComparison.Ordinal))
?? availableCommandSchemas.FirstOrDefault(c => c.IsDefault());
// Get remaining positional arguments
var commandArgumentsCount = targetSchema?.Name?.Split(new []{ ' ' }, StringSplitOptions.RemoveEmptyEntries).Length ?? 0;
var positionalArguments = commandInput.Arguments.Skip(commandArgumentsCount).ToList();
return targetSchema is null ? null : new CommandCandidate(targetSchema, positionalArguments, commandInput);
}
}
}

View File

@@ -19,7 +19,7 @@ namespace CliFx.Services
var row = 0;
// Get built-in option schemas (help and version)
var builtInOptionSchemas = new List<CommandOptionSchema> {CommandOptionSchema.HelpOption};
var builtInOptionSchemas = new List<CommandOptionSchema> { CommandOptionSchema.HelpOption };
if (source.TargetCommandSchema.IsDefault())
builtInOptionSchemas.Add(CommandOptionSchema.VersionOption);
@@ -104,7 +104,7 @@ namespace CliFx.Services
// Description
if (!string.IsNullOrWhiteSpace(source.ApplicationMetadata.Description))
{
Render(source.ApplicationMetadata.Description);
Render(source.ApplicationMetadata.Description!);
RenderNewLine();
}
}
@@ -122,7 +122,7 @@ namespace CliFx.Services
// Description
RenderIndent();
Render(source.TargetCommandSchema.Description);
Render(source.TargetCommandSchema.Description!);
RenderNewLine();
}
@@ -142,7 +142,7 @@ namespace CliFx.Services
if (!string.IsNullOrWhiteSpace(source.TargetCommandSchema.Name))
{
Render(" ");
RenderWithColor(source.TargetCommandSchema.Name, ConsoleColor.Cyan);
RenderWithColor(source.TargetCommandSchema.Name!, ConsoleColor.Cyan);
}
// Child command
@@ -152,12 +152,69 @@ namespace CliFx.Services
RenderWithColor("[command]", ConsoleColor.Cyan);
}
// Arguments
foreach (var argumentSchema in source.TargetCommandSchema.Arguments)
{
Render(" ");
if (!argumentSchema.IsRequired)
Render("[");
Render($"<{argumentSchema.DisplayName}>");
if (!argumentSchema.IsRequired)
Render("]");
}
// Options
Render(" ");
RenderWithColor("[options]", ConsoleColor.White);
RenderNewLine();
}
void RenderArguments()
{
// Do not render anything if the command has no arguments
if (source.TargetCommandSchema.Arguments.Count == 0)
return;
// Margin
RenderMargin();
// Header
RenderHeader("Arguments");
// Order arguments
var orderedArgumentSchemas = source.TargetCommandSchema.Arguments
.Ordered()
.ToArray();
// Arguments
foreach (var argumentSchema in orderedArgumentSchemas)
{
// Is required
if (argumentSchema.IsRequired)
{
RenderWithColor("* ", ConsoleColor.Red);
}
else
{
RenderIndent();
}
// Short name
RenderWithColor($"{argumentSchema.DisplayName}", ConsoleColor.White);
// Description
if (!string.IsNullOrWhiteSpace(argumentSchema.Description))
{
RenderColumnIndent();
Render(argumentSchema.Description!);
}
RenderNewLine();
}
}
void RenderOptions()
{
// Margin
@@ -207,7 +264,7 @@ namespace CliFx.Services
if (!string.IsNullOrWhiteSpace(optionSchema.Description))
{
RenderColumnIndent();
Render(optionSchema.Description);
Render(optionSchema.Description!);
}
RenderNewLine();
@@ -238,7 +295,7 @@ namespace CliFx.Services
if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
{
RenderColumnIndent();
Render(childCommandSchema.Description);
Render(childCommandSchema.Description!);
}
RenderNewLine();
@@ -275,6 +332,7 @@ namespace CliFx.Services
RenderApplicationInfo();
RenderDescription();
RenderUsage();
RenderArguments();
RenderOptions();
RenderChildCommands();
}
@@ -285,6 +343,6 @@ namespace CliFx.Services
private static string? GetRelativeCommandName(CommandSchema commandSchema, CommandSchema parentCommandSchema) =>
string.IsNullOrWhiteSpace(parentCommandSchema.Name) || string.IsNullOrWhiteSpace(commandSchema.Name)
? commandSchema.Name
: commandSchema.Name.Substring(parentCommandSchema.Name.Length + 1);
: commandSchema.Name!.Substring(parentCommandSchema.Name!.Length + 1);
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using CliFx.Models;
namespace CliFx.Services
{
/// <summary>
/// Validates command arguments.
/// </summary>
public interface ICommandArgumentSchemasValidator
{
/// <summary>
/// Validate the given command arguments.
/// </summary>
IEnumerable<ValidationError> ValidateArgumentSchemas(IReadOnlyCollection<CommandArgumentSchema> commandArgumentSchemas);
}
}

View File

@@ -10,6 +10,6 @@ namespace CliFx.Services
/// <summary>
/// Populates an instance of <see cref="ICommand"/> with specified input according to specified schema.
/// </summary>
void InitializeCommand(ICommand command, CommandSchema commandSchema, CommandInput commandInput);
void InitializeCommand(ICommand command, CommandCandidate commandCandidate);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using CliFx.Models;
namespace CliFx.Services
@@ -6,11 +7,16 @@ namespace CliFx.Services
/// <summary>
/// Converts input command options.
/// </summary>
public interface ICommandOptionInputConverter
public interface ICommandInputConverter
{
/// <summary>
/// Converts an option to specified target type.
/// </summary>
object? ConvertOptionInput(CommandOptionInput optionInput, Type targetType);
/// <summary>
/// Converts an argument to specified target type, using up arguments from the given enumerator.
/// </summary>
object? ConvertArgumentInput(IReadOnlyList<string> arguments, ref int currentIndex, Type targetType);
}
}

View File

@@ -13,5 +13,10 @@ namespace CliFx.Services
/// Resolves schemas of specified command types.
/// </summary>
IReadOnlyList<CommandSchema> GetCommandSchemas(IReadOnlyList<Type> commandTypes);
/// <summary>
/// Get the target command schema. The target command is the most specific command that matches the unbound input arguments.
/// </summary>
CommandCandidate? GetTargetCommandSchema(IReadOnlyList<CommandSchema> availableCommandSchemas, CommandInput commandInput);
}
}