mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Refactor (#56)
This commit is contained in:
@@ -6,6 +6,18 @@ namespace CliFx.Tests
|
||||
{
|
||||
public partial class ApplicationSpecs
|
||||
{
|
||||
[Command]
|
||||
private class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class AnotherDefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class NonImplementedCommand
|
||||
{
|
||||
@@ -118,6 +130,24 @@ namespace CliFx.Tests
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class ConflictWithHelpOptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-h", 'h')]
|
||||
public string? OptionH { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class ConflictWithVersionOptionCommand : ICommand
|
||||
{
|
||||
[CommandOption("version")]
|
||||
public string? Version { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class DuplicateOptionEnvironmentVariableNamesCommand : ICommand
|
||||
{
|
||||
@@ -130,12 +160,6 @@ namespace CliFx.Tests
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class ValidCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("hidden", Description = "Description")]
|
||||
private class HiddenPropertiesCommand : ICommand
|
||||
{
|
||||
|
||||
@@ -31,10 +31,10 @@ namespace CliFx.Tests
|
||||
{
|
||||
// Act
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ValidCommand))
|
||||
.AddCommandsFrom(typeof(ValidCommand).Assembly)
|
||||
.AddCommands(new[] {typeof(ValidCommand)})
|
||||
.AddCommandsFrom(new[] {typeof(ValidCommand).Assembly})
|
||||
.AddCommand(typeof(DefaultCommand))
|
||||
.AddCommandsFrom(typeof(DefaultCommand).Assembly)
|
||||
.AddCommands(new[] {typeof(DefaultCommand)})
|
||||
.AddCommandsFrom(new[] {typeof(DefaultCommand).Assembly})
|
||||
.AddCommandsFromThisAssembly()
|
||||
.AllowDebugMode()
|
||||
.AllowPreviewMode()
|
||||
@@ -57,7 +57,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = Array.Empty<Type>();
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(NonImplementedCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(NonAnnotatedCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,18 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateNameCommandA), typeof(DuplicateNameCommandB)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_can_be_default_but_only_if_it_is_the_only_such_command()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(DefaultCommand), typeof(AnotherDefaultCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -101,7 +112,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateParameterOrderCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -112,7 +123,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateParameterNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -123,7 +134,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(MultipleNonScalarParametersCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -134,7 +145,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(NonLastNonScalarParameterCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -145,7 +156,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(EmptyOptionNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -156,7 +167,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(SingleCharacterOptionNameCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -167,7 +178,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateOptionNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -178,7 +189,29 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateOptionShortNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_not_have_conflicts_with_the_implicit_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(ConflictWithHelpOptionCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Command_options_must_not_have_conflicts_with_the_implicit_version_option()
|
||||
{
|
||||
// Arrange
|
||||
var commandTypes = new[] {typeof(ConflictWithVersionOptionCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -189,7 +222,7 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(DuplicateOptionEnvironmentVariableNamesCommand)};
|
||||
|
||||
// Act & assert
|
||||
var ex = Assert.Throws<CliFxException>(() => ApplicationSchema.Resolve(commandTypes));
|
||||
var ex = Assert.Throws<CliFxException>(() => RootSchema.Resolve(commandTypes));
|
||||
_output.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
@@ -200,10 +233,10 @@ namespace CliFx.Tests
|
||||
var commandTypes = new[] {typeof(HiddenPropertiesCommand)};
|
||||
|
||||
// Act
|
||||
var schema = ApplicationSchema.Resolve(commandTypes);
|
||||
var schema = RootSchema.Resolve(commandTypes);
|
||||
|
||||
// Assert
|
||||
schema.Should().BeEquivalentTo(new ApplicationSchema(new[]
|
||||
schema.Should().BeEquivalentTo(new RootSchema(new[]
|
||||
{
|
||||
new CommandSchema(
|
||||
typeof(HiddenPropertiesCommand),
|
||||
@@ -225,7 +258,8 @@ namespace CliFx.Tests
|
||||
'o',
|
||||
"ENV",
|
||||
false,
|
||||
"Option description")
|
||||
"Option description"),
|
||||
CommandOptionSchema.HelpOption
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Tests.Internal;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
@@ -11,13 +13,14 @@ namespace CliFx.Tests
|
||||
public void Input_is_empty_if_no_arguments_are_provided()
|
||||
{
|
||||
// Arrange
|
||||
var args = Array.Empty<string>();
|
||||
var arguments = Array.Empty<string>();
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandLineInput.Parse(args);
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(CommandLineInput.Empty);
|
||||
input.Should().BeEquivalentTo(CommandInput.Empty);
|
||||
}
|
||||
|
||||
public static object[][] DirectivesTestData => new[]
|
||||
@@ -25,7 +28,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.Build()
|
||||
},
|
||||
@@ -33,7 +36,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]", "[debug]"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.AddDirective("debug")
|
||||
.Build()
|
||||
@@ -42,10 +45,13 @@ namespace CliFx.Tests
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DirectivesTestData))]
|
||||
internal void Directive_can_be_enabled_by_specifying_its_name_in_square_brackets(string[] arguments, CommandLineInput expectedInput)
|
||||
internal void Directive_can_be_enabled_by_specifying_its_name_in_square_brackets(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandLineInput.Parse(arguments);
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
@@ -56,7 +62,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option")
|
||||
.Build()
|
||||
},
|
||||
@@ -64,7 +70,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option", "value"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option", "value")
|
||||
.Build()
|
||||
},
|
||||
@@ -72,7 +78,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option", "value1", "value2"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option", "value1", "value2")
|
||||
.Build()
|
||||
},
|
||||
@@ -80,7 +86,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option", "same value"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option", "same value")
|
||||
.Build()
|
||||
},
|
||||
@@ -88,7 +94,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "--option2"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1")
|
||||
.AddOption("option2")
|
||||
.Build()
|
||||
@@ -97,7 +103,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "value1", "--option2", "value2"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1", "value1")
|
||||
.AddOption("option2", "value2")
|
||||
.Build()
|
||||
@@ -106,7 +112,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "value1", "value2", "--option2", "value3", "value4"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1", "value1", "value2")
|
||||
.AddOption("option2", "value3", "value4")
|
||||
.Build()
|
||||
@@ -115,7 +121,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"--option1", "value1", "value2", "--option2"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("option1", "value1", "value2")
|
||||
.AddOption("option2")
|
||||
.Build()
|
||||
@@ -124,10 +130,13 @@ namespace CliFx.Tests
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(OptionsTestData))]
|
||||
internal void Option_can_be_set_by_specifying_its_name_after_two_dashes(string[] arguments, CommandLineInput expectedInput)
|
||||
internal void Option_can_be_set_by_specifying_its_name_after_two_dashes(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandLineInput.Parse(arguments);
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
@@ -138,7 +147,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o")
|
||||
.Build()
|
||||
},
|
||||
@@ -146,7 +155,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o", "value"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o", "value")
|
||||
.Build()
|
||||
},
|
||||
@@ -154,7 +163,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o", "value1", "value2"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o", "value1", "value2")
|
||||
.Build()
|
||||
},
|
||||
@@ -162,7 +171,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-o", "same value"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("o", "same value")
|
||||
.Build()
|
||||
},
|
||||
@@ -170,7 +179,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "-b"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.Build()
|
||||
@@ -179,7 +188,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "value1", "-b", "value2"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a", "value1")
|
||||
.AddOption("b", "value2")
|
||||
.Build()
|
||||
@@ -188,7 +197,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "value1", "value2", "-b", "value3", "value4"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a", "value1", "value2")
|
||||
.AddOption("b", "value3", "value4")
|
||||
.Build()
|
||||
@@ -197,7 +206,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-a", "value1", "value2", "-b"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a", "value1", "value2")
|
||||
.AddOption("b")
|
||||
.Build()
|
||||
@@ -206,7 +215,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-abc"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c")
|
||||
@@ -216,7 +225,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-abc", "value"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c", "value")
|
||||
@@ -226,7 +235,7 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"-abc", "value1", "value2"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
.AddOption("c", "value1", "value2")
|
||||
@@ -236,48 +245,51 @@ namespace CliFx.Tests
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ShortOptionsTestData))]
|
||||
internal void Option_can_be_set_by_specifying_its_short_name_after_a_single_dash(string[] arguments, CommandLineInput expectedInput)
|
||||
internal void Option_can_be_set_by_specifying_its_short_name_after_a_single_dash(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandLineInput.Parse(arguments);
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
}
|
||||
|
||||
public static object[][] UnboundArgumentsTestData => new[]
|
||||
public static object[][] ParametersTestData => new[]
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new[] {"foo"},
|
||||
new CommandLineInputBuilder()
|
||||
.AddUnboundArgument("foo")
|
||||
new CommandInputBuilder()
|
||||
.AddParameter("foo")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"foo", "bar"},
|
||||
new CommandLineInputBuilder()
|
||||
.AddUnboundArgument("foo")
|
||||
.AddUnboundArgument("bar")
|
||||
new CommandInputBuilder()
|
||||
.AddParameter("foo")
|
||||
.AddParameter("bar")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]", "foo"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.AddUnboundArgument("foo")
|
||||
.AddParameter("foo")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"foo", "--option", "value", "-abc"},
|
||||
new CommandLineInputBuilder()
|
||||
.AddUnboundArgument("foo")
|
||||
new CommandInputBuilder()
|
||||
.AddParameter("foo")
|
||||
.AddOption("option", "value")
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
@@ -288,11 +300,11 @@ namespace CliFx.Tests
|
||||
new object[]
|
||||
{
|
||||
new[] {"[preview]", "[debug]", "foo", "bar", "--option", "value", "-abc"},
|
||||
new CommandLineInputBuilder()
|
||||
new CommandInputBuilder()
|
||||
.AddDirective("preview")
|
||||
.AddDirective("debug")
|
||||
.AddUnboundArgument("foo")
|
||||
.AddUnboundArgument("bar")
|
||||
.AddParameter("foo")
|
||||
.AddParameter("bar")
|
||||
.AddOption("option", "value")
|
||||
.AddOption("a")
|
||||
.AddOption("b")
|
||||
@@ -302,11 +314,62 @@ namespace CliFx.Tests
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(UnboundArgumentsTestData))]
|
||||
internal void Any_remaining_arguments_are_treated_as_unbound_arguments(string[] arguments, CommandLineInput expectedInput)
|
||||
[MemberData(nameof(ParametersTestData))]
|
||||
internal void Parameter_can_be_set_by_specifying_the_value_directly(IReadOnlyList<string> arguments, CommandInput expectedInput)
|
||||
{
|
||||
// Arrange
|
||||
var commandNames = Array.Empty<string>();
|
||||
|
||||
// Act
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
}
|
||||
|
||||
public static object[][] CommandNameTestData => new[]
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
new[] {"cmd"},
|
||||
new[] {"cmd"},
|
||||
new CommandInputBuilder()
|
||||
.SetCommandName("cmd")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"cmd"},
|
||||
new[] {"cmd", "foo", "bar", "-o", "value"},
|
||||
new CommandInputBuilder()
|
||||
.SetCommandName("cmd")
|
||||
.AddParameter("foo")
|
||||
.AddParameter("bar")
|
||||
.AddOption("o", "value")
|
||||
.Build()
|
||||
},
|
||||
|
||||
new object[]
|
||||
{
|
||||
new[] {"cmd", "cmd sub"},
|
||||
new[] {"cmd", "sub", "foo"},
|
||||
new CommandInputBuilder()
|
||||
.SetCommandName("cmd sub")
|
||||
.AddParameter("foo")
|
||||
.Build()
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CommandNameTestData))]
|
||||
internal void Command_name_is_matched_from_arguments_that_come_before_parameters(
|
||||
IReadOnlyList<string> commandNames,
|
||||
IReadOnlyList<string> arguments,
|
||||
CommandInput expectedInput)
|
||||
{
|
||||
// Act
|
||||
var input = CommandLineInput.Parse(arguments);
|
||||
var input = CommandInput.Parse(arguments, commandNames);
|
||||
|
||||
// Assert
|
||||
input.Should().BeEquivalentTo(expectedInput);
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace CliFx.Tests
|
||||
[Fact]
|
||||
public async Task Command_can_perform_additional_cleanup_if_cancellation_is_requested()
|
||||
{
|
||||
// Can't test it with a real console because CliWrap can't send Ctrl+C
|
||||
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Internal;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
@@ -9,7 +15,31 @@ namespace CliFx.Tests
|
||||
public partial class DirectivesSpecs
|
||||
{
|
||||
[Fact]
|
||||
public async Task Preview_directive_can_be_enabled_to_print_provided_arguments_as_they_were_parsed()
|
||||
public async Task Debug_directive_can_be_specified_to_have_the_application_wait_until_debugger_is_attached()
|
||||
{
|
||||
// We can't actually attach a debugger in tests, so instead just cancel execution after some time
|
||||
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
|
||||
var stdOut = new StringBuilder();
|
||||
|
||||
var command = Cli.Wrap("dotnet")
|
||||
.WithArguments(a => a
|
||||
.Add(Dummy.Program.Location)
|
||||
.Add("[debug]"))
|
||||
.WithEnvironmentVariables(e => e
|
||||
.Set("ENV_TARGET", "Mars")) | stdOut;
|
||||
|
||||
// Act
|
||||
await command.ExecuteAsync(cts.Token).Task.IgnoreCancellation();
|
||||
var stdOutData = stdOut.ToString();
|
||||
|
||||
// Assert
|
||||
stdOutData.Should().Contain("Attach debugger to");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Preview_directive_can_be_specified_to_print_provided_arguments_as_they_were_parsed()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
@@ -30,7 +60,7 @@ namespace CliFx.Tests
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
stdOutData.Should().ContainAll("cmd", "<param>", "[-a]", "[-b]", "[-c]", "[--option foo]");
|
||||
stdOutData.Should().ContainAll("cmd", "<param>", "[-a]", "[-b]", "[-c]", "[--option \"foo\"]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Tests.Internal;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
using FluentAssertions;
|
||||
@@ -53,19 +54,17 @@ namespace CliFx.Tests
|
||||
public void Option_of_non_scalar_type_can_take_multiple_separated_values_from_an_environment_variable()
|
||||
{
|
||||
// Arrange
|
||||
var schema = ApplicationSchema.Resolve(new[] {typeof(EnvironmentVariableCollectionCommand)});
|
||||
|
||||
var input = CommandLineInput.Empty;
|
||||
var input = CommandInput.Empty;
|
||||
var envVars = new Dictionary<string, string>
|
||||
{
|
||||
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
|
||||
};
|
||||
|
||||
// Act
|
||||
var command = schema.InitializeEntryPoint(input, envVars);
|
||||
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCollectionCommand>(input, envVars);
|
||||
|
||||
// Assert
|
||||
command.Should().BeEquivalentTo(new EnvironmentVariableCollectionCommand
|
||||
instance.Should().BeEquivalentTo(new EnvironmentVariableCollectionCommand
|
||||
{
|
||||
Option = new[] {"foo", "bar"}
|
||||
});
|
||||
@@ -75,19 +74,17 @@ namespace CliFx.Tests
|
||||
public void Option_of_scalar_type_can_only_take_a_single_value_from_an_environment_variable_even_if_it_contains_separators()
|
||||
{
|
||||
// Arrange
|
||||
var schema = ApplicationSchema.Resolve(new[] {typeof(EnvironmentVariableCommand)});
|
||||
|
||||
var input = CommandLineInput.Empty;
|
||||
var input = CommandInput.Empty;
|
||||
var envVars = new Dictionary<string, string>
|
||||
{
|
||||
["ENV_OPT"] = $"foo{Path.PathSeparator}bar"
|
||||
};
|
||||
|
||||
// Act
|
||||
var command = schema.InitializeEntryPoint(input, envVars);
|
||||
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCommand>(input, envVars);
|
||||
|
||||
// Assert
|
||||
command.Should().BeEquivalentTo(new EnvironmentVariableCommand
|
||||
instance.Should().BeEquivalentTo(new EnvironmentVariableCommand
|
||||
{
|
||||
Option = $"foo{Path.PathSeparator}bar"
|
||||
});
|
||||
|
||||
@@ -25,53 +25,10 @@ namespace CliFx.Tests
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
|
||||
}
|
||||
[CommandOption("show-help")]
|
||||
public bool ShowHelp { get; set; }
|
||||
|
||||
[Command("exc")]
|
||||
private class ShowHelpTextOnlyCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(null, showHelp: true);
|
||||
}
|
||||
|
||||
[Command("exc sub")]
|
||||
private class ShowHelpTextOnlySubCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("exc")]
|
||||
private class ShowErrorMessageThenHelpTextCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) =>
|
||||
throw new CommandException("Error message.", showHelp: true);
|
||||
}
|
||||
|
||||
[Command("exc sub")]
|
||||
private class ShowErrorMessageThenHelpTextSubCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("exc")]
|
||||
private class StackTraceOnlyCommand : ICommand
|
||||
{
|
||||
[CommandOption("msg", 'm')]
|
||||
public string? Message { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(null);
|
||||
}
|
||||
|
||||
[Command("inv")]
|
||||
private class InvalidUserInputCommand : ICommand
|
||||
{
|
||||
[CommandOption("required", 'r')]
|
||||
public string? RequiredOption { get; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,104 +82,7 @@ namespace CliFx.Tests
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"exc", "-m", "Kaput"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().NotBeEmpty();
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_may_throw_a_specialized_exception_which_shows_only_the_help_text()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
await using var stdErr = new MemoryStream();
|
||||
|
||||
var console = new VirtualConsole(output: stdOut);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ShowHelpTextOnlyCommand))
|
||||
.AddCommand(typeof(ShowHelpTextOnlySubCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"exc"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
var stdErrData = console.Output.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
stdErrData.Should().BeEmpty();
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"[command]",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text.",
|
||||
"Commands",
|
||||
"sub",
|
||||
"You can run", "to show help on a specific command."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdErrData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_may_throw_specialized_exception_which_shows_the_error_message_then_the_help_text()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
await using var stdErr = new MemoryStream();
|
||||
var console = new VirtualConsole(output: stdOut, error: stdErr);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(ShowErrorMessageThenHelpTextCommand))
|
||||
.AddCommand(typeof(ShowErrorMessageThenHelpTextSubCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] {"exc"});
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
stdErrData.Should().Be("Error message.");
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"[command]",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text.",
|
||||
"Commands",
|
||||
"sub",
|
||||
"You can run", "to show help on a specific command."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdErrData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_may_throw_a_specialized_exception_which_shows_only_a_stack_trace_and_no_help_text()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdErr = new MemoryStream();
|
||||
var console = new VirtualConsole(error: stdErr);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(GenericExceptionCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"exc", "-m", "Kaput"},
|
||||
new[] {"exc"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
@@ -187,15 +90,51 @@ namespace CliFx.Tests
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().ContainAll(
|
||||
"System.Exception:",
|
||||
"Kaput", "at",
|
||||
"CliFx.Exceptions.CommandException:",
|
||||
"at",
|
||||
"CliFx.Tests");
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_shows_help_text_on_exceptions_related_to_invalid_user_input()
|
||||
public async Task Command_may_throw_a_specialized_exception_which_exits_and_prints_help_text()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
await using var stdErr = new MemoryStream();
|
||||
|
||||
var console = new VirtualConsole(output: stdOut, error: stdErr);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(CommandExceptionCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
new[] {"exc", "-m", "Kaput", "--show-help"},
|
||||
new Dictionary<string, string>());
|
||||
|
||||
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().Be("Kaput");
|
||||
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdErrData);
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Command_shows_help_text_on_invalid_user_input()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
@@ -203,7 +142,7 @@ namespace CliFx.Tests
|
||||
var console = new VirtualConsole(output: stdOut, error: stdErr);
|
||||
|
||||
var application = new CliApplicationBuilder()
|
||||
.AddCommand(typeof(InvalidUserInputCommand))
|
||||
.AddCommand(typeof(CommandExceptionCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
@@ -217,22 +156,17 @@ namespace CliFx.Tests
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
stdErrData.Should().ContainAll(
|
||||
"Can't find a command that matches the following arguments:",
|
||||
"not-a-valid-command"
|
||||
);
|
||||
stdErrData.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"[command]",
|
||||
"Options",
|
||||
"-h|--help", "Shows help text.",
|
||||
"Commands",
|
||||
"inv",
|
||||
"You can run", "to show help on a specific command."
|
||||
"-h|--help", "Shows help text."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
_output.WriteLine(stdErrData);
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,14 @@ namespace CliFx.Tests
|
||||
[Command("cmd-with-req-opts")]
|
||||
private class RequiredOptionsCommand : ICommand
|
||||
{
|
||||
[CommandOption("option-f", 'f', IsRequired = true)]
|
||||
public string? OptionF { get; set; }
|
||||
[CommandOption("option-a", 'a', IsRequired = true)]
|
||||
public string? OptionA { get; set; }
|
||||
|
||||
[CommandOption("option-g", 'g', IsRequired = true)]
|
||||
public IEnumerable<int>? OptionG { get; set; }
|
||||
[CommandOption("option-b", 'b', IsRequired = true)]
|
||||
public IEnumerable<int>? OptionB { get; set; }
|
||||
|
||||
[CommandOption("option-h", 'h')]
|
||||
public string? OptionH { get; set; }
|
||||
[CommandOption("option-c", 'c')]
|
||||
public string? OptionC { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
@@ -230,18 +230,18 @@ namespace CliFx.Tests
|
||||
// Assert
|
||||
stdOutData.Should().ContainAll(
|
||||
"Usage",
|
||||
"cmd-with-req-opts", "--option-f <value>", "--option-g <values...>", "[options]",
|
||||
"cmd-with-req-opts", "--option-a <value>", "--option-b <values...>", "[options]",
|
||||
"Options",
|
||||
"* -f|--option-f",
|
||||
"* -g|--option-g",
|
||||
"-h|--option-h"
|
||||
"* -a|--option-a",
|
||||
"* -b|--option-b",
|
||||
"-c|--option-c"
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_shows_usage_format_which_lists_all_valid_values_for_enum_arguments()
|
||||
public async Task Help_text_lists_all_valid_values_for_enum_arguments()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
@@ -253,7 +253,7 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] { "cmd-with-enum-args", "--help" });
|
||||
await application.RunAsync(new[] {"cmd-with-enum-args", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
@@ -261,10 +261,10 @@ namespace CliFx.Tests
|
||||
"Usage",
|
||||
"cmd-with-enum-args", "[options]",
|
||||
"Parameters",
|
||||
"value", "Valid values: Value1, Value2, Value3.",
|
||||
"value", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
|
||||
"Options",
|
||||
"* --value", "Enum option.", "Valid values: Value1, Value2, Value3.",
|
||||
"--nullable-value", "Nullable enum option.", "Valid values: Value1, Value2, Value3."
|
||||
"* --value", "Enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\".",
|
||||
"--nullable-value", "Nullable enum option.", "Valid values: \"Value1\", \"Value2\", \"Value3\"."
|
||||
);
|
||||
|
||||
_output.WriteLine(stdOutData);
|
||||
@@ -297,7 +297,7 @@ namespace CliFx.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_text_shows_usage_format_which_lists_all_default_values_for_non_required_options()
|
||||
public async Task Help_text_shows_default_values_for_non_required_options()
|
||||
{
|
||||
// Arrange
|
||||
await using var stdOut = new MemoryStream();
|
||||
@@ -309,7 +309,7 @@ namespace CliFx.Tests
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
await application.RunAsync(new[] { "cmd-with-defaults", "--help" });
|
||||
await application.RunAsync(new[] {"cmd-with-defaults", "--help"});
|
||||
var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
|
||||
|
||||
// Assert
|
||||
|
||||
23
CliFx.Tests/Internal/CommandHelper.cs
Normal file
23
CliFx.Tests/Internal/CommandHelper.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Domain;
|
||||
|
||||
namespace CliFx.Tests.Internal
|
||||
{
|
||||
internal static class CommandHelper
|
||||
{
|
||||
public static TCommand ResolveCommand<TCommand>(CommandInput input, IReadOnlyDictionary<string, string> environmentVariables)
|
||||
where TCommand : ICommand, new()
|
||||
{
|
||||
var schema = CommandSchema.TryResolve(typeof(TCommand))!;
|
||||
|
||||
var instance = new TCommand();
|
||||
schema.Bind(instance, input, environmentVariables);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static TCommand ResolveCommand<TCommand>(CommandInput input)
|
||||
where TCommand : ICommand, new() =>
|
||||
ResolveCommand<TCommand>(input, new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
45
CliFx.Tests/Internal/CommandInputBuilder.cs
Normal file
45
CliFx.Tests/Internal/CommandInputBuilder.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Domain;
|
||||
|
||||
namespace CliFx.Tests.Internal
|
||||
{
|
||||
internal class CommandInputBuilder
|
||||
{
|
||||
private readonly List<CommandDirectiveInput> _directives = new List<CommandDirectiveInput>();
|
||||
private readonly List<CommandParameterInput> _parameters = new List<CommandParameterInput>();
|
||||
private readonly List<CommandOptionInput> _options = new List<CommandOptionInput>();
|
||||
|
||||
private string? _commandName;
|
||||
|
||||
public CommandInputBuilder SetCommandName(string commandName)
|
||||
{
|
||||
_commandName = commandName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputBuilder AddDirective(string directive)
|
||||
{
|
||||
_directives.Add(new CommandDirectiveInput(directive));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputBuilder AddParameter(string parameter)
|
||||
{
|
||||
_parameters.Add(new CommandParameterInput(parameter));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputBuilder AddOption(string alias, params string[] values)
|
||||
{
|
||||
_options.Add(new CommandOptionInput(alias, values));
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInput Build() => new CommandInput(
|
||||
_directives,
|
||||
_commandName,
|
||||
_parameters,
|
||||
_options
|
||||
);
|
||||
}
|
||||
}
|
||||
19
CliFx.Tests/Internal/TaskExtensions.cs
Normal file
19
CliFx.Tests/Internal/TaskExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CliFx.Tests.Internal
|
||||
{
|
||||
internal static class TaskExtensions
|
||||
{
|
||||
public static async Task IgnoreCancellation(this Task task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Domain;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
@@ -13,7 +14,7 @@ namespace CliFx
|
||||
/// <summary>
|
||||
/// Command line application facade.
|
||||
/// </summary>
|
||||
public class CliApplication
|
||||
public partial class CliApplication
|
||||
{
|
||||
private readonly ApplicationMetadata _metadata;
|
||||
private readonly ApplicationConfiguration _configuration;
|
||||
@@ -34,15 +35,14 @@ namespace CliFx
|
||||
_console = console;
|
||||
_typeActivator = typeActivator;
|
||||
|
||||
_helpTextWriter = new HelpTextWriter(metadata, console, typeActivator);
|
||||
_helpTextWriter = new HelpTextWriter(metadata, console);
|
||||
}
|
||||
|
||||
private async ValueTask<int?> HandleDebugDirectiveAsync(CommandLineInput commandLineInput)
|
||||
{
|
||||
var isDebugMode = _configuration.IsDebugModeAllowed && commandLineInput.IsDebugDirectiveSpecified;
|
||||
if (!isDebugMode)
|
||||
return null;
|
||||
private void WriteError(string message) => _console.WithForegroundColor(ConsoleColor.Red, () =>
|
||||
_console.Error.WriteLine(message));
|
||||
|
||||
private async ValueTask WaitForDebuggerAsync()
|
||||
{
|
||||
var processId = ProcessEx.GetCurrentProcessId();
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.Green, () =>
|
||||
@@ -50,31 +50,21 @@ namespace CliFx
|
||||
|
||||
while (!Debugger.IsAttached)
|
||||
await Task.Delay(100);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private int? HandlePreviewDirective(ApplicationSchema applicationSchema, CommandLineInput commandLineInput)
|
||||
private void WriteCommandLineInput(CommandInput input)
|
||||
{
|
||||
var isPreviewMode = _configuration.IsPreviewModeAllowed && commandLineInput.IsPreviewDirectiveSpecified;
|
||||
if (!isPreviewMode)
|
||||
return null;
|
||||
|
||||
var commandSchema = applicationSchema.TryFindCommand(commandLineInput, out var argumentOffset);
|
||||
|
||||
_console.Output.WriteLine("Parser preview:");
|
||||
|
||||
// Command name
|
||||
if (commandSchema != null && argumentOffset > 0)
|
||||
if (!string.IsNullOrWhiteSpace(input.CommandName))
|
||||
{
|
||||
_console.WithForegroundColor(ConsoleColor.Cyan, () =>
|
||||
_console.Output.Write(commandSchema.Name));
|
||||
_console.Output.Write(input.CommandName));
|
||||
|
||||
_console.Output.Write(' ');
|
||||
}
|
||||
|
||||
// Parameters
|
||||
foreach (var parameter in commandLineInput.UnboundArguments.Skip(argumentOffset))
|
||||
foreach (var parameter in input.Parameters)
|
||||
{
|
||||
_console.Output.Write('<');
|
||||
|
||||
@@ -86,118 +76,133 @@ namespace CliFx
|
||||
}
|
||||
|
||||
// Options
|
||||
foreach (var option in commandLineInput.Options)
|
||||
foreach (var option in input.Options)
|
||||
{
|
||||
_console.Output.Write('[');
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.White, () =>
|
||||
_console.Output.Write(option));
|
||||
{
|
||||
// Alias
|
||||
_console.Output.Write(option.GetRawAlias());
|
||||
|
||||
// Values
|
||||
if (option.Values.Any())
|
||||
{
|
||||
_console.Output.Write(' ');
|
||||
_console.Output.Write(option.GetRawValues());
|
||||
}
|
||||
});
|
||||
|
||||
_console.Output.Write(']');
|
||||
_console.Output.Write(' ');
|
||||
}
|
||||
|
||||
_console.Output.WriteLine();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int? HandleVersionOption(CommandLineInput commandLineInput)
|
||||
{
|
||||
// Version option is available only on the default command (i.e. when arguments are not specified)
|
||||
var shouldRenderVersion = !commandLineInput.UnboundArguments.Any() && commandLineInput.IsVersionOptionSpecified;
|
||||
if (!shouldRenderVersion)
|
||||
return null;
|
||||
|
||||
_console.Output.WriteLine(_metadata.VersionText);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int? HandleHelpOption(ApplicationSchema applicationSchema, CommandLineInput commandLineInput)
|
||||
{
|
||||
// Help is rendered either when it's requested or when the user provides no arguments and there is no default command
|
||||
var shouldRenderHelp =
|
||||
commandLineInput.IsHelpOptionSpecified ||
|
||||
!applicationSchema.Commands.Any(c => c.IsDefault) && !commandLineInput.UnboundArguments.Any() && !commandLineInput.Options.Any();
|
||||
|
||||
if (!shouldRenderHelp)
|
||||
return null;
|
||||
|
||||
// Get the command schema that matches the input or use a dummy default command as a fallback
|
||||
var commandSchema =
|
||||
applicationSchema.TryFindCommand(commandLineInput) ??
|
||||
CommandSchema.StubDefaultCommand.Schema;
|
||||
|
||||
_helpTextWriter.Write(applicationSchema, commandSchema);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async ValueTask<int> HandleCommandExecutionAsync(
|
||||
ApplicationSchema applicationSchema,
|
||||
CommandLineInput commandLineInput,
|
||||
IReadOnlyDictionary<string, string> environmentVariables)
|
||||
{
|
||||
await applicationSchema
|
||||
.InitializeEntryPoint(commandLineInput, environmentVariables, _typeActivator)
|
||||
.ExecuteAsync(_console);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int HandleCliFxException(IReadOnlyList<string> commandLineArguments, CliFxException ex)
|
||||
{
|
||||
var showHelp = ex.ShowHelp;
|
||||
|
||||
var errorMessage = ex.HasMessage
|
||||
? ex.Message
|
||||
: ex.ToString();
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
|
||||
|
||||
if (showHelp)
|
||||
{
|
||||
var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
|
||||
var commandLineInput = CommandLineInput.Parse(commandLineArguments);
|
||||
var commandSchema = applicationSchema.TryFindCommand(commandLineInput) ??
|
||||
CommandSchema.StubDefaultCommand.Schema;
|
||||
_helpTextWriter.Write(applicationSchema, commandSchema);
|
||||
}
|
||||
|
||||
return ex.ExitCode;
|
||||
}
|
||||
private ICommand GetCommandInstance(CommandSchema command) =>
|
||||
command != StubDefaultCommand.Schema
|
||||
? (ICommand) _typeActivator.CreateInstance(command.Type)
|
||||
: new StubDefaultCommand();
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a <see cref="CommandException"/> is thrown during command execution, it will be handled and routed to the console.
|
||||
/// Additionally, if the debugger is not attached (i.e. the app is running in production), all other exceptions thrown within
|
||||
/// this method will be handled and routed to the console as well.
|
||||
/// </remarks>
|
||||
public async ValueTask<int> RunAsync(
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
IReadOnlyDictionary<string, string> environmentVariables)
|
||||
{
|
||||
try
|
||||
{
|
||||
var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
|
||||
var commandLineInput = CommandLineInput.Parse(commandLineArguments);
|
||||
var root = RootSchema.Resolve(_configuration.CommandTypes);
|
||||
var input = CommandInput.Parse(commandLineArguments, root.GetCommandNames());
|
||||
|
||||
return
|
||||
await HandleDebugDirectiveAsync(commandLineInput) ??
|
||||
HandlePreviewDirective(applicationSchema, commandLineInput) ??
|
||||
HandleVersionOption(commandLineInput) ??
|
||||
HandleHelpOption(applicationSchema, commandLineInput) ??
|
||||
await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
|
||||
// Debug mode
|
||||
if (_configuration.IsDebugModeAllowed && input.IsDebugDirectiveSpecified)
|
||||
{
|
||||
// Ensure debugger is attached and continue
|
||||
await WaitForDebuggerAsync();
|
||||
}
|
||||
|
||||
// Preview mode
|
||||
if (_configuration.IsPreviewModeAllowed && input.IsPreviewDirectiveSpecified)
|
||||
{
|
||||
WriteCommandLineInput(input);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
// Try to get the command matching the input or fallback to default
|
||||
var command =
|
||||
root.TryFindCommand(input.CommandName) ??
|
||||
root.TryFindDefaultCommand() ??
|
||||
StubDefaultCommand.Schema;
|
||||
|
||||
// Version option
|
||||
if (command.IsVersionOptionAvailable && input.IsVersionOptionSpecified)
|
||||
{
|
||||
_console.Output.WriteLine(_metadata.VersionText);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
// Get command instance (also used in help text)
|
||||
var instance = GetCommandInstance(command);
|
||||
|
||||
// To avoid instantiating the command twice, we need to get default values
|
||||
// before the arguments are bound to the properties
|
||||
var defaultValues = command.GetArgumentValues(instance);
|
||||
|
||||
// Help option
|
||||
if (command.IsHelpOptionAvailable && input.IsHelpOptionSpecified ||
|
||||
command == StubDefaultCommand.Schema && !input.Parameters.Any() && !input.Options.Any())
|
||||
{
|
||||
_helpTextWriter.Write(root, command, defaultValues);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
// Bind arguments
|
||||
try
|
||||
{
|
||||
command.Bind(instance, input, environmentVariables);
|
||||
}
|
||||
// This may throw exceptions which are useful only to the end-user
|
||||
catch (CliFxException ex)
|
||||
{
|
||||
WriteError(ex.ToString());
|
||||
_helpTextWriter.Write(root, command, defaultValues);
|
||||
|
||||
return ExitCode.FromException(ex);
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
try
|
||||
{
|
||||
await instance.ExecuteAsync(_console);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
// Swallow command exceptions and route them to the console
|
||||
catch (CommandException ex)
|
||||
{
|
||||
WriteError(ex.ToString());
|
||||
|
||||
if (ex.ShowHelp)
|
||||
_helpTextWriter.Write(root, command, defaultValues);
|
||||
|
||||
return ex.ExitCode;
|
||||
}
|
||||
}
|
||||
catch (CliFxException ex)
|
||||
// To prevent the app from showing the annoying Windows troubleshooting dialog,
|
||||
// we handle all exceptions and route them to the console nicely.
|
||||
// However, we don't want to swallow unhandled exceptions when the debugger is attached,
|
||||
// because we still want the IDE to show them to the developer.
|
||||
catch (Exception ex) when (!Debugger.IsAttached)
|
||||
{
|
||||
// Some exceptions may specify exit code or request help
|
||||
return HandleCliFxException(commandLineArguments, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We want to catch all exceptions in order to print errors and return correct exit codes.
|
||||
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
|
||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.ToString()));
|
||||
return ex.HResult;
|
||||
WriteError(ex.ToString());
|
||||
return ExitCode.FromException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +210,11 @@ namespace CliFx
|
||||
/// Runs the application with specified command line arguments and returns the exit code.
|
||||
/// Environment variables are retrieved automatically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a <see cref="CommandException"/> is thrown during command execution, it will be handled and routed to the console.
|
||||
/// Additionally, if the debugger is not attached (i.e. the app is running in production), all other exceptions thrown within
|
||||
/// this method will be handled and routed to the console as well.
|
||||
/// </remarks>
|
||||
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
var environmentVariables = Environment.GetEnvironmentVariables()
|
||||
@@ -218,6 +228,11 @@ namespace CliFx
|
||||
/// Runs the application and returns the exit code.
|
||||
/// Command line arguments and environment variables are retrieved automatically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a <see cref="CommandException"/> is thrown during command execution, it will be handled and routed to the console.
|
||||
/// Additionally, if the debugger is not attached (i.e. the app is running in production), all other exceptions thrown within
|
||||
/// this method will be handled and routed to the console as well.
|
||||
/// </remarks>
|
||||
public async ValueTask<int> RunAsync()
|
||||
{
|
||||
var commandLineArguments = Environment.GetCommandLineArgs()
|
||||
@@ -227,4 +242,25 @@ namespace CliFx
|
||||
return await RunAsync(commandLineArguments);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class CliApplication
|
||||
{
|
||||
private static class ExitCode
|
||||
{
|
||||
public const int Success = 0;
|
||||
|
||||
public static int FromException(Exception ex) =>
|
||||
ex is CommandException cmdEx
|
||||
? cmdEx.ExitCode
|
||||
: ex.HResult;
|
||||
}
|
||||
|
||||
[Command]
|
||||
private class StubDefaultCommand : ICommand
|
||||
{
|
||||
public static CommandSchema Schema { get; } = CommandSchema.TryResolve(typeof(StubDefaultCommand))!;
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,9 +159,9 @@ namespace CliFx
|
||||
/// </summary>
|
||||
public CliApplication Build()
|
||||
{
|
||||
_title ??= GetDefaultTitle() ?? "App";
|
||||
_executableName ??= GetDefaultExecutableName() ?? "app";
|
||||
_versionText ??= GetDefaultVersionText() ?? "v1.0";
|
||||
_title ??= TryGetDefaultTitle() ?? "App";
|
||||
_executableName ??= TryGetDefaultExecutableName() ?? "app";
|
||||
_versionText ??= TryGetDefaultVersionText() ?? "v1.0";
|
||||
_console ??= new SystemConsole();
|
||||
_typeActivator ??= new DefaultTypeActivator();
|
||||
|
||||
@@ -179,9 +179,9 @@ namespace CliFx
|
||||
// Entry assembly is null in tests
|
||||
private static Assembly? EntryAssembly => LazyEntryAssembly.Value;
|
||||
|
||||
private static string? GetDefaultTitle() => EntryAssembly?.GetName().Name;
|
||||
private static string? TryGetDefaultTitle() => EntryAssembly?.GetName().Name;
|
||||
|
||||
private static string? GetDefaultExecutableName()
|
||||
private static string? TryGetDefaultExecutableName()
|
||||
{
|
||||
var entryAssemblyLocation = EntryAssembly?.Location;
|
||||
|
||||
@@ -193,7 +193,7 @@ namespace CliFx
|
||||
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
|
||||
}
|
||||
|
||||
private static string? GetDefaultVersionText() =>
|
||||
private static string? TryGetDefaultVersionText() =>
|
||||
EntryAssembly != null
|
||||
? $"v{EntryAssembly.GetName().Version.ToSemanticString()}"
|
||||
: null;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Exceptions;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal partial class ApplicationSchema
|
||||
{
|
||||
public IReadOnlyList<CommandSchema> Commands { get; }
|
||||
|
||||
public ApplicationSchema(IReadOnlyList<CommandSchema> commands)
|
||||
{
|
||||
Commands = commands;
|
||||
}
|
||||
|
||||
public CommandSchema? TryFindParentCommand(string? childCommandName)
|
||||
{
|
||||
// Default command has no parent
|
||||
if (string.IsNullOrWhiteSpace(childCommandName))
|
||||
return null;
|
||||
|
||||
// Try to find the parent command by repeatedly biting off chunks of its name
|
||||
var route = childCommandName.Split(' ');
|
||||
for (var i = route.Length - 1; i >= 1; i--)
|
||||
{
|
||||
var potentialParentCommandName = string.Join(" ", route.Take(i));
|
||||
var matchingParentCommand = Commands.FirstOrDefault(c => c.MatchesName(potentialParentCommandName));
|
||||
|
||||
if (matchingParentCommand != null)
|
||||
return matchingParentCommand;
|
||||
}
|
||||
|
||||
// If there's no parent - fall back to default command
|
||||
return Commands.FirstOrDefault(c => c.IsDefault);
|
||||
}
|
||||
|
||||
public IReadOnlyList<CommandSchema> GetChildCommands(string? parentCommandName) =>
|
||||
!string.IsNullOrWhiteSpace(parentCommandName) || Commands.Any(c => c.IsDefault)
|
||||
? Commands.Where(c => TryFindParentCommand(c.Name)?.MatchesName(parentCommandName) == true).ToArray()
|
||||
: Commands.Where(c => !string.IsNullOrWhiteSpace(c.Name) && TryFindParentCommand(c.Name) == null).ToArray();
|
||||
|
||||
// TODO: this out parameter is not a really nice design
|
||||
public CommandSchema? TryFindCommand(CommandLineInput commandLineInput, out int argumentOffset)
|
||||
{
|
||||
// Try to find the command that contains the most of the input arguments in its name
|
||||
for (var i = commandLineInput.UnboundArguments.Count; i >= 0; i--)
|
||||
{
|
||||
var potentialCommandName = string.Join(" ", commandLineInput.UnboundArguments.Take(i));
|
||||
var matchingCommand = Commands.FirstOrDefault(c => c.MatchesName(potentialCommandName));
|
||||
|
||||
if (matchingCommand != null)
|
||||
{
|
||||
argumentOffset = i;
|
||||
return matchingCommand;
|
||||
}
|
||||
}
|
||||
|
||||
argumentOffset = 0;
|
||||
return Commands.FirstOrDefault(c => c.IsDefault);
|
||||
}
|
||||
|
||||
public CommandSchema? TryFindCommand(CommandLineInput commandLineInput) =>
|
||||
TryFindCommand(commandLineInput, out _);
|
||||
|
||||
public ICommand InitializeEntryPoint(
|
||||
CommandLineInput commandLineInput,
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
ITypeActivator activator)
|
||||
{
|
||||
var command = TryFindCommand(commandLineInput, out var argumentOffset) ??
|
||||
throw CliFxException.CannotFindMatchingCommand(commandLineInput);
|
||||
|
||||
var parameterInputs = argumentOffset == 0
|
||||
? commandLineInput.UnboundArguments.ToArray()
|
||||
: commandLineInput.UnboundArguments.Skip(argumentOffset).ToArray();
|
||||
|
||||
return command.CreateInstance(parameterInputs, commandLineInput.Options, environmentVariables, activator);
|
||||
}
|
||||
|
||||
public ICommand InitializeEntryPoint(
|
||||
CommandLineInput commandLineInput,
|
||||
IReadOnlyDictionary<string, string> environmentVariables) =>
|
||||
InitializeEntryPoint(commandLineInput, environmentVariables, new DefaultTypeActivator());
|
||||
|
||||
public ICommand InitializeEntryPoint(CommandLineInput commandLineInput) =>
|
||||
InitializeEntryPoint(commandLineInput, new Dictionary<string, string>());
|
||||
|
||||
public override string ToString() => string.Join(Environment.NewLine, Commands);
|
||||
}
|
||||
|
||||
internal partial class ApplicationSchema
|
||||
{
|
||||
private static void ValidateParameters(CommandSchema command)
|
||||
{
|
||||
var duplicateOrderGroup = command.Parameters
|
||||
.GroupBy(a => a.Order)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateOrderGroup != null)
|
||||
{
|
||||
throw CliFxException.CommandParametersDuplicateOrder(
|
||||
command,
|
||||
duplicateOrderGroup.Key,
|
||||
duplicateOrderGroup.ToArray());
|
||||
}
|
||||
|
||||
var duplicateNameGroup = command.Parameters
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.Name))
|
||||
.GroupBy(a => a.Name!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw CliFxException.CommandParametersDuplicateName(
|
||||
command,
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var nonScalarParameters = command.Parameters
|
||||
.Where(p => !p.IsScalar)
|
||||
.ToArray();
|
||||
|
||||
if (nonScalarParameters.Length > 1)
|
||||
{
|
||||
throw CliFxException.CommandParametersTooManyNonScalar(
|
||||
command,
|
||||
nonScalarParameters);
|
||||
}
|
||||
|
||||
var nonLastNonScalarParameter = command.Parameters
|
||||
.OrderByDescending(a => a.Order)
|
||||
.Skip(1)
|
||||
.LastOrDefault(p => !p.IsScalar);
|
||||
|
||||
if (nonLastNonScalarParameter != null)
|
||||
{
|
||||
throw CliFxException.CommandParametersNonLastNonScalar(
|
||||
command,
|
||||
nonLastNonScalarParameter);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateOptions(CommandSchema command)
|
||||
{
|
||||
var noNameGroup = command.Options
|
||||
.Where(o => o.ShortName == null && string.IsNullOrWhiteSpace(o.Name))
|
||||
.ToArray();
|
||||
|
||||
if (noNameGroup.Any())
|
||||
{
|
||||
throw CliFxException.CommandOptionsNoName(
|
||||
command,
|
||||
noNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var invalidLengthNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
|
||||
.Where(o => o.Name!.Length <= 1)
|
||||
.ToArray();
|
||||
|
||||
if (invalidLengthNameGroup.Any())
|
||||
{
|
||||
throw CliFxException.CommandOptionsInvalidLengthName(
|
||||
command,
|
||||
invalidLengthNameGroup);
|
||||
}
|
||||
|
||||
var duplicateNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
|
||||
.GroupBy(o => o.Name!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw CliFxException.CommandOptionsDuplicateName(
|
||||
command,
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var duplicateShortNameGroup = command.Options
|
||||
.Where(o => o.ShortName != null)
|
||||
.GroupBy(o => o.ShortName!.Value)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateShortNameGroup != null)
|
||||
{
|
||||
throw CliFxException.CommandOptionsDuplicateShortName(
|
||||
command,
|
||||
duplicateShortNameGroup.Key,
|
||||
duplicateShortNameGroup.ToArray());
|
||||
}
|
||||
|
||||
var duplicateEnvironmentVariableNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.EnvironmentVariableName))
|
||||
.GroupBy(o => o.EnvironmentVariableName!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateEnvironmentVariableNameGroup != null)
|
||||
{
|
||||
throw CliFxException.CommandOptionsDuplicateEnvironmentVariableName(
|
||||
command,
|
||||
duplicateEnvironmentVariableNameGroup.Key,
|
||||
duplicateEnvironmentVariableNameGroup.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
|
||||
{
|
||||
if (!commands.Any())
|
||||
{
|
||||
throw CliFxException.CommandsNotRegistered();
|
||||
}
|
||||
|
||||
var duplicateNameGroup = commands
|
||||
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(duplicateNameGroup.Key))
|
||||
throw CliFxException.CommandsDuplicateName(
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray());
|
||||
|
||||
throw CliFxException.CommandsTooManyDefaults(duplicateNameGroup.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public static ApplicationSchema Resolve(IReadOnlyList<Type> commandTypes)
|
||||
{
|
||||
var commands = new List<CommandSchema>();
|
||||
|
||||
foreach (var commandType in commandTypes)
|
||||
{
|
||||
var command = CommandSchema.TryResolve(commandType) ??
|
||||
throw CliFxException.InvalidCommandType(commandType);
|
||||
|
||||
ValidateParameters(command);
|
||||
ValidateOptions(command);
|
||||
|
||||
commands.Add(command);
|
||||
}
|
||||
|
||||
ValidateCommands(commands);
|
||||
|
||||
return new ApplicationSchema(commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -11,20 +10,21 @@ namespace CliFx.Domain
|
||||
{
|
||||
internal abstract partial class CommandArgumentSchema
|
||||
{
|
||||
public PropertyInfo Property { get; }
|
||||
// Property can be null on built-in arguments (help and version options)
|
||||
public PropertyInfo? Property { get; }
|
||||
|
||||
public string? Description { get; }
|
||||
|
||||
public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null;
|
||||
|
||||
protected CommandArgumentSchema(PropertyInfo property, string? description)
|
||||
protected CommandArgumentSchema(PropertyInfo? property, string? description)
|
||||
{
|
||||
Property = property;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
private Type? TryGetEnumerableArgumentUnderlyingType() =>
|
||||
Property.PropertyType != typeof(string)
|
||||
Property != null && Property.PropertyType != typeof(string)
|
||||
? Property.PropertyType.GetEnumerableUnderlyingType()
|
||||
: null;
|
||||
|
||||
@@ -93,6 +93,10 @@ namespace CliFx.Domain
|
||||
|
||||
private object? Convert(IReadOnlyList<string> values)
|
||||
{
|
||||
// Short-circuit built-in arguments
|
||||
if (Property == null)
|
||||
return null;
|
||||
|
||||
var targetType = Property.PropertyType;
|
||||
var enumerableUnderlyingType = TryGetEnumerableArgumentUnderlyingType();
|
||||
|
||||
@@ -110,61 +114,26 @@ namespace CliFx.Domain
|
||||
}
|
||||
}
|
||||
|
||||
public void Inject(ICommand command, IReadOnlyList<string> values) =>
|
||||
Property.SetValue(command, Convert(values));
|
||||
public void BindOn(ICommand command, IReadOnlyList<string> values) =>
|
||||
Property?.SetValue(command, Convert(values));
|
||||
|
||||
public void Inject(ICommand command, params string[] values) =>
|
||||
Inject(command, (IReadOnlyList<string>) values);
|
||||
public void BindOn(ICommand command, params string[] values) =>
|
||||
BindOn(command, (IReadOnlyList<string>) values);
|
||||
|
||||
public IReadOnlyList<string> GetValidValues()
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
// Some arguments may have this as null due to a hack that enables built-in options
|
||||
// TODO fix this
|
||||
if (Property == null)
|
||||
return result;
|
||||
return Array.Empty<string>();
|
||||
|
||||
var underlyingType =
|
||||
Property.PropertyType.GetNullableUnderlyingType() ?? Property.PropertyType;
|
||||
Property.PropertyType.GetNullableUnderlyingType() ??
|
||||
Property.PropertyType;
|
||||
|
||||
// Enum
|
||||
if (underlyingType.IsEnum)
|
||||
result.AddRange(Enum.GetNames(underlyingType));
|
||||
return Enum.GetNames(underlyingType);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string? TryGetDefaultValue(ICommand instance)
|
||||
{
|
||||
// Some arguments may have this as null due to a hack that enables built-in options
|
||||
// TODO fix this
|
||||
if (Property == null)
|
||||
return null;
|
||||
|
||||
var rawDefaultValue = Property.GetValue(instance);
|
||||
|
||||
if (!(rawDefaultValue is string) && rawDefaultValue is IEnumerable rawDefaultValues)
|
||||
{
|
||||
var elementType = rawDefaultValues.GetType().GetEnumerableUnderlyingType() ?? typeof(object);
|
||||
|
||||
return elementType.IsToStringOverriden()
|
||||
? rawDefaultValues
|
||||
.Cast<object?>()
|
||||
.Where(o => o != null)
|
||||
.Select(o => o!.ToFormattableString(FormatProvider).Quote())
|
||||
.JoinToString(" ")
|
||||
: null;
|
||||
}
|
||||
|
||||
if (rawDefaultValue != null && !Equals(rawDefaultValue, rawDefaultValue.GetType().GetDefaultValue()))
|
||||
{
|
||||
return rawDefaultValue.GetType().IsToStringOverriden()
|
||||
? rawDefaultValue.ToFormattableString(FormatProvider).Quote()
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
238
CliFx/Domain/CommandInput.cs
Normal file
238
CliFx/Domain/CommandInput.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal partial class CommandInput
|
||||
{
|
||||
public IReadOnlyList<CommandDirectiveInput> Directives { get; }
|
||||
|
||||
public string? CommandName { get; }
|
||||
|
||||
public IReadOnlyList<CommandParameterInput> Parameters { get; }
|
||||
|
||||
public IReadOnlyList<CommandOptionInput> Options { get; }
|
||||
|
||||
public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective);
|
||||
|
||||
public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective);
|
||||
|
||||
public bool IsHelpOptionSpecified => Options.Any(o => o.IsHelpOption);
|
||||
|
||||
public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption);
|
||||
|
||||
public CommandInput(
|
||||
IReadOnlyList<CommandDirectiveInput> directives,
|
||||
string? commandName,
|
||||
IReadOnlyList<CommandParameterInput> parameters,
|
||||
IReadOnlyList<CommandOptionInput> options)
|
||||
{
|
||||
Directives = directives;
|
||||
CommandName = commandName;
|
||||
Parameters = parameters;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
foreach (var directive in Directives)
|
||||
{
|
||||
buffer
|
||||
.AppendIfNotEmpty(' ')
|
||||
.Append(directive);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(CommandName))
|
||||
{
|
||||
buffer
|
||||
.AppendIfNotEmpty(' ')
|
||||
.Append(CommandName);
|
||||
}
|
||||
|
||||
foreach (var parameter in Parameters)
|
||||
{
|
||||
buffer
|
||||
.AppendIfNotEmpty(' ')
|
||||
.Append(parameter);
|
||||
}
|
||||
|
||||
foreach (var option in Options)
|
||||
{
|
||||
buffer
|
||||
.AppendIfNotEmpty(' ')
|
||||
.Append(option);
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class CommandInput
|
||||
{
|
||||
private static IReadOnlyList<CommandDirectiveInput> ParseDirectives(
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
ref int index)
|
||||
{
|
||||
var result = new List<CommandDirectiveInput>();
|
||||
|
||||
for (; index < commandLineArguments.Count; index++)
|
||||
{
|
||||
var argument = commandLineArguments[index];
|
||||
|
||||
if (!argument.StartsWith('[') || !argument.EndsWith(']'))
|
||||
break;
|
||||
|
||||
var name = argument.Substring(1, argument.Length - 2);
|
||||
result.Add(new CommandDirectiveInput(name));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? ParseCommandName(
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
ISet<string> commandNames,
|
||||
ref int index)
|
||||
{
|
||||
var buffer = new List<string>();
|
||||
|
||||
var commandName = default(string?);
|
||||
var lastIndex = index;
|
||||
|
||||
// We need to look ahead to see if we can match as many consecutive arguments to a command name as possible
|
||||
for (var i = index; i < commandLineArguments.Count; i++)
|
||||
{
|
||||
var argument = commandLineArguments[i];
|
||||
buffer.Add(argument);
|
||||
|
||||
var potentialCommandName = buffer.JoinToString(" ");
|
||||
|
||||
if (commandNames.Contains(potentialCommandName))
|
||||
{
|
||||
commandName = potentialCommandName;
|
||||
lastIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the index only if command name was found in the arguments
|
||||
if (!string.IsNullOrWhiteSpace(commandName))
|
||||
index = lastIndex + 1;
|
||||
|
||||
return commandName;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CommandParameterInput> ParseParameters(
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
ref int index)
|
||||
{
|
||||
var result = new List<CommandParameterInput>();
|
||||
|
||||
for (; index < commandLineArguments.Count; index++)
|
||||
{
|
||||
var argument = commandLineArguments[index];
|
||||
|
||||
if (argument.StartsWith('-'))
|
||||
break;
|
||||
|
||||
result.Add(new CommandParameterInput(argument));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CommandOptionInput> ParseOptions(
|
||||
IReadOnlyList<string> commandLineArguments,
|
||||
ref int index)
|
||||
{
|
||||
var result = new List<CommandOptionInput>();
|
||||
|
||||
var currentOptionAlias = default(string?);
|
||||
var currentOptionValues = new List<string>();
|
||||
|
||||
for (; index < commandLineArguments.Count; index++)
|
||||
{
|
||||
var argument = commandLineArguments[index];
|
||||
|
||||
// Name
|
||||
if (argument.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
// Flush previous
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
result.Add(new CommandOptionInput(currentOptionAlias, currentOptionValues));
|
||||
|
||||
currentOptionAlias = argument.Substring(2);
|
||||
currentOptionValues = new List<string>();
|
||||
}
|
||||
// Short name
|
||||
else if (argument.StartsWith('-'))
|
||||
{
|
||||
foreach (var alias in argument.Substring(1))
|
||||
{
|
||||
// Flush previous
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
result.Add(new CommandOptionInput(currentOptionAlias, currentOptionValues));
|
||||
|
||||
currentOptionAlias = alias.AsString();
|
||||
currentOptionValues = new List<string>();
|
||||
}
|
||||
}
|
||||
// Value
|
||||
else if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
{
|
||||
currentOptionValues.Add(argument);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush last option
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
result.Add(new CommandOptionInput(currentOptionAlias, currentOptionValues));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static CommandInput Parse(IReadOnlyList<string> commandLineArguments, IReadOnlyList<string> availableCommandNames)
|
||||
{
|
||||
var availableCommandNamesSet = availableCommandNames.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var index = 0;
|
||||
|
||||
var directives = ParseDirectives(
|
||||
commandLineArguments,
|
||||
ref index
|
||||
);
|
||||
|
||||
var commandName = ParseCommandName(
|
||||
commandLineArguments,
|
||||
availableCommandNamesSet,
|
||||
ref index
|
||||
);
|
||||
|
||||
var parameters = ParseParameters(
|
||||
commandLineArguments,
|
||||
ref index
|
||||
);
|
||||
|
||||
var options = ParseOptions(
|
||||
commandLineArguments,
|
||||
ref index
|
||||
);
|
||||
|
||||
return new CommandInput(directives, commandName, parameters, options);
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class CommandInput
|
||||
{
|
||||
public static CommandInput Empty { get; } = new CommandInput(
|
||||
Array.Empty<CommandDirectiveInput>(),
|
||||
null,
|
||||
Array.Empty<CommandParameterInput>(),
|
||||
Array.Empty<CommandOptionInput>()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal partial class CommandLineInput
|
||||
{
|
||||
public IReadOnlyList<CommandDirectiveInput> Directives { get; }
|
||||
|
||||
public IReadOnlyList<CommandUnboundArgumentInput> UnboundArguments { get; }
|
||||
|
||||
public IReadOnlyList<CommandOptionInput> Options { get; }
|
||||
|
||||
public bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective);
|
||||
|
||||
public bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective);
|
||||
|
||||
public bool IsHelpOptionSpecified => Options.Any(o => o.IsHelpOption);
|
||||
|
||||
public bool IsVersionOptionSpecified => Options.Any(o => o.IsVersionOption);
|
||||
|
||||
public CommandLineInput(
|
||||
IReadOnlyList<CommandDirectiveInput> directives,
|
||||
IReadOnlyList<CommandUnboundArgumentInput> unboundArguments,
|
||||
IReadOnlyList<CommandOptionInput> options)
|
||||
{
|
||||
Directives = directives;
|
||||
UnboundArguments = unboundArguments;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
foreach (var directive in Directives)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
buffer.Append(directive);
|
||||
}
|
||||
|
||||
foreach (var argument in UnboundArguments)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
buffer.Append(argument);
|
||||
}
|
||||
|
||||
foreach (var option in Options)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
buffer.Append(option);
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class CommandLineInput
|
||||
{
|
||||
public static CommandLineInput Parse(IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
var builder = new CommandLineInputBuilder();
|
||||
|
||||
var currentOptionAlias = "";
|
||||
var currentOptionValues = new List<string>();
|
||||
|
||||
bool TryParseDirective(string argument)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
return false;
|
||||
|
||||
if (!argument.StartsWith("[", StringComparison.OrdinalIgnoreCase) ||
|
||||
!argument.EndsWith("]", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var directive = argument.Substring(1, argument.Length - 2);
|
||||
builder.AddDirective(directive);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseArgument(string argument)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
return false;
|
||||
|
||||
builder.AddUnboundArgument(argument);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseOptionName(string argument)
|
||||
{
|
||||
if (!argument.StartsWith("--", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
builder.AddOption(currentOptionAlias, currentOptionValues);
|
||||
|
||||
currentOptionAlias = argument.Substring(2);
|
||||
currentOptionValues = new List<string>();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseOptionShortName(string argument)
|
||||
{
|
||||
if (!argument.StartsWith("-", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
foreach (var c in argument.Substring(1))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
builder.AddOption(currentOptionAlias, currentOptionValues);
|
||||
|
||||
currentOptionAlias = c.AsString();
|
||||
currentOptionValues = new List<string>();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseOptionValue(string argument)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
return false;
|
||||
|
||||
currentOptionValues.Add(argument);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var argument in commandLineArguments)
|
||||
{
|
||||
var _ =
|
||||
TryParseOptionName(argument) ||
|
||||
TryParseOptionShortName(argument) ||
|
||||
TryParseDirective(argument) ||
|
||||
TryParseArgument(argument) ||
|
||||
TryParseOptionValue(argument);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||
builder.AddOption(currentOptionAlias, currentOptionValues);
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class CommandLineInput
|
||||
{
|
||||
private static IReadOnlyList<CommandDirectiveInput> EmptyDirectives { get; } = new CommandDirectiveInput[0];
|
||||
|
||||
private static IReadOnlyList<CommandUnboundArgumentInput> EmptyUnboundArguments { get; } = new CommandUnboundArgumentInput[0];
|
||||
|
||||
private static IReadOnlyList<CommandOptionInput> EmptyOptions { get; } = new CommandOptionInput[0];
|
||||
|
||||
public static CommandLineInput Empty { get; } = new CommandLineInput(EmptyDirectives, EmptyUnboundArguments, EmptyOptions);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal class CommandLineInputBuilder
|
||||
{
|
||||
private readonly List<CommandDirectiveInput> _directives = new List<CommandDirectiveInput>();
|
||||
private readonly List<CommandUnboundArgumentInput> _unboundArguments = new List<CommandUnboundArgumentInput>();
|
||||
private readonly List<CommandOptionInput> _options = new List<CommandOptionInput>();
|
||||
|
||||
public CommandLineInputBuilder AddDirective(CommandDirectiveInput directive)
|
||||
{
|
||||
_directives.Add(directive);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandLineInputBuilder AddDirective(string directive) =>
|
||||
AddDirective(new CommandDirectiveInput(directive));
|
||||
|
||||
public CommandLineInputBuilder AddUnboundArgument(CommandUnboundArgumentInput unboundArgument)
|
||||
{
|
||||
_unboundArguments.Add(unboundArgument);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandLineInputBuilder AddUnboundArgument(string unboundArgument) =>
|
||||
AddUnboundArgument(new CommandUnboundArgumentInput(unboundArgument));
|
||||
|
||||
public CommandLineInputBuilder AddOption(CommandOptionInput option)
|
||||
{
|
||||
_options.Add(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandLineInputBuilder AddOption(string optionAlias, IReadOnlyList<string> values) =>
|
||||
AddOption(new CommandOptionInput(optionAlias, values));
|
||||
|
||||
public CommandLineInputBuilder AddOption(string optionAlias, params string[] values) =>
|
||||
AddOption(optionAlias, (IReadOnlyList<string>) values);
|
||||
|
||||
public CommandLineInput Build() => new CommandLineInput(_directives, _unboundArguments, _options);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Domain
|
||||
@@ -8,10 +8,6 @@ namespace CliFx.Domain
|
||||
{
|
||||
public string Alias { get; }
|
||||
|
||||
public string RawAlias => Alias.Length > 1
|
||||
? $"--{Alias}"
|
||||
: $"-{Alias}";
|
||||
|
||||
public IReadOnlyList<string> Values { get; }
|
||||
|
||||
public bool IsHelpOption => CommandOptionSchema.HelpOption.MatchesNameOrShortName(Alias);
|
||||
@@ -24,28 +20,15 @@ namespace CliFx.Domain
|
||||
Values = values;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
public string GetRawAlias() => Alias switch
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
{Length: 0} => Alias,
|
||||
{Length: 1} => $"-{Alias}",
|
||||
_ => $"--{Alias}"
|
||||
};
|
||||
|
||||
buffer.Append(RawAlias);
|
||||
public string GetRawValues() => Values.Select(v => v.Quote()).JoinToString(" ");
|
||||
|
||||
foreach (var value in Values)
|
||||
{
|
||||
buffer.AppendIfNotEmpty(' ');
|
||||
|
||||
var isEscaped = value.Contains(" ");
|
||||
|
||||
if (isEscaped)
|
||||
buffer.Append('"');
|
||||
|
||||
buffer.Append(value);
|
||||
|
||||
if (isEscaped)
|
||||
buffer.Append('"');
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
public override string ToString() => $"{GetRawAlias()} {GetRawValues()}";
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace CliFx.Domain
|
||||
public bool IsRequired { get; }
|
||||
|
||||
public CommandOptionSchema(
|
||||
PropertyInfo property,
|
||||
PropertyInfo? property,
|
||||
string? name,
|
||||
char? shortName,
|
||||
string? environmentVariableName,
|
||||
@@ -73,7 +73,7 @@ namespace CliFx.Domain
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public string GetInternalDisplayString() => $"{Property.Name} ('{GetUserFacingDisplayString()}')";
|
||||
public string GetInternalDisplayString() => $"{Property?.Name ?? "<implicit>"} ('{GetUserFacingDisplayString()}')";
|
||||
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
@@ -86,9 +86,12 @@ namespace CliFx.Domain
|
||||
if (attribute == null)
|
||||
return null;
|
||||
|
||||
// The user may mistakenly specify dashes, thinking it's required, so trim them
|
||||
var name = attribute.Name?.TrimStart('-');
|
||||
|
||||
return new CommandOptionSchema(
|
||||
property,
|
||||
attribute.Name,
|
||||
name,
|
||||
attribute.ShortName,
|
||||
attribute.EnvironmentVariableName,
|
||||
attribute.IsRequired,
|
||||
@@ -100,9 +103,9 @@ namespace CliFx.Domain
|
||||
internal partial class CommandOptionSchema
|
||||
{
|
||||
public static CommandOptionSchema HelpOption { get; } =
|
||||
new CommandOptionSchema(null!, "help", 'h', null, false, "Shows help text.");
|
||||
new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text.");
|
||||
|
||||
public static CommandOptionSchema VersionOption { get; } =
|
||||
new CommandOptionSchema(null!, "version", null, null, false, "Shows version information.");
|
||||
new CommandOptionSchema(null, "version", null, null, false, "Shows version information.");
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal class CommandUnboundArgumentInput
|
||||
internal class CommandParameterInput
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public CommandUnboundArgumentInput(string value) => Value = value;
|
||||
public CommandParameterInput(string value) => Value = value;
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace CliFx.Domain
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public CommandParameterSchema(PropertyInfo property, int order, string name, string? description)
|
||||
public CommandParameterSchema(PropertyInfo? property, int order, string name, string? description)
|
||||
: base(property, description)
|
||||
{
|
||||
Order = order;
|
||||
@@ -29,7 +29,7 @@ namespace CliFx.Domain
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public string GetInternalDisplayString() => $"{Property.Name} ([{Order}] {GetUserFacingDisplayString()})";
|
||||
public string GetInternalDisplayString() => $"{Property?.Name ?? "<implicit>"} ([{Order}] {GetUserFacingDisplayString()})";
|
||||
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
@@ -24,6 +24,10 @@ namespace CliFx.Domain
|
||||
|
||||
public IReadOnlyList<CommandOptionSchema> Options { get; }
|
||||
|
||||
public bool IsHelpOptionAvailable => Options.Contains(CommandOptionSchema.HelpOption);
|
||||
|
||||
public bool IsVersionOptionAvailable => Options.Contains(CommandOptionSchema.VersionOption);
|
||||
|
||||
public CommandSchema(
|
||||
Type type,
|
||||
string? name,
|
||||
@@ -34,28 +38,42 @@ namespace CliFx.Domain
|
||||
Type = type;
|
||||
Name = name;
|
||||
Description = description;
|
||||
Options = options;
|
||||
Parameters = parameters;
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public bool MatchesName(string? name) => string.Equals(name, Name, StringComparison.OrdinalIgnoreCase);
|
||||
public bool MatchesName(string? name) =>
|
||||
!string.IsNullOrWhiteSpace(Name)
|
||||
? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase)
|
||||
: string.IsNullOrWhiteSpace(name);
|
||||
|
||||
public IReadOnlyList<CommandOptionSchema> GetBuiltInOptions()
|
||||
public IEnumerable<CommandArgumentSchema> GetArguments()
|
||||
{
|
||||
var result = new List<CommandOptionSchema>(2);
|
||||
foreach (var parameter in Parameters)
|
||||
yield return parameter;
|
||||
|
||||
var helpOption = CommandOptionSchema.HelpOption;
|
||||
var versionOption = CommandOptionSchema.VersionOption;
|
||||
foreach (var option in Options)
|
||||
yield return option;
|
||||
}
|
||||
|
||||
result.Add(helpOption);
|
||||
public IReadOnlyDictionary<CommandArgumentSchema, object?> GetArgumentValues(ICommand instance)
|
||||
{
|
||||
var result = new Dictionary<CommandArgumentSchema, object?>();
|
||||
|
||||
if (IsDefault)
|
||||
result.Add(versionOption);
|
||||
foreach (var argument in GetArguments())
|
||||
{
|
||||
// Skip built-in arguments
|
||||
if (argument.Property == null)
|
||||
continue;
|
||||
|
||||
var value = argument.Property.GetValue(instance);
|
||||
result[argument] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void InjectParameters(ICommand command, IReadOnlyList<CommandUnboundArgumentInput> parameterInputs)
|
||||
private void BindParameters(ICommand instance, IReadOnlyList<CommandParameterInput> parameterInputs)
|
||||
{
|
||||
// All inputs must be bound
|
||||
var remainingParameterInputs = parameterInputs.ToList();
|
||||
@@ -68,14 +86,14 @@ namespace CliFx.Domain
|
||||
|
||||
for (var i = 0; i < scalarParameters.Length; i++)
|
||||
{
|
||||
var scalarParameter = scalarParameters[i];
|
||||
var parameter = scalarParameters[i];
|
||||
|
||||
var scalarParameterInput = i < parameterInputs.Count
|
||||
var scalarInput = i < parameterInputs.Count
|
||||
? parameterInputs[i]
|
||||
: throw CliFxException.ParameterNotSet(scalarParameter);
|
||||
: throw CliFxException.ParameterNotSet(parameter);
|
||||
|
||||
scalarParameter.Inject(command, scalarParameterInput.Value);
|
||||
remainingParameterInputs.Remove(scalarParameterInput);
|
||||
parameter.BindOn(instance, scalarInput.Value);
|
||||
remainingParameterInputs.Remove(scalarInput);
|
||||
}
|
||||
|
||||
// Non-scalar parameter (only one is allowed)
|
||||
@@ -85,21 +103,23 @@ namespace CliFx.Domain
|
||||
|
||||
if (nonScalarParameter != null)
|
||||
{
|
||||
var nonScalarParameterValues = parameterInputs.Skip(scalarParameters.Length).Select(i => i.Value).ToArray();
|
||||
// TODO: Should it verify that at least one value is passed?
|
||||
var nonScalarValues = parameterInputs
|
||||
.Skip(scalarParameters.Length)
|
||||
.Select(p => p.Value)
|
||||
.ToArray();
|
||||
|
||||
nonScalarParameter.Inject(command, nonScalarParameterValues);
|
||||
nonScalarParameter.BindOn(instance, nonScalarValues);
|
||||
remainingParameterInputs.Clear();
|
||||
}
|
||||
|
||||
// Ensure all inputs were bound
|
||||
if (remainingParameterInputs.Any())
|
||||
{
|
||||
throw CliFxException.UnrecognizedParametersProvided(remainingParameterInputs);
|
||||
}
|
||||
}
|
||||
|
||||
private void InjectOptions(
|
||||
ICommand command,
|
||||
private void BindOptions(
|
||||
ICommand instance,
|
||||
IReadOnlyList<CommandOptionInput> optionInputs,
|
||||
IReadOnlyDictionary<string, string> environmentVariables)
|
||||
{
|
||||
@@ -113,19 +133,17 @@ namespace CliFx.Domain
|
||||
foreach (var (name, value) in environmentVariables)
|
||||
{
|
||||
var option = Options.FirstOrDefault(o => o.MatchesEnvironmentVariableName(name));
|
||||
if (option == null)
|
||||
continue;
|
||||
|
||||
if (option != null)
|
||||
{
|
||||
var values = option.IsScalar
|
||||
? new[] {value}
|
||||
: value.Split(Path.PathSeparator);
|
||||
var values = option.IsScalar
|
||||
? new[] {value}
|
||||
: value.Split(Path.PathSeparator);
|
||||
|
||||
option.Inject(command, values);
|
||||
unsetRequiredOptions.Remove(option);
|
||||
}
|
||||
option.BindOn(instance, values);
|
||||
unsetRequiredOptions.Remove(option);
|
||||
}
|
||||
|
||||
// TODO: refactor this part? I wrote this while sick
|
||||
// Direct input
|
||||
foreach (var option in Options)
|
||||
{
|
||||
@@ -133,49 +151,57 @@ namespace CliFx.Domain
|
||||
.Where(i => option.MatchesNameOrShortName(i.Alias))
|
||||
.ToArray();
|
||||
|
||||
if (inputs.Any())
|
||||
{
|
||||
var inputValues = inputs.SelectMany(i => i.Values).ToArray();
|
||||
option.Inject(command, inputValues);
|
||||
// Skip if the inputs weren't provided for this option
|
||||
if (!inputs.Any())
|
||||
continue;
|
||||
|
||||
foreach (var input in inputs)
|
||||
remainingOptionInputs.Remove(input);
|
||||
var inputValues = inputs.SelectMany(i => i.Values).ToArray();
|
||||
option.BindOn(instance, inputValues);
|
||||
|
||||
if (inputValues.Any())
|
||||
unsetRequiredOptions.Remove(option);
|
||||
}
|
||||
}
|
||||
remainingOptionInputs.RemoveRange(inputs);
|
||||
|
||||
// Ensure all required options were set
|
||||
if (unsetRequiredOptions.Any())
|
||||
{
|
||||
throw CliFxException.RequiredOptionsNotSet(unsetRequiredOptions);
|
||||
// Required option implies that the value has to be set and also be non-empty
|
||||
if (inputValues.Any())
|
||||
unsetRequiredOptions.Remove(option);
|
||||
}
|
||||
|
||||
// Ensure all inputs were bound
|
||||
if (remainingOptionInputs.Any())
|
||||
{
|
||||
throw CliFxException.UnrecognizedOptionsProvided(remainingOptionInputs);
|
||||
}
|
||||
|
||||
// Ensure all required options were set
|
||||
if (unsetRequiredOptions.Any())
|
||||
throw CliFxException.RequiredOptionsNotSet(unsetRequiredOptions);
|
||||
}
|
||||
|
||||
public ICommand CreateInstance(
|
||||
IReadOnlyList<CommandUnboundArgumentInput> parameterInputs,
|
||||
IReadOnlyList<CommandOptionInput> optionInputs,
|
||||
IReadOnlyDictionary<string, string> environmentVariables,
|
||||
ITypeActivator activator)
|
||||
public void Bind(
|
||||
ICommand instance,
|
||||
CommandInput input,
|
||||
IReadOnlyDictionary<string, string> environmentVariables)
|
||||
{
|
||||
var command = (ICommand) activator.CreateInstance(Type);
|
||||
|
||||
InjectParameters(command, parameterInputs);
|
||||
InjectOptions(command, optionInputs, environmentVariables);
|
||||
|
||||
return command;
|
||||
BindParameters(instance, input.Parameters);
|
||||
BindOptions(instance, input.Options, environmentVariables);
|
||||
}
|
||||
|
||||
public string GetUserFacingDisplayString() => Name ?? "";
|
||||
public string GetInternalDisplayString()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
public string GetInternalDisplayString() => $"{Type.FullName} ('{GetUserFacingDisplayString()}')";
|
||||
// Type
|
||||
buffer.Append(Type.FullName);
|
||||
|
||||
// Name
|
||||
buffer
|
||||
.Append(' ')
|
||||
.Append('(')
|
||||
.Append(IsDefault
|
||||
? "<default command>"
|
||||
: $"'{Name}'"
|
||||
)
|
||||
.Append(')');
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
public override string ToString() => GetInternalDisplayString();
|
||||
}
|
||||
@@ -195,6 +221,12 @@ namespace CliFx.Domain
|
||||
|
||||
var attribute = type.GetCustomAttribute<CommandAttribute>();
|
||||
|
||||
var name = attribute?.Name;
|
||||
|
||||
var builtInOptions = string.IsNullOrWhiteSpace(name)
|
||||
? new[] {CommandOptionSchema.HelpOption, CommandOptionSchema.VersionOption}
|
||||
: new[] {CommandOptionSchema.HelpOption};
|
||||
|
||||
var parameters = type.GetProperties()
|
||||
.Select(CommandParameterSchema.TryResolve)
|
||||
.Where(p => p != null)
|
||||
@@ -203,27 +235,16 @@ namespace CliFx.Domain
|
||||
var options = type.GetProperties()
|
||||
.Select(CommandOptionSchema.TryResolve)
|
||||
.Where(o => o != null)
|
||||
.Concat(builtInOptions)
|
||||
.ToArray();
|
||||
|
||||
return new CommandSchema(
|
||||
type,
|
||||
attribute?.Name,
|
||||
name,
|
||||
attribute?.Description,
|
||||
parameters!,
|
||||
options!
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class CommandSchema
|
||||
{
|
||||
// TODO: won't work with dep injection
|
||||
[Command]
|
||||
public class StubDefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
|
||||
public static CommandSchema Schema { get; } = TryResolve(typeof(StubDefaultCommand))!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal class HelpTextWriter
|
||||
internal partial class HelpTextWriter
|
||||
{
|
||||
private readonly ApplicationMetadata _metadata;
|
||||
private readonly IConsole _console;
|
||||
private readonly ITypeActivator _typeActivator;
|
||||
|
||||
private int _column;
|
||||
private int _row;
|
||||
|
||||
private bool IsEmpty => _column == 0 && _row == 0;
|
||||
|
||||
public HelpTextWriter(ApplicationMetadata metadata, IConsole console, ITypeActivator typeActivator)
|
||||
public HelpTextWriter(ApplicationMetadata metadata, IConsole console)
|
||||
{
|
||||
_metadata = metadata;
|
||||
_console = console;
|
||||
_typeActivator = typeActivator;
|
||||
}
|
||||
|
||||
private void Write(char value)
|
||||
@@ -49,23 +49,17 @@ namespace CliFx.Domain
|
||||
|
||||
private void WriteVerticalMargin(int size = 1)
|
||||
{
|
||||
if (IsEmpty)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < size; i++)
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
private void WriteHorizontalMargin(int size = 2)
|
||||
{
|
||||
if (IsEmpty)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < size; i++)
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
private void WriteHorizontalColumnMargin(int columnSize = 20, int offsetSize = 2)
|
||||
private void WriteColumnMargin(int columnSize = 20, int offsetSize = 2)
|
||||
{
|
||||
if (_column + offsetSize < columnSize)
|
||||
WriteHorizontalMargin(columnSize - _column);
|
||||
@@ -79,11 +73,8 @@ namespace CliFx.Domain
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
private void WriteApplicationInfo(CommandSchema commandSchema)
|
||||
private void WriteApplicationInfo()
|
||||
{
|
||||
if (!commandSchema.IsDefault)
|
||||
return;
|
||||
|
||||
// Title and version
|
||||
Write(ConsoleColor.Yellow, _metadata.Title);
|
||||
Write(' ');
|
||||
@@ -99,24 +90,26 @@ namespace CliFx.Domain
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCommandDescription(CommandSchema commandSchema)
|
||||
private void WriteCommandDescription(CommandSchema command)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandSchema.Description))
|
||||
if (string.IsNullOrWhiteSpace(command.Description))
|
||||
return;
|
||||
|
||||
WriteVerticalMargin();
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Description");
|
||||
|
||||
WriteHorizontalMargin();
|
||||
Write(commandSchema.Description);
|
||||
Write(command.Description);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
private void WriteCommandUsage(
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandSchema> childCommandSchemas)
|
||||
private void WriteCommandUsage(CommandSchema command, IReadOnlyList<CommandSchema> childCommands)
|
||||
{
|
||||
WriteVerticalMargin();
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Usage");
|
||||
|
||||
// Exe name
|
||||
@@ -124,40 +117,40 @@ namespace CliFx.Domain
|
||||
Write(_metadata.ExecutableName);
|
||||
|
||||
// Command name
|
||||
if (!string.IsNullOrWhiteSpace(commandSchema.Name))
|
||||
if (!string.IsNullOrWhiteSpace(command.Name))
|
||||
{
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, commandSchema.Name);
|
||||
Write(ConsoleColor.Cyan, command.Name);
|
||||
}
|
||||
|
||||
// Child command placeholder
|
||||
if (childCommandSchemas.Any())
|
||||
if (childCommands.Any())
|
||||
{
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, "[command]");
|
||||
}
|
||||
|
||||
// Parameters
|
||||
foreach (var parameterSchema in commandSchema.Parameters)
|
||||
foreach (var parameter in command.Parameters)
|
||||
{
|
||||
Write(' ');
|
||||
Write(parameterSchema.IsScalar
|
||||
? $"<{parameterSchema.Name}>"
|
||||
: $"<{parameterSchema.Name}...>"
|
||||
Write(parameter.IsScalar
|
||||
? $"<{parameter.Name}>"
|
||||
: $"<{parameter.Name}...>"
|
||||
);
|
||||
}
|
||||
|
||||
// Required options
|
||||
foreach (var optionSchema in commandSchema.Options.Where(o => o.IsRequired))
|
||||
foreach (var option in command.Options.Where(o => o.IsRequired))
|
||||
{
|
||||
Write(' ');
|
||||
Write(ConsoleColor.White, !string.IsNullOrWhiteSpace(optionSchema.Name)
|
||||
? $"--{optionSchema.Name}"
|
||||
: $"-{optionSchema.ShortName}"
|
||||
Write(ConsoleColor.White, !string.IsNullOrWhiteSpace(option.Name)
|
||||
? $"--{option.Name}"
|
||||
: $"-{option.ShortName}"
|
||||
);
|
||||
|
||||
Write(' ');
|
||||
Write(optionSchema.IsScalar
|
||||
Write(option.IsScalar
|
||||
? "<value>"
|
||||
: "<values...>"
|
||||
);
|
||||
@@ -170,51 +163,53 @@ namespace CliFx.Domain
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
private void WriteCommandParameters(CommandSchema commandSchema)
|
||||
private void WriteCommandParameters(CommandSchema command)
|
||||
{
|
||||
if (!commandSchema.Parameters.Any())
|
||||
if (!command.Parameters.Any())
|
||||
return;
|
||||
|
||||
WriteVerticalMargin();
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Parameters");
|
||||
|
||||
foreach (var parameterSchema in commandSchema.Parameters.OrderBy(p => p.Order))
|
||||
foreach (var parameter in command.Parameters.OrderBy(p => p.Order))
|
||||
{
|
||||
Write(ConsoleColor.Red, "* ");
|
||||
Write(ConsoleColor.White, $"{parameterSchema.Name}");
|
||||
Write(ConsoleColor.White, $"{parameter.Name}");
|
||||
|
||||
WriteHorizontalColumnMargin();
|
||||
WriteColumnMargin();
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(parameterSchema.Description))
|
||||
if (!string.IsNullOrWhiteSpace(parameter.Description))
|
||||
{
|
||||
Write(parameterSchema.Description);
|
||||
Write(parameter.Description);
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Valid values
|
||||
var validValues = parameterSchema.GetValidValues();
|
||||
var validValues = parameter.GetValidValues();
|
||||
if (validValues.Any())
|
||||
{
|
||||
Write($"Valid values: {string.Join(", ", validValues)}.");
|
||||
Write($"Valid values: {FormatValidValues(validValues)}.");
|
||||
}
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCommandOptions(CommandSchema commandSchema, ICommand command)
|
||||
private void WriteCommandOptions(
|
||||
CommandSchema command,
|
||||
IReadOnlyDictionary<CommandArgumentSchema, object?> argumentDefaultValues)
|
||||
{
|
||||
WriteVerticalMargin();
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Options");
|
||||
|
||||
var actualOptionSchemas = commandSchema.Options
|
||||
.OrderByDescending(o => o.IsRequired)
|
||||
.Concat(commandSchema.GetBuiltInOptions());
|
||||
|
||||
foreach (var optionSchema in actualOptionSchemas)
|
||||
foreach (var option in command.Options.OrderByDescending(o => o.IsRequired))
|
||||
{
|
||||
if (optionSchema.IsRequired)
|
||||
if (option.IsRequired)
|
||||
{
|
||||
Write(ConsoleColor.Red, "* ");
|
||||
}
|
||||
@@ -224,55 +219,55 @@ namespace CliFx.Domain
|
||||
}
|
||||
|
||||
// Short name
|
||||
if (optionSchema.ShortName != null)
|
||||
if (option.ShortName != null)
|
||||
{
|
||||
Write(ConsoleColor.White, $"-{optionSchema.ShortName}");
|
||||
Write(ConsoleColor.White, $"-{option.ShortName}");
|
||||
}
|
||||
|
||||
// Delimiter
|
||||
if (!string.IsNullOrWhiteSpace(optionSchema.Name) && optionSchema.ShortName != null)
|
||||
// Separator
|
||||
if (!string.IsNullOrWhiteSpace(option.Name) && option.ShortName != null)
|
||||
{
|
||||
Write('|');
|
||||
}
|
||||
|
||||
// Name
|
||||
if (!string.IsNullOrWhiteSpace(optionSchema.Name))
|
||||
if (!string.IsNullOrWhiteSpace(option.Name))
|
||||
{
|
||||
Write(ConsoleColor.White, $"--{optionSchema.Name}");
|
||||
Write(ConsoleColor.White, $"--{option.Name}");
|
||||
}
|
||||
|
||||
WriteHorizontalColumnMargin();
|
||||
WriteColumnMargin();
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(optionSchema.Description))
|
||||
if (!string.IsNullOrWhiteSpace(option.Description))
|
||||
{
|
||||
Write(optionSchema.Description);
|
||||
Write(option.Description);
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Valid values
|
||||
var validValues = optionSchema.GetValidValues();
|
||||
var validValues = option.GetValidValues();
|
||||
if (validValues.Any())
|
||||
{
|
||||
Write($"Valid values: {validValues.Select(v => v.Quote()).JoinToString(", ")}.");
|
||||
Write($"Valid values: {FormatValidValues(validValues)}.");
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Environment variable
|
||||
if (!string.IsNullOrWhiteSpace(optionSchema.EnvironmentVariableName))
|
||||
if (!string.IsNullOrWhiteSpace(option.EnvironmentVariableName))
|
||||
{
|
||||
Write($"Environment variable: \"{optionSchema.EnvironmentVariableName}\".");
|
||||
Write($"Environment variable: \"{option.EnvironmentVariableName}\".");
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Default value
|
||||
if (!optionSchema.IsRequired)
|
||||
if (!option.IsRequired)
|
||||
{
|
||||
// TODO: move quoting logic here?
|
||||
var defaultValue = optionSchema.TryGetDefaultValue(command);
|
||||
if (defaultValue != null)
|
||||
var defaultValue = argumentDefaultValues.GetValueOrDefault(option);
|
||||
var defaultValueFormatted = FormatDefaultValue(defaultValue);
|
||||
if (defaultValueFormatted != null)
|
||||
{
|
||||
Write($"Default: {defaultValue}.");
|
||||
Write($"Default: {defaultValueFormatted}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,30 +276,32 @@ namespace CliFx.Domain
|
||||
}
|
||||
|
||||
private void WriteCommandChildren(
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandSchema> childCommandSchemas)
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandSchema> childCommands)
|
||||
{
|
||||
if (!childCommandSchemas.Any())
|
||||
if (!childCommands.Any())
|
||||
return;
|
||||
|
||||
WriteVerticalMargin();
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Commands");
|
||||
|
||||
foreach (var childCommandSchema in childCommandSchemas)
|
||||
foreach (var childCommand in childCommands)
|
||||
{
|
||||
var relativeCommandName = !string.IsNullOrWhiteSpace(commandSchema.Name)
|
||||
? childCommandSchema.Name!.Substring(commandSchema.Name.Length + 1)
|
||||
: childCommandSchema.Name!;
|
||||
var relativeCommandName = !string.IsNullOrWhiteSpace(command.Name)
|
||||
? childCommand.Name!.Substring(command.Name.Length).Trim()
|
||||
: childCommand.Name!;
|
||||
|
||||
// Name
|
||||
WriteHorizontalMargin();
|
||||
Write(ConsoleColor.Cyan, relativeCommandName);
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(childCommandSchema.Description))
|
||||
if (!string.IsNullOrWhiteSpace(childCommand.Description))
|
||||
{
|
||||
WriteHorizontalColumnMargin();
|
||||
Write(childCommandSchema.Description);
|
||||
WriteColumnMargin();
|
||||
Write(childCommand.Description);
|
||||
}
|
||||
|
||||
WriteLine();
|
||||
@@ -315,10 +312,10 @@ namespace CliFx.Domain
|
||||
Write("You can run `");
|
||||
Write(_metadata.ExecutableName);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(commandSchema.Name))
|
||||
if (!string.IsNullOrWhiteSpace(command.Name))
|
||||
{
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, commandSchema.Name);
|
||||
Write(ConsoleColor.Cyan, command.Name);
|
||||
}
|
||||
|
||||
Write(' ');
|
||||
@@ -332,19 +329,60 @@ namespace CliFx.Domain
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public void Write(ApplicationSchema applicationSchema, CommandSchema commandSchema)
|
||||
public void Write(
|
||||
RootSchema root,
|
||||
CommandSchema command,
|
||||
IReadOnlyDictionary<CommandArgumentSchema, object?> defaultValues)
|
||||
{
|
||||
var childCommandSchemas = applicationSchema.GetChildCommands(commandSchema.Name);
|
||||
var command = (ICommand) _typeActivator.CreateInstance(commandSchema.Type);
|
||||
var childCommands = root.GetChildCommands(command.Name);
|
||||
|
||||
_console.ResetColor();
|
||||
|
||||
WriteApplicationInfo(commandSchema);
|
||||
WriteCommandDescription(commandSchema);
|
||||
WriteCommandUsage(commandSchema, childCommandSchemas);
|
||||
WriteCommandParameters(commandSchema);
|
||||
WriteCommandOptions(commandSchema, command);
|
||||
WriteCommandChildren(commandSchema, childCommandSchemas);
|
||||
if (command.IsDefault)
|
||||
WriteApplicationInfo();
|
||||
|
||||
WriteCommandDescription(command);
|
||||
WriteCommandUsage(command, childCommands);
|
||||
WriteCommandParameters(command);
|
||||
WriteCommandOptions(command, defaultValues);
|
||||
WriteCommandChildren(command, childCommands);
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class HelpTextWriter
|
||||
{
|
||||
private static string FormatValidValues(IReadOnlyList<string> values) =>
|
||||
values.Select(v => v.Quote()).JoinToString(", ");
|
||||
|
||||
private static string? FormatDefaultValue(object? defaultValue)
|
||||
{
|
||||
if (defaultValue == null)
|
||||
return null;
|
||||
|
||||
// Enumerable
|
||||
if (!(defaultValue is string) && defaultValue is IEnumerable defaultValues)
|
||||
{
|
||||
var elementType = defaultValues.GetType().GetEnumerableUnderlyingType() ?? typeof(object);
|
||||
|
||||
// If the ToString() method is not overriden, the default value can't be formatted nicely
|
||||
if (!elementType.IsToStringOverriden())
|
||||
return null;
|
||||
|
||||
return defaultValues
|
||||
.Cast<object?>()
|
||||
.Where(o => o != null)
|
||||
.Select(o => o!.ToFormattableString(CultureInfo.InvariantCulture).Quote())
|
||||
.JoinToString(" ");
|
||||
}
|
||||
// Non-enumerable
|
||||
else
|
||||
{
|
||||
// If the ToString() method is not overriden, the default value can't be formatted nicely
|
||||
if (!defaultValue.GetType().IsToStringOverriden())
|
||||
return null;
|
||||
|
||||
return defaultValue.ToFormattableString(CultureInfo.InvariantCulture).Quote();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
232
CliFx/Domain/RootSchema.cs
Normal file
232
CliFx/Domain/RootSchema.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Internal;
|
||||
|
||||
namespace CliFx.Domain
|
||||
{
|
||||
internal partial class RootSchema
|
||||
{
|
||||
public IReadOnlyList<CommandSchema> Commands { get; }
|
||||
|
||||
public RootSchema(IReadOnlyList<CommandSchema> commands)
|
||||
{
|
||||
Commands = commands;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetCommandNames() => Commands
|
||||
.Select(c => c.Name)
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n))
|
||||
.ToArray()!;
|
||||
|
||||
public CommandSchema? TryFindDefaultCommand() =>
|
||||
Commands.FirstOrDefault(c => c.IsDefault);
|
||||
|
||||
public CommandSchema? TryFindCommand(string? commandName) =>
|
||||
Commands.FirstOrDefault(c => c.MatchesName(commandName));
|
||||
|
||||
private IReadOnlyList<CommandSchema> GetDescendantCommands(
|
||||
IReadOnlyList<CommandSchema> potentialParentCommands,
|
||||
string? parentCommandName) =>
|
||||
potentialParentCommands
|
||||
// Default commands can't be children of anything
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Name))
|
||||
// Command can't be its own child
|
||||
.Where(c => !c.MatchesName(parentCommandName))
|
||||
.Where(c =>
|
||||
string.IsNullOrWhiteSpace(parentCommandName) ||
|
||||
c.Name!.StartsWith(parentCommandName + ' ', StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
public IReadOnlyList<CommandSchema> GetDescendantCommands(string? parentCommandName) =>
|
||||
GetDescendantCommands(Commands, parentCommandName);
|
||||
|
||||
public IReadOnlyList<CommandSchema> GetChildCommands(string? parentCommandName)
|
||||
{
|
||||
var descendants = GetDescendantCommands(parentCommandName);
|
||||
|
||||
// Filter out descendants of descendants, leave only children
|
||||
var result = new List<CommandSchema>(descendants);
|
||||
|
||||
foreach (var descendant in descendants)
|
||||
{
|
||||
var descendantDescendants = GetDescendantCommands(descendants, descendant.Name);
|
||||
result.RemoveRange(descendantDescendants);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class RootSchema
|
||||
{
|
||||
private static void ValidateParameters(CommandSchema command)
|
||||
{
|
||||
var duplicateOrderGroup = command.Parameters
|
||||
.GroupBy(a => a.Order)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateOrderGroup != null)
|
||||
{
|
||||
throw CliFxException.ParametersWithSameOrder(
|
||||
command,
|
||||
duplicateOrderGroup.Key,
|
||||
duplicateOrderGroup.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
var duplicateNameGroup = command.Parameters
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.Name))
|
||||
.GroupBy(a => a.Name!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw CliFxException.ParametersWithSameName(
|
||||
command,
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
var nonScalarParameters = command.Parameters
|
||||
.Where(p => !p.IsScalar)
|
||||
.ToArray();
|
||||
|
||||
if (nonScalarParameters.Length > 1)
|
||||
{
|
||||
throw CliFxException.TooManyNonScalarParameters(
|
||||
command,
|
||||
nonScalarParameters
|
||||
);
|
||||
}
|
||||
|
||||
var nonLastNonScalarParameter = command.Parameters
|
||||
.OrderByDescending(a => a.Order)
|
||||
.Skip(1)
|
||||
.LastOrDefault(p => !p.IsScalar);
|
||||
|
||||
if (nonLastNonScalarParameter != null)
|
||||
{
|
||||
throw CliFxException.NonLastNonScalarParameter(
|
||||
command,
|
||||
nonLastNonScalarParameter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateOptions(CommandSchema command)
|
||||
{
|
||||
var noNameGroup = command.Options
|
||||
.Where(o => o.ShortName == null && string.IsNullOrWhiteSpace(o.Name))
|
||||
.ToArray();
|
||||
|
||||
if (noNameGroup.Any())
|
||||
{
|
||||
throw CliFxException.OptionsWithNoName(
|
||||
command,
|
||||
noNameGroup.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
var invalidLengthNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
|
||||
.Where(o => o.Name!.Length <= 1)
|
||||
.ToArray();
|
||||
|
||||
if (invalidLengthNameGroup.Any())
|
||||
{
|
||||
throw CliFxException.OptionsWithInvalidLengthName(
|
||||
command,
|
||||
invalidLengthNameGroup
|
||||
);
|
||||
}
|
||||
|
||||
var duplicateNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.Name))
|
||||
.GroupBy(o => o.Name!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw CliFxException.OptionsWithSameName(
|
||||
command,
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
var duplicateShortNameGroup = command.Options
|
||||
.Where(o => o.ShortName != null)
|
||||
.GroupBy(o => o.ShortName!.Value)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateShortNameGroup != null)
|
||||
{
|
||||
throw CliFxException.OptionsWithSameShortName(
|
||||
command,
|
||||
duplicateShortNameGroup.Key,
|
||||
duplicateShortNameGroup.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
var duplicateEnvironmentVariableNameGroup = command.Options
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o.EnvironmentVariableName))
|
||||
.GroupBy(o => o.EnvironmentVariableName!, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateEnvironmentVariableNameGroup != null)
|
||||
{
|
||||
throw CliFxException.OptionsWithSameEnvironmentVariableName(
|
||||
command,
|
||||
duplicateEnvironmentVariableNameGroup.Key,
|
||||
duplicateEnvironmentVariableNameGroup.ToArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
|
||||
{
|
||||
if (!commands.Any())
|
||||
{
|
||||
throw CliFxException.NoCommandsDefined();
|
||||
}
|
||||
|
||||
var duplicateNameGroup = commands
|
||||
.GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
|
||||
if (duplicateNameGroup != null)
|
||||
{
|
||||
throw !string.IsNullOrWhiteSpace(duplicateNameGroup.Key)
|
||||
? CliFxException.CommandsWithSameName(
|
||||
duplicateNameGroup.Key,
|
||||
duplicateNameGroup.ToArray()
|
||||
)
|
||||
: CliFxException.TooManyDefaultCommands(duplicateNameGroup.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public static RootSchema Resolve(IReadOnlyList<Type> commandTypes)
|
||||
{
|
||||
var commands = new List<CommandSchema>();
|
||||
|
||||
foreach (var commandType in commandTypes)
|
||||
{
|
||||
var command =
|
||||
CommandSchema.TryResolve(commandType) ??
|
||||
throw CliFxException.InvalidCommandType(commandType);
|
||||
|
||||
ValidateParameters(command);
|
||||
ValidateOptions(command);
|
||||
|
||||
commands.Add(command);
|
||||
}
|
||||
|
||||
ValidateCommands(commands);
|
||||
|
||||
return new RootSchema(commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,54 +12,25 @@ namespace CliFx.Exceptions
|
||||
/// </summary>
|
||||
public partial class CliFxException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the default exit code assigned to exceptions in CliFx.
|
||||
/// </summary>
|
||||
protected const int DefaultExitCode = -100;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show the help text after handling this exception.
|
||||
/// </summary>
|
||||
public bool ShowHelp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this exception was constructed with a message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We cannot check against the 'Message' property because it will always return
|
||||
/// a default message if it was constructed with a null value or is currently null.
|
||||
/// </remarks>
|
||||
public bool HasMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns an exit code associated with this exception.
|
||||
/// </summary>
|
||||
public int ExitCode { get; }
|
||||
private readonly bool _isMessageSet;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||
/// </summary>
|
||||
public CliFxException(string? message, Exception? innerException, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
public CliFxException(string? message, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
ExitCode = exitCode != 0
|
||||
? exitCode
|
||||
: throw new ArgumentException("Exit code must not be zero in order to signify failure.");
|
||||
|
||||
HasMessage = !string.IsNullOrWhiteSpace(message);
|
||||
ShowHelp = showHelp;
|
||||
// Message property has a fallback so it's never empty, hence why we need this check
|
||||
_isMessageSet = !string.IsNullOrWhiteSpace(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||
/// </summary>
|
||||
public CliFxException(string? message, bool showHelp = false)
|
||||
: this(message, null, showHelp: showHelp)
|
||||
{
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _isMessageSet
|
||||
? Message
|
||||
: base.ToString();
|
||||
}
|
||||
|
||||
// Mid-user-facing exceptions
|
||||
// Internal exceptions
|
||||
// Provide more diagnostic information here
|
||||
public partial class CliFxException
|
||||
{
|
||||
@@ -83,7 +54,7 @@ Refer to the readme to learn how to integrate a dependency container of your cho
|
||||
Failed to create an instance of type '{type.FullName}', received <null> instead.
|
||||
|
||||
To fix this, ensure that the provided type activator was configured correctly, as it's not expected to return <null>.
|
||||
If you are using a dependency container, ensure this type is registered, because it may return <null> otherwise.";
|
||||
If you are using a dependency container, this error may signify that the type wasn't registered.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
@@ -103,7 +74,7 @@ If you're experiencing problems, please refer to the readme for a quickstart exa
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandsNotRegistered()
|
||||
internal static CliFxException NoCommandsDefined()
|
||||
{
|
||||
var message = $@"
|
||||
There are no commands configured in the application.
|
||||
@@ -114,11 +85,11 @@ If you're experiencing problems, please refer to the readme for a quickstart exa
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandsTooManyDefaults(IReadOnlyList<CommandSchema> invalidCommandSchemas)
|
||||
internal static CliFxException TooManyDefaultCommands(IReadOnlyList<CommandSchema> invalidCommands)
|
||||
{
|
||||
var message = $@"
|
||||
Application configuration is invalid because there are {invalidCommandSchemas.Count} default commands:
|
||||
{invalidCommandSchemas.JoinToString(Environment.NewLine)}
|
||||
Application configuration is invalid because there are {invalidCommands.Count} default commands:
|
||||
{invalidCommands.JoinToString(Environment.NewLine)}
|
||||
|
||||
There can only be one default command (i.e. command with no name) in an application.
|
||||
Other commands must have unique non-empty names that identify them.";
|
||||
@@ -126,13 +97,13 @@ Other commands must have unique non-empty names that identify them.";
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandsDuplicateName(
|
||||
internal static CliFxException CommandsWithSameName(
|
||||
string name,
|
||||
IReadOnlyList<CommandSchema> invalidCommandSchemas)
|
||||
IReadOnlyList<CommandSchema> invalidCommands)
|
||||
{
|
||||
var message = $@"
|
||||
Application configuration is invalid because there are {invalidCommandSchemas.Count} commands with the same name ('{name}'):
|
||||
{invalidCommandSchemas.JoinToString(Environment.NewLine)}
|
||||
Application configuration is invalid because there are {invalidCommands.Count} commands with the same name ('{name}'):
|
||||
{invalidCommands.JoinToString(Environment.NewLine)}
|
||||
|
||||
Commands must have unique names.
|
||||
Names are not case-sensitive.";
|
||||
@@ -140,28 +111,28 @@ Names are not case-sensitive.";
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersDuplicateOrder(
|
||||
CommandSchema commandSchema,
|
||||
internal static CliFxException ParametersWithSameOrder(
|
||||
CommandSchema command,
|
||||
int order,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} parameters with the same order ({order}):
|
||||
{invalidParameterSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameters with the same order ({order}):
|
||||
{invalidParameters.JoinToString(Environment.NewLine)}
|
||||
|
||||
Parameters must have unique order.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersDuplicateName(
|
||||
CommandSchema commandSchema,
|
||||
internal static CliFxException ParametersWithSameName(
|
||||
CommandSchema command,
|
||||
string name,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} parameters with the same name ('{name}'):
|
||||
{invalidParameterSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameters with the same name ('{name}'):
|
||||
{invalidParameters.JoinToString(Environment.NewLine)}
|
||||
|
||||
Parameters must have unique names to avoid potential confusion in the help text.
|
||||
Names are not case-sensitive.";
|
||||
@@ -169,15 +140,15 @@ Names are not case-sensitive.";
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersTooManyNonScalar(
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameterSchemas)
|
||||
internal static CliFxException TooManyNonScalarParameters(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidParameterSchemas.Count} non-scalar parameters:
|
||||
{invalidParameterSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} non-scalar parameters:
|
||||
{invalidParameters.JoinToString(Environment.NewLine)}
|
||||
|
||||
Non-scalar parameter is such that is bound from more than one value (e.g. array or a complex object).
|
||||
Non-scalar parameter is such that is bound from more than one value (e.g. array).
|
||||
Only one parameter in a command may be non-scalar and it must be the last one in order.
|
||||
|
||||
If it's not feasible to fit into these constraints, consider using options instead as they don't have these limitations.";
|
||||
@@ -185,15 +156,15 @@ If it's not feasible to fit into these constraints, consider using options inste
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandParametersNonLastNonScalar(
|
||||
CommandSchema commandSchema,
|
||||
CommandParameterSchema invalidParameterSchema)
|
||||
internal static CliFxException NonLastNonScalarParameter(
|
||||
CommandSchema command,
|
||||
CommandParameterSchema invalidParameter)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains a non-scalar parameter which is not the last in order:
|
||||
{invalidParameterSchema}
|
||||
Command '{command.Type.FullName}' is invalid because it contains a non-scalar parameter which is not the last in order:
|
||||
{invalidParameter}
|
||||
|
||||
Non-scalar parameter is such that is bound from more than one value (e.g. array or a complex object).
|
||||
Non-scalar parameter is such that is bound from more than one value (e.g. array).
|
||||
Only one parameter in a command may be non-scalar and it must be the last one in order.
|
||||
|
||||
If it's not feasible to fit into these constraints, consider using options instead as they don't have these limitations.";
|
||||
@@ -201,26 +172,26 @@ If it's not feasible to fit into these constraints, consider using options inste
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsNoName(
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
internal static CliFxException OptionsWithNoName(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains one or more options without a name:
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains one or more options without a name:
|
||||
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||
|
||||
Options must have either a name or a short name or both.";
|
||||
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsInvalidLengthName(
|
||||
CommandSchema commandSchema,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
internal static CliFxException OptionsWithInvalidLengthName(
|
||||
CommandSchema command,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains one or more options whose names are too short:
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains one or more options whose names are too short:
|
||||
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||
|
||||
Option names must be at least 2 characters long to avoid confusion with short names.
|
||||
If you intended to set the short name instead, use the attribute overload that accepts a char.";
|
||||
@@ -228,14 +199,14 @@ If you intended to set the short name instead, use the attribute overload that a
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateName(
|
||||
CommandSchema commandSchema,
|
||||
internal static CliFxException OptionsWithSameName(
|
||||
CommandSchema command,
|
||||
string name,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same name ('{name}'):
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same name ('{name}'):
|
||||
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||
|
||||
Options must have unique names.
|
||||
Names are not case-sensitive.";
|
||||
@@ -243,14 +214,14 @@ Names are not case-sensitive.";
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateShortName(
|
||||
CommandSchema commandSchema,
|
||||
internal static CliFxException OptionsWithSameShortName(
|
||||
CommandSchema command,
|
||||
char shortName,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same short name ('{shortName}'):
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same short name ('{shortName}'):
|
||||
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||
|
||||
Options must have unique short names.
|
||||
Short names are case-sensitive (i.e. 'a' and 'A' are different short names).";
|
||||
@@ -258,14 +229,14 @@ Short names are case-sensitive (i.e. 'a' and 'A' are different short names).";
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CommandOptionsDuplicateEnvironmentVariableName(
|
||||
CommandSchema commandSchema,
|
||||
internal static CliFxException OptionsWithSameEnvironmentVariableName(
|
||||
CommandSchema command,
|
||||
string environmentVariableName,
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptionSchemas)
|
||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||
{
|
||||
var message = $@"
|
||||
Command '{commandSchema.Type.FullName}' is invalid because it contains {invalidOptionSchemas.Count} options with the same fallback environment variable name ('{environmentVariableName}'):
|
||||
{invalidOptionSchemas.JoinToString(Environment.NewLine)}
|
||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same fallback environment variable name ('{environmentVariableName}'):
|
||||
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||
|
||||
Options cannot share the same environment variable as a fallback.
|
||||
Environment variable names are not case-sensitive.";
|
||||
@@ -278,154 +249,145 @@ Environment variable names are not case-sensitive.";
|
||||
// Avoid internal details and fix recommendations here
|
||||
public partial class CliFxException
|
||||
{
|
||||
internal static CliFxException CannotFindMatchingCommand(CommandLineInput input)
|
||||
{
|
||||
var message = $@"
|
||||
Can't find a command that matches the following arguments:
|
||||
{input.UnboundArguments.JoinToString(" ")}";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertMultipleValuesToNonScalar(
|
||||
CommandParameterSchema parameterSchema,
|
||||
CommandParameterSchema parameter,
|
||||
IReadOnlyList<string> values)
|
||||
{
|
||||
var message = $@"
|
||||
Parameter {parameterSchema.GetUserFacingDisplayString()} expects a single value, but provided with multiple:
|
||||
Parameter {parameter.GetUserFacingDisplayString()} expects a single value, but provided with multiple:
|
||||
{values.Select(v => v.Quote()).JoinToString(" ")}";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertMultipleValuesToNonScalar(
|
||||
CommandOptionSchema optionSchema,
|
||||
CommandOptionSchema option,
|
||||
IReadOnlyList<string> values)
|
||||
{
|
||||
var message = $@"
|
||||
Option {optionSchema.GetUserFacingDisplayString()} expects a single value, but provided with multiple:
|
||||
Option {option.GetUserFacingDisplayString()} expects a single value, but provided with multiple:
|
||||
{values.Select(v => v.Quote()).JoinToString(" ")}";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertMultipleValuesToNonScalar(
|
||||
CommandArgumentSchema argumentSchema,
|
||||
IReadOnlyList<string> values) => argumentSchema switch
|
||||
CommandArgumentSchema argument,
|
||||
IReadOnlyList<string> values) => argument switch
|
||||
{
|
||||
CommandParameterSchema parameterSchema => CannotConvertMultipleValuesToNonScalar(parameterSchema, values),
|
||||
CommandOptionSchema optionSchema => CannotConvertMultipleValuesToNonScalar(optionSchema, values),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(argumentSchema))
|
||||
CommandParameterSchema parameter => CannotConvertMultipleValuesToNonScalar(parameter, values),
|
||||
CommandOptionSchema option => CannotConvertMultipleValuesToNonScalar(option, values),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(argument))
|
||||
};
|
||||
|
||||
internal static CliFxException CannotConvertToType(
|
||||
CommandParameterSchema parameterSchema,
|
||||
CommandParameterSchema parameter,
|
||||
string? value,
|
||||
Type type,
|
||||
Exception? innerException = null)
|
||||
{
|
||||
var message = $@"
|
||||
Can't convert value ""{value ?? "<null>"}"" to type '{type.Name}' for parameter {parameterSchema.GetUserFacingDisplayString()}.
|
||||
Can't convert value ""{value ?? "<null>"}"" to type '{type.Name}' for parameter {parameter.GetUserFacingDisplayString()}.
|
||||
{innerException?.Message ?? "This type is not supported."}";
|
||||
|
||||
return new CliFxException(message.Trim(), innerException, showHelp: true);
|
||||
return new CliFxException(message.Trim(), innerException);
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertToType(
|
||||
CommandOptionSchema optionSchema,
|
||||
CommandOptionSchema option,
|
||||
string? value,
|
||||
Type type,
|
||||
Exception? innerException = null)
|
||||
{
|
||||
var message = $@"
|
||||
Can't convert value ""{value ?? "<null>"}"" to type '{type.Name}' for option {optionSchema.GetUserFacingDisplayString()}.
|
||||
Can't convert value ""{value ?? "<null>"}"" to type '{type.Name}' for option {option.GetUserFacingDisplayString()}.
|
||||
{innerException?.Message ?? "This type is not supported."}";
|
||||
|
||||
return new CliFxException(message.Trim(), innerException, showHelp: true);
|
||||
return new CliFxException(message.Trim(), innerException);
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertToType(
|
||||
CommandArgumentSchema argumentSchema,
|
||||
CommandArgumentSchema argument,
|
||||
string? value,
|
||||
Type type,
|
||||
Exception? innerException = null) => argumentSchema switch
|
||||
Exception? innerException = null) => argument switch
|
||||
{
|
||||
CommandParameterSchema parameterSchema => CannotConvertToType(parameterSchema, value, type, innerException),
|
||||
CommandOptionSchema optionSchema => CannotConvertToType(optionSchema, value, type, innerException),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(argumentSchema))
|
||||
CommandParameterSchema parameter => CannotConvertToType(parameter, value, type, innerException),
|
||||
CommandOptionSchema option => CannotConvertToType(option, value, type, innerException),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(argument))
|
||||
};
|
||||
|
||||
internal static CliFxException CannotConvertNonScalar(
|
||||
CommandParameterSchema parameterSchema,
|
||||
CommandParameterSchema parameter,
|
||||
IReadOnlyList<string> values,
|
||||
Type type)
|
||||
{
|
||||
var message = $@"
|
||||
Can't convert provided values to type '{type.Name}' for parameter {parameterSchema.GetUserFacingDisplayString()}:
|
||||
Can't convert provided values to type '{type.Name}' for parameter {parameter.GetUserFacingDisplayString()}:
|
||||
{values.Select(v => v.Quote()).JoinToString(" ")}
|
||||
|
||||
Target type is not assignable from array and doesn't have a public constructor that takes an array.";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertNonScalar(
|
||||
CommandOptionSchema optionSchema,
|
||||
CommandOptionSchema option,
|
||||
IReadOnlyList<string> values,
|
||||
Type type)
|
||||
{
|
||||
var message = $@"
|
||||
Can't convert provided values to type '{type.Name}' for option {optionSchema.GetUserFacingDisplayString()}:
|
||||
Can't convert provided values to type '{type.Name}' for option {option.GetUserFacingDisplayString()}:
|
||||
{values.Select(v => v.Quote()).JoinToString(" ")}
|
||||
|
||||
Target type is not assignable from array and doesn't have a public constructor that takes an array.";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException CannotConvertNonScalar(
|
||||
CommandArgumentSchema argumentSchema,
|
||||
CommandArgumentSchema argument,
|
||||
IReadOnlyList<string> values,
|
||||
Type type) => argumentSchema switch
|
||||
Type type) => argument switch
|
||||
{
|
||||
CommandParameterSchema parameterSchema => CannotConvertNonScalar(parameterSchema, values, type),
|
||||
CommandOptionSchema optionSchema => CannotConvertNonScalar(optionSchema, values, type),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(argumentSchema))
|
||||
CommandParameterSchema parameter => CannotConvertNonScalar(parameter, values, type),
|
||||
CommandOptionSchema option => CannotConvertNonScalar(option, values, type),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(argument))
|
||||
};
|
||||
|
||||
internal static CliFxException ParameterNotSet(CommandParameterSchema parameterSchema)
|
||||
internal static CliFxException ParameterNotSet(CommandParameterSchema parameter)
|
||||
{
|
||||
var message = $@"
|
||||
Missing value for parameter {parameterSchema.GetUserFacingDisplayString()}.";
|
||||
Missing value for parameter {parameter.GetUserFacingDisplayString()}.";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException RequiredOptionsNotSet(IReadOnlyList<CommandOptionSchema> optionSchemas)
|
||||
internal static CliFxException RequiredOptionsNotSet(IReadOnlyList<CommandOptionSchema> options)
|
||||
{
|
||||
var message = $@"
|
||||
Missing values for one or more required options:
|
||||
{optionSchemas.Select(o => o.GetUserFacingDisplayString()).JoinToString(Environment.NewLine)}";
|
||||
{options.Select(o => o.GetUserFacingDisplayString()).JoinToString(Environment.NewLine)}";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandUnboundArgumentInput> argumentInputs)
|
||||
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandParameterInput> parameterInputs)
|
||||
{
|
||||
var message = $@"
|
||||
Unrecognized parameters provided:
|
||||
{argumentInputs.Select(a => a.Value.Quote()).JoinToString(" ")}";
|
||||
{parameterInputs.Select(p => p.Value).JoinToString(Environment.NewLine)}";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
|
||||
internal static CliFxException UnrecognizedOptionsProvided(IReadOnlyList<CommandOptionInput> optionInputs)
|
||||
{
|
||||
var message = $@"
|
||||
Unrecognized options provided:
|
||||
{optionInputs.Select(o => o.RawAlias).JoinToString(Environment.NewLine)}";
|
||||
{optionInputs.Select(o => o.GetRawAlias()).JoinToString(Environment.NewLine)}";
|
||||
|
||||
return new CliFxException(message.Trim(), showHelp: true);
|
||||
return new CliFxException(message.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,36 @@ namespace CliFx.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Thrown when a command cannot proceed with normal execution due to an error.
|
||||
/// Use this exception if you want to report an error that occured during execution of a command.
|
||||
/// Use this exception if you want to report an error that occured during the execution of a command.
|
||||
/// This exception also allows specifying exit code which will be returned to the calling process.
|
||||
/// </summary>
|
||||
public class CommandException : CliFxException
|
||||
public class CommandException : Exception
|
||||
{
|
||||
private const int DefaultExitCode = -1;
|
||||
|
||||
private readonly bool _isMessageSet;
|
||||
|
||||
/// <summary>
|
||||
/// Returns an exit code associated with this exception.
|
||||
/// </summary>
|
||||
public int ExitCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show the help text after handling this exception.
|
||||
/// </summary>
|
||||
public bool ShowHelp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandException"/>.
|
||||
/// </summary>
|
||||
public CommandException(string? message, Exception? innerException,
|
||||
int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
: base(message, innerException, exitCode, showHelp)
|
||||
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||
: base(message, innerException)
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
ShowHelp = showHelp;
|
||||
|
||||
// Message property has a fallback so it's never empty, hence why we need this check
|
||||
_isMessageSet = !string.IsNullOrWhiteSpace(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,5 +51,10 @@ namespace CliFx.Exceptions
|
||||
: this(null, exitCode, showHelp)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _isMessageSet
|
||||
? Message
|
||||
: base.ToString();
|
||||
}
|
||||
}
|
||||
@@ -65,10 +65,15 @@ namespace CliFx
|
||||
int CursorTop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provides a token that signals when application cancellation is requested.
|
||||
/// Subsequent calls return the same token.
|
||||
/// When working with system console, the user can request cancellation by issuing an interrupt signal (Ctrl+C).
|
||||
/// Defers the application termination in case of a cancellation request and returns the token that represents it.
|
||||
/// Subsequent calls to this method return the same token.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When working with <see cref="SystemConsole"/>:<br/>
|
||||
/// - Cancellation can be requested by the user by pressing Ctrl+C.<br/>
|
||||
/// - Cancellation can only be deferred once, subsequent requests to cancel by the user will result in instant termination.<br/>
|
||||
/// - Any code executing prior to calling this method is not cancellation-aware and as such will terminate instantly when cancellation is requested.
|
||||
/// </remarks>
|
||||
CancellationToken GetCancellationToken();
|
||||
}
|
||||
|
||||
|
||||
13
CliFx/Internal/CollectionExtensions.cs
Normal file
13
CliFx/Internal/CollectionExtensions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CliFx.Internal
|
||||
{
|
||||
internal static class CollectionExtensions
|
||||
{
|
||||
public static void RemoveRange<T>(this ICollection<T> source, IEnumerable<T> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
source.Remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,23 @@
|
||||
// Polyfills to bridge the missing APIs in older versions of the framework/standard.
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
namespace System
|
||||
{
|
||||
using Linq;
|
||||
|
||||
internal static class Extensions
|
||||
{
|
||||
public static bool Contains(this string str, char c) =>
|
||||
str.Any(i => i == c);
|
||||
|
||||
public static bool StartsWith(this string str, char c) =>
|
||||
str.Length > 0 && str[0] == c;
|
||||
|
||||
public static bool EndsWith(this string str, char c) =>
|
||||
str.Length > 0 && str[str.Length - 1] == c;
|
||||
}
|
||||
}
|
||||
|
||||
namespace System.Collections.Generic
|
||||
{
|
||||
internal static class Extensions
|
||||
@@ -17,4 +34,15 @@ namespace System.Collections.Generic
|
||||
dic.TryGetValue(key, out var result) ? result! : default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace System.Linq
|
||||
{
|
||||
using Collections.Generic;
|
||||
|
||||
internal static class Extensions
|
||||
{
|
||||
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer) =>
|
||||
new HashSet<T>(source, comparer);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -8,11 +8,6 @@ namespace CliFx.Internal
|
||||
{
|
||||
internal static class TypeExtensions
|
||||
{
|
||||
public static object? GetDefaultValue(this Type type) =>
|
||||
type.IsValueType
|
||||
? Activator.CreateInstance(type)
|
||||
: null;
|
||||
|
||||
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
|
||||
|
||||
public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace CliFx
|
||||
|
||||
public partial class SystemConsole
|
||||
{
|
||||
// TODO: use StreamWriter.Synchronized?
|
||||
private static StreamReader WrapInput(Stream? stream) =>
|
||||
stream != null
|
||||
? new StreamReader(stream, Console.InputEncoding, false)
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Threading;
|
||||
namespace CliFx
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IConsole"/> that routes data to specified streams.
|
||||
/// Implementation of <see cref="IConsole"/> that routes all data to preconfigured streams.
|
||||
/// Does not leak to system console in any way.
|
||||
/// Use this class as a substitute for system console when running tests.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user