mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Add positional arguments (#32)
This commit is contained in:
committed by
Alexey Golub
parent
ed87373dc3
commit
e48839b938
@@ -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.")]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
132
CliFx.Tests/Services/CommandArgumentSchemasValidatorTests.cs
Normal file
132
CliFx.Tests/Services/CommandArgumentSchemasValidatorTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat", new[]
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat", new[]
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(EnvironmentVariableCommand)),
|
||||
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(EnvironmentVariableWithMultipleValuesCommand)),
|
||||
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(EnvironmentVariableCommand)),
|
||||
new CommandInput(null, new[]
|
||||
new string[0],
|
||||
new CommandInput(new string[0], new[]
|
||||
{
|
||||
new CommandOptionInput("opt", new[] { "X" })
|
||||
},
|
||||
EnvironmentVariablesProviderStub.EnvironmentVariables),
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(EnvironmentVariableWithoutCollectionPropertyCommand)),
|
||||
new CommandInput(null, new CommandOptionInput[0], EnvironmentVariablesProviderStub.EnvironmentVariables),
|
||||
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(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div")
|
||||
);
|
||||
new string[0],
|
||||
new CommandInput(new[] { "div" })
|
||||
));
|
||||
|
||||
yield return new TestCaseData(
|
||||
new DivideCommand(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(DivideCommand)),
|
||||
new CommandInput("div", new[]
|
||||
new string[0],
|
||||
new CommandInput(new[] { "div" }, new[]
|
||||
{
|
||||
new CommandOptionInput("D", "13")
|
||||
})
|
||||
);
|
||||
));
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ConcatCommand(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat")
|
||||
);
|
||||
new string[0],
|
||||
new CommandInput(new[] { "concat" })
|
||||
));
|
||||
|
||||
yield return new TestCaseData(
|
||||
new ConcatCommand(),
|
||||
new CommandCandidate(
|
||||
GetCommandSchema(typeof(ConcatCommand)),
|
||||
new CommandInput("concat", new[]
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -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[]
|
||||
{
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -86,13 +88,168 @@ namespace CliFx.Tests.Services
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
25
CliFx.Tests/TestCommands/ArgumentCommand.cs
Normal file
25
CliFx.Tests/TestCommands/ArgumentCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
21
CliFx.Tests/TestCommands/SimpleArgumentCommand.cs
Normal file
21
CliFx.Tests/TestCommands/SimpleArgumentCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
42
CliFx/Attributes/CommandArgumentAttribute.cs
Normal file
42
CliFx/Attributes/CommandArgumentAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"/>.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
CliFx/Models/CommandArgumentSchema.cs
Normal file
78
CliFx/Models/CommandArgumentSchema.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
CliFx/Models/CommandCandidate.cs
Normal file
35
CliFx/Models/CommandCandidate.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
92
CliFx/Services/CommandArgumentSchemasValidator.cs
Normal file
92
CliFx/Services/CommandArgumentSchemasValidator.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
16
CliFx/Services/ICommandArgumentSchemasValidator.cs
Normal file
16
CliFx/Services/ICommandArgumentSchemasValidator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user