Command line improvements (#1103)

Closes #187
Closes #203
Closes #1059
This commit is contained in:
Frank Ray 2023-04-02 21:43:21 +01:00 committed by GitHub
parent 70da3f40ff
commit 714cf179cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1052 additions and 269 deletions

View File

@ -49,7 +49,15 @@ public interface ICommandAppSettings
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not parsing is strict. /// Gets or sets a value indicating whether or not parsing is strict.
/// </summary> /// </summary>
bool StrictParsing { get; set; } bool StrictParsing { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not flags found on the commnd line
/// that would normally result in a <see cref="CommandParseException"/> being thrown
/// during parsing with the message "Flags cannot be assigned a value."
/// should instead be added to the remaining arguments collection.
/// </summary>
bool ConvertFlagsToRemainingArguments { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not exceptions should be propagated. /// Gets or sets a value indicating whether or not exceptions should be propagated.

View File

@ -15,7 +15,7 @@ public interface IConfigurator
/// </summary> /// </summary>
/// <param name="args">The example arguments.</param> /// <param name="args">The example arguments.</param>
void AddExample(string[] args); void AddExample(string[] args);
/// <summary> /// <summary>
/// Adds a command. /// Adds a command.
/// </summary> /// </summary>

View File

@ -19,6 +19,18 @@ public interface IConfigurator<in TSettings>
/// <param name="args">The example arguments.</param> /// <param name="args">The example arguments.</param>
void AddExample(string[] args); void AddExample(string[] args);
/// <summary>
/// Adds a default command.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <typeparam name="TDefaultCommand">The default command type.</typeparam>
void SetDefaultCommand<TDefaultCommand>()
where TDefaultCommand : class, ICommandLimiter<TSettings>;
/// <summary> /// <summary>
/// Marks the branch as hidden. /// Marks the branch as hidden.
/// Hidden branches do not show up in help documentation or /// Hidden branches do not show up in help documentation or

View File

@ -45,12 +45,10 @@ internal sealed class CommandExecutor
} }
// Parse and map the model against the arguments. // Parse and map the model against the arguments.
var parser = new CommandTreeParser(model, configuration.Settings); var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args);
var parsedResult = parser.Parse(args);
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
// Currently the root? // Currently the root?
if (parsedResult.Tree == null) if (parsedResult?.Tree == null)
{ {
// Display help. // Display help.
configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues)); configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues));
@ -75,6 +73,7 @@ internal sealed class CommandExecutor
} }
// Register the arguments with the container. // Register the arguments with the container.
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
_registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining);
// Create the resolver and the context. // 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); return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
} }
} }
private CommandTreeParserResult? ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> 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<string>(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) private static string ResolveApplicationVersion(IConfiguration configuration)
{ {

View File

@ -12,7 +12,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public bool PropagateExceptions { get; set; } public bool PropagateExceptions { get; set; }
public bool ValidateExamples { get; set; } public bool ValidateExamples { get; set; }
public bool TrimTrailingPeriod { get; set; } = true; 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 => public ParsingMode ParsingMode =>
StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed;

View File

@ -23,7 +23,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
public void AddExample(string[] args) public void AddExample(string[] args)
{ {
Examples.Add(args); Examples.Add(args);
} }
public ConfiguredCommand SetDefaultCommand<TDefaultCommand>() public ConfiguredCommand SetDefaultCommand<TDefaultCommand>()
where TDefaultCommand : class, ICommand where TDefaultCommand : class, ICommand
@ -36,7 +36,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
public ICommandConfigurator AddCommand<TCommand>(string name) public ICommandConfigurator AddCommand<TCommand>(string name)
where TCommand : class, ICommand where TCommand : class, ICommand
{ {
var command = Commands.AddAndReturn(ConfiguredCommand.FromType<TCommand>(name, false)); var command = Commands.AddAndReturn(ConfiguredCommand.FromType<TCommand>(name, isDefaultCommand: false));
return new CommandConfigurator(command); return new CommandConfigurator(command);
} }

View File

@ -1,92 +1,100 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConfigurator<TSettings> internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConfigurator<TSettings>
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
private readonly ConfiguredCommand _command; private readonly ConfiguredCommand _command;
private readonly ITypeRegistrar? _registrar; private readonly ITypeRegistrar? _registrar;
public Configurator(ConfiguredCommand command, ITypeRegistrar? registrar) public Configurator(ConfiguredCommand command, ITypeRegistrar? registrar)
{ {
_command = command; _command = command;
_registrar = registrar; _registrar = registrar;
} }
public void SetDescription(string description) public void SetDescription(string description)
{ {
_command.Description = description; _command.Description = description;
} }
public void AddExample(string[] args) public void AddExample(string[] args)
{ {
_command.Examples.Add(args); _command.Examples.Add(args);
} }
public void HideBranch() public void SetDefaultCommand<TDefaultCommand>()
{ where TDefaultCommand : class, ICommandLimiter<TSettings>
_command.IsHidden = true; {
} var defaultCommand = ConfiguredCommand.FromType<TDefaultCommand>(
CliConstants.DefaultCommandName, isDefaultCommand: true);
public ICommandConfigurator AddCommand<TCommand>(string name) _command.Children.Add(defaultCommand);
where TCommand : class, ICommandLimiter<TSettings> }
{
var command = ConfiguredCommand.FromType<TCommand>(name); public void HideBranch()
var configurator = new CommandConfigurator(command); {
_command.IsHidden = true;
_command.Children.Add(command); }
return configurator;
} public ICommandConfigurator AddCommand<TCommand>(string name)
where TCommand : class, ICommandLimiter<TSettings>
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func) {
where TDerivedSettings : TSettings var command = ConfiguredCommand.FromType<TCommand>(name, isDefaultCommand: false);
{ var configurator = new CommandConfigurator(command);
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
name, (context, settings) => func(context, (TDerivedSettings)settings)); _command.Children.Add(command);
return configurator;
_command.Children.Add(command); }
return new CommandConfigurator(command);
} public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func)
where TDerivedSettings : TSettings
public IBranchConfigurator AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action) {
where TDerivedSettings : TSettings var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
{ name, (context, settings) => func(context, (TDerivedSettings)settings));
var command = ConfiguredCommand.FromBranch<TDerivedSettings>(name);
action(new Configurator<TDerivedSettings>(command, _registrar)); _command.Children.Add(command);
var added = _command.Children.AddAndReturn(command); return new CommandConfigurator(command);
return new BranchConfigurator(added); }
}
public IBranchConfigurator AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action)
ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command) where TDerivedSettings : TSettings
{ {
var method = GetType().GetMethod("AddCommand"); var command = ConfiguredCommand.FromBranch<TDerivedSettings>(name);
if (method == null) action(new Configurator<TDerivedSettings>(command, _registrar));
{ var added = _command.Children.AddAndReturn(command);
throw new CommandConfigurationException("Could not find AddCommand by reflection."); return new BranchConfigurator(added);
} }
method = method.MakeGenericMethod(command); ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command)
{
if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result)) var method = GetType().GetMethod("AddCommand");
{ if (method == null)
throw new CommandConfigurationException("Invoking AddCommand returned null."); {
} throw new CommandConfigurationException("Could not find AddCommand by reflection.");
}
return result;
} method = method.MakeGenericMethod(command);
IBranchConfigurator IUnsafeConfigurator.AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action) if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result))
{ {
var command = ConfiguredCommand.FromBranch(settings, name); throw new CommandConfigurationException("Invoking AddCommand returned null.");
}
// Create the configurator.
var configuratorType = typeof(Configurator<>).MakeGenericType(settings); return result;
if (!(Activator.CreateInstance(configuratorType, new object?[] { command, _registrar }) is IUnsafeBranchConfigurator configurator)) }
{
throw new CommandConfigurationException("Could not create configurator by reflection."); IBranchConfigurator IUnsafeConfigurator.AddBranch(string name, Type settings, Action<IUnsafeBranchConfigurator> action)
} {
var command = ConfiguredCommand.FromBranch(settings, name);
action(configurator);
var added = _command.Children.AddAndReturn(command); // Create the configurator.
return new BranchConfigurator(added); 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);
}
} }

View File

@ -27,7 +27,10 @@ internal sealed class ConfiguredCommand
CommandType = commandType; CommandType = commandType;
SettingsType = settingsType; SettingsType = settingsType;
Delegate = @delegate; Delegate = @delegate;
IsDefaultCommand = isDefaultCommand; IsDefaultCommand = isDefaultCommand;
// Default commands are always created as hidden.
IsHidden = IsDefaultCommand;
Children = new List<ConfiguredCommand>(); Children = new List<ConfiguredCommand>();
Examples = new List<string[]>(); Examples = new List<string[]>();

View File

@ -6,10 +6,6 @@ internal static class TypeRegistrarExtensions
{ {
var stack = new Stack<CommandInfo>(); var stack = new Stack<CommandInfo>();
model.Commands.ForEach(c => stack.Push(c)); model.Commands.ForEach(c => stack.Push(c));
if (model.DefaultCommand != null)
{
stack.Push(model.DefaultCommand);
}
while (stack.Count > 0) while (stack.Count > 0)
{ {

View File

@ -1,7 +1,7 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal sealed class CommandInfo : ICommandContainer internal sealed class CommandInfo : ICommandContainer
{ {
public string Name { get; } public string Name { get; }
public HashSet<string> Aliases { get; } public HashSet<string> Aliases { get; }
public string? Description { get; } public string? Description { get; }
@ -10,14 +10,17 @@ internal sealed class CommandInfo : ICommandContainer
public Type SettingsType { get; } public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, int>? Delegate { get; } public Func<CommandContext, CommandSettings, int>? Delegate { get; }
public bool IsDefaultCommand { get; } public bool IsDefaultCommand { get; }
public bool IsHidden { get; }
public CommandInfo? Parent { get; } public CommandInfo? Parent { get; }
public IList<CommandInfo> Children { get; } public IList<CommandInfo> Children { get; }
public IList<CommandParameter> Parameters { get; } public IList<CommandParameter> Parameters { get; }
public IList<string[]> Examples { get; } public IList<string[]> Examples { get; }
public bool IsBranch => CommandType == null && Delegate == null; public bool IsBranch => CommandType == null && Delegate == null;
IList<CommandInfo> ICommandContainer.Commands => Children; IList<CommandInfo> 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) public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype)
{ {

View File

@ -4,21 +4,20 @@ internal sealed class CommandModel : ICommandContainer
{ {
public string? ApplicationName { get; } public string? ApplicationName { get; }
public ParsingMode ParsingMode { get; } public ParsingMode ParsingMode { get; }
public CommandInfo? DefaultCommand { get; }
public IList<CommandInfo> Commands { get; } public IList<CommandInfo> Commands { get; }
public IList<string[]> Examples { get; } public IList<string[]> Examples { get; }
public bool TrimTrailingPeriod { get; } public bool TrimTrailingPeriod { get; }
public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand);
public CommandModel( public CommandModel(
CommandAppSettings settings, CommandAppSettings settings,
CommandInfo? defaultCommand,
IEnumerable<CommandInfo> commands, IEnumerable<CommandInfo> commands,
IEnumerable<string[]> examples) IEnumerable<string[]> examples)
{ {
ApplicationName = settings.ApplicationName; ApplicationName = settings.ApplicationName;
ParsingMode = settings.ParsingMode; ParsingMode = settings.ParsingMode;
TrimTrailingPeriod = settings.TrimTrailingPeriod; TrimTrailingPeriod = settings.TrimTrailingPeriod;
DefaultCommand = defaultCommand;
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>()); Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>());
Examples = new List<string[]>(examples ?? Array.Empty<string[]>()); Examples = new List<string[]>(examples ?? Array.Empty<string[]>());
} }

View File

@ -1,5 +1,5 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal static class CommandModelBuilder internal static class CommandModelBuilder
{ {
// Consider removing this in favor for value tuples at some point. // Consider removing this in favor for value tuples at some point.
@ -25,18 +25,19 @@ internal static class CommandModelBuilder
result.Add(Build(null, command)); result.Add(Build(null, command));
} }
var defaultCommand = default(CommandInfo);
if (configuration.DefaultCommand != null) if (configuration.DefaultCommand != null)
{ {
// Add the examples from the configuration to the default command. // Add the examples from the configuration to the default command.
configuration.DefaultCommand.Examples.AddRange(configuration.Examples); configuration.DefaultCommand.Examples.AddRange(configuration.Examples);
// Build the default command. // 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. // 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); CommandModelValidator.Validate(model, configuration.Settings);
return model; return model;
@ -54,7 +55,7 @@ internal static class CommandModelBuilder
foreach (var childCommand in command.Children) foreach (var childCommand in command.Children)
{ {
var child = Build(info, childCommand); var child = Build(info, childCommand);
info.Children.Add(child); info.Children.Add(child);
} }
// Normalize argument positions. // Normalize argument positions.

View File

@ -14,7 +14,7 @@ internal static class CommandModelValidator
throw new ArgumentNullException(nameof(settings)); throw new ArgumentNullException(nameof(settings));
} }
if (model.Commands.Count == 0 && model.DefaultCommand == null) if (model.Commands.Count == 0)
{ {
throw CommandConfigurationException.NoCommandConfigured(); throw CommandConfigurationException.NoCommandConfigured();
} }
@ -31,7 +31,6 @@ internal static class CommandModelValidator
} }
} }
Validate(model.DefaultCommand);
foreach (var command in model.Commands) foreach (var command in model.Commands)
{ {
Validate(command); Validate(command);
@ -147,7 +146,7 @@ internal static class CommandModelValidator
{ {
try try
{ {
var parser = new CommandTreeParser(model, settings, ParsingMode.Strict); var parser = new CommandTreeParser(model, settings.CaseSensitivity, ParsingMode.Strict);
parser.Parse(example); parser.Parse(example);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -8,5 +8,13 @@ internal interface ICommandContainer
/// <summary> /// <summary>
/// Gets all commands in the container. /// Gets all commands in the container.
/// </summary> /// </summary>
IList<CommandInfo> Commands { get; } IList<CommandInfo> Commands { get; }
/// <summary>
/// Gets the default command for the container.
/// </summary>
/// <remarks>
/// Returns null if a default command has not been set.
/// </remarks>
CommandInfo? DefaultCommand { get; }
} }

View File

@ -1,10 +1,13 @@
using static Spectre.Console.Cli.CommandTreeTokenizer;
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal class CommandTreeParser internal class CommandTreeParser
{ {
private readonly CommandModel _configuration; private readonly CommandModel _configuration;
private readonly ParsingMode _parsingMode; private readonly ParsingMode _parsingMode;
private readonly CommandOptionAttribute _help; private readonly CommandOptionAttribute _help;
private readonly bool _convertFlagsToRemainingArguments;
public CaseSensitivity CaseSensitivity { get; } public CaseSensitivity CaseSensitivity { get; }
@ -14,25 +17,26 @@ internal class CommandTreeParser
Remaining = 1, 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) _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
{
throw new ArgumentNullException(nameof(settings));
}
_configuration = configuration;
_parsingMode = parsingMode ?? _configuration.ParsingMode; _parsingMode = parsingMode ?? _configuration.ParsingMode;
_help = new CommandOptionAttribute("-h|--help"); _help = new CommandOptionAttribute("-h|--help");
_convertFlagsToRemainingArguments = convertFlagsToRemainingArguments ?? false;
CaseSensitivity = settings.CaseSensitivity;
}
CaseSensitivity = caseSensitivity;
}
public CommandTreeParserResult Parse(IEnumerable<string> args) public CommandTreeParserResult Parse(IEnumerable<string> 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 tokens = tokenizerResult.Tokens;
var rawRemaining = tokenizerResult.Remaining; var rawRemaining = tokenizerResult.Remaining;
@ -254,10 +258,8 @@ internal class CommandTreeParser
// Find the option. // Find the option.
var option = node.FindOption(token.Value, isLongOption, CaseSensitivity); var option = node.FindOption(token.Value, isLongOption, CaseSensitivity);
if (option != null) if (option != null)
{ {
node.Mapped.Add(new MappedCommandParameter( ParseOptionValue(context, stream, token, node, option);
option, ParseOptionValue(context, stream, token, node, option)));
return; return;
} }
@ -271,7 +273,7 @@ internal class CommandTreeParser
if (context.State == State.Remaining) if (context.State == State.Remaining)
{ {
ParseOptionValue(context, stream, token, node, null); ParseOptionValue(context, stream, token, node);
return; return;
} }
@ -281,17 +283,19 @@ internal class CommandTreeParser
} }
else else
{ {
ParseOptionValue(context, stream, token, node, null); ParseOptionValue(context, stream, token, node);
} }
} }
private string? ParseOptionValue( private void ParseOptionValue(
CommandTreeParserContext context, CommandTreeParserContext context,
CommandTreeTokenStream stream, CommandTreeTokenStream stream,
CommandTreeToken token, CommandTreeToken token,
CommandTree current, CommandTree current,
CommandParameter? parameter) CommandParameter? parameter = null)
{ {
bool addToMappedCommandParameters = parameter != null;
var value = default(string); var value = default(string);
// Parse the value of the token (if any). // Parse the value of the token (if any).
@ -325,7 +329,21 @@ internal class CommandTreeParser
else else
{ {
// Flags cannot be assigned a value. // 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 else
@ -352,13 +370,14 @@ internal class CommandTreeParser
} }
} }
else else
{ {
context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null); context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null);
} }
} }
else 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); context.AddRemainingArgument(token.Value, null);
} }
@ -379,10 +398,12 @@ internal class CommandTreeParser
{ {
if (parameter.IsFlagValue()) if (parameter.IsFlagValue())
{ {
return null; value = null;
}
else
{
throw CommandParseException.OptionHasNoValue(context.Arguments, token, option);
} }
throw CommandParseException.OptionHasNoValue(context.Arguments, token, option);
} }
else else
{ {
@ -394,6 +415,9 @@ internal class CommandTreeParser
} }
} }
return value; if (parameter != null && addToMappedCommandParameters)
{
current.Mapped.Add(new MappedCommandParameter(parameter, value));
}
} }
} }

View File

@ -26,19 +26,16 @@ internal class CommandTreeParserContext
public void IncreaseArgumentPosition() public void IncreaseArgumentPosition()
{ {
CurrentArgumentPosition++; CurrentArgumentPosition++;
} }
public void AddRemainingArgument(string key, string? value) public void AddRemainingArgument(string key, string? value)
{ {
if (State == CommandTreeParser.State.Remaining || ParsingMode == ParsingMode.Relaxed) if (!_remaining.ContainsKey(key))
{ {
if (!_remaining.ContainsKey(key)) _remaining.Add(key, new List<string?>());
{ }
_remaining.Add(key, new List<string?>());
} _remaining[key].Add(value);
_remaining[key].Add(value);
}
} }
[SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")] [SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")]

View File

@ -5,7 +5,8 @@ internal sealed class CommandTreeTokenStream : IReadOnlyList<CommandTreeToken>
private readonly List<CommandTreeToken> _tokens; private readonly List<CommandTreeToken> _tokens;
private int _position; private int _position;
public int Count => _tokens.Count; public int Count => _tokens.Count;
public int Position => _position;
public CommandTreeToken this[int index] => _tokens[index]; public CommandTreeToken this[int index] => _tokens[index];

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
<Parameters>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<Description>The number of legs.</Description>
<Validators>
<Validator ClrType="Spectre.Console.Tests.Data.EvenNumberValidatorAttribute" Message="Animals must have an even number of legs." />
<Validator ClrType="Spectre.Console.Tests.Data.PositiveNumberValidatorAttribute" Message="Number of legs must be greater than 0." />
</Validators>
</Argument>
<Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean">
<Description>Indicates whether or not the animal is alive.</Description>
</Option>
</Parameters>
<!--MAMMAL-->
<Command Name="mammal" IsBranch="true" Settings="Spectre.Console.Tests.Data.MammalSettings">
<Parameters>
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
<!--__DEFAULT_COMMAND-->
<Command Name="__default_command" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
</Parameters>
</Command>
</Command>
</Command>
</Model>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
<Parameters>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<Description>The number of legs.</Description>
<Validators>
<Validator ClrType="Spectre.Console.Tests.Data.EvenNumberValidatorAttribute" Message="Animals must have an even number of legs." />
<Validator ClrType="Spectre.Console.Tests.Data.PositiveNumberValidatorAttribute" Message="Number of legs must be greater than 0." />
</Validators>
</Argument>
<Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean">
<Description>Indicates whether or not the animal is alive.</Description>
</Option>
</Parameters>
<!--DOG-->
<Command Name="dog" IsBranch="false" ClrType="Spectre.Console.Tests.Data.DogCommand" Settings="Spectre.Console.Tests.Data.DogSettings">
<Parameters>
<Argument Name="AGE" Position="0" Required="true" Kind="scalar" ClrType="System.Int32" />
<Option Short="g" Long="good-boy" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
<!--__DEFAULT_COMMAND-->
<Command Name="__default_command" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
</Command>
</Model>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<Model>
<!--DEFAULT COMMAND-->
<Command Name="__default_command" IsBranch="false" IsDefault="true" ClrType="Spectre.Console.Tests.Data.EmptyCommand" Settings="Spectre.Console.Cli.EmptyCommandSettings" />
<!--ANIMAL-->
<Command Name="animal" IsBranch="true" Settings="Spectre.Console.Tests.Data.AnimalSettings">
<Parameters>
<Argument Name="LEGS" Position="0" Required="false" Kind="scalar" ClrType="System.Int32">
<Description>The number of legs.</Description>
<Validators>
<Validator ClrType="Spectre.Console.Tests.Data.EvenNumberValidatorAttribute" Message="Animals must have an even number of legs." />
<Validator ClrType="Spectre.Console.Tests.Data.PositiveNumberValidatorAttribute" Message="Number of legs must be greater than 0." />
</Validators>
</Argument>
<Option Short="a" Long="alive,not-dead" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean">
<Description>Indicates whether or not the animal is alive.</Description>
</Option>
</Parameters>
<!--DOG-->
<Command Name="dog" IsBranch="false" ClrType="Spectre.Console.Tests.Data.DogCommand" Settings="Spectre.Console.Tests.Data.DogSettings">
<Parameters>
<Argument Name="AGE" Position="0" Required="true" Kind="scalar" ClrType="System.Int32" />
<Option Short="g" Long="good-boy" Value="NULL" Required="false" Kind="flag" ClrType="System.Boolean" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
<!--__DEFAULT_COMMAND-->
<Command Name="__default_command" IsBranch="false" ClrType="Spectre.Console.Tests.Data.HorseCommand" Settings="Spectre.Console.Tests.Data.HorseSettings">
<Parameters>
<Option Short="d" Long="day" Value="NULL" Required="false" Kind="scalar" ClrType="System.DayOfWeek" />
<Option Short="" Long="directory" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.DirectoryInfo" />
<Option Short="" Long="file" Value="NULL" Required="false" Kind="scalar" ClrType="System.IO.FileInfo" />
<Option Short="n,p" Long="name,pet-name" Value="VALUE" Required="false" Kind="scalar" ClrType="System.String" />
</Parameters>
</Command>
</Command>
</Model>

View File

@ -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<AnimalSettings>("animal", animal =>
{
animal.SetDefaultCommand<CatCommand>();
});
});
// When
var result = app.Run(new[]
{
"animal", "4",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>();
}
[Fact]
public void Should_Throw_When_No_Default_Command_On_Branch()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal => { });
});
// When
var result = Record.Exception(() =>
{
app.Run(new[]
{
"animal", "4",
});
});
// Then
result.ShouldBeOfType<CommandConfigurationException>().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<AnimalSettings>("animal", animal =>
{
animal.SetDefaultCommand<CatCommand>();
});
});
// 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<CatSettings>().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<AnimalSettings>("animal", animal =>
{
animal.SetDefaultCommand<CatCommand>();
});
});
// 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<CommandParseException>().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<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.SetDefaultCommand<CatCommand>();
});
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "mammal",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>();
}
[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<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.SetDefaultCommand<CatCommand>();
});
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "mammal", "--name", "Kitty",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>().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<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<CatCommand>();
});
});
// When
var result = app.Run(new[]
{
"animal", "4",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>();
}
[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<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<LionCommand>();
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "dog", "12", "--good-boy", "--name", "Rufus",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().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<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.AddCommand<DogCommand>("dog");
mammal.AddCommand<CatCommand>("cat");
});
});
});
// When
var result = app.Run(new[]
{
"animal", "--alive", "mammal", "--name",
"Rufus", "dog", "12", "--good-boy",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().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<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.AddCommand<CatCommand>("cat");
});
});
// When
var result = app.Run(new[]
{
"animal", "dog", "12", "--good-boy",
"--name", "Rufus",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().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<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "dog", "12", "--good-boy",
"--name", "Rufus",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().And(dog =>
{
dog.Legs.ShouldBe(4);
dog.Age.ShouldBe(12);
dog.GoodBoy.ShouldBe(true);
dog.IsAlive.ShouldBe(false);
dog.Name.ShouldBe("Rufus");
});
}
}
}

View File

@ -3,7 +3,167 @@ namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests public sealed partial class CommandAppTests
{ {
public sealed class Remaining 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<DogCommand>("dog");
});
// When
var result = app.Run(new[]
{
"dog", "12", "4",
knownFlag,
});
// Then
result.Settings.ShouldBeOfType<DogSettings>().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<DogCommand>("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<DogCommand>("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<DogCommand>("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<DogCommand>("dog");
});
// When
var result = Record.Exception(() => app.Run(new[]
{
"dog", "12", "4",
unknownFlag,
}));
// Then
result.ShouldBeOfType<CommandParseException>().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<DogCommand>("dog");
});
// When
var result = Record.Exception(() => app.Run(new[]
{
"dog", "12", "4",
"-agr",
}));
// Then
result.ShouldBeOfType<CommandParseException>().And(ex =>
{
ex.Message.ShouldBe($"Unknown option 'r'.");
});
}
[Fact] [Fact]
public void Should_Register_Remaining_Parsed_Arguments_With_Context() 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[0].ShouldBe("/c");
result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\""); result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\"");
result.Context.Remaining.Raw[2].ShouldBe("Name=\" -Rufus --' "); 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<DogCommand>("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
} }
} }
} }

View File

@ -130,6 +130,77 @@ public sealed partial class CommandAppTests
return Verifier.Verify(result.Output); 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<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.SetDefaultCommand<HorseCommand>();
});
});
});
// 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<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<HorseCommand>();
});
});
// 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<EmptyCommand>();
fixture.Configure(configuration =>
{
configuration.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
animal.SetDefaultCommand<HorseCommand>();
});
});
// When
var result = fixture.Run(Constants.XmlDocCommand);
// Then
return Verifier.Verify(result.Output);
}
[Fact] [Fact]
[Expectation("Hidden_Command_Options")] [Expectation("Hidden_Command_Options")]
public Task Should_Not_Dump_Hidden_Options_On_A_Command() public Task Should_Not_Dump_Hidden_Options_On_A_Command()

View File

@ -4,42 +4,6 @@ public sealed partial class CommandAppTests
{ {
[Fact] [Fact]
public void Should_Pass_Case_1() public void Should_Pass_Case_1()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddBranch<MammalSettings>("mammal", mammal =>
{
mammal.AddCommand<DogCommand>("dog");
mammal.AddCommand<HorseCommand>("horse");
});
});
});
// When
var result = app.Run(new[]
{
"animal", "--alive", "mammal", "--name",
"Rufus", "dog", "12", "--good-boy",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().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 // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
@ -52,8 +16,8 @@ public sealed partial class CommandAppTests
// When // When
var result = app.Run(new[] var result = app.Run(new[]
{ {
"dog", "12", "4", "--good-boy", "dog", "12", "4", "--good-boy",
"--name", "Rufus", "--alive", "--name", "Rufus", "--alive",
}); });
// Then // Then
@ -69,7 +33,7 @@ public sealed partial class CommandAppTests
} }
[Fact] [Fact]
public void Should_Pass_Case_3() public void Should_Pass_Case_2()
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
@ -197,7 +161,7 @@ public sealed partial class CommandAppTests
} }
[Fact] [Fact]
public void Should_Pass_Case_7() public void Should_Pass_Case_3()
{ {
// Given // Given
var app = new CommandAppTester(); var app = new CommandAppTester();
@ -904,6 +868,86 @@ public sealed partial class CommandAppTests
result.Context.ShouldHaveRemainingArgument("foo", values: new[] { (string)null }); result.Context.ShouldHaveRemainingArgument("foo", values: new[] { (string)null });
} }
[Fact]
public void Should_Run_The_Default_Command()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<DogCommand>();
// When
var result = app.Run(new[]
{
"4", "12", "--good-boy", "--name", "Rufus",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().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<HorseCommand>("horse");
});
app.SetDefaultCommand<DogCommand>();
// When
var result = app.Run(new[]
{
"4", "12", "--good-boy", "--name", "Rufus",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<DogSettings>().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<HorseCommand>("horse");
});
app.SetDefaultCommand<DogCommand>();
// When
var result = app.Run(new[]
{
"horse", "4", "--name", "Arkle",
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<HorseSettings>().And(horse =>
{
horse.Legs.ShouldBe(4);
horse.Name.ShouldBe("Arkle");
});
}
[Fact] [Fact]
public void Should_Set_Command_Name_In_Context() public void Should_Set_Command_Name_In_Context()
{ {
@ -1081,67 +1125,4 @@ public sealed partial class CommandAppTests
data.ShouldBe(2); 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<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("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<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("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");
}
}
} }