From 41ccc0b46478c62a7516c366845088b040df3fd2 Mon Sep 17 00:00:00 2001 From: Phil Scott Date: Sun, 4 Apr 2021 12:47:45 -0400 Subject: [PATCH] Adds CLI explain command Wanted a command to break down how Spectre views the command model. This outputs all the relevant settings in a tree. You can get a short explanation for all the commands, or a detailed explanation for all commands (there is a flag to go vice versa on the detailed view if you want) app cli explain app cli explain myappcommand app cli explain -d --- src/Spectre.Console/Cli/CommandApp.cs | 1 + .../Cli/Internal/Commands/ExplainCommand.cs | 250 ++++++++++++++++++ src/Spectre.Console/Cli/Internal/Constants.cs | 1 + 3 files changed, 252 insertions(+) create mode 100644 src/Spectre.Console/Cli/Internal/Commands/ExplainCommand.cs diff --git a/src/Spectre.Console/Cli/CommandApp.cs b/src/Spectre.Console/Cli/CommandApp.cs index 3e857fb..ce52a0e 100644 --- a/src/Spectre.Console/Cli/CommandApp.cs +++ b/src/Spectre.Console/Cli/CommandApp.cs @@ -78,6 +78,7 @@ namespace Spectre.Console.Cli cli.HideBranch(); cli.AddCommand(CliConstants.Commands.Version); cli.AddCommand(CliConstants.Commands.XmlDoc); + cli.AddCommand(CliConstants.Commands.Explain); }); _executed = true; diff --git a/src/Spectre.Console/Cli/Internal/Commands/ExplainCommand.cs b/src/Spectre.Console/Cli/Internal/Commands/ExplainCommand.cs new file mode 100644 index 0000000..ce2c5e3 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Commands/ExplainCommand.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli +{ + [Description("Displays diagnostics about CLI configurations")] + [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] + internal sealed class ExplainCommand : Command + { + private readonly CommandModel _commandModel; + private readonly IAnsiConsole _writer; + + public ExplainCommand(IConfiguration configuration, CommandModel commandModel) + { + _commandModel = commandModel ?? throw new ArgumentNullException(nameof(commandModel)); + _writer = configuration.Settings.Console.GetConsole(); + } + + public sealed class Settings : CommandSettings + { + public Settings(string[]? commands, bool? detailed) + { + Commands = commands; + Detailed = detailed; + } + + [CommandArgument(0, "[command]")] + public string[]? Commands { get; } + + [Description("Include detailed information about the commands.")] + [CommandOption("-d|--detailed")] + public bool? Detailed { get; } + } + + public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) + { + var tree = new Tree("CLI Configuration"); + tree.AddNode(ValueMarkup("Application Name", _commandModel.ApplicationName, "no application name")); + tree.AddNode(ValueMarkup("Parsing Mode", _commandModel.ParsingMode.ToString())); + + if (settings.Commands == null || settings.Commands.Length == 0) + { + AddCommands( + tree.AddNode(ParentMarkup("Commands")), + _commandModel.Commands, + settings.Detailed ?? false); + } + else + { + var currentCommandTier = _commandModel.Commands; + CommandInfo? currentCommand = null; + foreach (var command in settings.Commands) + { + currentCommand = currentCommandTier + .SingleOrDefault(i => + i.Name.Equals(command, StringComparison.CurrentCultureIgnoreCase) || + i.Aliases + .Any(alias => alias.Equals(command, StringComparison.CurrentCultureIgnoreCase))); + + if (currentCommand == null) + { + break; + } + + currentCommandTier = currentCommand.Children; + } + + if (currentCommand == null) + { + throw new Exception($"Command {string.Join(" ", settings.Commands)} not found"); + } + + AddCommands( + tree.AddNode(ParentMarkup("Commands")), + new[] { currentCommand }, + settings.Detailed ?? true); + } + + _writer.Write(tree); + + return 0; + } + + private IRenderable ValueMarkup(string key, string value) + { + return new Markup($"{key}: [blue]{value.EscapeMarkup()}[/]"); + } + + private IRenderable ValueMarkup(string key, string? value, string noValueText) + { + if (string.IsNullOrWhiteSpace(value)) + { + return new Markup($"{key}: [grey]({noValueText.EscapeMarkup()})[/]"); + } + + var table = new Table().NoBorder().HideHeaders().AddColumns("key", "value"); + table.AddRow($"{key}", $"[blue]{value.EscapeMarkup()}[/]"); + return table; + } + + private string ParentMarkup(string description) + { + return $"[yellow]{description.EscapeMarkup()}[/]"; + } + + private void AddStringList(TreeNode node, string description, IList? strings) + { + if (strings == null || strings.Count == 0) + { + return; + } + + var parentNode = node.AddNode(ParentMarkup(description)); + foreach (var s in strings) + { + parentNode.AddNode(s); + } + } + + private void AddCommands(TreeNode node, IEnumerable commands, bool detailed) + { + foreach (var command in commands) + { + var commandName = $"[green]{command.Name}[/]"; + if (command.IsDefaultCommand) + { + commandName += " (default)"; + } + + var commandNode = node.AddNode(commandName); + commandNode.AddNode(ValueMarkup("Description", command.Description, "no description")); + if (command.IsHidden) + { + commandNode.AddNode(ValueMarkup("IsHidden", command.IsHidden.ToString())); + } + + if (!command.IsBranch) + { + commandNode.AddNode(ValueMarkup("Type", command.CommandType?.ToString(), "no command type")); + commandNode.AddNode(ValueMarkup("Settings Type", command.SettingsType.ToString())); + } + + if (command.Parameters.Count > 0) + { + var parametersNode = commandNode.AddNode(ParentMarkup("Parameters")); + foreach (var parameter in command.Parameters) + { + AddParameter(parametersNode, parameter, detailed); + } + } + + AddStringList(commandNode, "Aliases", command.Aliases.ToList()); + AddStringList(commandNode, "Examples", command.Examples.Select(i => string.Join(" ", i)).ToList()); + + if (command.Children.Count > 0) + { + var childNode = commandNode.AddNode(ParentMarkup("Child Commands")); + AddCommands(childNode, command.Children, detailed); + } + } + } + + private void AddParameter(TreeNode parametersNode, CommandParameter parameter, bool detailed) + { + if (!detailed) + { + parametersNode.AddNode( + $"{parameter.PropertyName} [purple]{GetShortOptions(parameter)}[/] [grey]{parameter.Property.PropertyType.ToString().EscapeMarkup()}[/]"); + + return; + } + + var parameterNode = parametersNode.AddNode( + $"{parameter.PropertyName} [grey]{parameter.Property.PropertyType.ToString().EscapeMarkup()}[/]"); + + parameterNode.AddNode(ValueMarkup("Description", parameter.Description, "no description")); + parameterNode.AddNode(ValueMarkup("Parameter Kind", parameter.ParameterKind.ToString())); + + if (parameter is CommandOption commandOptionParameter) + { + if (commandOptionParameter.IsShadowed) + { + parameterNode.AddNode(ValueMarkup("IsShadowed", commandOptionParameter.IsShadowed.ToString())); + } + + if (commandOptionParameter.LongNames.Count > 0) + { + parameterNode.AddNode(ValueMarkup( + "Long Names", + string.Join("|", commandOptionParameter.LongNames.Select(i => $"--{i}")))); + + parameterNode.AddNode(ValueMarkup( + "Short Names", + string.Join("|", commandOptionParameter.ShortNames.Select(i => $"-{i}")))); + } + } + else if (parameter is CommandArgument commandArgumentParameter) + { + parameterNode.AddNode(ValueMarkup("Position", commandArgumentParameter.Position.ToString())); + parameterNode.AddNode(ValueMarkup("Value", commandArgumentParameter.Value)); + } + + parameterNode.AddNode(ValueMarkup("Required", parameter.Required.ToString())); + + if (parameter.Converter != null) + { + parameterNode.AddNode(ValueMarkup( + "Converter", $"\"{parameter.Converter.ConverterTypeName}\"")); + } + + if (parameter.DefaultValue != null) + { + parameterNode.AddNode(ValueMarkup( + "Default Value", $"\"{parameter.DefaultValue.Value}\"")); + } + + if (parameter.PairDeconstructor != null) + { + parameterNode.AddNode(ValueMarkup("Pair Deconstructor", parameter.PairDeconstructor.Type.ToString())); + } + + AddStringList( + parameterNode, + "Validators", + parameter.Validators.Select(i => i.GetType().ToString()).ToList()); + } + + private static string GetShortOptions(CommandParameter parameter) + { + if (parameter is CommandOption commandOptionParameter) + { + return string.Join( + " | ", + commandOptionParameter.LongNames.Select(i => $"--{i}") + .Concat(commandOptionParameter.ShortNames.Select(i => $"-{i}"))); + } + + if (parameter is CommandArgument commandArgumentParameter) + { + return $"{commandArgumentParameter.Value} position {commandArgumentParameter.Position}"; + } + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Constants.cs b/src/Spectre.Console/Cli/Internal/Constants.cs index f476945..f7cb115 100644 --- a/src/Spectre.Console/Cli/Internal/Constants.cs +++ b/src/Spectre.Console/Cli/Internal/Constants.cs @@ -17,6 +17,7 @@ namespace Spectre.Console.Cli public const string Branch = "cli"; public const string Version = "version"; public const string XmlDoc = "xmldoc"; + public const string Explain = "explain"; } } }