Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help writer improvements #1252

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/input/cli/commandApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ For more complex command hierarchical configurations, they can also be composed

## Customizing Command Configurations

The `Configure` method is also used to change how help for the commands is generated. This configuration will give our command an additional alias of `file-size` and a description to be used when displaying the help. Additional, an example is specified that will be parsed and displayed for users asking for help. Multiple examples can be provided. Commands can also be marked as hidden. With this option they are still executable, but will not be displayed in help screens.
The `Configure` method is also used to change how help for the commands is generated. This configuration will give our command an additional alias of `file-size` and a description to be used when displaying the help. Additionally, an example is specified that will be parsed and displayed for users asking for help. Multiple examples can be provided. Commands can also be marked as hidden. With this option they are still executable, but will not be displayed in help screens.

``` csharp
var app = new CommandApp();
Expand Down
2 changes: 1 addition & 1 deletion docs/input/cli/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This setting file tells `Spectre.Console.Cli` that our command has two parameter

## CommandArgument

Arguments have a position and a name. The name is not only used for generating help, but its formatting is used to determine whether or not the argument is optional. The name must either be surrounded by square brackets (e.g. `[name]`) or angle brackets (e.g. `<name>`). Angle brackets denote required whereas square brackets denote optional. If neither are specified an exception will be thrown.
Arguments have a position and a name. The name is not only used for generating help, but its formatting is used to determine whether or not the argument is optional. Angle brackets denote a required argument (e.g. `<name>`) whereas square brackets denote an optional argument (e.g. `[name]`). If neither are specified an exception will be thrown.

The position is used for scenarios where there could be more than one argument.

Expand Down
34 changes: 17 additions & 17 deletions examples/Cli/Demo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@ public static int Main(string[] args)
{
config.SetApplicationName("fake-dotnet");
config.ValidateExamples();
config.AddExample(new[] { "run", "--no-build" });

// Run
config.AddCommand<RunCommand>("run");

// Add
config.AddBranch<AddSettings>("add", add =>
{
add.SetDescription("Add a package or reference to a .NET project");
add.AddCommand<AddPackageCommand>("package");
add.AddCommand<AddReferenceCommand>("reference");
config.AddExample("run", "--no-build");

// Run
config.AddCommand<RunCommand>("run");

// Add
config.AddBranch<AddSettings>("add", add =>
{
add.SetDescription("Add a package or reference to a .NET project");
add.AddCommand<AddPackageCommand>("package");
add.AddCommand<AddReferenceCommand>("reference");
});

// Serve
config.AddCommand<ServeCommand>("serve")
.WithExample("serve", "-o", "firefox")
.WithExample("serve", "--port", "80", "-o", "firefox");
});

// Serve
config.AddCommand<ServeCommand>("serve")
.WithExample(new[] { "serve", "-o", "firefox" })
.WithExample(new[] { "serve", "--port", "80", "-o", "firefox" });
});

return app.Run(args);
}
}
11 changes: 10 additions & 1 deletion src/Spectre.Console.Cli/ICommandAppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets the application version (use it to override auto-detected value).
/// </summary>
string? ApplicationVersion { 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>
/// <remarks>
/// The <see cref="HelpWriter"/> uses examples from a command's direct children
/// when no examples have been explicity set on the command itself.
/// </remarks>
int MaximumIndirectExamples { get; set; }

/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
Expand Down
2 changes: 1 addition & 1 deletion src/Spectre.Console.Cli/IConfiguratorOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
57 changes: 28 additions & 29 deletions src/Spectre.Console.Cli/Internal/CommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,47 @@ public CommandExecutor(ITypeRegistrar registrar)
{
_registrar = registrar ?? throw new ArgumentNullException(nameof(registrar));
_registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
}

}

#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}

_registrar.RegisterInstance(typeof(IConfiguration), configuration);
}

args ??= new List<string>();


_registrar.RegisterInstance(typeof(IConfiguration), configuration);
_registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole());

// 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));
configuration.Settings.Console.SafeRender(HelpWriter.Write(model, configuration.Settings.MaximumIndirectExamples, configuration.Settings.ShowOptionDefaultValues));
return 0;
}

Expand All @@ -60,17 +57,18 @@ public async Task<int> Execute(IConfiguration configuration, IEnumerable<string>
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));
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command, configuration.Settings.MaximumIndirectExamples, 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));
configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command, configuration.Settings.MaximumIndirectExamples, configuration.Settings.ShowOptionDefaultValues));
return 1;
}


// Register the arguments with the container.
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
Expand All @@ -84,7 +82,8 @@ public async Task<int> Execute(IConfiguration configuration, IEnumerable<string>
// Execute the command tree.
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
}
}
}
#pragma warning restore SA1507 // Code should not contain multiple blank lines in a row

private CommandTreeParserResult? ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> args)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -24,7 +25,8 @@ public CommandAppSettings(ITypeRegistrar registrar)
{
Registrar = new TypeRegistrar(registrar);
CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true;
ShowOptionDefaultValues = true;
MaximumIndirectExamples = 5;
}

public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void SetDescription(string description)
_command.Description = description;
}

public void AddExample(string[] args)
public void AddExample(params string[] args)
{
_command.Examples.Add(args);
}
Expand Down
55 changes: 35 additions & 20 deletions src/Spectre.Console.Cli/Internal/HelpWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ public HelpOption(string? @short, string? @long, string? @value, bool? valueIsOp
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.Add(new HelpOption("h", "help", null, null, "Prints help information", null));
// At the root?
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 =>
Expand All @@ -67,20 +67,20 @@ public static IReadOnlyList<HelpOption> Get(CommandModel model, CommandInfo? com
}
}

public static IEnumerable<IRenderable> Write(CommandModel model, bool writeOptionsDefaultValues)
public static IEnumerable<IRenderable> Write(CommandModel model, int maxIndirectExamples, bool writeOptionsDefaultValues)
{
return WriteCommand(model, null, writeOptionsDefaultValues);
return WriteCommand(model, null, maxIndirectExamples, writeOptionsDefaultValues);
}

public static IEnumerable<IRenderable> WriteCommand(CommandModel model, CommandInfo? command, bool writeOptionsDefaultValues)
public static IEnumerable<IRenderable> WriteCommand(CommandModel model, CommandInfo? command, int maxIndirectExamples, 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(GetExamples(model, command, maxIndirectExamples));
result.AddRange(GetArguments(command));
result.AddRange(GetOptions(model, command, writeOptionsDefaultValues));
result.AddRange(GetCommands(model, container, isDefaultCommand));
Expand Down Expand Up @@ -159,9 +159,27 @@ private static IEnumerable<IRenderable> GetUsage(CommandModel model, CommandInfo
}
}

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.Children.Count > 0)
{
// We are on a branch with a default commnd
// 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]][/]");
}
}
}

Expand All @@ -173,8 +191,8 @@ private static IEnumerable<IRenderable> GetUsage(CommandModel model, CommandInfo
composer,
};
}

private static IEnumerable<IRenderable> GetExamples(CommandModel model, CommandInfo? command)
private static IEnumerable<IRenderable> GetExamples(CommandModel model, CommandInfo? command, int maxIndirectExamples)
{
var maxExamples = int.MaxValue;

Expand All @@ -183,7 +201,7 @@ private static IEnumerable<IRenderable> GetExamples(CommandModel model, CommandI
{
// Since we're not checking direct examples,
// make sure that we limit the number of examples.
maxExamples = 5;
maxExamples = maxIndirectExamples;

// Get the current root command.
var root = command ?? (ICommandContainer)model;
Expand Down Expand Up @@ -212,7 +230,7 @@ private static IEnumerable<IRenderable> GetExamples(CommandModel model, CommandI
}
}

if (examples.Count > 0)
if (Math.Min(maxExamples, examples.Count) > 0)
{
var composer = new Composer();
composer.LineBreak();
Expand Down Expand Up @@ -371,10 +389,7 @@ static string GetOptionParts(HelpOption option)
return result;
}

private static IEnumerable<IRenderable> GetCommands(
CommandModel model,
ICommandContainer command,
bool isDefaultCommand)
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();
Expand Down
9 changes: 7 additions & 2 deletions src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype)
Description = description.Description;
}
}
}

}

/// <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 List<CommandInfo> Flatten()
{
var result = new Stack<CommandInfo>();
Expand Down
5 changes: 0 additions & 5 deletions test/Spectre.Console.Cli.Tests/Data/Settings/EmptySettings.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public sealed class OptionalArgumentWithPropertyInitializerSettings : CommandSet
[CommandOption("-c")]
public int Count { get; set; } = 1;

[CommandOption("-v")]
[CommandOption("--value")]
public int Value { get; set; } = 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ ARGUMENTS:
[QUX]

OPTIONS:
-h, --help Prints help information
-h, --help Prints help information
-v, --version Prints version information
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
DESCRIPTION:
Contains settings for a cat.

USAGE:
myapp cat [LEGS] [OPTIONS] <COMMAND>

ARGUMENTS:
[LEGS] The number of legs

OPTIONS:
DEFAULT
-h, --help Prints help information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

COMMANDS:
lion <TEETH> The lion command
Loading