using Spectre.Console.Cli.Resources; namespace Spectre.Console.Cli.Help; /// /// The help provider for Spectre.Console. /// /// /// Other IHelpProvider implementations can be injected into the CommandApp, if desired. /// public class HelpProvider : IHelpProvider { private HelpProviderResources resources; /// /// Gets a value indicating how many examples from direct children to show in the help text. /// protected virtual int MaximumIndirectExamples { get; } /// /// Gets a value indicating whether any default values for command options are shown in the help text. /// protected virtual bool ShowOptionDefaultValues { get; } /// /// Gets a value indicating whether a trailing period of a command description is trimmed in the help text. /// protected virtual bool TrimTrailingPeriod { get; } 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 Get(ICommandInfo? command) { var arguments = new List(); arguments.AddRange(command?.Parameters?.OfType()?.Select( x => new HelpArgument(x.Value, x.Position, x.Required, x.Description)) ?? Array.Empty()); 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 Get(ICommandInfo? command, HelpProviderResources resources) { var parameters = new List(); parameters.Add(new HelpOption("h", "help", null, null, resources.PrintHelpDescription, 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, resources.PrintVersionDescription, null)); } parameters.AddRange(command?.Parameters.OfType().Where(o => !o.IsHidden).Select(o => new HelpOption( o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(), o.ValueName, o.ValueIsOptional, o.Description, o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value)) ?? Array.Empty()); return parameters; } } /// /// Initializes a new instance of the class. /// /// The command line application settings used for configuration. public HelpProvider(ICommandAppSettings settings) { this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues; this.MaximumIndirectExamples = settings.MaximumIndirectExamples; this.TrimTrailingPeriod = settings.TrimTrailingPeriod; resources = new HelpProviderResources(settings.Culture); } /// public virtual IEnumerable Write(ICommandModel model, ICommandInfo? command) { var result = new List(); result.AddRange(GetHeader(model, command)); result.AddRange(GetDescription(model, command)); result.AddRange(GetUsage(model, command)); result.AddRange(GetExamples(model, command)); result.AddRange(GetArguments(model, command)); result.AddRange(GetOptions(model, command)); result.AddRange(GetCommands(model, command)); result.AddRange(GetFooter(model, command)); return result; } /// /// Gets the header for the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. public virtual IEnumerable GetHeader(ICommandModel model, ICommandInfo? command) { yield break; } /// /// Gets the description section of the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. public virtual IEnumerable GetDescription(ICommandModel model, ICommandInfo? command) { if (command?.Description == null) { yield break; } var composer = new Composer(); composer.Style("yellow", $"{resources.Description}:").LineBreak(); composer.Text(command.Description).LineBreak(); yield return composer.LineBreak(); } /// /// Gets the usage section of the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. public virtual IEnumerable GetUsage(ICommandModel model, ICommandInfo? command) { var composer = new Composer(); composer.Style("yellow", $"{resources.Usage}:").LineBreak(); composer.Tab().Text(model.ApplicationName); var parameters = new List(); if (command == null) { parameters.Add($"[grey][[{resources.Options}]][/]"); parameters.Add($"[aqua]<{resources.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().Any()) { if (isCurrent) { foreach (var argument in current.Parameters.OfType() .Where(a => a.Required).OrderBy(a => a.Position).ToArray()) { parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]"); } } var optionalArguments = current.Parameters.OfType().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][[{resources.Options}]][/]"); } } if (command.IsBranch && command.DefaultCommand == null) { // The user must specify the command parameters.Add($"[aqua]<{resources.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][[{resources.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][[{resources.Command}]][/]"); } } } composer.Join(" ", parameters); composer.LineBreak(); return new[] { composer, }; } /// /// Gets the examples section of the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. /// /// Examples from the command's direct children are used /// if no examples have been set on the specified command or model. /// public virtual IEnumerable GetExamples(ICommandModel model, ICommandInfo? command) { var maxExamples = int.MaxValue; var examples = command?.Examples?.ToList() ?? model.Examples?.ToList() ?? new List(); if (examples.Count == 0) { // Since we're not checking direct examples, // make sure that we limit the number of examples. maxExamples = MaximumIndirectExamples; // Start at the current command (if exists) // or alternatively commence at the model. var commandContainer = command ?? (ICommandContainer)model; var queue = new Queue(new[] { commandContainer }); // 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 child in current.Commands.Where(x => !x.IsHidden)) { if (child.Examples.Count > 0) { examples.AddRange(child.Examples); } queue.Enqueue(child); } if (examples.Count >= maxExamples) { break; } } } if (Math.Min(maxExamples, examples.Count) > 0) { var composer = new Composer(); composer.LineBreak(); composer.Style("yellow", $"{resources.Examples}:").LineBreak(); for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++) { var args = string.Join(" ", examples[index]); composer.Tab().Text(model.ApplicationName).Space().Style("grey", args); composer.LineBreak(); } return new[] { composer }; } return Array.Empty(); } /// /// Gets the arguments section of the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. public virtual IEnumerable GetArguments(ICommandModel model, ICommandInfo? command) { var arguments = HelpArgument.Get(command); if (arguments.Count == 0) { return Array.Empty(); } var result = new List { new Markup(Environment.NewLine), new Markup($"[yellow]{resources.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; } /// /// Gets the options section of the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. public virtual IEnumerable GetOptions(ICommandModel model, ICommandInfo? command) { // Collect all options into a single structure. var parameters = HelpOption.Get(command, resources); if (parameters.Count == 0) { return Array.Empty(); } var result = new List { new Markup(Environment.NewLine), new Markup($"[yellow]{resources.Options}:[/]"), new Markup(Environment.NewLine), }; var helpOptions = parameters.ToArray(); var defaultValueColumn = ShowOptionDefaultValues && 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]{resources.Default}[/]", " "); } foreach (var option in helpOptions) { var columns = new List { 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().Select(Bold)), _ => Bold(option.DefaultValue), }; columns.Add(defaultValue); } columns.Add(option.Description?.TrimEnd('.') ?? " "); grid.AddRow(columns.ToArray()); } result.Add(grid); return result; } /// /// Gets the commands section of the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. public virtual IEnumerable GetCommands(ICommandModel model, ICommandInfo? command) { var commandContainer = command ?? (ICommandContainer)model; bool isDefaultCommand = command?.IsDefaultCommand ?? false; var commands = isDefaultCommand ? model.Commands : commandContainer.Commands; commands = commands.Where(x => !x.IsHidden).ToList(); if (commands.Count == 0) { return Array.Empty(); } var result = new List { new Markup(Environment.NewLine), new Markup($"[yellow]{resources.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 (TrimTrailingPeriod) { grid.AddRow( arguments.ToString().TrimEnd(), child.Description?.TrimEnd('.') ?? " "); } else { grid.AddRow( arguments.ToString().TrimEnd(), child.Description ?? " "); } } result.Add(grid); return result; } /// /// Gets the footer for the help information. /// /// The command model to write help for. /// The command for which to write help information (optional). /// An enumerable collection of objects. public virtual IEnumerable GetFooter(ICommandModel model, ICommandInfo? command) { yield break; } }