This commit is contained in:
Tyrrrz
2024-08-15 03:02:44 +03:00
parent 545c7c3fbd
commit 0532d724a1
38 changed files with 507 additions and 564 deletions

View File

@@ -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<Book> Books)
public partial record Library
{
public static Library Empty { get; } = new(Array.Empty<Book>());
public static Library Empty { get; } = new([]);
}

View File

@@ -0,0 +1,6 @@
using System.Text.Json.Serialization;
namespace CliFx.Demo.Domain;
[JsonSerializable(typeof(Library))]
public partial class LibraryJsonContext : JsonSerializerContext;

View File

@@ -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<Library>(data) ?? Library.Empty;
return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library) ?? Library.Empty;
}
public Book? TryGetBook(string title) =>

View File

@@ -19,7 +19,7 @@ public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp
.UseConsole(FakeConsole)
.Build();
var exitCode = await app.RunAsync(Array.Empty<string>(), new Dictionary<string, string>());
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
// 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<string>(), new Dictionary<string, string>());
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
// 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<string>(), new Dictionary<string, string>());
var exitCode = await app.RunAsync([], new Dictionary<string, string>());
// Assert
exitCode.Should().NotBe(0);

View File

@@ -94,7 +94,7 @@ public class CancellationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOut
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);

View File

@@ -88,7 +88,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -144,7 +144,7 @@ public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
FakeConsole.WriteInput("Hello world");
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -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<string>(),
[],
new Dictionary<string, string>()
);

View File

@@ -90,7 +90,7 @@ public class EnvironmentSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutp
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string> { ["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<string>(),
[],
new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" }
);

View File

@@ -34,7 +34,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -73,7 +73,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -119,7 +119,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -156,7 +156,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -194,7 +194,7 @@ public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);

View File

@@ -22,7 +22,7 @@ public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);

View File

@@ -612,7 +612,7 @@ public class OptionBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOu
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);

View File

@@ -56,7 +56,7 @@ public class RoutingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);

View File

@@ -39,7 +39,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -75,7 +75,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -117,7 +117,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -172,7 +172,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);
@@ -210,7 +210,7 @@ public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testO
// Act
var exitCode = await application.RunAsync(
Array.Empty<string>(),
[],
new Dictionary<string, string>()
);

View File

@@ -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(
/// </summary>
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<int> RunAsync(
ApplicationSchema applicationSchema,
CommandInput commandInput
CommandArguments commandArguments,
IReadOnlyDictionary<string, string?> 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<ICommand>(commandSchema.Type);
: typeActivator.CreateInstance<ICommand>(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(
/// </remarks>
public async ValueTask<int> RunAsync(
IReadOnlyList<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables
IReadOnlyDictionary<string, string?> 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<string, string>(StringComparer.Ordinal)
.ToDictionary<string, string?>(StringComparer.Ordinal)
);
/// <summary>

View File

@@ -15,7 +15,7 @@ namespace CliFx;
/// </summary>
public partial class CliApplicationBuilder
{
private readonly HashSet<CommandSchema> _commandSchemas = [];
private readonly HashSet<CommandSchema> _commands = [];
private bool _isDebugModeAllowed = true;
private bool _isPreviewModeAllowed = true;
@@ -33,19 +33,19 @@ public partial class CliApplicationBuilder
/// <summary>
/// Adds a command to the application.
/// </summary>
public CliApplicationBuilder AddCommand(CommandSchema commandSchema)
public CliApplicationBuilder AddCommand(CommandSchema command)
{
_commandSchemas.Add(commandSchema);
_commands.Add(command);
return this;
}
/// <summary>
/// Adds multiple commands to the application.
/// </summary>
public CliApplicationBuilder AddCommands(IReadOnlyList<CommandSchema> commandSchemas)
public CliApplicationBuilder AddCommands(IReadOnlyList<CommandSchema> 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
);

View File

@@ -8,8 +8,8 @@ namespace CliFx.Extensibility;
public abstract class BindingConverter<T> : IBindingConverter
{
/// <inheritdoc cref="IBindingConverter.Convert" />
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);
}

View File

@@ -8,6 +8,6 @@ namespace CliFx.Extensibility;
public class BoolBindingConverter : BindingConverter<bool>
{
/// <inheritdoc />
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);
}

View File

@@ -9,6 +9,6 @@ public class ConvertibleBindingConverter<T> : BindingConverter<T>
where T : IConvertible
{
/// <inheritdoc />
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);
}

View File

@@ -8,6 +8,6 @@ namespace CliFx.Extensibility;
public class DateTimeOffsetBindingConverter : BindingConverter<DateTimeOffset>
{
/// <inheritdoc />
public override DateTimeOffset Convert(string? rawArgument, IFormatProvider? formatProvider) =>
DateTimeOffset.Parse(rawArgument!, formatProvider);
public override DateTimeOffset Convert(string? rawValue, IFormatProvider? formatProvider) =>
DateTimeOffset.Parse(rawValue!, formatProvider);
}

View File

@@ -15,6 +15,6 @@ public class DelegateBindingConverter<T>(Func<string?, IFormatProvider?, T> conv
: this((rawArgument, _) => convert(rawArgument)) { }
/// <inheritdoc />
public override T Convert(string? rawArgument, IFormatProvider? formatProvider) =>
convert(rawArgument, formatProvider);
public override T Convert(string? rawValue, IFormatProvider? formatProvider) =>
convert(rawValue, formatProvider);
}

View File

@@ -9,6 +9,6 @@ public class EnumBindingConverter<T> : BindingConverter<T>
where T : struct, Enum
{
/// <inheritdoc />
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);
}

View File

@@ -13,5 +13,5 @@ public interface IBindingConverter
/// <summary>
/// Parses the value from a raw command-line argument.
/// </summary>
object? Convert(string? rawArgument, IFormatProvider? formatProvider);
object? Convert(string? rawValue, IFormatProvider? formatProvider);
}

View File

@@ -8,5 +8,5 @@ namespace CliFx.Extensibility;
public class NoopBindingConverter : IBindingConverter
{
/// <inheritdoc />
public object? Convert(string? rawArgument, IFormatProvider? formatProvider) => rawArgument;
public object? Convert(string? rawValue, IFormatProvider? formatProvider) => rawValue;
}

View File

@@ -9,8 +9,8 @@ public class NullableBindingConverter<T>(BindingConverter<T> innerConverter) : B
where T : struct
{
/// <inheritdoc />
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;
}

View File

@@ -8,6 +8,6 @@ namespace CliFx.Extensibility;
public class TimeSpanBindingConverter : BindingConverter<TimeSpan>
{
/// <inheritdoc />
public override TimeSpan Convert(string? rawArgument, IFormatProvider? formatProvider) =>
TimeSpan.Parse(rawArgument!, formatProvider);
public override TimeSpan Convert(string? rawValue, IFormatProvider? formatProvider) =>
TimeSpan.Parse(rawValue!, formatProvider);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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()
);
}

View File

@@ -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<CommandInputSchema, object?> 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<CommandInputSchema, object?> CommandDefaultValues { get; } =
commandDefaultValues;

View File

@@ -1,232 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Utils.Extensions;
namespace CliFx.Input;
/// <summary>
/// Input provided by the user for a command.
/// </summary>
public partial class CommandInput(
string? commandName,
IReadOnlyList<CommandDirectiveInput> directives,
IReadOnlyList<CommandParameterInput> parameters,
IReadOnlyList<CommandOptionInput> options,
IReadOnlyList<EnvironmentVariableInput> environmentVariables
)
{
/// <summary>
/// Name of the requested command.
/// </summary>
public string? CommandName { get; } = commandName;
/// <summary>
/// Provided directives.
/// </summary>
public IReadOnlyList<CommandDirectiveInput> Directives { get; } = directives;
/// <summary>
/// Provided parameters.
/// </summary>
public IReadOnlyList<CommandParameterInput> Parameters { get; } = parameters;
/// <summary>
/// Provided options.
/// </summary>
public IReadOnlyList<CommandOptionInput> Options { get; } = options;
/// <summary>
/// Provided environment variables.
/// </summary>
public IReadOnlyList<EnvironmentVariableInput> 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<CommandDirectiveInput> ParseDirectives(
IReadOnlyList<string> commandLineArguments,
ref int index
)
{
var result = new List<CommandDirectiveInput>();
// 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<string> commandLineArguments,
ISet<string> commandNames,
ref int index
)
{
var potentialCommandNameComponents = new List<string>();
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<CommandParameterInput> ParseParameters(
IReadOnlyList<string> commandLineArguments,
ref int index
)
{
var result = new List<CommandParameterInput>();
// 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<CommandOptionInput> ParseOptions(
IReadOnlyList<string> commandLineArguments,
ref int index
)
{
var result = new List<CommandOptionInput>();
var lastOptionIdentifier = default(string?);
var lastOptionValues = new List<string>();
// 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<string> commandLineArguments,
IReadOnlyDictionary<string, string> environmentVariables,
IReadOnlyList<string> 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
);
}
}

View File

@@ -1,22 +0,0 @@
using System.Collections.Generic;
using System.IO;
namespace CliFx.Input;
/// <summary>
/// Input provided by the means of an environment variable.
/// </summary>
public class EnvironmentVariableInput(string name, string value)
{
/// <summary>
/// Environment variable name.
/// </summary>
public string Name { get; } = name;
/// <summary>
/// Environment variable value.
/// </summary>
public string Value { get; } = value;
internal IReadOnlyList<string> SplitValues() => Value.Split(Path.PathSeparator);
}

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Utils.Extensions;
namespace CliFx.Parsing;
/// <summary>
/// Command-line arguments provided by the user, parsed into their semantic form.
/// </summary>
public partial class CommandArguments(
string? commandName,
IReadOnlyList<CommandDirectiveToken> directives,
IReadOnlyList<CommandParameterToken> parameters,
IReadOnlyList<CommandOptionToken> options
)
{
/// <summary>
/// Name of the requested command.
/// </summary>
public string? CommandName { get; } = commandName;
/// <summary>
/// Provided directives.
/// </summary>
public IReadOnlyList<CommandDirectiveToken> Directives { get; } = directives;
/// <summary>
/// Provided parameters.
/// </summary>
public IReadOnlyList<CommandParameterToken> Parameters { get; } = parameters;
/// <summary>
/// Provided options.
/// </summary>
public IReadOnlyList<CommandOptionToken> 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<CommandDirectiveToken> ParseDirectives(
IReadOnlyList<string> rawArguments,
ref int index
)
{
var result = new List<CommandDirectiveToken>();
// 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<string> rawArguments,
ISet<string> commandNames,
ref int index
)
{
var potentialCommandNameComponents = new List<string>();
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<CommandParameterToken> ParseParameters(
IReadOnlyList<string> rawArguments,
ref int index
)
{
var result = new List<CommandParameterToken>();
// 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<CommandOptionToken> ParseOptions(
IReadOnlyList<string> rawArguments,
ref int index
)
{
var result = new List<CommandOptionToken>();
var lastOptionIdentifier = default(string?);
var lastOptionValues = new List<string>();
// 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<string> rawArguments,
IReadOnlyList<string> 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);
}
}

View File

@@ -1,11 +1,11 @@
using System;
namespace CliFx.Input;
namespace CliFx.Parsing;
/// <summary>
/// Input provided by the means of a directive.
/// Command-line argument that sets a directive.
/// </summary>
public class CommandDirectiveInput(string name)
public class CommandDirectiveToken(string name)
{
/// <summary>
/// Directive name.

View File

@@ -1,14 +1,14 @@
using System.Collections.Generic;
namespace CliFx.Input;
namespace CliFx.Parsing;
/// <summary>
/// Input provided by the means of an option.
/// Command-line arguments that provide one or more values to an option input of a command.
/// </summary>
public class CommandOptionInput(string identifier, IReadOnlyList<string> values)
public class CommandOptionToken(string identifier, IReadOnlyList<string> values)
{
/// <summary>
/// Option identifier (either the name or the short name).
/// Option identifier (either name or short name).
/// </summary>
public string Identifier { get; } = identifier;

View File

@@ -1,9 +1,9 @@
namespace CliFx.Input;
namespace CliFx.Parsing;
/// <summary>
/// Input provided by the means of a parameter.
/// Command-line argument that provide a value to a parameter input of a command.
/// </summary>
public class CommandParameterInput(int order, string value)
public class CommandParameterToken(int order, string value)
{
/// <summary>
/// Parameter order.
@@ -15,5 +15,5 @@ public class CommandParameterInput(int order, string value)
/// </summary>
public string Value { get; } = value;
internal string GetFormattedIdentifier() => $"<{Value}>";
internal string FormattedIdentifier { get; } = $"<{value}>";
}

View File

@@ -66,7 +66,7 @@ public abstract class CommandInputSchema(
}
}
internal void Activate(ICommand instance, IReadOnlyList<string?> rawArguments)
internal void Activate(ICommand instance, IReadOnlyList<string?> 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

View File

@@ -27,7 +27,7 @@ public class CommandParameterSchema(
public int Order { get; } = order;
/// <summary>
/// Parameter name.
/// Parameter name, used in the help text.
/// </summary>
public string Name { get; } = name;

View File

@@ -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<CommandInputSchema, object?>();
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<string, string?> 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<string, string?> environmentVariables
)
{
ActivateParameters(instance, input);
ActivateOptions(instance, input);
ActivateParameters(instance, arguments);
ActivateOptions(instance, arguments, environmentVariables);
}
/// <inheritdoc />
[ExcludeFromCodeCoverage]
public override string ToString() => Name ?? "<default>";
public override string ToString() => Name ?? "{default}";
}
/// <inheritdoc cref="CommandSchema" />

View File

@@ -13,8 +13,8 @@ public class PropertyBinding(
DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
)]
Type type,
Func<object, object?> getValue,
Action<object, object?> setValue
Func<object, object?> get,
Action<object, object?> set
)
{
/// <summary>
@@ -28,12 +28,12 @@ public class PropertyBinding(
/// <summary>
/// Gets the current value of the property on the specified instance.
/// </summary>
public object? GetValue(object instance) => getValue(instance);
public object? Get(object instance) => get(instance);
/// <summary>
/// Sets the current value of the property on the specified instance.
/// </summary>
public void SetValue(object instance, object? value) => setValue(instance, value);
public void Set(object instance, object? value) => set(instance, value);
internal IReadOnlyList<object?>? TryGetValidValues()
{
@@ -67,9 +67,9 @@ public class PropertyBinding<
DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicMethods
)]
TProperty
>(Func<TObject, TProperty?> getValue, Action<TObject, TProperty?> setValue)
>(Func<TObject, TProperty?> get, Action<TObject, TProperty?> 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)
);