mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-14 16:02:50 +08:00
Merge branch 'spectreconsole:main' into main
This commit is contained in:
commit
4dcbd30285
@ -49,7 +49,15 @@ public interface ICommandAppSettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not parsing is strict.
|
||||
/// </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>
|
||||
/// Gets or sets a value indicating whether or not exceptions should be propagated.
|
||||
|
@ -15,7 +15,7 @@ public interface IConfigurator
|
||||
/// </summary>
|
||||
/// <param name="args">The example arguments.</param>
|
||||
void AddExample(string[] args);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds a command.
|
||||
/// </summary>
|
||||
|
@ -19,6 +19,18 @@ public interface IConfigurator<in TSettings>
|
||||
/// <param name="args">The example arguments.</param>
|
||||
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>
|
||||
/// Marks the branch as hidden.
|
||||
/// Hidden branches do not show up in help documentation or
|
||||
|
@ -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<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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -23,7 +23,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
|
||||
public void AddExample(string[] args)
|
||||
{
|
||||
Examples.Add(args);
|
||||
}
|
||||
}
|
||||
|
||||
public ConfiguredCommand SetDefaultCommand<TDefaultCommand>()
|
||||
where TDefaultCommand : class, ICommand
|
||||
@ -36,7 +36,7 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
|
||||
public ICommandConfigurator AddCommand<TCommand>(string name)
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1,92 +1,100 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConfigurator<TSettings>
|
||||
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<TCommand>(string name)
|
||||
where TCommand : class, ICommandLimiter<TSettings>
|
||||
{
|
||||
var command = ConfiguredCommand.FromType<TCommand>(name);
|
||||
var configurator = new CommandConfigurator(command);
|
||||
|
||||
_command.Children.Add(command);
|
||||
return configurator;
|
||||
}
|
||||
|
||||
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func)
|
||||
where TDerivedSettings : TSettings
|
||||
{
|
||||
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
|
||||
name, (context, settings) => func(context, (TDerivedSettings)settings));
|
||||
|
||||
_command.Children.Add(command);
|
||||
return new CommandConfigurator(command);
|
||||
}
|
||||
|
||||
public IBranchConfigurator AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action)
|
||||
where TDerivedSettings : TSettings
|
||||
{
|
||||
var command = ConfiguredCommand.FromBranch<TDerivedSettings>(name);
|
||||
action(new Configurator<TDerivedSettings>(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<IUnsafeBranchConfigurator> 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<TSettings> : IUnsafeBranchConfigurator, IConfigurator<TSettings>
|
||||
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<TDefaultCommand>()
|
||||
where TDefaultCommand : class, ICommandLimiter<TSettings>
|
||||
{
|
||||
var defaultCommand = ConfiguredCommand.FromType<TDefaultCommand>(
|
||||
CliConstants.DefaultCommandName, isDefaultCommand: true);
|
||||
_command.Children.Add(defaultCommand);
|
||||
}
|
||||
|
||||
public void HideBranch()
|
||||
{
|
||||
_command.IsHidden = true;
|
||||
}
|
||||
|
||||
public ICommandConfigurator AddCommand<TCommand>(string name)
|
||||
where TCommand : class, ICommandLimiter<TSettings>
|
||||
{
|
||||
var command = ConfiguredCommand.FromType<TCommand>(name, isDefaultCommand: false);
|
||||
var configurator = new CommandConfigurator(command);
|
||||
|
||||
_command.Children.Add(command);
|
||||
return configurator;
|
||||
}
|
||||
|
||||
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func)
|
||||
where TDerivedSettings : TSettings
|
||||
{
|
||||
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
|
||||
name, (context, settings) => func(context, (TDerivedSettings)settings));
|
||||
|
||||
_command.Children.Add(command);
|
||||
return new CommandConfigurator(command);
|
||||
}
|
||||
|
||||
public IBranchConfigurator AddBranch<TDerivedSettings>(string name, Action<IConfigurator<TDerivedSettings>> action)
|
||||
where TDerivedSettings : TSettings
|
||||
{
|
||||
var command = ConfiguredCommand.FromBranch<TDerivedSettings>(name);
|
||||
action(new Configurator<TDerivedSettings>(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<IUnsafeBranchConfigurator> 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);
|
||||
}
|
||||
}
|
@ -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<ConfiguredCommand>();
|
||||
Examples = new List<string[]>();
|
||||
|
@ -6,10 +6,6 @@ internal static class TypeRegistrarExtensions
|
||||
{
|
||||
var stack = new Stack<CommandInfo>();
|
||||
model.Commands.ForEach(c => stack.Push(c));
|
||||
if (model.DefaultCommand != null)
|
||||
{
|
||||
stack.Push(model.DefaultCommand);
|
||||
}
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal sealed class CommandInfo : ICommandContainer
|
||||
{
|
||||
{
|
||||
public string Name { get; }
|
||||
public HashSet<string> Aliases { get; }
|
||||
public string? Description { get; }
|
||||
@ -10,14 +10,17 @@ internal sealed class CommandInfo : ICommandContainer
|
||||
public Type SettingsType { get; }
|
||||
public Func<CommandContext, CommandSettings, int>? Delegate { get; }
|
||||
public bool IsDefaultCommand { get; }
|
||||
public bool IsHidden { get; }
|
||||
public CommandInfo? Parent { get; }
|
||||
public IList<CommandInfo> Children { get; }
|
||||
public IList<CommandParameter> Parameters { get; }
|
||||
public IList<string[]> Examples { get; }
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -4,21 +4,20 @@ internal sealed class CommandModel : ICommandContainer
|
||||
{
|
||||
public string? ApplicationName { get; }
|
||||
public ParsingMode ParsingMode { get; }
|
||||
public CommandInfo? DefaultCommand { get; }
|
||||
public IList<CommandInfo> Commands { get; }
|
||||
public IList<string[]> Examples { get; }
|
||||
public bool TrimTrailingPeriod { get; }
|
||||
|
||||
public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand);
|
||||
|
||||
public CommandModel(
|
||||
CommandAppSettings settings,
|
||||
CommandInfo? defaultCommand,
|
||||
IEnumerable<CommandInfo> commands,
|
||||
IEnumerable<string[]> examples)
|
||||
{
|
||||
ApplicationName = settings.ApplicationName;
|
||||
ParsingMode = settings.ParsingMode;
|
||||
TrimTrailingPeriod = settings.TrimTrailingPeriod;
|
||||
DefaultCommand = defaultCommand;
|
||||
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>());
|
||||
Examples = new List<string[]>(examples ?? Array.Empty<string[]>());
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -8,5 +8,13 @@ internal interface ICommandContainer
|
||||
/// <summary>
|
||||
/// Gets all commands in the container.
|
||||
/// </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; }
|
||||
}
|
@ -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<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 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string?>());
|
||||
}
|
||||
|
||||
_remaining[key].Add(value);
|
||||
}
|
||||
{
|
||||
if (!_remaining.ContainsKey(key))
|
||||
{
|
||||
_remaining.Add(key, new List<string?>());
|
||||
}
|
||||
|
||||
_remaining[key].Add(value);
|
||||
}
|
||||
|
||||
[SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")]
|
||||
|
@ -5,7 +5,8 @@ internal sealed class CommandTreeTokenStream : IReadOnlyList<CommandTreeToken>
|
||||
private readonly List<CommandTreeToken> _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];
|
||||
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
351
test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Branches.cs
Normal file
351
test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Branches.cs
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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<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]
|
||||
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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<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]
|
||||
[Expectation("Hidden_Command_Options")]
|
||||
public Task Should_Not_Dump_Hidden_Options_On_A_Command()
|
||||
|
@ -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<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
|
||||
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<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]
|
||||
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<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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user