mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-07-05 03:58:16 +08:00
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:
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -1,428 +0,0 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal static class HelpWriter
|
||||
{
|
||||
private sealed class HelpArgument
|
||||
{
|
||||
public string Name { get; }
|
||||
public int Position { get; set; }
|
||||
public bool Required { get; }
|
||||
public string? Description { get; }
|
||||
|
||||
public HelpArgument(string name, int position, bool required, string? description)
|
||||
{
|
||||
Name = name;
|
||||
Position = position;
|
||||
Required = required;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<HelpArgument> Get(CommandInfo? command)
|
||||
{
|
||||
var arguments = new List<HelpArgument>();
|
||||
arguments.AddRange(command?.Parameters?.OfType<CommandArgument>()?.Select(
|
||||
x => new HelpArgument(x.Value, x.Position, x.Required, x.Description))
|
||||
?? Array.Empty<HelpArgument>());
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class HelpOption
|
||||
{
|
||||
public string? Short { get; }
|
||||
public string? Long { get; }
|
||||
public string? Value { get; }
|
||||
public bool? ValueIsOptional { get; }
|
||||
public string? Description { get; }
|
||||
public object? DefaultValue { get; }
|
||||
|
||||
public HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description, object? defaultValue)
|
||||
{
|
||||
Short = @short;
|
||||
Long = @long;
|
||||
Value = value;
|
||||
ValueIsOptional = valueIsOptional;
|
||||
Description = description;
|
||||
DefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<HelpOption> Get(CommandModel model, CommandInfo? 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.AddRange(command?.Parameters.OfType<CommandOption>().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))
|
||||
?? Array.Empty<HelpOption>());
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<IRenderable> Write(CommandModel model, bool writeOptionsDefaultValues)
|
||||
{
|
||||
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));
|
||||
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));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<IRenderable> GetDescription(CommandInfo? command)
|
||||
{
|
||||
if (command?.Description == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var composer = new Composer();
|
||||
composer.Style("yellow", "DESCRIPTION:").LineBreak();
|
||||
composer.Text(command.Description).LineBreak();
|
||||
yield return composer.LineBreak();
|
||||
}
|
||||
|
||||
private static IEnumerable<IRenderable> GetUsage(CommandModel model, CommandInfo? command)
|
||||
{
|
||||
var composer = new Composer();
|
||||
composer.Style("yellow", "USAGE:").LineBreak();
|
||||
composer.Tab().Text(model.GetApplicationName());
|
||||
|
||||
var parameters = new List<string>();
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
parameters.Add("[grey][[OPTIONS]][/]");
|
||||
parameters.Add("[aqua]<COMMAND>[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var current in command.Flatten())
|
||||
{
|
||||
var isCurrent = current == command;
|
||||
|
||||
if (!current.IsDefaultCommand)
|
||||
{
|
||||
if (isCurrent)
|
||||
{
|
||||
parameters.Add($"[underline]{current.Name.EscapeMarkup()}[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
parameters.Add($"{current.Name.EscapeMarkup()}");
|
||||
}
|
||||
}
|
||||
|
||||
if (current.Parameters.OfType<CommandArgument>().Any())
|
||||
{
|
||||
if (isCurrent)
|
||||
{
|
||||
foreach (var argument in current.Parameters.OfType<CommandArgument>()
|
||||
.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();
|
||||
if (optionalArguments.Length > 0 || !isCurrent)
|
||||
{
|
||||
foreach (var optionalArgument in optionalArguments)
|
||||
{
|
||||
parameters.Add($"[silver][[{optionalArgument.Value.EscapeMarkup()}]][/]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCurrent)
|
||||
{
|
||||
parameters.Add("[grey][[OPTIONS]][/]");
|
||||
}
|
||||
}
|
||||
|
||||
if (command.IsBranch)
|
||||
{
|
||||
parameters.Add("[aqua]<COMMAND>[/]");
|
||||
}
|
||||
}
|
||||
|
||||
composer.Join(" ", parameters);
|
||||
composer.LineBreak();
|
||||
|
||||
return new[]
|
||||
{
|
||||
composer,
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<IRenderable> GetExamples(CommandModel model, CommandInfo? command)
|
||||
{
|
||||
var maxExamples = int.MaxValue;
|
||||
|
||||
var examples = command?.Examples ?? model.Examples ?? 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;
|
||||
|
||||
// Get the current root command.
|
||||
var root = command ?? (ICommandContainer)model;
|
||||
var queue = new Queue<ICommandContainer>(new[] { root });
|
||||
|
||||
// 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))
|
||||
{
|
||||
if (cmd.Examples.Count > 0)
|
||||
{
|
||||
examples.AddRange(cmd.Examples);
|
||||
}
|
||||
|
||||
queue.Enqueue(cmd);
|
||||
}
|
||||
|
||||
if (examples.Count >= maxExamples)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (examples.Count > 0)
|
||||
{
|
||||
var composer = new Composer();
|
||||
composer.LineBreak();
|
||||
composer.Style("yellow", "EXAMPLES:").LineBreak();
|
||||
|
||||
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.LineBreak();
|
||||
}
|
||||
|
||||
return new[] { composer };
|
||||
}
|
||||
|
||||
return Array.Empty<IRenderable>();
|
||||
}
|
||||
|
||||
private static IEnumerable<IRenderable> GetArguments(CommandInfo? command)
|
||||
{
|
||||
var arguments = HelpArgument.Get(command);
|
||||
if (arguments.Count == 0)
|
||||
{
|
||||
return Array.Empty<IRenderable>();
|
||||
}
|
||||
|
||||
var result = new List<IRenderable>
|
||||
{
|
||||
new Markup(Environment.NewLine),
|
||||
new Markup("[yellow]ARGUMENTS:[/]"),
|
||||
new Markup(Environment.NewLine),
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
|
||||
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
|
||||
|
||||
foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position))
|
||||
{
|
||||
grid.AddRow(
|
||||
$"[silver]<{argument.Name.EscapeMarkup()}>[/]",
|
||||
argument.Description?.TrimEnd('.') ?? " ");
|
||||
}
|
||||
|
||||
foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position))
|
||||
{
|
||||
grid.AddRow(
|
||||
$"[grey][[{argument.Name.EscapeMarkup()}]][/]",
|
||||
argument.Description?.TrimEnd('.') ?? " ");
|
||||
}
|
||||
|
||||
result.Add(grid);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<IRenderable> GetOptions(CommandModel model, CommandInfo? command, bool writeDefaultValues)
|
||||
{
|
||||
// Collect all options into a single structure.
|
||||
var parameters = HelpOption.Get(model, command);
|
||||
if (parameters.Count == 0)
|
||||
{
|
||||
return Array.Empty<IRenderable>();
|
||||
}
|
||||
|
||||
var result = new List<IRenderable>
|
||||
{
|
||||
new Markup(Environment.NewLine),
|
||||
new Markup("[yellow]OPTIONS:[/]"),
|
||||
new Markup(Environment.NewLine),
|
||||
};
|
||||
|
||||
var helpOptions = parameters.ToArray();
|
||||
var defaultValueColumn = writeDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
|
||||
|
||||
var grid = new Grid();
|
||||
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
|
||||
if (defaultValueColumn)
|
||||
{
|
||||
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0, 4, 0) });
|
||||
}
|
||||
|
||||
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
|
||||
|
||||
static string GetOptionParts(HelpOption option)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
if (option.Short != null)
|
||||
{
|
||||
builder.Append('-').Append(option.Short.EscapeMarkup());
|
||||
if (option.Long != null)
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(" ");
|
||||
if (option.Long != null)
|
||||
{
|
||||
builder.Append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
if (option.Long != null)
|
||||
{
|
||||
builder.Append("--").Append(option.Long.EscapeMarkup());
|
||||
}
|
||||
|
||||
if (option.Value != null)
|
||||
{
|
||||
builder.Append(' ');
|
||||
if (option.ValueIsOptional ?? false)
|
||||
{
|
||||
builder.Append("[grey][[").Append(option.Value.EscapeMarkup()).Append("]][/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("[silver]<").Append(option.Value.EscapeMarkup()).Append(">[/]");
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
if (defaultValueColumn)
|
||||
{
|
||||
grid.AddRow(" ", "[lime]DEFAULT[/]", " ");
|
||||
}
|
||||
|
||||
foreach (var option in helpOptions)
|
||||
{
|
||||
var columns = new List<string> { GetOptionParts(option) };
|
||||
if (defaultValueColumn)
|
||||
{
|
||||
static string Bold(object obj) => $"[bold]{obj.ToString().EscapeMarkup()}[/]";
|
||||
|
||||
var defaultValue = option.DefaultValue switch
|
||||
{
|
||||
null => " ",
|
||||
"" => " ",
|
||||
Array { Length: 0 } => " ",
|
||||
Array array => string.Join(", ", array.Cast<object>().Select(Bold)),
|
||||
_ => Bold(option.DefaultValue),
|
||||
};
|
||||
columns.Add(defaultValue);
|
||||
}
|
||||
|
||||
columns.Add(option.Description?.TrimEnd('.') ?? " ");
|
||||
|
||||
grid.AddRow(columns.ToArray());
|
||||
}
|
||||
|
||||
result.Add(grid);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<IRenderable> GetCommands(
|
||||
CommandModel model,
|
||||
ICommandContainer command,
|
||||
bool isDefaultCommand)
|
||||
{
|
||||
var commands = isDefaultCommand ? model.Commands : command.Commands;
|
||||
commands = commands.Where(x => !x.IsHidden).ToList();
|
||||
|
||||
if (commands.Count == 0)
|
||||
{
|
||||
return Array.Empty<IRenderable>();
|
||||
}
|
||||
|
||||
var result = new List<IRenderable>
|
||||
{
|
||||
new Markup(Environment.NewLine),
|
||||
new Markup("[yellow]COMMANDS:[/]"),
|
||||
new Markup(Environment.NewLine),
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
|
||||
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
|
||||
|
||||
foreach (var child in commands)
|
||||
{
|
||||
var arguments = new Composer();
|
||||
arguments.Style("silver", child.Name.EscapeMarkup());
|
||||
arguments.Space();
|
||||
|
||||
foreach (var argument in HelpArgument.Get(child).Where(a => a.Required))
|
||||
{
|
||||
arguments.Style("silver", $"<{argument.Name.EscapeMarkup()}>");
|
||||
arguments.Space();
|
||||
}
|
||||
|
||||
if (model.TrimTrailingPeriod)
|
||||
{
|
||||
grid.AddRow(
|
||||
arguments.ToString().TrimEnd(),
|
||||
child.Description?.TrimEnd('.') ?? " ");
|
||||
}
|
||||
else
|
||||
{
|
||||
grid.AddRow(
|
||||
arguments.ToString().TrimEnd(),
|
||||
child.Description ?? " ");
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(grid);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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; }
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user