diff --git a/src/Spectre.Console.Cli/CommandContext.cs b/src/Spectre.Console.Cli/CommandContext.cs index c175ec4b8..89a2876ba 100644 --- a/src/Spectre.Console.Cli/CommandContext.cs +++ b/src/Spectre.Console.Cli/CommandContext.cs @@ -13,6 +13,11 @@ public sealed class CommandContext /// public IRemainingArguments Remaining { get; } + /// + /// Gets all the arguments that were passed to the applicaton. + /// + public IReadOnlyList Arguments { get; } + /// /// Gets the name of the command. /// @@ -32,11 +37,17 @@ public sealed class CommandContext /// /// Initializes a new instance of the class. /// + /// All arguments that were passed to the application. /// The remaining arguments. /// The command name. /// The command data. - public CommandContext(IRemainingArguments remaining, string name, object? data) + public CommandContext( + IEnumerable arguments, + IRemainingArguments remaining, + string name, + object? data) { + Arguments = arguments.ToSafeReadOnlyList(); Remaining = remaining ?? throw new System.ArgumentNullException(nameof(remaining)); Name = name ?? throw new System.ArgumentNullException(nameof(name)); Data = data; diff --git a/src/Spectre.Console.Cli/IRemainingArguments.cs b/src/Spectre.Console.Cli/IRemainingArguments.cs index 8127416f1..acd3b6055 100644 --- a/src/Spectre.Console.Cli/IRemainingArguments.cs +++ b/src/Spectre.Console.Cli/IRemainingArguments.cs @@ -12,6 +12,7 @@ public interface IRemainingArguments /// /// Gets the raw, non-parsed remaining arguments. + /// This is normally everything after the `--` delimiter. /// IReadOnlyList Raw { get; } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs index 0c34da5b3..2c2b1594f 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -17,7 +17,7 @@ public async Task Execute(IConfiguration configuration, IEnumerable throw new ArgumentNullException(nameof(configuration)); } - args ??= new List(); + var arguments = args.ToSafeReadOnlyList(); _registrar.RegisterInstance(typeof(IConfiguration), configuration); _registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole()); @@ -31,7 +31,7 @@ public async Task Execute(IConfiguration configuration, IEnumerable if (model.DefaultCommand == null) { // Got at least one argument? - var firstArgument = args.FirstOrDefault(); + var firstArgument = arguments.FirstOrDefault(); if (firstArgument != null) { // Asking for version? Kind of a hack, but it's alright. @@ -47,7 +47,7 @@ public async Task Execute(IConfiguration configuration, IEnumerable } // Parse and map the model against the arguments. - var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args); + var parsedResult = ParseCommandLineArguments(model, configuration.Settings, arguments); // Register the arguments with the container. _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); @@ -79,7 +79,7 @@ public async Task Execute(IConfiguration configuration, IEnumerable } // Is this the default and is it called without arguments when there are required arguments? - if (leaf.Command.IsDefaultCommand && args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required)) + if (leaf.Command.IsDefaultCommand && arguments.Count == 0 && leaf.Command.Parameters.Any(p => p.Required)) { // Display help for default command. configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command)); @@ -87,15 +87,18 @@ public async Task Execute(IConfiguration configuration, IEnumerable } // Create the content. - var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data); + var context = new CommandContext( + arguments, + parsedResult.Remaining, + leaf.Command.Name, + leaf.Command.Data); // Execute the command tree. return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); } } -#pragma warning disable CS8603 // Possible null reference return. - private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable args) + private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IReadOnlyList args) { var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments); @@ -103,7 +106,7 @@ private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, Co var tokenizerResult = CommandTreeTokenizer.Tokenize(args); var parsedResult = parser.Parse(parserContext, tokenizerResult); - var lastParsedLeaf = parsedResult?.Tree?.GetLeafCommand(); + var lastParsedLeaf = parsedResult.Tree?.GetLeafCommand(); var lastParsedCommand = lastParsedLeaf?.Command; if (lastParsedLeaf != null && lastParsedCommand != null && lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp && @@ -122,7 +125,6 @@ private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, Co return parsedResult; } -#pragma warning restore CS8603 // Possible null reference return. private static string ResolveApplicationVersion(IConfiguration configuration) { diff --git a/src/Spectre.Console.Cli/Internal/Extensions/EnumerableExtensions.cs b/src/Spectre.Console.Cli/Internal/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..ea775cff5 --- /dev/null +++ b/src/Spectre.Console.Cli/Internal/Extensions/EnumerableExtensions.cs @@ -0,0 +1,14 @@ +namespace Spectre.Console.Cli; + +internal static class EnumerableExtensions +{ + public static IReadOnlyList ToSafeReadOnlyList(this IEnumerable source) + { + return source switch + { + null => new List(), + IReadOnlyList list => list, + _ => source.ToList(), + }; + } +} \ No newline at end of file diff --git a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj index e74335a6c..69ba6c246 100644 --- a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj +++ b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj @@ -13,7 +13,7 @@ - + diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Context.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Context.cs new file mode 100644 index 000000000..ec88fd135 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Context.cs @@ -0,0 +1,23 @@ +namespace Spectre.Console.Tests.Unit.Cli; + +public sealed partial class CommandAppTests +{ + [Fact] + [Expectation("Should_Expose_Raw_Arguments")] + public void Should_Return_Correct_Text_When_Command_Is_Unknown() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.AddCommand("test"); + }); + + // When + var result = app.Run("test", "--foo", "32", "--lol"); + + // Then + result.Context.ShouldNotBeNull(); + result.Context.Arguments.ShouldBe(new[] { "test", "--foo", "32", "--lol" }); + } +} \ No newline at end of file