spectre.console icon indicating copy to clipboard operation
spectre.console copied to clipboard

Detecting whether help was provided via command line arguments to Spectre.Console

Open tapika opened this issue 3 years ago • 1 comments

If you code simple application Spectre.Console - then you're interested not only on parsing command line arguments, but also you're interested in figuring out whether --help was used as command line argument.

Using .net reflection it's possible:

https://github.com/tapika/swupd/blob/master/cakebuild/CommandLineArgs.cs#L248

but uses bit ugly syntax for that purpose.

Is there a simpler solution to this ?

tapika avatar Sep 15 '22 18:09 tapika

Apparently help command is built-in, and quite well hidden.

// 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));
    return leaf.ShowHelp ? 0 : 1;
}

Can't imagine calling this code as user from outside of Spectre.Console.

In theory you can also override default console output, like this:

TestConsole console = new TestConsole();
var actualConsole = AnsiConsole.Console;
AnsiConsole.Console = console;
AnsiConsole.Record();

var app = new CommandApp<ParseCommandLineArgs>();

...
int exitCode = app.Run(args);
actualConsole.Write(AnsiConsole.ExportText());

and then display help at the end. (text will be without colors)

Sounds bit complex solution as well.

tapika avatar Sep 15 '22 19:09 tapika

The CommandExecutor.Execute(...) method (which the first code snippet above from @tapika was taken from) does explicitly handle the -v|--version argument:

        // 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;
                }
            }
        }

I do wonder if this kind of explicit handling of command arguments could also be used for the -h|--help help option.

FrankRay78 avatar Sep 26 '22 19:09 FrankRay78

When this issue is triaged/is found to be valid - please assign it to myself and I'm happy to fix.

FrankRay78 avatar Sep 30 '22 12:09 FrankRay78

I would prefer that no need to assume order of arguments - whether help comes first or not.

Previously I have help parameter handling as hardcoded, and it was parsed like this:

var helpOpt = new CommandOptionAttribute("-h|--help");
var isMatch = helpOpt.GetType().GetMethod("IsMatch", BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var arg in args)
{
    if ((bool)isMatch.Invoke(helpOpt, new object[] { arg.TrimStart('-') }))
    { 
            displaysHelp = true;
    }
}

But this also implies that help command is the same as "-h|--help" - I can easily think that in future such variations as -?|--? could be added to be more flexible. Help option needs to be centralized and preferably within Spectre.Console.

Could this be coded somehow like this:

var (exitCode, helpWasShown) = app.Run(args);
if(helpWasShown) { Environment.Exit(exitCode); }
... continue with processing command line options...

?

tapika avatar Oct 01 '22 07:10 tapika

Would you propose to also refactor the code snippet above that handles the -v|--version argument into the same approach used for -h|--help, if for no other reason than consistency ?

FrankRay78 avatar Oct 01 '22 09:10 FrankRay78

Generally API's and functionalities they provide:

API1: CommandOptionAttribute :
1. parse command line argument

- bad: not public api

API2: app.Run() :
1. parse command line arguments
2. print help if help requested

- bad: combines two api's, not possible to separate
- bad: does inform end-user that help printing occurred

For API1 - consistency, and simplity to use.

Maybe it would make sense to also refactor API2 to have parse and print as a separate operations, but I think returning two return parameters would be beneficial already.

tapika avatar Oct 01 '22 18:10 tapika