diff --git a/src/Spectre.Console.Cli/ICommandAppSettings.cs b/src/Spectre.Console.Cli/ICommandAppSettings.cs index 4e3b5b9..91af178 100644 --- a/src/Spectre.Console.Cli/ICommandAppSettings.cs +++ b/src/Spectre.Console.Cli/ICommandAppSettings.cs @@ -49,7 +49,15 @@ public interface ICommandAppSettings /// /// Gets or sets a value indicating whether or not parsing is strict. /// - bool StrictParsing { get; set; } + bool StrictParsing { get; set; } + + /// + /// Gets or sets a value indicating whether or not flags found on the commnd line + /// that would normally result in a being thrown + /// during parsing with the message "Flags cannot be assigned a value." + /// should instead be added to the remaining arguments collection. + /// + bool ConvertFlagsToRemainingArguments { get; set; } /// /// Gets or sets a value indicating whether or not exceptions should be propagated. diff --git a/src/Spectre.Console.Cli/IConfigurator.cs b/src/Spectre.Console.Cli/IConfigurator.cs index f6108b6..1276a7f 100644 --- a/src/Spectre.Console.Cli/IConfigurator.cs +++ b/src/Spectre.Console.Cli/IConfigurator.cs @@ -15,7 +15,7 @@ public interface IConfigurator /// /// The example arguments. void AddExample(string[] args); - + /// /// Adds a command. /// diff --git a/src/Spectre.Console.Cli/IConfiguratorOfT.cs b/src/Spectre.Console.Cli/IConfiguratorOfT.cs index 1dabf0c..25a5556 100644 --- a/src/Spectre.Console.Cli/IConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/IConfiguratorOfT.cs @@ -19,6 +19,18 @@ public interface IConfigurator /// The example arguments. void AddExample(string[] args); + /// + /// Adds a default command. + /// + /// + /// This is the command that will run if the user doesn't specify one on the command line. + /// It must be able to execute successfully by itself ie. without requiring any command line + /// arguments, flags or option values. + /// + /// The default command type. + void SetDefaultCommand() + where TDefaultCommand : class, ICommandLimiter; + /// /// Marks the branch as hidden. /// Hidden branches do not show up in help documentation or diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs index e80a294..3b7b735 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -45,12 +45,10 @@ internal sealed class CommandExecutor } // Parse and map the model against the arguments. - var parser = new CommandTreeParser(model, configuration.Settings); - var parsedResult = parser.Parse(args); - _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); + var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args); // Currently the root? - if (parsedResult.Tree == null) + if (parsedResult?.Tree == null) { // Display help. configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues)); @@ -75,6 +73,7 @@ internal sealed class CommandExecutor } // Register the arguments with the container. + _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); // Create the resolver and the context. @@ -86,6 +85,34 @@ internal sealed class CommandExecutor return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); } } + + private CommandTreeParserResult? ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable args) + { + var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments); + + var parserContext = new CommandTreeParserContext(args, settings.ParsingMode); + var tokenizerResult = CommandTreeTokenizer.Tokenize(args); + var parsedResult = parser.Parse(parserContext, tokenizerResult); + + var lastParsedLeaf = parsedResult?.Tree?.GetLeafCommand(); + var lastParsedCommand = lastParsedLeaf?.Command; + if (lastParsedLeaf != null && lastParsedCommand != null && + lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp && + lastParsedCommand.DefaultCommand != null) + { + // Insert this branch's default command into the command line + // arguments and try again to see if it will parse. + var argsWithDefaultCommand = new List(args); + + argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name); + + parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode); + tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand); + parsedResult = parser.Parse(parserContext, tokenizerResult); + } + + return parsedResult; + } private static string ResolveApplicationVersion(IConfiguration configuration) { diff --git a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs index b1d5300..dc46992 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs @@ -12,7 +12,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings public bool PropagateExceptions { get; set; } public bool ValidateExamples { get; set; } public bool TrimTrailingPeriod { get; set; } = true; - public bool StrictParsing { get; set; } + public bool StrictParsing { get; set; } + public bool ConvertFlagsToRemainingArguments { get; set; } = false; public ParsingMode ParsingMode => StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; diff --git a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs index e4e5036..42e7d70 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs @@ -23,7 +23,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig public void AddExample(string[] args) { Examples.Add(args); - } + } public ConfiguredCommand SetDefaultCommand() where TDefaultCommand : class, ICommand @@ -36,7 +36,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig public ICommandConfigurator AddCommand(string name) where TCommand : class, ICommand { - var command = Commands.AddAndReturn(ConfiguredCommand.FromType(name, false)); + var command = Commands.AddAndReturn(ConfiguredCommand.FromType(name, isDefaultCommand: false)); return new CommandConfigurator(command); } diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs index 83dc591..fb23cd2 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs @@ -1,92 +1,100 @@ -namespace Spectre.Console.Cli; - -internal sealed class Configurator : IUnsafeBranchConfigurator, IConfigurator - where TSettings : CommandSettings -{ - private readonly ConfiguredCommand _command; - private readonly ITypeRegistrar? _registrar; - - public Configurator(ConfiguredCommand command, ITypeRegistrar? registrar) - { - _command = command; - _registrar = registrar; - } - - public void SetDescription(string description) - { - _command.Description = description; - } - - public void AddExample(string[] args) - { - _command.Examples.Add(args); - } - - public void HideBranch() - { - _command.IsHidden = true; - } - - public ICommandConfigurator AddCommand(string name) - where TCommand : class, ICommandLimiter - { - var command = ConfiguredCommand.FromType(name); - var configurator = new CommandConfigurator(command); - - _command.Children.Add(command); - return configurator; - } - - public ICommandConfigurator AddDelegate(string name, Func func) - where TDerivedSettings : TSettings - { - var command = ConfiguredCommand.FromDelegate( - name, (context, settings) => func(context, (TDerivedSettings)settings)); - - _command.Children.Add(command); - return new CommandConfigurator(command); - } - - public IBranchConfigurator AddBranch(string name, Action> action) - where TDerivedSettings : TSettings - { - var command = ConfiguredCommand.FromBranch(name); - action(new Configurator(command, _registrar)); - var added = _command.Children.AddAndReturn(command); - return new BranchConfigurator(added); - } - - ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command) - { - var method = GetType().GetMethod("AddCommand"); - if (method == null) - { - throw new CommandConfigurationException("Could not find AddCommand by reflection."); - } - - method = method.MakeGenericMethod(command); - - if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result)) - { - throw new CommandConfigurationException("Invoking AddCommand returned null."); - } - - return result; - } - - IBranchConfigurator IUnsafeConfigurator.AddBranch(string name, Type settings, Action action) - { - var command = ConfiguredCommand.FromBranch(settings, name); - - // Create the configurator. - var configuratorType = typeof(Configurator<>).MakeGenericType(settings); - if (!(Activator.CreateInstance(configuratorType, new object?[] { command, _registrar }) is IUnsafeBranchConfigurator configurator)) - { - throw new CommandConfigurationException("Could not create configurator by reflection."); - } - - action(configurator); - var added = _command.Children.AddAndReturn(command); - return new BranchConfigurator(added); - } +namespace Spectre.Console.Cli; + +internal sealed class Configurator : IUnsafeBranchConfigurator, IConfigurator + where TSettings : CommandSettings +{ + private readonly ConfiguredCommand _command; + private readonly ITypeRegistrar? _registrar; + + public Configurator(ConfiguredCommand command, ITypeRegistrar? registrar) + { + _command = command; + _registrar = registrar; + } + + public void SetDescription(string description) + { + _command.Description = description; + } + + public void AddExample(string[] args) + { + _command.Examples.Add(args); + } + + public void SetDefaultCommand() + where TDefaultCommand : class, ICommandLimiter + { + var defaultCommand = ConfiguredCommand.FromType( + CliConstants.DefaultCommandName, isDefaultCommand: true); + _command.Children.Add(defaultCommand); + } + + public void HideBranch() + { + _command.IsHidden = true; + } + + public ICommandConfigurator AddCommand(string name) + where TCommand : class, ICommandLimiter + { + var command = ConfiguredCommand.FromType(name, isDefaultCommand: false); + var configurator = new CommandConfigurator(command); + + _command.Children.Add(command); + return configurator; + } + + public ICommandConfigurator AddDelegate(string name, Func func) + where TDerivedSettings : TSettings + { + var command = ConfiguredCommand.FromDelegate( + name, (context, settings) => func(context, (TDerivedSettings)settings)); + + _command.Children.Add(command); + return new CommandConfigurator(command); + } + + public IBranchConfigurator AddBranch(string name, Action> action) + where TDerivedSettings : TSettings + { + var command = ConfiguredCommand.FromBranch(name); + action(new Configurator(command, _registrar)); + var added = _command.Children.AddAndReturn(command); + return new BranchConfigurator(added); + } + + ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command) + { + var method = GetType().GetMethod("AddCommand"); + if (method == null) + { + throw new CommandConfigurationException("Could not find AddCommand by reflection."); + } + + method = method.MakeGenericMethod(command); + + if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result)) + { + throw new CommandConfigurationException("Invoking AddCommand returned null."); + } + + return result; + } + + IBranchConfigurator IUnsafeConfigurator.AddBranch(string name, Type settings, Action action) + { + var command = ConfiguredCommand.FromBranch(settings, name); + + // Create the configurator. + var configuratorType = typeof(Configurator<>).MakeGenericType(settings); + if (!(Activator.CreateInstance(configuratorType, new object?[] { command, _registrar }) is IUnsafeBranchConfigurator configurator)) + { + throw new CommandConfigurationException("Could not create configurator by reflection."); + } + + action(configurator); + var added = _command.Children.AddAndReturn(command); + return new BranchConfigurator(added); + } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs index f855dcf..fbc022e 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs @@ -27,7 +27,10 @@ internal sealed class ConfiguredCommand CommandType = commandType; SettingsType = settingsType; Delegate = @delegate; - IsDefaultCommand = isDefaultCommand; + IsDefaultCommand = isDefaultCommand; + + // Default commands are always created as hidden. + IsHidden = IsDefaultCommand; Children = new List(); Examples = new List(); diff --git a/src/Spectre.Console.Cli/Internal/Extensions/TypeRegistrarExtensions.cs b/src/Spectre.Console.Cli/Internal/Extensions/TypeRegistrarExtensions.cs index d1b0b2b..109bebf 100644 --- a/src/Spectre.Console.Cli/Internal/Extensions/TypeRegistrarExtensions.cs +++ b/src/Spectre.Console.Cli/Internal/Extensions/TypeRegistrarExtensions.cs @@ -6,10 +6,6 @@ internal static class TypeRegistrarExtensions { var stack = new Stack(); model.Commands.ForEach(c => stack.Push(c)); - if (model.DefaultCommand != null) - { - stack.Push(model.DefaultCommand); - } while (stack.Count > 0) { diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs index f2751f4..a50b471 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs @@ -1,7 +1,7 @@ namespace Spectre.Console.Cli; internal sealed class CommandInfo : ICommandContainer -{ +{ public string Name { get; } public HashSet Aliases { get; } public string? Description { get; } @@ -10,14 +10,17 @@ internal sealed class CommandInfo : ICommandContainer public Type SettingsType { get; } public Func? Delegate { get; } public bool IsDefaultCommand { get; } - public bool IsHidden { get; } public CommandInfo? Parent { get; } public IList Children { get; } public IList Parameters { get; } public IList Examples { get; } public bool IsBranch => CommandType == null && Delegate == null; - IList ICommandContainer.Commands => Children; + IList ICommandContainer.Commands => Children; + + // only branches can have a default command + public CommandInfo? DefaultCommand => IsBranch ? Children.FirstOrDefault(c => c.IsDefaultCommand) : null; + public bool IsHidden { get; } public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype) { diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs index 7068db4..526f0ee 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs @@ -4,21 +4,20 @@ internal sealed class CommandModel : ICommandContainer { public string? ApplicationName { get; } public ParsingMode ParsingMode { get; } - public CommandInfo? DefaultCommand { get; } public IList Commands { get; } public IList Examples { get; } public bool TrimTrailingPeriod { get; } + + public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand); public CommandModel( CommandAppSettings settings, - CommandInfo? defaultCommand, IEnumerable commands, IEnumerable examples) { ApplicationName = settings.ApplicationName; ParsingMode = settings.ParsingMode; TrimTrailingPeriod = settings.TrimTrailingPeriod; - DefaultCommand = defaultCommand; Commands = new List(commands ?? Array.Empty()); Examples = new List(examples ?? Array.Empty()); } diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs index da15174..c31a9bf 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandModelBuilder.cs @@ -1,5 +1,5 @@ namespace Spectre.Console.Cli; - + internal static class CommandModelBuilder { // Consider removing this in favor for value tuples at some point. @@ -25,18 +25,19 @@ internal static class CommandModelBuilder result.Add(Build(null, command)); } - var defaultCommand = default(CommandInfo); if (configuration.DefaultCommand != null) { // Add the examples from the configuration to the default command. configuration.DefaultCommand.Examples.AddRange(configuration.Examples); // Build the default command. - defaultCommand = Build(null, configuration.DefaultCommand); + var defaultCommand = Build(null, configuration.DefaultCommand); + + result.Add(defaultCommand); } // Create the command model and validate it. - var model = new CommandModel(configuration.Settings, defaultCommand, result, configuration.Examples); + var model = new CommandModel(configuration.Settings, result, configuration.Examples); CommandModelValidator.Validate(model, configuration.Settings); return model; @@ -54,7 +55,7 @@ internal static class CommandModelBuilder foreach (var childCommand in command.Children) { var child = Build(info, childCommand); - info.Children.Add(child); + info.Children.Add(child); } // Normalize argument positions. diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandModelValidator.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandModelValidator.cs index dbce5a5..86624f5 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandModelValidator.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandModelValidator.cs @@ -14,7 +14,7 @@ internal static class CommandModelValidator throw new ArgumentNullException(nameof(settings)); } - if (model.Commands.Count == 0 && model.DefaultCommand == null) + if (model.Commands.Count == 0) { throw CommandConfigurationException.NoCommandConfigured(); } @@ -31,7 +31,6 @@ internal static class CommandModelValidator } } - Validate(model.DefaultCommand); foreach (var command in model.Commands) { Validate(command); @@ -147,7 +146,7 @@ internal static class CommandModelValidator { try { - var parser = new CommandTreeParser(model, settings, ParsingMode.Strict); + var parser = new CommandTreeParser(model, settings.CaseSensitivity, ParsingMode.Strict); parser.Parse(example); } catch (Exception ex) diff --git a/src/Spectre.Console.Cli/Internal/Modelling/ICommandContainer.cs b/src/Spectre.Console.Cli/Internal/Modelling/ICommandContainer.cs index a197310..e96b564 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/ICommandContainer.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/ICommandContainer.cs @@ -8,5 +8,13 @@ internal interface ICommandContainer /// /// Gets all commands in the container. /// - IList Commands { get; } + IList Commands { get; } + + /// + /// Gets the default command for the container. + /// + /// + /// Returns null if a default command has not been set. + /// + CommandInfo? DefaultCommand { get; } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs index 197f423..899b117 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs @@ -1,10 +1,13 @@ +using static Spectre.Console.Cli.CommandTreeTokenizer; + namespace Spectre.Console.Cli; internal class CommandTreeParser { private readonly CommandModel _configuration; private readonly ParsingMode _parsingMode; - private readonly CommandOptionAttribute _help; + private readonly CommandOptionAttribute _help; + private readonly bool _convertFlagsToRemainingArguments; public CaseSensitivity CaseSensitivity { get; } @@ -14,25 +17,26 @@ internal class CommandTreeParser Remaining = 1, } - public CommandTreeParser(CommandModel configuration, ICommandAppSettings settings, ParsingMode? parsingMode = null) + public CommandTreeParser(CommandModel configuration, CaseSensitivity caseSensitivity, ParsingMode? parsingMode = null, bool? convertFlagsToRemainingArguments = null) { - if (settings is null) - { - throw new ArgumentNullException(nameof(settings)); - } - - _configuration = configuration; + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _parsingMode = parsingMode ?? _configuration.ParsingMode; - _help = new CommandOptionAttribute("-h|--help"); - - CaseSensitivity = settings.CaseSensitivity; - } + _help = new CommandOptionAttribute("-h|--help"); + _convertFlagsToRemainingArguments = convertFlagsToRemainingArguments ?? false; + CaseSensitivity = caseSensitivity; + } + public CommandTreeParserResult Parse(IEnumerable args) + { + var parserContext = new CommandTreeParserContext(args, _parsingMode); + var tokenizerResult = CommandTreeTokenizer.Tokenize(args); + + return Parse(parserContext, tokenizerResult); + } + + public CommandTreeParserResult Parse(CommandTreeParserContext context, CommandTreeTokenizerResult tokenizerResult) { - var context = new CommandTreeParserContext(args, _parsingMode); - - var tokenizerResult = CommandTreeTokenizer.Tokenize(context.Arguments); var tokens = tokenizerResult.Tokens; var rawRemaining = tokenizerResult.Remaining; @@ -254,10 +258,8 @@ internal class CommandTreeParser // Find the option. var option = node.FindOption(token.Value, isLongOption, CaseSensitivity); if (option != null) - { - node.Mapped.Add(new MappedCommandParameter( - option, ParseOptionValue(context, stream, token, node, option))); - + { + ParseOptionValue(context, stream, token, node, option); return; } @@ -271,7 +273,7 @@ internal class CommandTreeParser if (context.State == State.Remaining) { - ParseOptionValue(context, stream, token, node, null); + ParseOptionValue(context, stream, token, node); return; } @@ -281,17 +283,19 @@ internal class CommandTreeParser } else { - ParseOptionValue(context, stream, token, node, null); + ParseOptionValue(context, stream, token, node); } } - private string? ParseOptionValue( + private void ParseOptionValue( CommandTreeParserContext context, CommandTreeTokenStream stream, CommandTreeToken token, - CommandTree current, - CommandParameter? parameter) - { + CommandTree current, + CommandParameter? parameter = null) + { + bool addToMappedCommandParameters = parameter != null; + var value = default(string); // Parse the value of the token (if any). @@ -325,7 +329,21 @@ internal class CommandTreeParser else { // Flags cannot be assigned a value. - throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); + if (_convertFlagsToRemainingArguments) + { + value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + + context.AddRemainingArgument(token.Value, value); + + // Prevent the option and it's non-boolean value from being added to + // mapped parameters (otherwise an exception will be thrown later + // when binding the value to the flag in the comand settings) + addToMappedCommandParameters = false; + } + else + { + throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); + } } } else @@ -352,13 +370,14 @@ internal class CommandTreeParser } } else - { + { context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null); } } else { - if (context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed) + if (parameter == null && // Only add tokens which have not been matched to a command parameter + (context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed)) { context.AddRemainingArgument(token.Value, null); } @@ -379,10 +398,12 @@ internal class CommandTreeParser { if (parameter.IsFlagValue()) { - return null; + value = null; + } + else + { + throw CommandParseException.OptionHasNoValue(context.Arguments, token, option); } - - throw CommandParseException.OptionHasNoValue(context.Arguments, token, option); } else { @@ -394,6 +415,9 @@ internal class CommandTreeParser } } - return value; + if (parameter != null && addToMappedCommandParameters) + { + current.Mapped.Add(new MappedCommandParameter(parameter, value)); + } } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParserContext.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParserContext.cs index 65dad51..4aaff0c 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParserContext.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParserContext.cs @@ -26,19 +26,16 @@ internal class CommandTreeParserContext public void IncreaseArgumentPosition() { CurrentArgumentPosition++; - } - + } + public void AddRemainingArgument(string key, string? value) - { - if (State == CommandTreeParser.State.Remaining || ParsingMode == ParsingMode.Relaxed) - { - if (!_remaining.ContainsKey(key)) - { - _remaining.Add(key, new List()); - } - - _remaining[key].Add(value); - } + { + if (!_remaining.ContainsKey(key)) + { + _remaining.Add(key, new List()); + } + + _remaining[key].Add(value); } [SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")] diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenStream.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenStream.cs index 39d8d53..1db9979 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenStream.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenStream.cs @@ -5,7 +5,8 @@ internal sealed class CommandTreeTokenStream : IReadOnlyList private readonly List _tokens; private int _position; - public int Count => _tokens.Count; + public int Count => _tokens.Count; + public int Position => _position; public CommandTreeToken this[int index] => _tokens[index]; diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_7.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_7.Output.verified.txt new file mode 100644 index 0000000..c15cf5c --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_7.Output.verified.txt @@ -0,0 +1,32 @@ + + + + + + + The number of legs. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_8.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_8.Output.verified.txt new file mode 100644 index 0000000..27f08cf --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_8.Output.verified.txt @@ -0,0 +1,35 @@ + + + + + + + The number of legs. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_9.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_9.Output.verified.txt new file mode 100644 index 0000000..e1c460d --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_9.Output.verified.txt @@ -0,0 +1,37 @@ + + + + + + + + + The number of legs. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Branches.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Branches.cs new file mode 100644 index 0000000..5df7feb --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Branches.cs @@ -0,0 +1,351 @@ +namespace Spectre.Console.Tests.Unit.Cli; + +public sealed partial class CommandAppTests +{ + public sealed class Branches + { + [Fact] + public void Should_Run_The_Default_Command_On_Branch() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.SetDefaultCommand(); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "4", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType(); + } + + [Fact] + public void Should_Throw_When_No_Default_Command_On_Branch() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => { }); + }); + + // When + var result = Record.Exception(() => + { + app.Run(new[] + { + "animal", "4", + }); + }); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The branch 'animal' does not define any commands."); + }); + } + + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:SingleLineCommentMustBePrecededByBlankLine", Justification = "Helps to illustrate the expected behaviour of this unit test.")] + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1005:SingleLineCommentsMustBeginWithSingleSpace", Justification = "Helps to illustrate the expected behaviour of this unit test.")] + [Fact] + public void Should_Be_Unable_To_Parse_Default_Command_Arguments_Relaxed_Parsing() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.SetDefaultCommand(); + }); + }); + + // When + var result = app.Run(new[] + { + // The CommandTreeParser should be unable to determine which command line + // arguments belong to the branch and which belong to the branch's + // default command (once inserted). + "animal", "4", "--name", "Kitty", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(cat => + { + cat.Legs.ShouldBe(4); + //cat.Name.ShouldBe("Kitty"); //<-- Should normally be correct, but instead name will be added to the remaining arguments (see below). + }); + result.Context.Remaining.Parsed.Count.ShouldBe(1); + result.Context.ShouldHaveRemainingArgument("name", values: new[] { "Kitty", }); + } + + [Fact] + public void Should_Be_Unable_To_Parse_Default_Command_Arguments_Strict_Parsing() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.SetDefaultCommand(); + }); + }); + + // When + var result = Record.Exception(() => + { + app.Run(new[] + { + // The CommandTreeParser should be unable to determine which command line + // arguments belong to the branch and which belong to the branch's + // default command (once inserted). + "animal", "4", "--name", "Kitty", + }); + }); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Unknown option 'name'."); + }); + } + + [Fact] + public void Should_Run_The_Default_Command_On_Branch_On_Branch() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddBranch("mammal", mammal => + { + mammal.SetDefaultCommand(); + }); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "4", "mammal", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType(); + } + + [Fact] + public void Should_Run_The_Default_Command_On_Branch_On_Branch_With_Arguments() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddBranch("mammal", mammal => + { + mammal.SetDefaultCommand(); + }); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "4", "mammal", "--name", "Kitty", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(cat => + { + cat.Legs.ShouldBe(4); + cat.Name.ShouldBe("Kitty"); + }); + } + + [Fact] + public void Should_Run_The_Default_Command_Not_The_Named_Command_On_Branch() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + + animal.SetDefaultCommand(); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "4", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType(); + } + + [Fact] + public void Should_Run_The_Named_Command_Not_The_Default_Command_On_Branch() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + + animal.SetDefaultCommand(); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "4", "dog", "12", "--good-boy", "--name", "Rufus", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + }); + } + + [Fact] + public void Should_Allow_Multiple_Branches_Multiple_Commands() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddBranch("mammal", mammal => + { + mammal.AddCommand("dog"); + mammal.AddCommand("cat"); + }); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "--alive", "mammal", "--name", + "Rufus", "dog", "12", "--good-boy", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(true); + }); + } + + [Fact] + public void Should_Allow_Single_Branch_Multiple_Commands() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("cat"); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "dog", "12", "--good-boy", + "--name", "Rufus", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(false); + }); + } + + [Fact] + public void Should_Allow_Single_Branch_Single_Command() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var result = app.Run(new[] + { + "animal", "4", "dog", "12", "--good-boy", + "--name", "Rufus", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.IsAlive.ShouldBe(false); + dog.Name.ShouldBe("Rufus"); + }); + } + } +} \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs index 5425123..8a3eefe 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs @@ -3,7 +3,167 @@ namespace Spectre.Console.Tests.Unit.Cli; public sealed partial class CommandAppTests { public sealed class Remaining - { + { + [Theory] + [InlineData("-a")] + [InlineData("--alive")] + public void Should_Not_Add_Known_Flags_To_Remaining_Arguments_RelaxedParsing(string knownFlag) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] + { + "dog", "12", "4", + knownFlag, + }); + + // Then + result.Settings.ShouldBeOfType().And(dog => + { + dog.IsAlive.ShouldBe(true); + }); + + result.Context.Remaining.Parsed.Count.ShouldBe(0); + result.Context.Remaining.Raw.Count.ShouldBe(0); + } + + [Theory] + [InlineData("-r")] + [InlineData("--romeo")] + public void Should_Add_Unknown_Flags_To_Remaining_Arguments_RelaxedParsing(string unknownFlag) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] + { + "dog", "12", "4", + unknownFlag, + }); + + // Then + result.Context.Remaining.Parsed.Count.ShouldBe(1); + result.Context.ShouldHaveRemainingArgument(unknownFlag.TrimStart('-'), values: new[] { (string)null }); + result.Context.Remaining.Raw.Count.ShouldBe(0); + } + + [Fact] + public void Should_Add_Unknown_Flags_When_Grouped_To_Remaining_Arguments_RelaxedParsing() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] + { + "dog", "12", "4", + "-agr", + }); + + // Then + result.Context.Remaining.Parsed.Count.ShouldBe(1); + result.Context.ShouldHaveRemainingArgument("r", values: new[] { (string)null }); + result.Context.Remaining.Raw.Count.ShouldBe(0); + } + + [Theory] + [InlineData("-a")] + [InlineData("--alive")] + public void Should_Not_Add_Known_Flags_To_Remaining_Arguments_StrictParsing(string knownFlag) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] + { + "dog", "12", "4", + knownFlag, + }); + + // Then + result.Context.Remaining.Parsed.Count.ShouldBe(0); + result.Context.Remaining.Raw.Count.ShouldBe(0); + } + + [Theory] + [InlineData("-r")] + [InlineData("--romeo")] + public void Should_Not_Add_Unknown_Flags_To_Remaining_Arguments_StrictParsing(string unknownFlag) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] + { + "dog", "12", "4", + unknownFlag, + })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe($"Unknown option '{unknownFlag.TrimStart('-')}'."); + }); + } + + [Fact] + public void Should_Not_Add_Unknown_Flags_When_Grouped_To_Remaining_Arguments_StrictParsing() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] + { + "dog", "12", "4", + "-agr", + })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe($"Unknown option 'r'."); + }); + } + [Fact] public void Should_Register_Remaining_Parsed_Arguments_With_Context() { @@ -94,6 +254,35 @@ public sealed partial class CommandAppTests result.Context.Remaining.Raw[0].ShouldBe("/c"); result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\""); result.Context.Remaining.Raw[2].ShouldBe("Name=\" -Rufus --' "); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Should_Convert_Flags_To_Remaining_Arguments_If_Cannot_Be_Assigned(bool useStrictParsing) + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.Settings.ConvertFlagsToRemainingArguments = true; + config.Settings.StrictParsing = useStrictParsing; + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var result = app.Run(new[] + { + "dog", "12", "4", + "--good-boy=Please be good Rufus!", + }); + + // Then + result.Context.Remaining.Parsed.Count.ShouldBe(1); + result.Context.ShouldHaveRemainingArgument("good-boy", values: new[] { "Please be good Rufus!" }); + + result.Context.Remaining.Raw.Count.ShouldBe(0); // nb. there are no "raw" remaining arguments on the command line } } } diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Xml.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Xml.cs index 1c5348f..a43e07d 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Xml.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Xml.cs @@ -130,6 +130,77 @@ public sealed partial class CommandAppTests return Verifier.Verify(result.Output); } + [Fact] + [Expectation("Test_7")] + public Task Should_Dump_Correct_Model_For_Model_With_Single_Branch_Single_Branch_Default_Command() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configuration => + { + configuration.AddBranch("animal", animal => + { + animal.AddBranch("mammal", mammal => + { + mammal.SetDefaultCommand(); + }); + }); + }); + + // When + var result = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Test_8")] + public Task Should_Dump_Correct_Model_For_Model_With_Single_Branch_Single_Command_Default_Command() + { + // Given + var fixture = new CommandAppTester(); + fixture.Configure(configuration => + { + configuration.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + + animal.SetDefaultCommand(); + }); + }); + + // When + var result = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Test_9")] + public Task Should_Dump_Correct_Model_For_Model_With_Default_Command_Single_Branch_Single_Command_Default_Command() + { + // Given + var fixture = new CommandAppTester(); + fixture.SetDefaultCommand(); + fixture.Configure(configuration => + { + configuration.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + + animal.SetDefaultCommand(); + }); + }); + + // When + var result = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(result.Output); + } + [Fact] [Expectation("Hidden_Command_Options")] public Task Should_Not_Dump_Hidden_Options_On_A_Command() diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs index 65c1525..653a5ae 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs @@ -4,42 +4,6 @@ public sealed partial class CommandAppTests { [Fact] public void Should_Pass_Case_1() - { - // Given - var app = new CommandAppTester(); - app.Configure(config => - { - config.PropagateExceptions(); - config.AddBranch("animal", animal => - { - animal.AddBranch("mammal", mammal => - { - mammal.AddCommand("dog"); - mammal.AddCommand("horse"); - }); - }); - }); - - // When - var result = app.Run(new[] - { - "animal", "--alive", "mammal", "--name", - "Rufus", "dog", "12", "--good-boy", - }); - - // Then - result.ExitCode.ShouldBe(0); - result.Settings.ShouldBeOfType().And(dog => - { - dog.Age.ShouldBe(12); - dog.GoodBoy.ShouldBe(true); - dog.Name.ShouldBe("Rufus"); - dog.IsAlive.ShouldBe(true); - }); - } - - [Fact] - public void Should_Pass_Case_2() { // Given var app = new CommandAppTester(); @@ -52,8 +16,8 @@ public sealed partial class CommandAppTests // When var result = app.Run(new[] { - "dog", "12", "4", "--good-boy", - "--name", "Rufus", "--alive", + "dog", "12", "4", "--good-boy", + "--name", "Rufus", "--alive", }); // Then @@ -69,7 +33,7 @@ public sealed partial class CommandAppTests } [Fact] - public void Should_Pass_Case_3() + public void Should_Pass_Case_2() { // Given var app = new CommandAppTester(); @@ -197,7 +161,7 @@ public sealed partial class CommandAppTests } [Fact] - public void Should_Pass_Case_7() + public void Should_Pass_Case_3() { // Given var app = new CommandAppTester(); @@ -904,6 +868,86 @@ public sealed partial class CommandAppTests result.Context.ShouldHaveRemainingArgument("foo", values: new[] { (string)null }); } + [Fact] + public void Should_Run_The_Default_Command() + { + // Given + var app = new CommandAppTester(); + app.SetDefaultCommand(); + + // When + var result = app.Run(new[] + { + "4", "12", "--good-boy", "--name", "Rufus", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + }); + } + + [Fact] + public void Should_Run_The_Default_Command_Not_The_Named_Command() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("horse"); + }); + app.SetDefaultCommand(); + + // When + var result = app.Run(new[] + { + "4", "12", "--good-boy", "--name", "Rufus", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + }); + } + + [Fact] + public void Should_Run_The_Named_Command_Not_The_Default_Command() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("horse"); + }); + app.SetDefaultCommand(); + + // When + var result = app.Run(new[] + { + "horse", "4", "--name", "Arkle", + }); + + // Then + result.ExitCode.ShouldBe(0); + result.Settings.ShouldBeOfType().And(horse => + { + horse.Legs.ShouldBe(4); + horse.Name.ShouldBe("Arkle"); + }); + } + [Fact] public void Should_Set_Command_Name_In_Context() { @@ -1081,67 +1125,4 @@ public sealed partial class CommandAppTests data.ShouldBe(2); } } - - public sealed class Remaining_Arguments - { - [Fact] - public void Should_Register_Remaining_Parsed_Arguments_With_Context() - { - // Given - var app = new CommandAppTester(); - app.Configure(config => - { - config.PropagateExceptions(); - config.AddBranch("animal", animal => - { - animal.AddCommand("dog"); - }); - }); - - // When - var result = app.Run(new[] - { - "animal", "4", "dog", "12", "--", - "--foo", "bar", "--foo", "baz", - "-bar", "\"baz\"", "qux", - }); - - // Then - result.Context.Remaining.Parsed.Count.ShouldBe(4); - result.Context.ShouldHaveRemainingArgument("foo", values: new[] { "bar", "baz" }); - result.Context.ShouldHaveRemainingArgument("b", values: new[] { (string)null }); - result.Context.ShouldHaveRemainingArgument("a", values: new[] { (string)null }); - result.Context.ShouldHaveRemainingArgument("r", values: new[] { (string)null }); - } - - [Fact] - public void Should_Register_Remaining_Raw_Arguments_With_Context() - { - // Given - var app = new CommandAppTester(); - app.Configure(config => - { - config.PropagateExceptions(); - config.AddBranch("animal", animal => - { - animal.AddCommand("dog"); - }); - }); - - // When - var result = app.Run(new[] - { - "animal", "4", "dog", "12", "--", - "--foo", "bar", "-bar", "\"baz\"", "qux", - }); - - // Then - result.Context.Remaining.Raw.Count.ShouldBe(5); - result.Context.Remaining.Raw[0].ShouldBe("--foo"); - result.Context.Remaining.Raw[1].ShouldBe("bar"); - result.Context.Remaining.Raw[2].ShouldBe("-bar"); - result.Context.Remaining.Raw[3].ShouldBe("\"baz\""); - result.Context.Remaining.Raw[4].ShouldBe("qux"); - } - } }