Allow custom help providers (#1259)

Allow custom help providers

* Version option will show in help even with a default command

* Reserve `-v` and `--version` as special Spectre.Console command line arguments (nb. breaking change for Spectre.Console users who have a default command with a settings class that uses either of these switches).

* Help writer correctly determines if trailing commands exist and whether to display them as optional or mandatory in the usage statement.

* Ability to control the number of indirect commands to display in the help text when the command itself doesn't have any examples of its own. Defaults to 5 (for backward compatibility) but can be set to any integer or zero to disable completely.

* Significant increase in unit test coverage for the help writer.

* Minor grammatical improvements to website documentation.
This commit is contained in:
Frank Ray
2023-09-08 08:51:33 +01:00
committed by GitHub
parent 813a53cdfa
commit 131b37fff8
70 changed files with 1646 additions and 330 deletions

View File

@ -5,7 +5,42 @@ namespace Spectre.Console.Cli;
/// and <see cref="IConfigurator{TSettings}"/>.
/// </summary>
public static class ConfiguratorExtensions
{
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="helpProvider">The help provider to use.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.SetHelpProvider(helpProvider);
return configurator;
}
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider<T>(this IConfigurator configurator)
where T : IHelpProvider
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.SetHelpProvider<T>();
return configurator;
}
/// <summary>
/// Sets the name of the application.
/// </summary>

View File

@ -1,7 +1,28 @@
namespace Spectre.Console.Cli;
namespace Spectre.Console.Cli.Help;
internal static class HelpWriter
{
/// <summary>
/// The help provider for Spectre.Console.
/// </summary>
/// <remarks>
/// Other IHelpProvider implementations can be injected into the CommandApp, if desired.
/// </remarks>
public class HelpProvider : IHelpProvider
{
/// <summary>
/// Gets a value indicating how many examples from direct children to show in the help text.
/// </summary>
protected virtual int MaximumIndirectExamples { get; }
/// <summary>
/// Gets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
protected virtual bool ShowOptionDefaultValues { get; }
/// <summary>
/// Gets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// </summary>
protected virtual bool TrimTrailingPeriod { get; }
private sealed class HelpArgument
{
public string Name { get; }
@ -17,10 +38,10 @@ internal static class HelpWriter
Description = description;
}
public static IReadOnlyList<HelpArgument> Get(CommandInfo? command)
public static IReadOnlyList<HelpArgument> Get(ICommandInfo? command)
{
var arguments = new List<HelpArgument>();
arguments.AddRange(command?.Parameters?.OfType<CommandArgument>()?.Select(
arguments.AddRange(command?.Parameters?.OfType<ICommandArgument>()?.Select(
x => new HelpArgument(x.Value, x.Position, x.Required, x.Description))
?? Array.Empty<HelpArgument>());
return arguments;
@ -46,49 +67,75 @@ internal static class HelpWriter
DefaultValue = defaultValue;
}
public static IReadOnlyList<HelpOption> Get(CommandModel model, CommandInfo? command)
public static IReadOnlyList<HelpOption> Get(ICommandInfo? command)
{
var parameters = new List<HelpOption>();
parameters.Add(new HelpOption("h", "help", null, null, "Prints help information", null));
// At the root and no default command?
if (command == null && model?.DefaultCommand == null)
{
parameters.Add(new HelpOption("v", "version", null, null, "Prints version information", null));
parameters.Add(new HelpOption("h", "help", null, null, "Prints help information", null));
// Version information applies to the entire application
// Include the "-v" option in the help when at the root of the command line application
// Don't allow the "-v" option if users have specified one or more sub-commands
if ((command == null || command?.Parent == null) && !(command?.IsBranch ?? false))
{
parameters.Add(new HelpOption("v", "version", null, null, "Prints version information", null));
}
parameters.AddRange(command?.Parameters.OfType<CommandOption>().Where(o => !o.IsHidden).Select(o =>
parameters.AddRange(command?.Parameters.OfType<ICommandOption>().Where(o => !o.IsHidden).Select(o =>
new HelpOption(
o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(),
o.ValueName, o.ValueIsOptional, o.Description,
o.ParameterKind == ParameterKind.Flag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value))
o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value))
?? Array.Empty<HelpOption>());
return parameters;
}
}
public static IEnumerable<IRenderable> Write(CommandModel model, bool writeOptionsDefaultValues)
}
/// <summary>
/// Initializes a new instance of the <see cref="HelpProvider"/> class.
/// </summary>
/// <param name="settings">The command line application settings used for configuration.</param>
public HelpProvider(ICommandAppSettings settings)
{
this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues;
this.MaximumIndirectExamples = settings.MaximumIndirectExamples;
this.TrimTrailingPeriod = settings.TrimTrailingPeriod;
}
/// <inheritdoc/>
public virtual IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command)
{
return WriteCommand(model, null, writeOptionsDefaultValues);
}
public static IEnumerable<IRenderable> WriteCommand(CommandModel model, CommandInfo? command, bool writeOptionsDefaultValues)
{
var container = command as ICommandContainer ?? model;
var isDefaultCommand = command?.IsDefaultCommand ?? false;
var result = new List<IRenderable>();
result.AddRange(GetDescription(command));
var result = new List<IRenderable>();
result.AddRange(GetHeader(model, command));
result.AddRange(GetDescription(model, command));
result.AddRange(GetUsage(model, command));
result.AddRange(GetExamples(model, command));
result.AddRange(GetArguments(command));
result.AddRange(GetOptions(model, command, writeOptionsDefaultValues));
result.AddRange(GetCommands(model, container, isDefaultCommand));
result.AddRange(GetArguments(model, command));
result.AddRange(GetOptions(model, command));
result.AddRange(GetCommands(model, command));
result.AddRange(GetFooter(model, command));
return result;
}
private static IEnumerable<IRenderable> GetDescription(CommandInfo? command)
}
/// <summary>
/// Gets the header for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command)
{
yield break;
}
/// <summary>
/// Gets the description section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetDescription(ICommandModel model, ICommandInfo? command)
{
if (command?.Description == null)
{
@ -99,13 +146,19 @@ internal static class HelpWriter
composer.Style("yellow", "DESCRIPTION:").LineBreak();
composer.Text(command.Description).LineBreak();
yield return composer.LineBreak();
}
private static IEnumerable<IRenderable> GetUsage(CommandModel model, CommandInfo? command)
}
/// <summary>
/// Gets the usage section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetUsage(ICommandModel model, ICommandInfo? command)
{
var composer = new Composer();
composer.Style("yellow", "USAGE:").LineBreak();
composer.Tab().Text(model.GetApplicationName());
composer.Tab().Text(model.ApplicationName);
var parameters = new List<string>();
@ -132,18 +185,18 @@ internal static class HelpWriter
}
}
if (current.Parameters.OfType<CommandArgument>().Any())
if (current.Parameters.OfType<ICommandArgument>().Any())
{
if (isCurrent)
{
foreach (var argument in current.Parameters.OfType<CommandArgument>()
foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray())
{
parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]");
}
}
var optionalArguments = current.Parameters.OfType<CommandArgument>().Where(x => !x.Required).ToArray();
var optionalArguments = current.Parameters.OfType<ICommandArgument>().Where(x => !x.Required).ToArray();
if (optionalArguments.Length > 0 || !isCurrent)
{
foreach (var optionalArgument in optionalArguments)
@ -159,9 +212,27 @@ internal static class HelpWriter
}
}
if (command.IsBranch)
{
if (command.IsBranch && command.DefaultCommand == null)
{
// The user must specify the command
parameters.Add("[aqua]<COMMAND>[/]");
}
else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{
// We are on a branch with a default command
// The user can optionally specify the command
parameters.Add("[aqua][[COMMAND]][/]");
}
else if (command.IsDefaultCommand)
{
var commands = model.Commands.Where(x => !x.IsHidden && !x.IsDefaultCommand).ToList();
if (commands.Count > 0)
{
// Commands other than the default are present
// So make these optional in the usage statement
parameters.Add("[aqua][[COMMAND]][/]");
}
}
}
@ -172,37 +243,48 @@ internal static class HelpWriter
{
composer,
};
}
private static IEnumerable<IRenderable> GetExamples(CommandModel model, CommandInfo? command)
}
/// <summary>
/// Gets the examples section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
/// <remarks>
/// Examples from the command's direct children are used
/// if no examples have been set on the specified command or model.
/// </remarks>
public virtual IEnumerable<IRenderable> GetExamples(ICommandModel model, ICommandInfo? command)
{
var maxExamples = int.MaxValue;
var examples = command?.Examples ?? model.Examples ?? new List<string[]>();
var examples = command?.Examples?.ToList() ?? model.Examples?.ToList() ?? new List<string[]>();
if (examples.Count == 0)
{
// Since we're not checking direct examples,
// make sure that we limit the number of examples.
maxExamples = 5;
maxExamples = MaximumIndirectExamples;
// Get the current root command.
var root = command ?? (ICommandContainer)model;
var queue = new Queue<ICommandContainer>(new[] { root });
// Start at the current command (if exists)
// or alternatively commence at the model.
var commandContainer = command ?? (ICommandContainer)model;
var queue = new Queue<ICommandContainer>(new[] { commandContainer });
// Traverse the command tree and look for examples.
// Traverse the command tree and look for examples.
// As soon as a node contains commands, bail.
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var cmd in current.Commands.Where(x => !x.IsHidden))
foreach (var child in current.Commands.Where(x => !x.IsHidden))
{
if (cmd.Examples.Count > 0)
if (child.Examples.Count > 0)
{
examples.AddRange(cmd.Examples);
examples.AddRange(child.Examples);
}
queue.Enqueue(cmd);
queue.Enqueue(child);
}
if (examples.Count >= maxExamples)
@ -212,7 +294,7 @@ internal static class HelpWriter
}
}
if (examples.Count > 0)
if (Math.Min(maxExamples, examples.Count) > 0)
{
var composer = new Composer();
composer.LineBreak();
@ -221,7 +303,7 @@ internal static class HelpWriter
for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{
var args = string.Join(" ", examples[index]);
composer.Tab().Text(model.GetApplicationName()).Space().Style("grey", args);
composer.Tab().Text(model.ApplicationName).Space().Style("grey", args);
composer.LineBreak();
}
@ -229,9 +311,15 @@ internal static class HelpWriter
}
return Array.Empty<IRenderable>();
}
private static IEnumerable<IRenderable> GetArguments(CommandInfo? command)
}
/// <summary>
/// Gets the arguments section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetArguments(ICommandModel model, ICommandInfo? command)
{
var arguments = HelpArgument.Get(command);
if (arguments.Count == 0)
@ -267,12 +355,18 @@ internal static class HelpWriter
result.Add(grid);
return result;
}
private static IEnumerable<IRenderable> GetOptions(CommandModel model, CommandInfo? command, bool writeDefaultValues)
}
/// <summary>
/// Gets the options section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetOptions(ICommandModel model, ICommandInfo? command)
{
// Collect all options into a single structure.
var parameters = HelpOption.Get(model, command);
var parameters = HelpOption.Get(command);
if (parameters.Count == 0)
{
return Array.Empty<IRenderable>();
@ -286,7 +380,7 @@ internal static class HelpWriter
};
var helpOptions = parameters.ToArray();
var defaultValueColumn = writeDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@ -369,14 +463,20 @@ internal static class HelpWriter
result.Add(grid);
return result;
}
}
/// <summary>
/// Gets the commands section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetCommands(ICommandModel model, ICommandInfo? command)
{
var commandContainer = command ?? (ICommandContainer)model;
bool isDefaultCommand = command?.IsDefaultCommand ?? false;
private static IEnumerable<IRenderable> GetCommands(
CommandModel model,
ICommandContainer command,
bool isDefaultCommand)
{
var commands = isDefaultCommand ? model.Commands : command.Commands;
var commands = isDefaultCommand ? model.Commands : commandContainer.Commands;
commands = commands.Where(x => !x.IsHidden).ToList();
if (commands.Count == 0)
@ -407,7 +507,7 @@ internal static class HelpWriter
arguments.Space();
}
if (model.TrimTrailingPeriod)
if (TrimTrailingPeriod)
{
grid.AddRow(
arguments.ToString().TrimEnd(),
@ -424,5 +524,16 @@ internal static class HelpWriter
result.Add(grid);
return result;
}
/// <summary>
/// Gets the footer for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetFooter(ICommandModel model, ICommandInfo? command)
{
yield break;
}
}

View File

@ -0,0 +1,17 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command argument.
/// </summary>
public interface ICommandArgument : ICommandParameter
{
/// <summary>
/// Gets the value of the argument.
/// </summary>
string Value { get; }
/// <summary>
/// Gets the position of the argument.
/// </summary>
int Position { get; }
}

View File

@ -0,0 +1,25 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command container.
/// </summary>
public interface ICommandContainer
{
/// <summary>
/// Gets all the examples for the container.
/// </summary>
IReadOnlyList<string[]> Examples { get; }
/// <summary>
/// Gets all commands in the container.
/// </summary>
IReadOnlyList<ICommandInfo> Commands { get; }
/// <summary>
/// Gets the default command for the container.
/// </summary>
/// <remarks>
/// Returns null if a default command has not been set.
/// </remarks>
ICommandInfo? DefaultCommand { get; }
}

View File

@ -0,0 +1,42 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents an executable command.
/// </summary>
public interface ICommandInfo : ICommandContainer
{
/// <summary>
/// Gets the name of the command.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the description of the command.
/// </summary>
string? Description { get; }
/// <summary>
/// Gets a value indicating whether the command is a branch.
/// </summary>
bool IsBranch { get; }
/// <summary>
/// Gets a value indicating whether the command is the default command within its container.
/// </summary>
bool IsDefaultCommand { get; }
/// <summary>
/// Gets a value indicating whether the command is hidden.
/// </summary>
bool IsHidden { get; }
/// <summary>
/// Gets the parameters associated with the command.
/// </summary>
IReadOnlyList<ICommandParameter> Parameters { get; }
/// <summary>
/// Gets the parent command, if any.
/// </summary>
ICommandInfo? Parent { get; }
}

View File

@ -0,0 +1,23 @@
namespace Spectre.Console.Cli.Help;
internal static class ICommandInfoExtensions
{
/// <summary>
/// Walks up the command.Parent tree, adding each command into a list as it goes.
/// </summary>
/// <remarks>The first command added to the list is the current (ie. this one).</remarks>
/// <returns>The list of commands from current to root, as traversed by <see cref="CommandInfo.Parent"/>.</returns>
public static List<ICommandInfo> Flatten(this ICommandInfo commandInfo)
{
var result = new Stack<Help.ICommandInfo>();
var current = commandInfo;
while (current != null)
{
result.Push(current);
current = current.Parent;
}
return result.ToList();
}
}

View File

@ -0,0 +1,12 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command model.
/// </summary>
public interface ICommandModel : ICommandContainer
{
/// <summary>
/// Gets the name of the application.
/// </summary>
string ApplicationName { get; }
}

View File

@ -0,0 +1,27 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command option.
/// </summary>
public interface ICommandOption : ICommandParameter
{
/// <summary>
/// Gets the long names of the option.
/// </summary>
IReadOnlyList<string> LongNames { get; }
/// <summary>
/// Gets the short names of the option.
/// </summary>
IReadOnlyList<string> ShortNames { get; }
/// <summary>
/// Gets the value name of the option, if applicable.
/// </summary>
string? ValueName { get; }
/// <summary>
/// Gets a value indicating whether the option value is optional.
/// </summary>
bool ValueIsOptional { get; }
}

View File

@ -0,0 +1,32 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Represents a command parameter.
/// </summary>
public interface ICommandParameter
{
/// <summary>
/// Gets a value indicating whether the parameter is a flag.
/// </summary>
bool IsFlag { get; }
/// <summary>
/// Gets a value indicating whether the parameter is required.
/// </summary>
bool Required { get; }
/// <summary>
/// Gets the description of the parameter.
/// </summary>
string? Description { get; }
/// <summary>
/// Gets the default value of the parameter, if specified.
/// </summary>
DefaultValueAttribute? DefaultValue { get; }
/// <summary>
/// Gets a value indicating whether the parameter is hidden.
/// </summary>
bool IsHidden { get; }
}

View File

@ -0,0 +1,20 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// The help provider interface for Spectre.Console.
/// </summary>
/// <remarks>
/// Implementations of this interface are responsbile
/// for writing command help to the terminal when the
/// `-h` or `--help` has been specified on the command line.
/// </remarks>
public interface IHelpProvider
{
/// <summary>
/// Writes help information for the application.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects representing the help information.</returns>
IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command);
}

View File

@ -13,12 +13,22 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets the application version (use it to override auto-detected value).
/// </summary>
string? ApplicationVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
bool ShowOptionDefaultValues { get; set; }
string? ApplicationVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating how many examples from direct children to show in the help text.
/// </summary>
int MaximumIndirectExamples { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
bool ShowOptionDefaultValues { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// </summary>
bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>.
@ -41,11 +51,6 @@ public interface ICommandAppSettings
/// </summary>
CaseSensitivity CaseSensitivity { get; set; }
/// <summary>
/// Gets or sets a value indicating whether trailing period of a description is trimmed.
/// </summary>
bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not parsing is strict.
/// </summary>

View File

@ -4,7 +4,20 @@ namespace Spectre.Console.Cli;
/// Represents a configurator.
/// </summary>
public interface IConfigurator
{
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="helpProvider">The help provider to use.</param>
public void SetHelpProvider(IHelpProvider helpProvider);
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
public void SetHelpProvider<T>()
where T : IHelpProvider;
/// <summary>
/// Gets the command app settings.
/// </summary>
@ -53,5 +66,5 @@ public interface IConfigurator
/// <param name="action">The command branch configurator.</param>
/// <returns>A branch configurator that can be used to configure the branch further.</returns>
IBranchConfigurator AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action)
where TSettings : CommandSettings;
where TSettings : CommandSettings;
}

View File

@ -17,7 +17,7 @@ public interface IConfigurator<in TSettings>
/// Adds an example of how to use the branch.
/// </summary>
/// <param name="args">The example arguments.</param>
void AddExample(string[] args);
void AddExample(params string[] args);
/// <summary>
/// Adds a default command.

View File

@ -8,85 +8,87 @@ internal sealed class CommandExecutor
{
_registrar = registrar ?? throw new ArgumentNullException(nameof(registrar));
_registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
}
}
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
_registrar.RegisterInstance(typeof(IConfiguration), configuration);
_registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole());
}
args ??= new List<string>();
_registrar.RegisterInstance(typeof(IConfiguration), configuration);
_registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole());
// Register the help provider
var defaultHelpProvider = new HelpProvider(configuration.Settings);
_registrar.RegisterInstance(typeof(IHelpProvider), defaultHelpProvider);
// Create the command model.
var model = CommandModelBuilder.Build(configuration);
_registrar.RegisterInstance(typeof(CommandModel), model);
_registrar.RegisterDependencies(model);
// No default command?
if (model.DefaultCommand == null)
{
// Got at least one argument?
var firstArgument = args.FirstOrDefault();
if (firstArgument != null)
{
// Asking for version? Kind of a hack, but it's alright.
// We should probably make this a bit better in the future.
if (firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) ||
firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase))
{
var console = configuration.Settings.Console.GetConsole();
console.WriteLine(ResolveApplicationVersion(configuration));
return 0;
}
}
}
_registrar.RegisterDependencies(model);
// Asking for version? Kind of a hack, but it's alright.
// We should probably make this a bit better in the future.
if (args.Contains("-v") || args.Contains("--version"))
{
var console = configuration.Settings.Console.GetConsole();
console.WriteLine(ResolveApplicationVersion(configuration));
return 0;
}
// Parse and map the model against the arguments.
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args);
// Currently the root?
if (parsedResult?.Tree == null)
{
// Display help.
configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.ShowOptionDefaultValues));
return 0;
}
// Get the command to execute.
var leaf = parsedResult.Tree.GetLeafCommand();
if (leaf.Command.IsBranch || leaf.ShowHelp)
{
// Branches can't be executed. Show help.
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command, configuration.Settings.ShowOptionDefaultValues));
return leaf.ShowHelp ? 0 : 1;
}
// Is this the default and is it called without arguments when there are required arguments?
if (leaf.Command.IsDefaultCommand && args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required))
{
// Display help for default command.
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command, configuration.Settings.ShowOptionDefaultValues));
return 1;
}
// Register the arguments with the container.
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args);
// Register the arguments with the container.
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
_registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining);
// Create the resolver and the context.
// Create the resolver.
using (var resolver = new TypeResolverAdapter(_registrar.Build()))
{
{
// Get the registered help provider, falling back to the default provider
// registered above if no custom implementations have been registered.
var helpProvider = resolver.Resolve(typeof(IHelpProvider)) as IHelpProvider ?? defaultHelpProvider;
// Currently the root?
if (parsedResult?.Tree == null)
{
// Display help.
configuration.Settings.Console.SafeRender(helpProvider.Write(model, null));
return 0;
}
// Get the command to execute.
var leaf = parsedResult.Tree.GetLeafCommand();
if (leaf.Command.IsBranch || leaf.ShowHelp)
{
// Branches can't be executed. Show help.
configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command));
return leaf.ShowHelp ? 0 : 1;
}
// Is this the default and is it called without arguments when there are required arguments?
if (leaf.Command.IsDefaultCommand && args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required))
{
// Display help for default command.
configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command));
return 1;
}
// Create the content.
var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data);
// Execute the command tree.
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
}
}
}
private CommandTreeParserResult? ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> args)
#pragma warning disable CS8603 // Possible null reference return.
private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> args)
{
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
@ -113,7 +115,8 @@ internal sealed class CommandExecutor
return parsedResult;
}
#pragma warning restore CS8603 // Possible null reference return.
private static string ResolveApplicationVersion(IConfiguration configuration)
{
return

View File

@ -35,11 +35,11 @@ internal sealed class ComponentRegistry : IDisposable
foreach (var type in new HashSet<Type>(registration.RegistrationTypes))
{
if (!_registrations.ContainsKey(type))
{
_registrations.Add(type, new HashSet<ComponentRegistration>());
{
// Only add each registration type once.
_registrations.Add(type, new HashSet<ComponentRegistration>());
_registrations[type].Add(registration);
}
_registrations[type].Add(registration);
}
}

View File

@ -4,7 +4,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings
{
public string? ApplicationName { get; set; }
public string? ApplicationVersion { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public int MaximumIndirectExamples { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public IAnsiConsole? Console { get; set; }
public ICommandInterceptor? Interceptor { get; set; }
public ITypeRegistrarFrontend Registrar { get; set; }
@ -24,7 +25,8 @@ internal sealed class CommandAppSettings : ICommandAppSettings
{
Registrar = new TypeRegistrar(registrar);
CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true;
ShowOptionDefaultValues = true;
MaximumIndirectExamples = 5;
}
public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)

View File

@ -19,6 +19,19 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
Settings = new CommandAppSettings(registrar);
Examples = new List<string[]>();
}
public void SetHelpProvider(IHelpProvider helpProvider)
{
// Register the help provider
_registrar.RegisterInstance(typeof(IHelpProvider), helpProvider);
}
public void SetHelpProvider<T>()
where T : IHelpProvider
{
// Register the help provider
_registrar.Register(typeof(IHelpProvider), typeof(T));
}
public void AddExample(params string[] args)
{

View File

@ -17,7 +17,7 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
_command.Description = description;
}
public void AddExample(string[] args)
public void AddExample(params string[] args)
{
_command.Examples.Add(args);
}

View File

@ -1,6 +1,6 @@
namespace Spectre.Console.Cli;
internal sealed class CommandArgument : CommandParameter
internal sealed class CommandArgument : CommandParameter, ICommandArgument
{
public string Value { get; }
public int Position { get; set; }

View File

@ -1,6 +1,6 @@
namespace Spectre.Console.Cli;
internal sealed class CommandInfo : ICommandContainer
internal sealed class CommandInfo : ICommandContainer, ICommandInfo
{
public string Name { get; }
public HashSet<string> Aliases { get; }
@ -20,8 +20,14 @@ internal sealed class CommandInfo : ICommandContainer
// only branches can have a default command
public CommandInfo? DefaultCommand => IsBranch ? Children.FirstOrDefault(c => c.IsDefaultCommand) : null;
public bool IsHidden { get; }
public bool IsHidden { get; }
IReadOnlyList<ICommandInfo> Help.ICommandContainer.Commands => Children.Cast<ICommandInfo>().ToList();
ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand;
IReadOnlyList<ICommandParameter> ICommandInfo.Parameters => Parameters.Cast<ICommandParameter>().ToList();
ICommandInfo? ICommandInfo.Parent => Parent;
IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples;
public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype)
{
Parent = parent;
@ -48,19 +54,5 @@ internal sealed class CommandInfo : ICommandContainer
Description = description.Description;
}
}
}
public List<CommandInfo> Flatten()
{
var result = new Stack<CommandInfo>();
var current = this;
while (current != null)
{
result.Push(current);
current = current.Parent;
}
return result.ToList();
}
}
}

View File

@ -1,14 +1,18 @@
namespace Spectre.Console.Cli;
internal sealed class CommandModel : ICommandContainer
internal sealed class CommandModel : ICommandContainer, ICommandModel
{
public string? ApplicationName { get; }
public ParsingMode ParsingMode { get; }
public IList<CommandInfo> Commands { get; }
public IList<string[]> Examples { get; }
public bool TrimTrailingPeriod { get; }
public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand);
public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand);
string ICommandModel.ApplicationName => GetApplicationName(ApplicationName);
IReadOnlyList<ICommandInfo> Help.ICommandContainer.Commands => Commands.Cast<ICommandInfo>().ToList();
ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand;
IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples;
public CommandModel(
CommandAppSettings settings,
@ -17,22 +21,32 @@ internal sealed class CommandModel : ICommandContainer
{
ApplicationName = settings.ApplicationName;
ParsingMode = settings.ParsingMode;
TrimTrailingPeriod = settings.TrimTrailingPeriod;
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>());
Examples = new List<string[]>(examples ?? Array.Empty<string[]>());
}
public string GetApplicationName()
}
/// <summary>
/// Gets the name of the application.
/// If the provided <paramref name="applicationName"/> is not null or empty,
/// it is returned. Otherwise the name of the current application
/// is determined based on the executable file's name.
/// </summary>
/// <param name="applicationName">The optional name of the application.</param>
/// <returns>
/// The name of the application, or a default value of "?" if no valid application name can be determined.
/// </returns>
private static string GetApplicationName(string? applicationName)
{
return
ApplicationName ??
applicationName ??
Path.GetFileName(GetApplicationFile()) ?? // null is propagated by GetFileName
"?";
}
private static string? GetApplicationFile()
{
var location = Assembly.GetEntryAssembly()?.Location;
var location = Assembly.GetEntryAssembly()?.Location;
if (string.IsNullOrWhiteSpace(location))
{
// this is special case for single file executable

View File

@ -1,6 +1,6 @@
namespace Spectre.Console.Cli;
internal sealed class CommandOption : CommandParameter
internal sealed class CommandOption : CommandParameter, ICommandOption
{
public IReadOnlyList<string> LongNames { get; }
public IReadOnlyList<string> ShortNames { get; }

View File

@ -1,6 +1,6 @@
namespace Spectre.Console.Cli;
internal abstract class CommandParameter : ICommandParameterInfo
internal abstract class CommandParameter : ICommandParameterInfo, ICommandParameter
{
public Guid Id { get; }
public Type ParameterType { get; }
@ -17,8 +17,10 @@ internal abstract class CommandParameter : ICommandParameterInfo
public string PropertyName => Property.Name;
public virtual bool WantRawValue => ParameterType.IsPairDeconstructable()
&& (PairDeconstructor != null || Converter == null);
&& (PairDeconstructor != null || Converter == null);
public bool IsFlag => ParameterKind == ParameterKind.Flag;
protected CommandParameter(
Type parameterType, ParameterKind parameterKind, PropertyInfo property,
string? description, TypeConverterAttribute? converter,

View File

@ -10,6 +10,7 @@ global using System.Linq;
global using System.Reflection;
global using System.Text;
global using System.Threading.Tasks;
global using System.Xml;
global using System.Xml;
global using Spectre.Console.Cli.Help;
global using Spectre.Console.Cli.Unsafe;
global using Spectre.Console.Rendering;
global using Spectre.Console.Rendering;

View File

@ -35,10 +35,12 @@ public sealed class FakeTypeResolver : ITypeResolver
}
if (_registrations.TryGetValue(type, out var registrations))
{
{
// The type might be an interface, but the registration should be a class.
// So call CreateInstance on the first registration rather than the type.
return registrations.Count == 0
? null
: Activator.CreateInstance(type);
? null
: Activator.CreateInstance(registrations[0]);
}
return null;