From 0532d724a1c2db1be13dd207e0398c92d94d594a Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Thu, 15 Aug 2024 03:02:44 +0300 Subject: [PATCH] asd --- CliFx.Demo/Domain/Library.cs | 5 +- CliFx.Demo/Domain/LibraryJsonContext.cs | 6 + CliFx.Demo/Domain/LibraryProvider.cs | 4 +- CliFx.Tests/ApplicationSpecs.cs | 6 +- CliFx.Tests/CancellationSpecs.cs | 2 +- CliFx.Tests/ConsoleSpecs.cs | 6 +- CliFx.Tests/EnvironmentSpecs.cs | 4 +- CliFx.Tests/ErrorReportingSpecs.cs | 10 +- CliFx.Tests/HelpTextSpecs.cs | 2 +- CliFx.Tests/OptionBindingSpecs.cs | 2 +- CliFx.Tests/RoutingSpecs.cs | 2 +- CliFx.Tests/TypeActivationSpecs.cs | 10 +- CliFx/CliApplication.cs | 47 ++-- CliFx/CliApplicationBuilder.cs | 14 +- CliFx/Extensibility/BindingConverter.cs | 6 +- CliFx/Extensibility/BoolBindingConverter.cs | 4 +- .../ConvertibleBindingConverter.cs | 4 +- .../DateTimeOffsetBindingConverter.cs | 4 +- .../Extensibility/DelegateBindingConverter.cs | 4 +- CliFx/Extensibility/EnumBindingConverter.cs | 4 +- CliFx/Extensibility/IBindingConverter.cs | 2 +- CliFx/Extensibility/NoopBindingConverter.cs | 2 +- .../Extensibility/NullableBindingConverter.cs | 6 +- .../Extensibility/TimeSpanBindingConverter.cs | 4 +- .../CommandArgumentsConsoleFormatter.cs | 65 +++++ .../CommandInputConsoleFormatter.cs | 98 -------- CliFx/Formatting/HelpConsoleFormatter.cs | 72 +++--- CliFx/Formatting/HelpContext.cs | 12 +- CliFx/Input/CommandInput.cs | 232 ------------------ CliFx/Input/EnvironmentVariableInput.cs | 22 -- CliFx/Parsing/CommandArguments.cs | 218 ++++++++++++++++ .../CommandDirectiveToken.cs} | 6 +- .../CommandOptionToken.cs} | 8 +- .../CommandParameterToken.cs} | 8 +- CliFx/Schema/CommandInputSchema.cs | 24 +- CliFx/Schema/CommandParameterSchema.cs | 2 +- CliFx/Schema/CommandSchema.cs | 130 +++++----- CliFx/Schema/PropertyBinding.cs | 14 +- 38 files changed, 507 insertions(+), 564 deletions(-) create mode 100644 CliFx.Demo/Domain/LibraryJsonContext.cs create mode 100644 CliFx/Formatting/CommandArgumentsConsoleFormatter.cs delete mode 100644 CliFx/Formatting/CommandInputConsoleFormatter.cs delete mode 100644 CliFx/Input/CommandInput.cs delete mode 100644 CliFx/Input/EnvironmentVariableInput.cs create mode 100644 CliFx/Parsing/CommandArguments.cs rename CliFx/{Input/CommandDirectiveInput.cs => Parsing/CommandDirectiveToken.cs} (75%) rename CliFx/{Input/CommandOptionInput.cs => Parsing/CommandOptionToken.cs} (67%) rename CliFx/{Input/CommandParameterInput.cs => Parsing/CommandParameterToken.cs} (50%) diff --git a/CliFx.Demo/Domain/Library.cs b/CliFx.Demo/Domain/Library.cs index ee4098d..9c82b36 100644 --- a/CliFx.Demo/Domain/Library.cs +++ b/CliFx.Demo/Domain/Library.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; namespace CliFx.Demo.Domain; @@ -24,5 +23,5 @@ public partial record Library(IReadOnlyList Books) public partial record Library { - public static Library Empty { get; } = new(Array.Empty()); + public static Library Empty { get; } = new([]); } diff --git a/CliFx.Demo/Domain/LibraryJsonContext.cs b/CliFx.Demo/Domain/LibraryJsonContext.cs new file mode 100644 index 0000000..833e67e --- /dev/null +++ b/CliFx.Demo/Domain/LibraryJsonContext.cs @@ -0,0 +1,6 @@ +using System.Text.Json.Serialization; + +namespace CliFx.Demo.Domain; + +[JsonSerializable(typeof(Library))] +public partial class LibraryJsonContext : JsonSerializerContext; \ No newline at end of file diff --git a/CliFx.Demo/Domain/LibraryProvider.cs b/CliFx.Demo/Domain/LibraryProvider.cs index 16d6588..9db7f8d 100644 --- a/CliFx.Demo/Domain/LibraryProvider.cs +++ b/CliFx.Demo/Domain/LibraryProvider.cs @@ -11,7 +11,7 @@ public class LibraryProvider private void StoreLibrary(Library library) { - var data = JsonSerializer.Serialize(library); + var data = JsonSerializer.Serialize(library, LibraryJsonContext.Default.Library); File.WriteAllText(StorageFilePath, data); } @@ -22,7 +22,7 @@ public class LibraryProvider var data = File.ReadAllText(StorageFilePath); - return JsonSerializer.Deserialize(data) ?? Library.Empty; + return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library) ?? Library.Empty; } public Book? TryGetBook(string title) => diff --git a/CliFx.Tests/ApplicationSpecs.cs b/CliFx.Tests/ApplicationSpecs.cs index 4b03589..bedf5d0 100644 --- a/CliFx.Tests/ApplicationSpecs.cs +++ b/CliFx.Tests/ApplicationSpecs.cs @@ -19,7 +19,7 @@ public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp .UseConsole(FakeConsole) .Build(); - var exitCode = await app.RunAsync(Array.Empty(), new Dictionary()); + var exitCode = await app.RunAsync([], new Dictionary()); // Assert exitCode.Should().Be(0); @@ -45,7 +45,7 @@ public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp .UseTypeActivator(Activator.CreateInstance!) .Build(); - var exitCode = await app.RunAsync(Array.Empty(), new Dictionary()); + var exitCode = await app.RunAsync([], new Dictionary()); // Assert exitCode.Should().Be(0); @@ -60,7 +60,7 @@ public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp .UseConsole(FakeConsole) .Build(); - var exitCode = await app.RunAsync(Array.Empty(), new Dictionary()); + var exitCode = await app.RunAsync([], new Dictionary()); // Assert exitCode.Should().NotBe(0); diff --git a/CliFx.Tests/CancellationSpecs.cs b/CliFx.Tests/CancellationSpecs.cs index aa6a6b5..369e728 100644 --- a/CliFx.Tests/CancellationSpecs.cs +++ b/CliFx.Tests/CancellationSpecs.cs @@ -94,7 +94,7 @@ public class CancellationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOut // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); diff --git a/CliFx.Tests/ConsoleSpecs.cs b/CliFx.Tests/ConsoleSpecs.cs index 852de83..002ef1c 100644 --- a/CliFx.Tests/ConsoleSpecs.cs +++ b/CliFx.Tests/ConsoleSpecs.cs @@ -88,7 +88,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -144,7 +144,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) FakeConsole.WriteInput("Hello world"); var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -191,7 +191,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) FakeConsole.EnqueueKey(new ConsoleKeyInfo('\0', ConsoleKey.Backspace, false, false, false)); var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); diff --git a/CliFx.Tests/EnvironmentSpecs.cs b/CliFx.Tests/EnvironmentSpecs.cs index a419c7d..e070644 100644 --- a/CliFx.Tests/EnvironmentSpecs.cs +++ b/CliFx.Tests/EnvironmentSpecs.cs @@ -90,7 +90,7 @@ public class EnvironmentSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" } ); @@ -130,7 +130,7 @@ public class EnvironmentSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" } ); diff --git a/CliFx.Tests/ErrorReportingSpecs.cs b/CliFx.Tests/ErrorReportingSpecs.cs index 4a2a234..699fe70 100644 --- a/CliFx.Tests/ErrorReportingSpecs.cs +++ b/CliFx.Tests/ErrorReportingSpecs.cs @@ -34,7 +34,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -73,7 +73,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -119,7 +119,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -156,7 +156,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -194,7 +194,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); diff --git a/CliFx.Tests/HelpTextSpecs.cs b/CliFx.Tests/HelpTextSpecs.cs index 4fae4bb..8b4ec73 100644 --- a/CliFx.Tests/HelpTextSpecs.cs +++ b/CliFx.Tests/HelpTextSpecs.cs @@ -22,7 +22,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); diff --git a/CliFx.Tests/OptionBindingSpecs.cs b/CliFx.Tests/OptionBindingSpecs.cs index 4e7dbb6..5a17216 100644 --- a/CliFx.Tests/OptionBindingSpecs.cs +++ b/CliFx.Tests/OptionBindingSpecs.cs @@ -612,7 +612,7 @@ public class OptionBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOu // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); diff --git a/CliFx.Tests/RoutingSpecs.cs b/CliFx.Tests/RoutingSpecs.cs index a302e67..380785f 100644 --- a/CliFx.Tests/RoutingSpecs.cs +++ b/CliFx.Tests/RoutingSpecs.cs @@ -56,7 +56,7 @@ public class RoutingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); diff --git a/CliFx.Tests/TypeActivationSpecs.cs b/CliFx.Tests/TypeActivationSpecs.cs index 24c1ad3..75695e3 100644 --- a/CliFx.Tests/TypeActivationSpecs.cs +++ b/CliFx.Tests/TypeActivationSpecs.cs @@ -39,7 +39,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -75,7 +75,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -117,7 +117,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -172,7 +172,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); @@ -210,7 +210,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO // Act var exitCode = await application.RunAsync( - Array.Empty(), + [], new Dictionary() ); diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 7709b7f..ab4b7a3 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using CliFx.Exceptions; using CliFx.Formatting; using CliFx.Infrastructure; -using CliFx.Input; +using CliFx.Parsing; using CliFx.Schema; using CliFx.Utils; using CliFx.Utils.Extensions; @@ -33,11 +33,11 @@ public class CliApplication( /// public ApplicationConfiguration Configuration { get; } = configuration; - private bool IsDebugModeEnabled(CommandInput commandInput) => - Configuration.IsDebugModeAllowed && commandInput.IsDebugDirectiveSpecified; + private bool IsDebugModeEnabled(CommandArguments commandArguments) => + Configuration.IsDebugModeAllowed && commandArguments.IsDebugDirectiveSpecified; - private bool IsPreviewModeEnabled(CommandInput commandInput) => - Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified; + private bool IsPreviewModeEnabled(CommandArguments commandArguments) => + Configuration.IsPreviewModeAllowed && commandArguments.IsPreviewDirectiveSpecified; private async ValueTask PromptDebuggerAsync() { @@ -57,7 +57,8 @@ public class CliApplication( private async ValueTask RunAsync( ApplicationSchema applicationSchema, - CommandInput commandInput + CommandArguments commandArguments, + IReadOnlyDictionary environmentVariables ) { // Console colors may have already been overridden by the parent process, @@ -65,26 +66,26 @@ public class CliApplication( console.ResetColor(); // Handle the debug directive - if (IsDebugModeEnabled(commandInput)) + if (IsDebugModeEnabled(commandArguments)) { await PromptDebuggerAsync(); } // Handle the preview directive - if (IsPreviewModeEnabled(commandInput)) + if (IsPreviewModeEnabled(commandArguments)) { - console.WriteCommandInput(commandInput); + console.WriteCommandInput(commandArguments); return 0; } // Try to get the command schema that matches the input - var commandSchema = + var command = ( - !string.IsNullOrWhiteSpace(commandInput.CommandName) + !string.IsNullOrWhiteSpace(commandArguments.CommandName) // If the command name is specified, try to find the command by name. // This should always succeed, because the input parsing relies on // the list of available command names. - ? applicationSchema.TryFindCommand(commandInput.CommandName) + ? applicationSchema.TryFindCommand(commandArguments.CommandName) // Otherwise, try to find the default command : applicationSchema.TryFindDefaultCommand() ) @@ -95,16 +96,16 @@ public class CliApplication( // Initialize an instance of the command type var commandInstance = - commandSchema == FallbackDefaultCommand.Schema + command == FallbackDefaultCommand.Schema ? new FallbackDefaultCommand() // bypass the activator - : typeActivator.CreateInstance(commandSchema.Type); + : typeActivator.CreateInstance(command.Type); // Assemble the help context var helpContext = new HelpContext( Metadata, applicationSchema, - commandSchema, - commandSchema.GetValues(commandInstance) + command, + command.GetValues(commandInstance) ); // Starting from this point, we may produce exceptions that are meant for the @@ -113,8 +114,8 @@ public class CliApplication( // propagate further. try { - // Activate the command instance with the provided input - commandSchema.Activate(commandInstance, commandInput); + // Activate the command instance with the provided user input + command.Activate(commandInstance, commandArguments, environmentVariables); // Handle the version option if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true }) @@ -163,18 +164,18 @@ public class CliApplication( /// public async ValueTask RunAsync( IReadOnlyList commandLineArguments, - IReadOnlyDictionary environmentVariables + IReadOnlyDictionary environmentVariables ) { try { return await RunAsync( Configuration.Schema, - CommandInput.Parse( + CommandArguments.Parse( commandLineArguments, - environmentVariables, Configuration.Schema.GetCommandNames() - ) + ), + environmentVariables ); } // To prevent the app from showing the annoying troubleshooting dialog on Windows, @@ -203,7 +204,7 @@ public class CliApplication( commandLineArguments, Environment .GetEnvironmentVariables() - .ToDictionary(StringComparer.Ordinal) + .ToDictionary(StringComparer.Ordinal) ); /// diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index c55c98c..9b3ef40 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -15,7 +15,7 @@ namespace CliFx; /// public partial class CliApplicationBuilder { - private readonly HashSet _commandSchemas = []; + private readonly HashSet _commands = []; private bool _isDebugModeAllowed = true; private bool _isPreviewModeAllowed = true; @@ -33,19 +33,19 @@ public partial class CliApplicationBuilder /// /// Adds a command to the application. /// - public CliApplicationBuilder AddCommand(CommandSchema commandSchema) + public CliApplicationBuilder AddCommand(CommandSchema command) { - _commandSchemas.Add(commandSchema); + _commands.Add(command); return this; } /// /// Adds multiple commands to the application. /// - public CliApplicationBuilder AddCommands(IReadOnlyList commandSchemas) + public CliApplicationBuilder AddCommands(IReadOnlyList commands) { - foreach (var commandSchema in commandSchemas) - AddCommand(commandSchema); + foreach (var command in commands) + AddCommand(command); return this; } @@ -157,7 +157,7 @@ public partial class CliApplicationBuilder ); var configuration = new ApplicationConfiguration( - new ApplicationSchema(_commandSchemas.ToArray()), + new ApplicationSchema(_commands.ToArray()), _isDebugModeAllowed, _isPreviewModeAllowed ); diff --git a/CliFx/Extensibility/BindingConverter.cs b/CliFx/Extensibility/BindingConverter.cs index 51abe3d..066a248 100644 --- a/CliFx/Extensibility/BindingConverter.cs +++ b/CliFx/Extensibility/BindingConverter.cs @@ -8,8 +8,8 @@ namespace CliFx.Extensibility; public abstract class BindingConverter : IBindingConverter { /// - public abstract T? Convert(string? rawArgument, IFormatProvider? formatProvider); + public abstract T? Convert(string? rawValue, IFormatProvider? formatProvider); - object? IBindingConverter.Convert(string? rawArgument, IFormatProvider? formatProvider) => - Convert(rawArgument, formatProvider); + object? IBindingConverter.Convert(string? rawValue, IFormatProvider? formatProvider) => + Convert(rawValue, formatProvider); } diff --git a/CliFx/Extensibility/BoolBindingConverter.cs b/CliFx/Extensibility/BoolBindingConverter.cs index 1f72ca6..400973a 100644 --- a/CliFx/Extensibility/BoolBindingConverter.cs +++ b/CliFx/Extensibility/BoolBindingConverter.cs @@ -8,6 +8,6 @@ namespace CliFx.Extensibility; public class BoolBindingConverter : BindingConverter { /// - public override bool Convert(string? rawArgument, IFormatProvider? formatProvider) => - string.IsNullOrWhiteSpace(rawArgument) || bool.Parse(rawArgument); + public override bool Convert(string? rawValue, IFormatProvider? formatProvider) => + string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue); } diff --git a/CliFx/Extensibility/ConvertibleBindingConverter.cs b/CliFx/Extensibility/ConvertibleBindingConverter.cs index 0612a45..073367a 100644 --- a/CliFx/Extensibility/ConvertibleBindingConverter.cs +++ b/CliFx/Extensibility/ConvertibleBindingConverter.cs @@ -9,6 +9,6 @@ public class ConvertibleBindingConverter : BindingConverter where T : IConvertible { /// - public override T? Convert(string? rawArgument, IFormatProvider? formatProvider) => - (T?)System.Convert.ChangeType(rawArgument, typeof(T), formatProvider); + public override T? Convert(string? rawValue, IFormatProvider? formatProvider) => + (T?)System.Convert.ChangeType(rawValue, typeof(T), formatProvider); } diff --git a/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs b/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs index ec53316..546ed40 100644 --- a/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs +++ b/CliFx/Extensibility/DateTimeOffsetBindingConverter.cs @@ -8,6 +8,6 @@ namespace CliFx.Extensibility; public class DateTimeOffsetBindingConverter : BindingConverter { /// - public override DateTimeOffset Convert(string? rawArgument, IFormatProvider? formatProvider) => - DateTimeOffset.Parse(rawArgument!, formatProvider); + public override DateTimeOffset Convert(string? rawValue, IFormatProvider? formatProvider) => + DateTimeOffset.Parse(rawValue!, formatProvider); } diff --git a/CliFx/Extensibility/DelegateBindingConverter.cs b/CliFx/Extensibility/DelegateBindingConverter.cs index 828c640..db334bf 100644 --- a/CliFx/Extensibility/DelegateBindingConverter.cs +++ b/CliFx/Extensibility/DelegateBindingConverter.cs @@ -15,6 +15,6 @@ public class DelegateBindingConverter(Func conv : this((rawArgument, _) => convert(rawArgument)) { } /// - public override T Convert(string? rawArgument, IFormatProvider? formatProvider) => - convert(rawArgument, formatProvider); + public override T Convert(string? rawValue, IFormatProvider? formatProvider) => + convert(rawValue, formatProvider); } diff --git a/CliFx/Extensibility/EnumBindingConverter.cs b/CliFx/Extensibility/EnumBindingConverter.cs index 417c302..02a62ac 100644 --- a/CliFx/Extensibility/EnumBindingConverter.cs +++ b/CliFx/Extensibility/EnumBindingConverter.cs @@ -9,6 +9,6 @@ public class EnumBindingConverter : BindingConverter where T : struct, Enum { /// - public override T Convert(string? rawArgument, IFormatProvider? formatProvider) => - (T)Enum.Parse(typeof(T), rawArgument!, true); + public override T Convert(string? rawValue, IFormatProvider? formatProvider) => + (T)Enum.Parse(typeof(T), rawValue!, true); } diff --git a/CliFx/Extensibility/IBindingConverter.cs b/CliFx/Extensibility/IBindingConverter.cs index 86f9fe0..fa4fbd9 100644 --- a/CliFx/Extensibility/IBindingConverter.cs +++ b/CliFx/Extensibility/IBindingConverter.cs @@ -13,5 +13,5 @@ public interface IBindingConverter /// /// Parses the value from a raw command-line argument. /// - object? Convert(string? rawArgument, IFormatProvider? formatProvider); + object? Convert(string? rawValue, IFormatProvider? formatProvider); } diff --git a/CliFx/Extensibility/NoopBindingConverter.cs b/CliFx/Extensibility/NoopBindingConverter.cs index fef732e..92537cd 100644 --- a/CliFx/Extensibility/NoopBindingConverter.cs +++ b/CliFx/Extensibility/NoopBindingConverter.cs @@ -8,5 +8,5 @@ namespace CliFx.Extensibility; public class NoopBindingConverter : IBindingConverter { /// - public object? Convert(string? rawArgument, IFormatProvider? formatProvider) => rawArgument; + public object? Convert(string? rawValue, IFormatProvider? formatProvider) => rawValue; } diff --git a/CliFx/Extensibility/NullableBindingConverter.cs b/CliFx/Extensibility/NullableBindingConverter.cs index be3646b..3150e2b 100644 --- a/CliFx/Extensibility/NullableBindingConverter.cs +++ b/CliFx/Extensibility/NullableBindingConverter.cs @@ -9,8 +9,8 @@ public class NullableBindingConverter(BindingConverter innerConverter) : B where T : struct { /// - public override T? Convert(string? rawArgument, IFormatProvider? formatProvider) => - !string.IsNullOrWhiteSpace(rawArgument) - ? innerConverter.Convert(rawArgument, formatProvider) + public override T? Convert(string? rawValue, IFormatProvider? formatProvider) => + !string.IsNullOrWhiteSpace(rawValue) + ? innerConverter.Convert(rawValue, formatProvider) : null; } diff --git a/CliFx/Extensibility/TimeSpanBindingConverter.cs b/CliFx/Extensibility/TimeSpanBindingConverter.cs index 0e11016..f9fd4f2 100644 --- a/CliFx/Extensibility/TimeSpanBindingConverter.cs +++ b/CliFx/Extensibility/TimeSpanBindingConverter.cs @@ -8,6 +8,6 @@ namespace CliFx.Extensibility; public class TimeSpanBindingConverter : BindingConverter { /// - public override TimeSpan Convert(string? rawArgument, IFormatProvider? formatProvider) => - TimeSpan.Parse(rawArgument!, formatProvider); + public override TimeSpan Convert(string? rawValue, IFormatProvider? formatProvider) => + TimeSpan.Parse(rawValue!, formatProvider); } diff --git a/CliFx/Formatting/CommandArgumentsConsoleFormatter.cs b/CliFx/Formatting/CommandArgumentsConsoleFormatter.cs new file mode 100644 index 0000000..2d4faaf --- /dev/null +++ b/CliFx/Formatting/CommandArgumentsConsoleFormatter.cs @@ -0,0 +1,65 @@ +using System; +using CliFx.Infrastructure; +using CliFx.Parsing; + +namespace CliFx.Formatting; + +internal class CommandArgumentsConsoleFormatter(ConsoleWriter consoleWriter) + : ConsoleFormatter(consoleWriter) +{ + public void WriteCommandArguments(CommandArguments commandArguments) + { + // Command name + if (!string.IsNullOrWhiteSpace(commandArguments.CommandName)) + { + Write(ConsoleColor.Cyan, commandArguments.CommandName); + Write(' '); + } + + // Parameters + foreach (var parameterInput in commandArguments.Parameters) + { + Write('<'); + Write(ConsoleColor.White, parameterInput.Value); + Write('>'); + Write(' '); + } + + // Options + foreach (var optionInput in commandArguments.Options) + { + Write('['); + + // Identifier + Write(ConsoleColor.White, optionInput.FormattedIdentifier); + + // Value(s) + foreach (var value in optionInput.Values) + { + Write(' '); + Write('"'); + Write(value); + Write('"'); + } + + Write(']'); + Write(' '); + } + + WriteLine(); + } +} + +internal static class CommandInputConsoleFormatterExtensions +{ + public static void WriteCommandInput( + this ConsoleWriter consoleWriter, + CommandArguments commandArguments + ) => + new CommandArgumentsConsoleFormatter(consoleWriter).WriteCommandArguments(commandArguments); + + public static void WriteCommandInput( + this IConsole console, + CommandArguments commandArguments + ) => console.Output.WriteCommandInput(commandArguments); +} diff --git a/CliFx/Formatting/CommandInputConsoleFormatter.cs b/CliFx/Formatting/CommandInputConsoleFormatter.cs deleted file mode 100644 index 3bac812..0000000 --- a/CliFx/Formatting/CommandInputConsoleFormatter.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using CliFx.Infrastructure; -using CliFx.Input; - -namespace CliFx.Formatting; - -internal class CommandInputConsoleFormatter(ConsoleWriter consoleWriter) - : ConsoleFormatter(consoleWriter) -{ - private void WriteCommandLineArguments(CommandInput commandInput) - { - Write("Command-line:"); - WriteLine(); - - WriteHorizontalMargin(); - - // Command name - if (!string.IsNullOrWhiteSpace(commandInput.CommandName)) - { - Write(ConsoleColor.Cyan, commandInput.CommandName); - Write(' '); - } - - // Parameters - foreach (var parameterInput in commandInput.Parameters) - { - Write('<'); - Write(ConsoleColor.White, parameterInput.Value); - Write('>'); - Write(' '); - } - - // Options - foreach (var optionInput in commandInput.Options) - { - Write('['); - - // Identifier - Write(ConsoleColor.White, optionInput.FormattedIdentifier); - - // Value(s) - foreach (var value in optionInput.Values) - { - Write(' '); - Write('"'); - Write(value); - Write('"'); - } - - Write(']'); - Write(' '); - } - - WriteLine(); - } - - private void WriteEnvironmentVariables(CommandInput commandInput) - { - Write("Environment:"); - WriteLine(); - - // Environment variables - foreach (var environmentVariableInput in commandInput.EnvironmentVariables) - { - WriteHorizontalMargin(); - - // Name - Write(ConsoleColor.White, environmentVariableInput.Name); - - Write('='); - - // Value - Write('"'); - Write(environmentVariableInput.Value); - Write('"'); - - WriteLine(); - } - } - - public void WriteCommandInput(CommandInput commandInput) - { - WriteCommandLineArguments(commandInput); - WriteLine(); - WriteEnvironmentVariables(commandInput); - } -} - -internal static class CommandInputConsoleFormatterExtensions -{ - public static void WriteCommandInput( - this ConsoleWriter consoleWriter, - CommandInput commandInput - ) => new CommandInputConsoleFormatter(consoleWriter).WriteCommandInput(commandInput); - - public static void WriteCommandInput(this IConsole console, CommandInput commandInput) => - console.Output.WriteCommandInput(commandInput); -} diff --git a/CliFx/Formatting/HelpConsoleFormatter.cs b/CliFx/Formatting/HelpConsoleFormatter.cs index 4a8d7a8..2a8cce4 100644 --- a/CliFx/Formatting/HelpConsoleFormatter.cs +++ b/CliFx/Formatting/HelpConsoleFormatter.cs @@ -20,13 +20,13 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con private void WriteCommandInvocation() { - Write(context.ApplicationMetadata.ExecutableName); + Write(context.Metadata.ExecutableName); // Command name - if (!string.IsNullOrWhiteSpace(context.CommandSchema.Name)) + if (!string.IsNullOrWhiteSpace(context.Command.Name)) { Write(' '); - Write(ConsoleColor.Cyan, context.CommandSchema.Name); + Write(ConsoleColor.Cyan, context.Command.Name); } } @@ -36,16 +36,16 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con WriteVerticalMargin(); // Title and version - Write(ConsoleColor.White, context.ApplicationMetadata.Title); + Write(ConsoleColor.White, context.Metadata.Title); Write(' '); - Write(ConsoleColor.Yellow, context.ApplicationMetadata.Version); + Write(ConsoleColor.Yellow, context.Metadata.Version); WriteLine(); // Description - if (!string.IsNullOrWhiteSpace(context.ApplicationMetadata.Description)) + if (!string.IsNullOrWhiteSpace(context.Metadata.Description)) { WriteHorizontalMargin(); - Write(context.ApplicationMetadata.Description); + Write(context.Metadata.Description); WriteLine(); } } @@ -65,7 +65,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con Write(' '); // Parameters - foreach (var parameter in context.CommandSchema.Parameters.OrderBy(p => p.Order)) + foreach (var parameter in context.Command.Parameters.OrderBy(p => p.Order)) { Write( ConsoleColor.DarkCyan, @@ -75,7 +75,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con } // Required options - foreach (var option in context.CommandSchema.Options.Where(o => o.IsRequired)) + foreach (var option in context.Command.Options.Where(o => o.IsRequired)) { Write( ConsoleColor.Yellow, @@ -90,7 +90,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con } // Placeholder for non-required options - if (context.CommandSchema.Options.Any(o => !o.IsRequired)) + if (context.Command.Options.Any(o => !o.IsRequired)) { Write(ConsoleColor.Yellow, "[options]"); } @@ -99,11 +99,9 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con } // Child command usage - var childCommandSchemas = context.ApplicationSchema.GetChildCommands( - context.CommandSchema.Name - ); + var childCommands = context.Application.GetChildCommands(context.Command.Name); - if (childCommandSchemas.Any()) + if (childCommands.Any()) { WriteHorizontalMargin(); @@ -123,7 +121,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con private void WriteCommandDescription() { - if (string.IsNullOrWhiteSpace(context.CommandSchema.Description)) + if (string.IsNullOrWhiteSpace(context.Command.Description)) return; if (!IsEmpty) @@ -133,13 +131,13 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con WriteHorizontalMargin(); - Write(context.CommandSchema.Description); + Write(context.Command.Description); WriteLine(); } private void WriteCommandParameters() { - if (!context.CommandSchema.Parameters.Any()) + if (!context.Command.Parameters.Any()) return; if (!IsEmpty) @@ -147,9 +145,9 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con WriteHeader("Parameters"); - foreach (var parameterSchema in context.CommandSchema.Parameters.OrderBy(p => p.Order)) + foreach (var parameter in context.Command.Parameters.OrderBy(p => p.Order)) { - if (parameterSchema.IsRequired) + if (parameter.IsRequired) { Write(ConsoleColor.Red, "* "); } @@ -158,19 +156,19 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con WriteHorizontalMargin(); } - Write(ConsoleColor.DarkCyan, $"{parameterSchema.Name}"); + Write(ConsoleColor.DarkCyan, $"{parameter.Name}"); WriteColumnMargin(); // Description - if (!string.IsNullOrWhiteSpace(parameterSchema.Description)) + if (!string.IsNullOrWhiteSpace(parameter.Description)) { - Write(parameterSchema.Description); + Write(parameter.Description); Write(' '); } // Valid values - var validValues = parameterSchema.Property.TryGetValidValues(); + var validValues = parameter.Property.TryGetValidValues(); if (validValues?.Any() == true) { Write(ConsoleColor.White, "Choices: "); @@ -200,9 +198,9 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con } // Default value - if (!parameterSchema.IsRequired) + if (!parameter.IsRequired) { - WriteDefaultValue(parameterSchema); + WriteDefaultValue(parameter); } WriteLine(); @@ -216,9 +214,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con WriteHeader("Options"); - foreach ( - var optionSchema in context.CommandSchema.Options.OrderByDescending(o => o.IsRequired) - ) + foreach (var optionSchema in context.Command.Options.OrderByDescending(o => o.IsRequired)) { if (optionSchema.IsRequired) { @@ -358,12 +354,12 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con private void WriteCommandChildren() { - var childCommandSchemas = context - .ApplicationSchema.GetChildCommands(context.CommandSchema.Name) + var childCommands = context + .Application.GetChildCommands(context.Command.Name) .OrderBy(a => a.Name, StringComparer.Ordinal) .ToArray(); - if (!childCommandSchemas.Any()) + if (!childCommands.Any()) return; if (!IsEmpty) @@ -371,14 +367,14 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con WriteHeader("Commands"); - foreach (var childCommandSchema in childCommandSchemas) + foreach (var childCommandSchema in childCommands) { // Name WriteHorizontalMargin(); Write( ConsoleColor.Cyan, // Relative to current command - childCommandSchema.Name?.Substring(context.CommandSchema.Name?.Length ?? 0).Trim() + childCommandSchema.Name?.Substring(context.Command.Name?.Length ?? 0).Trim() ); WriteColumnMargin(); @@ -391,17 +387,17 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con } // Child commands of child command - var grandChildCommandSchemas = context - .ApplicationSchema.GetChildCommands(childCommandSchema.Name) + var grandChildCommands = context + .Application.GetChildCommands(childCommandSchema.Name) .OrderBy(c => c.Name, StringComparer.Ordinal) .ToArray(); - if (grandChildCommandSchemas.Any()) + if (grandChildCommands.Any()) { Write(ConsoleColor.White, "Subcommands: "); var isFirst = true; - foreach (var grandChildCommandSchema in grandChildCommandSchemas) + foreach (var grandChildCommandSchema in grandChildCommands) { if (isFirst) { @@ -416,7 +412,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con ConsoleColor.Cyan, // Relative to current command (not the parent) grandChildCommandSchema - .Name?.Substring(context.CommandSchema.Name?.Length ?? 0) + .Name?.Substring(context.Command.Name?.Length ?? 0) .Trim() ); } diff --git a/CliFx/Formatting/HelpContext.cs b/CliFx/Formatting/HelpContext.cs index a05ab44..1a73824 100644 --- a/CliFx/Formatting/HelpContext.cs +++ b/CliFx/Formatting/HelpContext.cs @@ -4,17 +4,17 @@ using CliFx.Schema; namespace CliFx.Formatting; internal class HelpContext( - ApplicationMetadata applicationMetadata, - ApplicationSchema applicationSchema, - CommandSchema commandSchema, + ApplicationMetadata metadata, + ApplicationSchema application, + CommandSchema command, IReadOnlyDictionary commandDefaultValues ) { - public ApplicationMetadata ApplicationMetadata { get; } = applicationMetadata; + public ApplicationMetadata Metadata { get; } = metadata; - public ApplicationSchema ApplicationSchema { get; } = applicationSchema; + public ApplicationSchema Application { get; } = application; - public CommandSchema CommandSchema { get; } = commandSchema; + public CommandSchema Command { get; } = command; public IReadOnlyDictionary CommandDefaultValues { get; } = commandDefaultValues; diff --git a/CliFx/Input/CommandInput.cs b/CliFx/Input/CommandInput.cs deleted file mode 100644 index a6751f4..0000000 --- a/CliFx/Input/CommandInput.cs +++ /dev/null @@ -1,232 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using CliFx.Utils.Extensions; - -namespace CliFx.Input; - -/// -/// Input provided by the user for a command. -/// -public partial class CommandInput( - string? commandName, - IReadOnlyList directives, - IReadOnlyList parameters, - IReadOnlyList options, - IReadOnlyList environmentVariables -) -{ - /// - /// Name of the requested command. - /// - public string? CommandName { get; } = commandName; - - /// - /// Provided directives. - /// - public IReadOnlyList Directives { get; } = directives; - - /// - /// Provided parameters. - /// - public IReadOnlyList Parameters { get; } = parameters; - - /// - /// Provided options. - /// - public IReadOnlyList Options { get; } = options; - - /// - /// Provided environment variables. - /// - public IReadOnlyList EnvironmentVariables { get; } = - environmentVariables; - - internal bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective); - - internal bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective); -} - -public partial class CommandInput -{ - private static IReadOnlyList ParseDirectives( - IReadOnlyList commandLineArguments, - ref int index - ) - { - var result = new List(); - - // Consume all consecutive directive arguments - for (; index < commandLineArguments.Count; index++) - { - var argument = commandLineArguments[index]; - - // Break on the first non-directive argument - if (!argument.StartsWith('[') || !argument.EndsWith(']')) - break; - - var directiveName = argument.Substring(1, argument.Length - 2); - result.Add(new CommandDirectiveInput(directiveName)); - } - - return result; - } - - private static string? ParseCommandName( - IReadOnlyList commandLineArguments, - ISet commandNames, - ref int index - ) - { - var potentialCommandNameComponents = new List(); - var commandName = default(string?); - - var lastIndex = index; - - // Append arguments to a buffer until we find the longest sequence that represents - // a valid command name. - for (var i = index; i < commandLineArguments.Count; i++) - { - var argument = commandLineArguments[i]; - - potentialCommandNameComponents.Add(argument); - - var potentialCommandName = potentialCommandNameComponents.JoinToString(" "); - if (commandNames.Contains(potentialCommandName)) - { - // Record the position but continue the loop in case we find - // a longer (more specific) match. - commandName = potentialCommandName; - lastIndex = i; - } - } - - // Move the index to the position where the command name ended - if (!string.IsNullOrWhiteSpace(commandName)) - index = lastIndex + 1; - - return commandName; - } - - private static IReadOnlyList ParseParameters( - IReadOnlyList commandLineArguments, - ref int index - ) - { - var result = new List(); - - // Consume all arguments until the first option identifier - for (; index < commandLineArguments.Count; index++) - { - var argument = commandLineArguments[index]; - - var isOptionIdentifier = - // Name - argument.StartsWith("--", StringComparison.Ordinal) - && argument.Length > 2 - && char.IsLetter(argument[2]) - || - // Short name - argument.StartsWith('-') - && argument.Length > 1 - && char.IsLetter(argument[1]); - - // Break on the first option identifier - if (isOptionIdentifier) - break; - - result.Add(new CommandParameterInput(index, argument)); - } - - return result; - } - - private static IReadOnlyList ParseOptions( - IReadOnlyList commandLineArguments, - ref int index - ) - { - var result = new List(); - - var lastOptionIdentifier = default(string?); - var lastOptionValues = new List(); - - // Consume and group all remaining arguments into options - for (; index < commandLineArguments.Count; index++) - { - var argument = commandLineArguments[index]; - - // Name - if ( - argument.StartsWith("--", StringComparison.Ordinal) - && argument.Length > 2 - && char.IsLetter(argument[2]) - ) - { - // Flush previous - if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues)); - - lastOptionIdentifier = argument[2..]; - lastOptionValues = []; - } - // Short name - else if (argument.StartsWith('-') && argument.Length > 1 && char.IsLetter(argument[1])) - { - foreach (var identifier in argument[1..]) - { - // Flush previous - if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues)); - - lastOptionIdentifier = identifier.AsString(); - lastOptionValues = []; - } - } - // Value - else if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - { - lastOptionValues.Add(argument); - } - } - - // Flush the last option - if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) - result.Add(new CommandOptionInput(lastOptionIdentifier, lastOptionValues)); - - return result; - } - - internal static CommandInput Parse( - IReadOnlyList commandLineArguments, - IReadOnlyDictionary environmentVariables, - IReadOnlyList availableCommandNames - ) - { - var index = 0; - - var parsedDirectives = ParseDirectives(commandLineArguments, ref index); - - var parsedCommandName = ParseCommandName( - commandLineArguments, - availableCommandNames.ToHashSet(StringComparer.OrdinalIgnoreCase), - ref index - ); - - var parsedParameters = ParseParameters(commandLineArguments, ref index); - - var parsedOptions = ParseOptions(commandLineArguments, ref index); - - var parsedEnvironmentVariables = environmentVariables - .Select(kvp => new EnvironmentVariableInput(kvp.Key, kvp.Value)) - .ToArray(); - - return new CommandInput( - parsedCommandName, - parsedDirectives, - parsedParameters, - parsedOptions, - parsedEnvironmentVariables - ); - } -} diff --git a/CliFx/Input/EnvironmentVariableInput.cs b/CliFx/Input/EnvironmentVariableInput.cs deleted file mode 100644 index 01419f7..0000000 --- a/CliFx/Input/EnvironmentVariableInput.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -namespace CliFx.Input; - -/// -/// Input provided by the means of an environment variable. -/// -public class EnvironmentVariableInput(string name, string value) -{ - /// - /// Environment variable name. - /// - public string Name { get; } = name; - - /// - /// Environment variable value. - /// - public string Value { get; } = value; - - internal IReadOnlyList SplitValues() => Value.Split(Path.PathSeparator); -} diff --git a/CliFx/Parsing/CommandArguments.cs b/CliFx/Parsing/CommandArguments.cs new file mode 100644 index 0000000..14ee814 --- /dev/null +++ b/CliFx/Parsing/CommandArguments.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CliFx.Utils.Extensions; + +namespace CliFx.Parsing; + +/// +/// Command-line arguments provided by the user, parsed into their semantic form. +/// +public partial class CommandArguments( + string? commandName, + IReadOnlyList directives, + IReadOnlyList parameters, + IReadOnlyList options +) +{ + /// + /// Name of the requested command. + /// + public string? CommandName { get; } = commandName; + + /// + /// Provided directives. + /// + public IReadOnlyList Directives { get; } = directives; + + /// + /// Provided parameters. + /// + public IReadOnlyList Parameters { get; } = parameters; + + /// + /// Provided options. + /// + public IReadOnlyList Options { get; } = options; + + internal bool IsDebugDirectiveSpecified => Directives.Any(d => d.IsDebugDirective); + + internal bool IsPreviewDirectiveSpecified => Directives.Any(d => d.IsPreviewDirective); +} + +public partial class CommandArguments +{ + private static IReadOnlyList ParseDirectives( + IReadOnlyList rawArguments, + ref int index + ) + { + var result = new List(); + + // Consume all consecutive directive arguments + for (; index < rawArguments.Count; index++) + { + var rawArgument = rawArguments[index]; + + // Break on the first non-directive argument + if (!rawArgument.StartsWith('[') || !rawArgument.EndsWith(']')) + break; + + var directiveName = rawArgument.Substring(1, rawArgument.Length - 2); + result.Add(new CommandDirectiveToken(directiveName)); + } + + return result; + } + + private static string? ParseCommandName( + IReadOnlyList rawArguments, + ISet commandNames, + ref int index + ) + { + var potentialCommandNameComponents = new List(); + var commandName = default(string?); + + var lastIndex = index; + + // Append arguments to a buffer until we find the longest sequence that represents + // a valid command name. + for (var i = index; i < rawArguments.Count; i++) + { + var rawArgument = rawArguments[i]; + + potentialCommandNameComponents.Add(rawArgument); + + var potentialCommandName = potentialCommandNameComponents.JoinToString(" "); + if (commandNames.Contains(potentialCommandName)) + { + // Record the position but continue the loop in case we find + // a longer (more specific) match. + commandName = potentialCommandName; + lastIndex = i; + } + } + + // Move the index to the position where the command name ended + if (!string.IsNullOrWhiteSpace(commandName)) + index = lastIndex + 1; + + return commandName; + } + + private static IReadOnlyList ParseParameters( + IReadOnlyList rawArguments, + ref int index + ) + { + var result = new List(); + + // Consume all arguments until the first option identifier + for (; index < rawArguments.Count; index++) + { + var rawArgument = rawArguments[index]; + + var isOptionIdentifier = + // Name + rawArgument.StartsWith("--", StringComparison.Ordinal) + && rawArgument.Length > 2 + && char.IsLetter(rawArgument[2]) + || + // Short name + rawArgument.StartsWith('-') + && rawArgument.Length > 1 + && char.IsLetter(rawArgument[1]); + + // Break on the first option identifier + if (isOptionIdentifier) + break; + + result.Add(new CommandParameterToken(index, rawArgument)); + } + + return result; + } + + private static IReadOnlyList ParseOptions( + IReadOnlyList rawArguments, + ref int index + ) + { + var result = new List(); + + var lastOptionIdentifier = default(string?); + var lastOptionValues = new List(); + + // Consume and group all remaining arguments into options + for (; index < rawArguments.Count; index++) + { + var rawArgument = rawArguments[index]; + + // Name + if ( + rawArgument.StartsWith("--", StringComparison.Ordinal) + && rawArgument.Length > 2 + && char.IsLetter(rawArgument[2]) + ) + { + // Flush previous + if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) + result.Add(new CommandOptionToken(lastOptionIdentifier, lastOptionValues)); + + lastOptionIdentifier = rawArgument[2..]; + lastOptionValues = []; + } + // Short name + else if ( + rawArgument.StartsWith('-') + && rawArgument.Length > 1 + && char.IsLetter(rawArgument[1]) + ) + { + foreach (var identifier in rawArgument[1..]) + { + // Flush previous + if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) + result.Add(new CommandOptionToken(lastOptionIdentifier, lastOptionValues)); + + lastOptionIdentifier = identifier.AsString(); + lastOptionValues = []; + } + } + // Value + else if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) + { + lastOptionValues.Add(rawArgument); + } + } + + // Flush the last option + if (!string.IsNullOrWhiteSpace(lastOptionIdentifier)) + result.Add(new CommandOptionToken(lastOptionIdentifier, lastOptionValues)); + + return result; + } + + internal static CommandArguments Parse( + IReadOnlyList rawArguments, + IReadOnlyList availableCommandNames + ) + { + var index = 0; + + var directives = ParseDirectives(rawArguments, ref index); + + var commandName = ParseCommandName( + rawArguments, + availableCommandNames.ToHashSet(StringComparer.OrdinalIgnoreCase), + ref index + ); + + var parameters = ParseParameters(rawArguments, ref index); + + var options = ParseOptions(rawArguments, ref index); + + return new CommandArguments(commandName, directives, parameters, options); + } +} diff --git a/CliFx/Input/CommandDirectiveInput.cs b/CliFx/Parsing/CommandDirectiveToken.cs similarity index 75% rename from CliFx/Input/CommandDirectiveInput.cs rename to CliFx/Parsing/CommandDirectiveToken.cs index 2253695..8352d33 100644 --- a/CliFx/Input/CommandDirectiveInput.cs +++ b/CliFx/Parsing/CommandDirectiveToken.cs @@ -1,11 +1,11 @@ using System; -namespace CliFx.Input; +namespace CliFx.Parsing; /// -/// Input provided by the means of a directive. +/// Command-line argument that sets a directive. /// -public class CommandDirectiveInput(string name) +public class CommandDirectiveToken(string name) { /// /// Directive name. diff --git a/CliFx/Input/CommandOptionInput.cs b/CliFx/Parsing/CommandOptionToken.cs similarity index 67% rename from CliFx/Input/CommandOptionInput.cs rename to CliFx/Parsing/CommandOptionToken.cs index a90e623..1772fa2 100644 --- a/CliFx/Input/CommandOptionInput.cs +++ b/CliFx/Parsing/CommandOptionToken.cs @@ -1,14 +1,14 @@ using System.Collections.Generic; -namespace CliFx.Input; +namespace CliFx.Parsing; /// -/// Input provided by the means of an option. +/// Command-line arguments that provide one or more values to an option input of a command. /// -public class CommandOptionInput(string identifier, IReadOnlyList values) +public class CommandOptionToken(string identifier, IReadOnlyList values) { /// - /// Option identifier (either the name or the short name). + /// Option identifier (either name or short name). /// public string Identifier { get; } = identifier; diff --git a/CliFx/Input/CommandParameterInput.cs b/CliFx/Parsing/CommandParameterToken.cs similarity index 50% rename from CliFx/Input/CommandParameterInput.cs rename to CliFx/Parsing/CommandParameterToken.cs index 6e87a2a..59912c8 100644 --- a/CliFx/Input/CommandParameterInput.cs +++ b/CliFx/Parsing/CommandParameterToken.cs @@ -1,9 +1,9 @@ -namespace CliFx.Input; +namespace CliFx.Parsing; /// -/// Input provided by the means of a parameter. +/// Command-line argument that provide a value to a parameter input of a command. /// -public class CommandParameterInput(int order, string value) +public class CommandParameterToken(int order, string value) { /// /// Parameter order. @@ -15,5 +15,5 @@ public class CommandParameterInput(int order, string value) /// public string Value { get; } = value; - internal string GetFormattedIdentifier() => $"<{Value}>"; + internal string FormattedIdentifier { get; } = $"<{value}>"; } diff --git a/CliFx/Schema/CommandInputSchema.cs b/CliFx/Schema/CommandInputSchema.cs index f7b31d4..93b45ec 100644 --- a/CliFx/Schema/CommandInputSchema.cs +++ b/CliFx/Schema/CommandInputSchema.cs @@ -66,7 +66,7 @@ public abstract class CommandInputSchema( } } - internal void Activate(ICommand instance, IReadOnlyList rawArguments) + internal void Activate(ICommand instance, IReadOnlyList rawValues) { var formatProvider = CultureInfo.InvariantCulture; @@ -75,31 +75,29 @@ public abstract class CommandInputSchema( // Multiple values expected, single or multiple values provided if (IsSequence) { - var value = rawArguments - .Select(v => Converter.Convert(v, formatProvider)) - .ToArray(); + var values = rawValues.Select(v => Converter.Convert(v, formatProvider)).ToArray(); // TODO: cast array to the proper type - Validate(value); + Validate(values); - Property.SetValue(instance, value); + Property.Set(instance, values); } // Single value expected, single value provided - else if (rawArguments.Count <= 1) + else if (rawValues.Count <= 1) { - var value = Converter.Convert(rawArguments.SingleOrDefault(), formatProvider); + var value = Converter.Convert(rawValues.SingleOrDefault(), formatProvider); Validate(value); - Property.SetValue(instance, value); + Property.Set(instance, value); } // Single value expected, multiple values provided else { throw CliFxException.UserError( $""" - {Kind} {FormattedIdentifier} expects a single argument, but provided with multiple: - {rawArguments.Select(v => '<' + v + '>').JoinToString(" ")} + {Kind} {FormattedIdentifier} expects a single value, but provided with multiple: + {rawValues.Select(v => '<' + v + '>').JoinToString(" ")} """ ); } @@ -108,8 +106,8 @@ public abstract class CommandInputSchema( { throw CliFxException.UserError( $""" - {Kind} {FormattedIdentifier} cannot be set from the provided argument(s): - {rawArguments.Select(v => '<' + v + '>').JoinToString(" ")} + {Kind} {FormattedIdentifier} cannot be set from the provided value(s): + {rawValues.Select(v => '<' + v + '>').JoinToString(" ")} Error: {ex.Message} """, ex diff --git a/CliFx/Schema/CommandParameterSchema.cs b/CliFx/Schema/CommandParameterSchema.cs index b37e232..9fd9c27 100644 --- a/CliFx/Schema/CommandParameterSchema.cs +++ b/CliFx/Schema/CommandParameterSchema.cs @@ -27,7 +27,7 @@ public class CommandParameterSchema( public int Order { get; } = order; /// - /// Parameter name. + /// Parameter name, used in the help text. /// public string Name { get; } = name; diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs index 310a929..7d8b56e 100644 --- a/CliFx/Schema/CommandSchema.cs +++ b/CliFx/Schema/CommandSchema.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using CliFx.Exceptions; -using CliFx.Input; +using CliFx.Parsing; using CliFx.Utils.Extensions; namespace CliFx.Schema; @@ -65,74 +66,74 @@ public class CommandSchema( { var result = new Dictionary(); - foreach (var parameterSchema in Parameters) + foreach (var parameter in Parameters) { - var value = parameterSchema.Property.GetValue(instance); - result[parameterSchema] = value; + var value = parameter.Property.Get(instance); + result[parameter] = value; } - foreach (var optionSchema in Options) + foreach (var option in Options) { - var value = optionSchema.Property.GetValue(instance); - result[optionSchema] = value; + var value = option.Property.Get(instance); + result[option] = value; } return result; } - private void ActivateParameters(ICommand instance, CommandInput input) + private void ActivateParameters(ICommand instance, CommandArguments arguments) { // Ensure there are no unexpected parameters and that all parameters are provided - var remainingParameterInputs = input.Parameters.ToList(); - var remainingRequiredParameterSchemas = Parameters.Where(p => p.IsRequired).ToList(); + var remainingParameterTokens = arguments.Parameters.ToList(); + var remainingRequiredParameters = Parameters.Where(p => p.IsRequired).ToList(); var position = 0; - foreach (var parameterSchema in Parameters.OrderBy(p => p.Order)) + foreach (var parameter in Parameters.OrderBy(p => p.Order)) { // Break when there are no remaining inputs - if (position >= input.Parameters.Count) + if (position >= arguments.Parameters.Count) break; - // Non-sequence: take one input at the current position - if (!parameterSchema.IsSequence) - { - var parameterInput = input.Parameters[position]; - parameterSchema.Activate(instance, [parameterInput.Value]); - - position++; - remainingParameterInputs.Remove(parameterInput); - } // Sequence: take all remaining inputs starting from the current position + if (parameter.IsSequence) + { + var parameterTokens = arguments.Parameters.Skip(position).ToArray(); + + parameter.Activate(instance, parameterTokens.Select(p => p.Value).ToArray()); + + position += parameterTokens.Length; + remainingParameterTokens.RemoveRange(parameterTokens); + } + // Non-sequence: take one input at the current position else { - var parameterInputs = input.Parameters.Skip(position).ToArray(); + var parameterToken = arguments.Parameters[position]; + parameter.Activate(instance, [parameterToken.Value]); - parameterSchema.Activate(instance, parameterInputs.Select(p => p.Value).ToArray()); - - position += parameterInputs.Length; - remainingParameterInputs.RemoveRange(parameterInputs); + position++; + remainingParameterTokens.Remove(parameterToken); } - remainingRequiredParameterSchemas.Remove(parameterSchema); + remainingRequiredParameters.Remove(parameter); } - if (remainingParameterInputs.Any()) + if (remainingParameterTokens.Any()) { throw CliFxException.UserError( $""" Unexpected parameter(s): - {remainingParameterInputs.Select(p => p.GetFormattedIdentifier()).JoinToString(" ")} + {remainingParameterTokens.Select(p => p.FormattedIdentifier).JoinToString(" ")} """ ); } - if (remainingRequiredParameterSchemas.Any()) + if (remainingRequiredParameters.Any()) { throw CliFxException.UserError( $""" - Missing required parameter(s): - {remainingRequiredParameterSchemas + Missing equired parameter(s): + {remainingRequiredParameters .Select(p => p.FormattedIdentifier) .JoinToString(" ")} """ @@ -140,45 +141,52 @@ public class CommandSchema( } } - private void ActivateOptions(ICommand instance, CommandInput input) + private void ActivateOptions( + ICommand instance, + CommandArguments arguments, + IReadOnlyDictionary environmentVariables + ) { // Ensure there are no unrecognized options and that all required options are set - var remainingOptionInputs = input.Options.ToList(); - var remainingRequiredOptionSchemas = Options.Where(o => o.IsRequired).ToList(); + var remainingOptionTokens = arguments.Options.ToList(); + var remainingRequiredOptions = Options.Where(o => o.IsRequired).ToList(); - foreach (var optionSchema in Options) + foreach (var option in Options) { - var optionInputs = input - .Options.Where(o => optionSchema.MatchesIdentifier(o.Identifier)) + var optionToken = arguments + .Options.Where(o => option.MatchesIdentifier(o.Identifier)) .ToArray(); - var environmentVariableInput = input.EnvironmentVariables.FirstOrDefault(e => - optionSchema.MatchesEnvironmentVariable(e.Name) + var environmentVariable = environmentVariables.FirstOrDefault(v => + option.MatchesEnvironmentVariable(v.Key) ); // Direct input - if (optionInputs.Any()) + if (optionToken.Any()) { - var rawValues = optionInputs.SelectMany(o => o.Values).ToArray(); + var rawValues = optionToken.SelectMany(o => o.Values).ToArray(); - optionSchema.Activate(instance, rawValues); + option.Activate(instance, rawValues); // Required options need at least one value to be set if (rawValues.Any()) - remainingRequiredOptionSchemas.Remove(optionSchema); + remainingRequiredOptions.Remove(option); } // Environment variable - else if (environmentVariableInput is not null) + else if (!string.IsNullOrEmpty(environmentVariable.Value)) { - var rawValues = !optionSchema.IsSequence - ? [environmentVariableInput.Value] - : environmentVariableInput.SplitValues(); + var rawValues = !option.IsSequence + ? [environmentVariable.Value] + : environmentVariable.Value.Split( + Path.PathSeparator, + StringSplitOptions.RemoveEmptyEntries + ); - optionSchema.Activate(instance, rawValues); + option.Activate(instance, rawValues); // Required options need at least one value to be set if (rawValues.Any()) - remainingRequiredOptionSchemas.Remove(optionSchema); + remainingRequiredOptions.Remove(option); } // No input, skip else @@ -186,25 +194,25 @@ public class CommandSchema( continue; } - remainingOptionInputs.RemoveRange(optionInputs); + remainingOptionTokens.RemoveRange(optionToken); } - if (remainingOptionInputs.Any()) + if (remainingOptionTokens.Any()) { throw CliFxException.UserError( $""" Unrecognized option(s): - {remainingOptionInputs.Select(o => o.FormattedIdentifier).JoinToString(", ")} + {remainingOptionTokens.Select(o => o.FormattedIdentifier).JoinToString(", ")} """ ); } - if (remainingRequiredOptionSchemas.Any()) + if (remainingRequiredOptions.Any()) { throw CliFxException.UserError( $""" Missing required option(s): - {remainingRequiredOptionSchemas + {remainingRequiredOptions .Select(o => o.FormattedIdentifier) .JoinToString(", ")} """ @@ -212,15 +220,19 @@ public class CommandSchema( } } - internal void Activate(ICommand instance, CommandInput input) + internal void Activate( + ICommand instance, + CommandArguments arguments, + IReadOnlyDictionary environmentVariables + ) { - ActivateParameters(instance, input); - ActivateOptions(instance, input); + ActivateParameters(instance, arguments); + ActivateOptions(instance, arguments, environmentVariables); } /// [ExcludeFromCodeCoverage] - public override string ToString() => Name ?? ""; + public override string ToString() => Name ?? "{default}"; } /// diff --git a/CliFx/Schema/PropertyBinding.cs b/CliFx/Schema/PropertyBinding.cs index 36130a8..3c9a945 100644 --- a/CliFx/Schema/PropertyBinding.cs +++ b/CliFx/Schema/PropertyBinding.cs @@ -13,8 +13,8 @@ public class PropertyBinding( DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods )] Type type, - Func getValue, - Action setValue + Func get, + Action set ) { /// @@ -28,12 +28,12 @@ public class PropertyBinding( /// /// Gets the current value of the property on the specified instance. /// - public object? GetValue(object instance) => getValue(instance); + public object? Get(object instance) => get(instance); /// /// Sets the current value of the property on the specified instance. /// - public void SetValue(object instance, object? value) => setValue(instance, value); + public void Set(object instance, object? value) => set(instance, value); internal IReadOnlyList? TryGetValidValues() { @@ -67,9 +67,9 @@ public class PropertyBinding< DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods )] TProperty ->(Func getValue, Action setValue) +>(Func get, Action set) : PropertyBinding( typeof(TProperty), - o => getValue((TObject)o), - (o, v) => setValue((TObject)o, (TProperty?)v) + o => get((TObject)o), + (o, v) => set((TObject)o, (TProperty?)v) );