mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b17341b56c | ||
|
|
5bda964fb5 | ||
|
|
432430489a | ||
|
|
9a20101f30 | ||
|
|
b491818779 | ||
|
|
69c24c8dfc | ||
|
|
004f906148 | ||
|
|
ac83233dc2 | ||
|
|
082910c968 |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 24 KiB |
10
Changelog.md
10
Changelog.md
@@ -1,3 +1,13 @@
|
|||||||
|
### v1.3.2 (31-Jul-2020)
|
||||||
|
|
||||||
|
- Fixed an issue where a command was incorrectly allowed to execute when the user did not specify any value for a non-scalar parameter. Since they are always required, a parameter needs to be bound to (at least) one value. (Thanks [@Daniel Hix](https://github.com/ADustyOldMuffin))
|
||||||
|
- Fixed an issue where `CliApplication.RunAsync(...)` threw `ArgumentException` if there were two environment variables, whose names differed only in case. Environment variable names are now treated case-sensitively. (Thanks [@Ron Myers](https://github.com/ron-myers))
|
||||||
|
|
||||||
|
### v1.3.1 (19-Jul-2020)
|
||||||
|
|
||||||
|
- Running the application with the debug directive (`myapp [debug]`) will now also try to launch a debugger instance. In most cases it will save time as you won't need to attach the debugger manually. (Thanks [@Volodymyr Shkolka](https://github.com/BlackGad))
|
||||||
|
- Fixed an issue where unhandled generic exceptions (i.e. not `CommandException`) sometimes caused the application to incorrectly return successful exit code due to an overflow issue on Unix systems. Starting from this version, all unhandled generic exceptions will produce `1` as the exit code when thrown. Instances of `CommandException` can still be configured to return any specified exit code, but it's recommended to constrain the values between `1` and `255` to avoid overflow issues. (Thanks [@Ihor Nechyporuk](https://github.com/inech))
|
||||||
|
|
||||||
### v1.3 (23-May-2020)
|
### v1.3 (23-May-2020)
|
||||||
|
|
||||||
- Changed analyzers to report errors instead of warnings. If you find that some analyzer works incorrectly, please report it on GitHub. You can also configure inspection severity overrides in your project if you need to.
|
- Changed analyzers to report errors instead of warnings. If you find that some analyzer works incorrectly, please report it on GitHub. You can also configure inspection severity overrides in your project if you need to.
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ namespace CliFx.Analyzers
|
|||||||
// Duplicate environment variable name
|
// Duplicate environment variable name
|
||||||
var duplicateEnvironmentVariableNameOptions = options
|
var duplicateEnvironmentVariableNameOptions = options
|
||||||
.Where(p => !string.IsNullOrWhiteSpace(p.EnvironmentVariableName))
|
.Where(p => !string.IsNullOrWhiteSpace(p.EnvironmentVariableName))
|
||||||
.GroupBy(p => p.EnvironmentVariableName, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(p => p.EnvironmentVariableName, StringComparer.Ordinal)
|
||||||
.Where(g => g.Count() > 1)
|
.Where(g => g.Count() > 1)
|
||||||
.SelectMany(g => g.AsEnumerable())
|
.SelectMany(g => g.AsEnumerable())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|||||||
@@ -134,11 +134,17 @@ namespace CliFx.Tests
|
|||||||
[Command]
|
[Command]
|
||||||
private class RequiredOptionCommand : ICommand
|
private class RequiredOptionCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption(nameof(OptionA))]
|
[CommandOption(nameof(Option), IsRequired = true)]
|
||||||
public string? OptionA { get; set; }
|
public string? Option { get; set; }
|
||||||
|
|
||||||
[CommandOption(nameof(OptionB), IsRequired = true)]
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
public string? OptionB { get; set; }
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
private class RequiredArrayOptionCommand : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption(nameof(Option), IsRequired = true)]
|
||||||
|
public IReadOnlyList<string>? Option { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -825,88 +825,6 @@ namespace CliFx.Tests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Property_annotated_as_an_option_can_be_bound_from_multiple_values_even_if_the_inputs_use_mixed_naming()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var input = new CommandInputBuilder()
|
|
||||||
.AddOption("option", "foo")
|
|
||||||
.AddOption("o", "bar")
|
|
||||||
.AddOption("option", "baz")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var instance = CommandHelper.ResolveCommand<ArrayOptionCommand>(input);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
instance.Should().BeEquivalentTo(new ArrayOptionCommand
|
|
||||||
{
|
|
||||||
Option = new[] {"foo", "bar", "baz"}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Property_annotated_as_a_required_option_must_always_be_set()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var input = new CommandInputBuilder()
|
|
||||||
.AddOption(nameof(RequiredOptionCommand.OptionA), "foo")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<RequiredOptionCommand>(input));
|
|
||||||
_output.WriteLine(ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Property_annotated_as_a_required_option_must_always_be_bound_to_some_value()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var input = new CommandInputBuilder()
|
|
||||||
.AddOption(nameof(RequiredOptionCommand.OptionB))
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<RequiredOptionCommand>(input));
|
|
||||||
_output.WriteLine(ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var input = new CommandInputBuilder()
|
|
||||||
.AddParameter("foo")
|
|
||||||
.AddParameter("bar")
|
|
||||||
.AddParameter("hello")
|
|
||||||
.AddParameter("world")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var instance = CommandHelper.ResolveCommand<ParametersCommand>(input);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
instance.Should().BeEquivalentTo(new ParametersCommand
|
|
||||||
{
|
|
||||||
ParameterA = "foo",
|
|
||||||
ParameterB = "bar",
|
|
||||||
ParameterC = new[] {"hello", "world"}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Property_annotated_as_parameter_must_always_be_bound_to_some_value()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var input = new CommandInputBuilder()
|
|
||||||
.AddParameter("foo")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Act & assert
|
|
||||||
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<ParametersCommand>(input));
|
|
||||||
_output.WriteLine(ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Property_of_custom_type_that_implements_IEnumerable_can_only_be_bound_if_that_type_has_a_constructor_accepting_an_array()
|
public void Property_of_custom_type_that_implements_IEnumerable_can_only_be_bound_if_that_type_has_a_constructor_accepting_an_array()
|
||||||
{
|
{
|
||||||
@@ -959,6 +877,114 @@ namespace CliFx.Tests
|
|||||||
_output.WriteLine(ex.Message);
|
_output.WriteLine(ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Property_annotated_as_an_option_can_be_bound_from_multiple_values_even_if_the_inputs_use_mixed_naming()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new CommandInputBuilder()
|
||||||
|
.AddOption("option", "foo")
|
||||||
|
.AddOption("o", "bar")
|
||||||
|
.AddOption("option", "baz")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var instance = CommandHelper.ResolveCommand<ArrayOptionCommand>(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
instance.Should().BeEquivalentTo(new ArrayOptionCommand
|
||||||
|
{
|
||||||
|
Option = new[] {"foo", "bar", "baz"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Property_annotated_as_a_required_option_must_always_be_set()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new CommandInputBuilder()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<RequiredOptionCommand>(input));
|
||||||
|
_output.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Property_annotated_as_a_required_option_must_always_be_bound_to_some_value()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new CommandInputBuilder()
|
||||||
|
.AddOption(nameof(RequiredOptionCommand.Option))
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<RequiredOptionCommand>(input));
|
||||||
|
_output.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Property_annotated_as_a_required_option_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new CommandInputBuilder()
|
||||||
|
.AddOption(nameof(RequiredOptionCommand.Option))
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<RequiredArrayOptionCommand>(input));
|
||||||
|
_output.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new CommandInputBuilder()
|
||||||
|
.AddParameter("foo")
|
||||||
|
.AddParameter("bar")
|
||||||
|
.AddParameter("hello")
|
||||||
|
.AddParameter("world")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var instance = CommandHelper.ResolveCommand<ParametersCommand>(input);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
instance.Should().BeEquivalentTo(new ParametersCommand
|
||||||
|
{
|
||||||
|
ParameterA = "foo",
|
||||||
|
ParameterB = "bar",
|
||||||
|
ParameterC = new[] {"hello", "world"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Property_annotated_as_parameter_must_always_be_bound_to_some_value()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new CommandInputBuilder()
|
||||||
|
.AddParameter("foo")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<ParametersCommand>(input));
|
||||||
|
_output.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Property_annotated_as_parameter_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = new CommandInputBuilder()
|
||||||
|
.AddParameter("foo")
|
||||||
|
.AddParameter("bar")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
var ex = Assert.Throws<CliFxException>(() => CommandHelper.ResolveCommand<ParametersCommand>(input));
|
||||||
|
_output.WriteLine(ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void All_provided_option_arguments_must_be_bound_to_corresponding_properties()
|
public void All_provided_option_arguments_must_be_bound_to_corresponding_properties()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Tests.Internal;
|
|
||||||
using CliWrap;
|
|
||||||
using CliWrap.Buffered;
|
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -14,30 +8,6 @@ namespace CliFx.Tests
|
|||||||
{
|
{
|
||||||
public partial class DirectivesSpecs
|
public partial class DirectivesSpecs
|
||||||
{
|
{
|
||||||
[Fact]
|
|
||||||
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]
|
[Fact]
|
||||||
public async Task Preview_directive_can_be_specified_to_print_provided_arguments_as_they_were_parsed()
|
public async Task Preview_directive_can_be_specified_to_print_provided_arguments_as_they_were_parsed()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,5 +89,27 @@ namespace CliFx.Tests
|
|||||||
Option = $"foo{Path.PathSeparator}bar"
|
Option = $"foo{Path.PathSeparator}bar"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Option_can_use_a_specific_environment_variable_as_fallback_while_respecting_case()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string expected = "foobar";
|
||||||
|
var input = CommandInput.Empty;
|
||||||
|
var envVars = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["ENV_OPT"] = expected,
|
||||||
|
["env_opt"] = "2"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var instance = CommandHelper.ResolveCommand<EnvironmentVariableCommand>(input, envVars);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
instance.Should().BeEquivalentTo(new EnvironmentVariableCommand
|
||||||
|
{
|
||||||
|
Option = expected
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace CliFx.Tests
|
|||||||
private class CommandExceptionCommand : ICommand
|
private class CommandExceptionCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("code", 'c')]
|
[CommandOption("code", 'c')]
|
||||||
public int ExitCode { get; set; } = 1337;
|
public int ExitCode { get; set; } = 133;
|
||||||
|
|
||||||
[CommandOption("msg", 'm')]
|
[CommandOption("msg", 'm')]
|
||||||
public string? Message { get; set; }
|
public string? Message { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System.IO;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.3</Version>
|
<Version>1.3.2</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ namespace CliFx
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ApplicationConfiguration(
|
public ApplicationConfiguration(
|
||||||
IReadOnlyList<Type> commandTypes,
|
IReadOnlyList<Type> commandTypes,
|
||||||
bool isDebugModeAllowed, bool isPreviewModeAllowed)
|
bool isDebugModeAllowed,
|
||||||
|
bool isPreviewModeAllowed)
|
||||||
{
|
{
|
||||||
CommandTypes = commandTypes;
|
CommandTypes = commandTypes;
|
||||||
IsDebugModeAllowed = isDebugModeAllowed;
|
IsDebugModeAllowed = isDebugModeAllowed;
|
||||||
|
|||||||
@@ -41,13 +41,15 @@ namespace CliFx
|
|||||||
private void WriteError(string message) => _console.WithForegroundColor(ConsoleColor.Red, () =>
|
private void WriteError(string message) => _console.WithForegroundColor(ConsoleColor.Red, () =>
|
||||||
_console.Error.WriteLine(message));
|
_console.Error.WriteLine(message));
|
||||||
|
|
||||||
private async ValueTask WaitForDebuggerAsync()
|
private async ValueTask LaunchAndWaitForDebuggerAsync()
|
||||||
{
|
{
|
||||||
var processId = ProcessEx.GetCurrentProcessId();
|
var processId = ProcessEx.GetCurrentProcessId();
|
||||||
|
|
||||||
_console.WithForegroundColor(ConsoleColor.Green, () =>
|
_console.WithForegroundColor(ConsoleColor.Green, () =>
|
||||||
_console.Output.WriteLine($"Attach debugger to PID {processId} to continue."));
|
_console.Output.WriteLine($"Attach debugger to PID {processId} to continue."));
|
||||||
|
|
||||||
|
Debugger.Launch();
|
||||||
|
|
||||||
while (!Debugger.IsAttached)
|
while (!Debugger.IsAttached)
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
}
|
}
|
||||||
@@ -125,8 +127,7 @@ namespace CliFx
|
|||||||
// Debug mode
|
// Debug mode
|
||||||
if (_configuration.IsDebugModeAllowed && input.IsDebugDirectiveSpecified)
|
if (_configuration.IsDebugModeAllowed && input.IsDebugDirectiveSpecified)
|
||||||
{
|
{
|
||||||
// Ensure debugger is attached and continue
|
await LaunchAndWaitForDebuggerAsync();
|
||||||
await WaitForDebuggerAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preview mode
|
// Preview mode
|
||||||
@@ -217,9 +218,10 @@ namespace CliFx
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||||
{
|
{
|
||||||
|
// Environment variable names are case-insensitive on Windows but are case-sensitive on Linux and macOS
|
||||||
var environmentVariables = Environment.GetEnvironmentVariables()
|
var environmentVariables = Environment.GetEnvironmentVariables()
|
||||||
.Cast<DictionaryEntry>()
|
.Cast<DictionaryEntry>()
|
||||||
.ToDictionary(e => (string) e.Key, e => (string) e.Value, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(e => (string) e.Key, e => (string) e.Value, StringComparer.Ordinal);
|
||||||
|
|
||||||
return await RunAsync(commandLineArguments, environmentVariables);
|
return await RunAsync(commandLineArguments, environmentVariables);
|
||||||
}
|
}
|
||||||
@@ -252,7 +254,7 @@ namespace CliFx
|
|||||||
public static int FromException(Exception ex) =>
|
public static int FromException(Exception ex) =>
|
||||||
ex is CommandException cmdEx
|
ex is CommandException cmdEx
|
||||||
? cmdEx.ExitCode
|
? cmdEx.ExitCode
|
||||||
: ex.HResult;
|
: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command]
|
[Command]
|
||||||
@@ -263,4 +265,4 @@ namespace CliFx
|
|||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using CliFx.Domain;
|
using CliFx.Domain;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx
|
namespace CliFx
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx.Domain
|
namespace CliFx.Domain
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx.Domain
|
namespace CliFx.Domain
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx.Domain
|
namespace CliFx.Domain
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace CliFx.Domain
|
|||||||
|
|
||||||
public bool MatchesEnvironmentVariableName(string environmentVariableName) =>
|
public bool MatchesEnvironmentVariableName(string environmentVariableName) =>
|
||||||
!string.IsNullOrWhiteSpace(EnvironmentVariableName) &&
|
!string.IsNullOrWhiteSpace(EnvironmentVariableName) &&
|
||||||
string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.OrdinalIgnoreCase);
|
string.Equals(EnvironmentVariableName, environmentVariableName, StringComparison.Ordinal);
|
||||||
|
|
||||||
public string GetUserFacingDisplayString()
|
public string GetUserFacingDisplayString()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using System.Reflection;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx.Domain
|
namespace CliFx.Domain
|
||||||
{
|
{
|
||||||
@@ -103,12 +103,16 @@ namespace CliFx.Domain
|
|||||||
|
|
||||||
if (nonScalarParameter != null)
|
if (nonScalarParameter != null)
|
||||||
{
|
{
|
||||||
// TODO: Should it verify that at least one value is passed?
|
|
||||||
var nonScalarValues = parameterInputs
|
var nonScalarValues = parameterInputs
|
||||||
.Skip(scalarParameters.Length)
|
.Skip(scalarParameters.Length)
|
||||||
.Select(p => p.Value)
|
.Select(p => p.Value)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
// Parameters are required by default and so a non-scalar parameter must
|
||||||
|
// be bound to at least one value
|
||||||
|
if(!nonScalarValues.Any())
|
||||||
|
throw CliFxException.ParameterNotSet(nonScalarParameter);
|
||||||
|
|
||||||
nonScalarParameter.BindOn(instance, nonScalarValues);
|
nonScalarParameter.BindOn(instance, nonScalarValues);
|
||||||
remainingParameterInputs.Clear();
|
remainingParameterInputs.Clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Collections;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx.Domain
|
namespace CliFx.Domain
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx.Domain
|
namespace CliFx.Domain
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Domain;
|
using CliFx.Domain;
|
||||||
using CliFx.Internal;
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx.Exceptions
|
namespace CliFx.Exceptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ namespace CliFx.Exceptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommandException : Exception
|
public class CommandException : Exception
|
||||||
{
|
{
|
||||||
private const int DefaultExitCode = -1;
|
private const int DefaultExitCode = 1;
|
||||||
|
|
||||||
private readonly bool _isMessageSet;
|
private readonly bool _isMessageSet;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an exit code associated with this exception.
|
/// Exit code returned by the application when this exception is handled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ExitCode { get; }
|
public int ExitCode { get; }
|
||||||
|
|
||||||
@@ -26,6 +26,9 @@ namespace CliFx.Exceptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandException"/>.
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// On Unix systems an exit code is 8-bit unsigned integer so it's strongly recommended to use values between 1 and 255 to avoid overflow.
|
||||||
|
/// </remarks>
|
||||||
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode, bool showHelp = false)
|
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
@@ -39,6 +42,9 @@ namespace CliFx.Exceptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandException"/>.
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// On Unix systems an exit code is 8-bit unsigned integer so it's strongly recommended to use values between 1 and 255 to avoid overflow.
|
||||||
|
/// </remarks>
|
||||||
public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false)
|
public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||||
: this(message, null, exitCode, showHelp)
|
: this(message, null, exitCode, showHelp)
|
||||||
{
|
{
|
||||||
@@ -47,6 +53,9 @@ namespace CliFx.Exceptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandException"/>.
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// On Unix systems an exit code is 8-bit unsigned integer so it's strongly recommended to use values between 1 and 255 to avoid overflow.
|
||||||
|
/// </remarks>
|
||||||
public CommandException(int exitCode = DefaultExitCode, bool showHelp = false)
|
public CommandException(int exitCode = DefaultExitCode, bool showHelp = false)
|
||||||
: this(null, exitCode, showHelp)
|
: this(null, exitCode, showHelp)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CliFx.Internal
|
namespace CliFx.Internal.Extensions
|
||||||
{
|
{
|
||||||
internal static class CollectionExtensions
|
internal static class CollectionExtensions
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CliFx.Internal
|
namespace CliFx.Internal.Extensions
|
||||||
{
|
{
|
||||||
internal static class StringExtensions
|
internal static class StringExtensions
|
||||||
{
|
{
|
||||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace CliFx.Internal
|
namespace CliFx.Internal.Extensions
|
||||||
{
|
{
|
||||||
internal static class TypeExtensions
|
internal static class TypeExtensions
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CliFx.Internal
|
namespace CliFx.Internal.Extensions
|
||||||
{
|
{
|
||||||
internal static class VersionExtensions
|
internal static class VersionExtensions
|
||||||
{
|
{
|
||||||
50
Readme.md
50
Readme.md
@@ -168,7 +168,7 @@ Parameters
|
|||||||
* value Value whose logarithm is to be found.
|
* value Value whose logarithm is to be found.
|
||||||
|
|
||||||
Options
|
Options
|
||||||
-b|--base Logarithm base.
|
-b|--base Logarithm base. Default: "10".
|
||||||
-h|--help Shows help text.
|
-h|--help Shows help text.
|
||||||
--version Shows version information.
|
--version Shows version information.
|
||||||
```
|
```
|
||||||
@@ -215,9 +215,9 @@ As a general guideline, prefer to use parameters for required inputs that the co
|
|||||||
|
|
||||||
### Argument syntax
|
### Argument syntax
|
||||||
|
|
||||||
This library supports an argument syntax which is based on the POSIX standard. To be fair, nobody really knows what the standard is about and very few tools actually follow it as they're supposed to, so for the purpose of having dashes and spaces, CliFx is using the "standard command line syntax".
|
This library supports an argument syntax which is based on the POSIX standard. To be fair, nobody really knows what the standard is about and very few tools actually follow it to the letter, so for the purpose of having dashes and spaces, CliFx is using the "standard command line syntax".
|
||||||
|
|
||||||
In more detail, the following examples are all valid:
|
More specifically, the following examples are all valid:
|
||||||
|
|
||||||
- `myapp --foo bar` sets option `"foo"` to value `"bar"`
|
- `myapp --foo bar` sets option `"foo"` to value `"bar"`
|
||||||
- `myapp -f bar` sets option `'f'` to value `"bar"`
|
- `myapp -f bar` sets option `'f'` to value `"bar"`
|
||||||
@@ -227,32 +227,36 @@ In more detail, the following examples are all valid:
|
|||||||
- `myapp -xqf bar` sets options `'x'` and `'q'` without value, and option `'f'` to value `"bar"`
|
- `myapp -xqf bar` sets options `'x'` and `'q'` without value, and option `'f'` to value `"bar"`
|
||||||
- `myapp -i file1.txt file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
- `myapp -i file1.txt file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
||||||
- `myapp -i file1.txt -i file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
- `myapp -i file1.txt -i file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
||||||
- `myapp jar new -o cookie` sets option `'o'` to value `"cookie"` and retains two unbound arguments `"jar"` and `"new"`
|
- `myapp cmd abc -o` routes to command `cmd` (assuming it exists) with parameter `abc` and sets option `'o'` without value
|
||||||
|
|
||||||
Note that CliFx purposely employs a context-free parser when consuming command line arguments. That means that every input is parsed the same way.
|
Argument parsing in CliFx aims to be as deterministic as possible, ideally yielding the same result no matter the context. The only context-sensitive part in the parser is the command name resolution which needs to know what commands are available in order to discern between arguments that correspond to the command name and arguments which are parameters.
|
||||||
|
|
||||||
This also means that `myapp -i file1.txt file2.txt` will _always_ be parsed as an option with multiple values, even if the underlying bound property is not enumerable. For the same reason, unseparated arguments such as `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to `"file"`.
|
Options are always parsed the same way, disregarding the arity of the actual property it binds to. This means that `myapp -i file1.txt file2.txt` will _always_ be parsed as an option with multiple values, even if the underlying bound property is not enumerable. For the same reason, unseparated arguments such as `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to `"file"`.
|
||||||
|
|
||||||
When it comes to command name and parameters, they must appear in a strict order, before any options. The parser can't distinguish between arguments that make up a part of the command name and arguments that belong to command parameters, which is why the non-option arguments are bound at a later stage. It is done by trying to find a command that matches the longest sequence of arguments starting from the first, binding any remaining arguments to positional parameters.
|
Because of these rules, order of arguments is semantically important and it always goes like this:
|
||||||
|
|
||||||
The above design may seem like a deficiency, but it actually provides value in the fact that it's deterministic -- given a set of command line arguments, the semantics behind them always remain the same. This leads to a more consistent experience for both you as a developer, as well as for the users of your application.
|
```ini
|
||||||
|
{directives} {command name} {parameters} {options}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above design makes the usage of your applications a lot more intuitive and predictable, providing a better end-user experience.
|
||||||
|
|
||||||
### Value conversion
|
### Value conversion
|
||||||
|
|
||||||
Parameters and options can have different underlying types:
|
Parameters and options can have different underlying types:
|
||||||
|
|
||||||
- Standard types
|
- Standard types
|
||||||
- Primitive types (`int`, `bool`, `double`, `ulong`, `char`, etc)
|
- Primitive types (`int`, `bool`, `double`, `ulong`, `char`, etc.)
|
||||||
- Date and time types (`DateTime`, `DateTimeOffset`, `TimeSpan`)
|
- Date and time types (`DateTime`, `DateTimeOffset`, `TimeSpan`)
|
||||||
- Enum types
|
- Enum types (converted from either name or value)
|
||||||
- String-initializable types
|
- String-initializable types
|
||||||
- Types with a constructor that accepts a single `string` parameter (`FileInfo`, `DirectoryInfo`, etc)
|
- Types with a constructor that accepts a single `string` parameter (`FileInfo`, `DirectoryInfo`, etc.)
|
||||||
- Types with a static method `Parse` that accepts a single `string` parameter (and optionally `IFormatProvider`)
|
- Types with a static method `Parse` that accepts a single `string` parameter (and optionally `IFormatProvider`)
|
||||||
- Nullable versions of all above types (`decimal?`, `TimeSpan?`, etc)
|
- Nullable versions of all above types (`decimal?`, `TimeSpan?`, etc.)
|
||||||
- Collections of all above types
|
- Collections of all above types
|
||||||
- Array types (`T[]`)
|
- Array types (`T[]`)
|
||||||
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc)
|
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc.)
|
||||||
- Types with a constructor that accepts a single `T[]` parameter (`HashSet<T>`, `List<T>`, etc)
|
- Types with a constructor that accepts a single `T[]` parameter (`HashSet<T>`, `List<T>`, etc.)
|
||||||
|
|
||||||
When defining a parameter of an enumerable type, keep in mind that it has to be the only such parameter and it must be the last in order. Options, on the other hand, don't have this limitation.
|
When defining a parameter of an enumerable type, keep in mind that it has to be the only such parameter and it must be the last in order. Options, on the other hand, don't have this limitation.
|
||||||
|
|
||||||
@@ -392,10 +396,12 @@ You can run `myapp.exe cmd1 [command] --help` to show help on a specific command
|
|||||||
|
|
||||||
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered a higher-level concern and thus handled by `CliApplication`, not by individual commands.
|
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered a higher-level concern and thus handled by `CliApplication`, not by individual commands.
|
||||||
|
|
||||||
Commands can report execution failure simply by throwing exceptions just like any other C# code. When an exception is thrown, `CliApplication` will catch it, print the error, and return an appropriate exit code to the calling process.
|
Commands can report execution failure simply by throwing exceptions just like any other C# code. When an exception is thrown, `CliApplication` will catch it, print the error, and return `1` as the exit code to the calling process.
|
||||||
|
|
||||||
If you want to communicate a specific error through exit code, you can instead throw an instance of `CommandException` which takes an exit code as a parameter. When a command throws an exception of type `CommandException`, it is assumed that this was a result of a handled error and, as such, only the exception message will be printed to the error stream. If a command throws an exception of any other type, the full stack trace will be printed as well.
|
If you want to communicate a specific error through exit code, you can instead throw an instance of `CommandException` which takes an exit code as a parameter. When a command throws an exception of type `CommandException`, it is assumed that this was a result of a handled error and, as such, only the exception message will be printed to the error stream. If a command throws an exception of any other type, the full stack trace will be printed as well.
|
||||||
|
|
||||||
|
> Note: Unix systems rely on 8-bit unsigned integers for exit codes, so it's strongly recommended to use values between `1` and `255` to avoid potential overflow issues.
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command]
|
[Command]
|
||||||
public class DivideCommand : ICommand
|
public class DivideCommand : ICommand
|
||||||
@@ -410,8 +416,8 @@ public class DivideCommand : ICommand
|
|||||||
{
|
{
|
||||||
if (Math.Abs(Divisor) < double.Epsilon)
|
if (Math.Abs(Divisor) < double.Epsilon)
|
||||||
{
|
{
|
||||||
// This will print the error and set exit code to 1337
|
// This will print the error and set exit code to 133
|
||||||
throw new CommandException("Division by zero is not supported.", 1337);
|
throw new CommandException("Division by zero is not supported.", 133);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = Dividend / Divisor;
|
var result = Dividend / Divisor;
|
||||||
@@ -430,10 +436,10 @@ Division by zero is not supported.
|
|||||||
|
|
||||||
> $LastExitCode
|
> $LastExitCode
|
||||||
|
|
||||||
1337
|
133
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also specify the `showHelp` parameter to instruct whether to show the help text after printing the error:
|
You can also specify the `showHelp` parameter to instruct whether to show the help text for the current command after printing the error:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command]
|
[Command]
|
||||||
@@ -448,7 +454,7 @@ public class ExampleCommand : ICommand
|
|||||||
|
|
||||||
### Graceful cancellation
|
### Graceful cancellation
|
||||||
|
|
||||||
It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break), but you can easily override this behavior.
|
It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break), but you can override this behavior.
|
||||||
|
|
||||||
In order to make a command cancellation-aware, you need to call `console.GetCancellationToken()`. This method returns a `CancellationToken` that will trigger when the user issues an interrupt signal. Note that any code that comes before the first call to `GetCancellationToken()` will not be cancellation-aware and as such will terminate instantly. Any subsequent calls to this method will return the same token.
|
In order to make a command cancellation-aware, you need to call `console.GetCancellationToken()`. This method returns a `CancellationToken` that will trigger when the user issues an interrupt signal. Note that any code that comes before the first call to `GetCancellationToken()` will not be cancellation-aware and as such will terminate instantly. Any subsequent calls to this method will return the same token.
|
||||||
|
|
||||||
@@ -506,7 +512,7 @@ public static class Program
|
|||||||
|
|
||||||
CliFx provides an easy way to write functional tests for your commands thanks to the `IConsole` interface.
|
CliFx provides an easy way to write functional tests for your commands thanks to the `IConsole` interface.
|
||||||
|
|
||||||
You can use `VirtualConsole` to replace the application's stdin, stdout and stderr with your own streams. It has multiple constructor overloads allowing you to specify the exact set of streams that you want. Streams that are not provided are replaced with stubs, i.e. `VirtualConsole` doesn't leak to `System.Console` in any way.
|
You can use `VirtualConsole` to replace the application's stdin, stdout and stderr with your own streams. It has multiple constructor overloads allowing you to specify the exact set of streams that you want. Streams which are not provided by you are replaced with stubs, i.e. `VirtualConsole` doesn't leak to `System.Console` in any way.
|
||||||
|
|
||||||
Let's assume you want to test a simple command such as this one.
|
Let's assume you want to test a simple command such as this one.
|
||||||
|
|
||||||
@@ -633,7 +639,7 @@ for (var i = 0.0; i <= 1; i += 0.01)
|
|||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
An option can be configured to use the value of a specific environment variable as a fallback.
|
An option can be configured to use the value of an environment variable as a fallback. If an option was not specified by the user, the value will be extracted from that environment variable instead. This also works on options which are marked as required.
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command]
|
[Command]
|
||||||
|
|||||||
Reference in New Issue
Block a user