From 714cf179cb349597a8b775287ac8299584b18617 Mon Sep 17 00:00:00 2001
From: Frank Ray <52075808+FrankRay78@users.noreply.github.com>
Date: Sun, 2 Apr 2023 21:43:21 +0100
Subject: [PATCH] Command line improvements (#1103)
Closes #187
Closes #203
Closes #1059
---
.../ICommandAppSettings.cs | 10 +-
src/Spectre.Console.Cli/IConfigurator.cs | 2 +-
src/Spectre.Console.Cli/IConfiguratorOfT.cs | 12 +
.../Internal/CommandExecutor.cs | 35 +-
.../Configuration/CommandAppSettings.cs | 3 +-
.../Internal/Configuration/Configurator.cs | 4 +-
.../Internal/Configuration/ConfiguratorOfT.cs | 190 +++++-----
.../Configuration/ConfiguredCommand.cs | 5 +-
.../Extensions/TypeRegistrarExtensions.cs | 4 -
.../Internal/Modelling/CommandInfo.cs | 9 +-
.../Internal/Modelling/CommandModel.cs | 5 +-
.../Internal/Modelling/CommandModelBuilder.cs | 11 +-
.../Modelling/CommandModelValidator.cs | 5 +-
.../Internal/Modelling/ICommandContainer.cs | 10 +-
.../Internal/Parsing/CommandTreeParser.cs | 88 +++--
.../Parsing/CommandTreeParserContext.cs | 21 +-
.../Parsing/CommandTreeTokenStream.cs | 3 +-
.../Xml/Test_7.Output.verified.txt | 32 ++
.../Xml/Test_8.Output.verified.txt | 35 ++
.../Xml/Test_9.Output.verified.txt | 37 ++
.../Unit/CommandAppTests.Branches.cs | 351 ++++++++++++++++++
.../Unit/CommandAppTests.Remaining.cs | 191 +++++++++-
.../Unit/CommandAppTests.Xml.cs | 71 ++++
.../Unit/CommandAppTests.cs | 187 +++++-----
24 files changed, 1052 insertions(+), 269 deletions(-)
create mode 100644 test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_7.Output.verified.txt
create mode 100644 test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_8.Output.verified.txt
create mode 100644 test/Spectre.Console.Cli.Tests/Expectations/Xml/Test_9.Output.verified.txt
create mode 100644 test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Branches.cs
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");
- }
- }
}