command-line-api icon indicating copy to clipboard operation
command-line-api copied to clipboard

Honor user defined order of commands, subcommands and options

Open gabrielmaldi opened this issue 4 years ago • 4 comments

Currently, HelpBuilder sorts help items, which results in the following behavior:

Example 1

new CommandEx(new[] { "migrations", "mig" })
Commands:
  mig, migrations    Commands to manage migrations.

Example 2

new CommandEx(new[] { "health-checks", "hc" })
new CommandEx(new[] { "mini-profiler", "mp" })
mini-profiler, mp                Generates the MiniProfiler script
hc, health-checks <from> <to>    Generates the HealthChecks script [from: '0' (the initial database), to: the last migration]

User defined order should be respected; otherwise, the aliases (hc) can be shown before the main command (health-checks). Also, multiple commands can result in odd ordering: mini-profiler, mp and hc, health-checks (main/alias, alias/main; see Example 2).


For completeness:

public CommandEx(string[] aliases, string description)
    : base(aliases.First(), description)
{
    foreach (var alias in aliases.Skip(1))
    {
        AddAlias(alias);
    }
}

gabrielmaldi avatar Feb 26 '21 19:02 gabrielmaldi

CLA assistant check
All CLA requirements met.

dnfadmin avatar Feb 26 '21 20:02 dnfadmin

The convention in most command line help (at least for options) is for the short-form aliases to precede the long-form aliases, so the current ordering is deliberate.

There's been discussion though about making help easier to customize, in a way that would accommodate the desire to reorder the aliases.

@Keboo @adamhewitt627 ...thoughts?

jonsequitur avatar Feb 26 '21 21:02 jonsequitur

I agree, I think this is another case that needs to be handled with making configuring the help output

Keboo avatar Feb 28 '21 06:02 Keboo

This is how I'm handling it right now:

new CommandLineBuilder(rootCommand)
    .UseHelpBuilder(context => new HelpBuilderEx(context.Console))
    .UseDefaults()
    .Build()
    .Invoke(args);
public class HelpBuilderEx : HelpBuilder
{
    public HelpBuilderEx(
        IConsole console,
        int? columnGutter = null,
        int? indentationSize = null,
        int? maxWidth = null)
        : base(console, columnGutter, indentationSize, maxWidth)
    {
    }

    protected override string DefaultValueHint(IArgument argument, bool isSingleArgument)
    {
        if (argument is IArgumentEx argumentEx && argumentEx.DefaultValueHintFactory != null)
        {
            var surrogate = new Argument(argument.Name);
            surrogate.SetDefaultValueFactory(argumentEx.DefaultValueHintFactory);
            argument = surrogate;
        }

        return base.DefaultValueHint(argument, isSingleArgument);
    }
}

Which of course is less than ideal. I agree that it would be better to make HelpBuilder more configurable instead of just removing the call to ThenBy. In that regard, I would change this method to protected:

private IEnumerable<HelpItem> GetOptionHelpItems(IIdentifierSymbol symbol)
{
    var rawAliases = symbol
                     .Aliases
                     .Select(r => r.SplitPrefix())
                     .OrderBy(r => r.prefix, StringComparer.OrdinalIgnoreCase)
                     .GroupBy(t => t.alias)
                     .Select(t => t.First())
                     .Select(t => $"{t.prefix}{t.alias}");

And extract this bit (rawAliases) into a new protected method so it can be overridden. I would be happy to submit a new PR and close this one.

gabrielmaldi avatar Feb 28 '21 12:02 gabrielmaldi

Is this needed after https://github.com/dotnet/command-line-api/pull/2073?

CliCommand and CliOption now have both public string Name { get; } and public ICollection<string> Aliases { get; }, and help displays Name before Aliases:

https://github.com/dotnet/command-line-api/blob/e26fc94b029143e312d0aafdef2df99b9a5bdc31/src/System.CommandLine/Help/HelpBuilder.Default.cs#L90-L98

Even if you still need to preserve the order of aliases, you cannot do that just by making HelpBuilder sort the aliases, because AliasSet stores them to HashSet<string> _aliases, which already does not preserve order.

KalleOlaviNiemitalo avatar Apr 02 '23 04:04 KalleOlaviNiemitalo