ConsoleAppFramework icon indicating copy to clipboard operation
ConsoleAppFramework copied to clipboard

[Question] Handling global options in CLI apps with v5 framework

Open rozumak opened this issue 1 year ago • 1 comments

It's common for CLI apps to have global/common options that apply to most commands. These options are usually placed before the command text.

For example, in Entity Framework:

dotnet ef [options] [command]

Global options:
  --no-color       Don't colorize output.
  --prefix-output  Prefix output with level.

What is your recommendation for handling these cases using your v5 framework? Is there any support or plans to support this?

Thanks for the great framework!

rozumak avatar Sep 02 '24 12:09 rozumak

That's a good suggestion, I'd like to consider adding it.

neuecc avatar Sep 09 '24 10:09 neuecc

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 30 days.

github-actions[bot] avatar Mar 09 '25 00:03 github-actions[bot]

@neuecc Looks like this issue has been raised several times but auto-closed due to inactivity. Any chance you could look at porting @riddlemd 's PR to the latest version? #100

I realise that implementation may not be sufficient or applicable in the latest version. I would ask, if there's a global parameter --quite , for example, do I access that through a property base.Quite, or do I need a bool quiet parameter on all my commands? Thinking about something like --nologo it would be cool to have something that runs before all commands.

public abstract class BaseCommand
{
    /// <param name="nologo">Hide the logo</param>
    public void Before(bool nologo) // somehow register this to run before ALL commands
    {
        if (!nologo) PrintLogo();
    }
}

Thanks.

jamesfoster avatar Apr 07 '25 15:04 jamesfoster

Following:
I'm looking at this as replacement for deprecation announced System.CommandLine.NamingConventionBinder, was looking to see how easy it would be to switch, and it seems that a couple things will be difficult, 1) no deterministic way to define an option as optional and have a default value (nullable is not an option), and 2) no obvious way to get access to global options in sub-commands.

ptr727 avatar Jul 13 '25 21:07 ptr727

I came up with an idea where only global options are parsed immediately, which would allow us to:

  • Extract values in a typed manner
  • Use them in DI setup

What do you think?

var app = ConsoleApp.Create();

// parse immediately
var verbose = app.AddOptionalGlobalOptions<bool>(ref args, "-v|--verbose");
var noColor = app.AddOptionalGlobalOptions<bool>(ref args, "--no-color", "Don't colorize output.");
var prefixOutput = app.AddRequiredGlobalOptions<string>(ref args, "--prefix-output", "Prefix output with level.");
var dryRun = app.AddOptionalGlobalOptions<bool>(ref args, "--dry-run");

app.ConfigureServices(x =>
{
    // to use command body
    x.AddSingleton<GlobalOptions>(new GlobalOptions(verbose, noColor, prefixOutput, dryRun));

    // variable for setup other DI
    x.AddLogging(l =>
    {
        var console = l.AddSimpleConsole();
        if (verbose)
        {
            console.SetMinimumLevel(LogLevel.Trace);
        }
    });
});

app.Run(args);

record GlobalOptions(bool Verbose, bool NoColor, string PrefixOutput, bool DryRun);

We also need some idea for how to handle display in help (whether to separate them as Global options, or display them attached to command options).

neuecc avatar Oct 23 '25 06:10 neuecc

It looks definitely helpful and nice that it gives full control over what to do with these global options. Will it parse them in any part of the string, or only when the global options are specified before the command? (It would be better not to enforce strict ordering on where the global option is placed in a cli string)

As for help, I would prefer showing them in the [options] section of a command, after the command-specific options. If you run -h on a subcommand, it should include these global options in the options list as well, not in a separate global options section, since from a feature perspective users don’t care whether it’s global or not, as it works the same way. This applies if we can place a global option in any part of the CLI.

Usage: [command] [options...] [-h|--help] [--version]

Root command test.

Options:
  -m|--msg <string>    Message to show. (Required)
  -v|--verbose
  --no-color           Don't colorize output.
  --prefix-output      Prefix output with level.
  --dry-run

Commands:
  echo    Display message.
  sum     Sum parameters.

rozumak avatar Oct 23 '25 11:10 rozumak

When AddGlobalOptions is called, it immediately performs parsing (regardless of order, searching through all of args) and returns the removed items in ref args. Therefore, using GlobalOptions will decrease performance. However, in practical terms, it's not significant enough to worry about, so I think this degradation is acceptable.

Yes, I agree. I checked the specifications in System.CommandLine as well, and it seems appropriate to display them mixed in with the command's options.

neuecc avatar Oct 24 '25 01:10 neuecc

To continue discussion from #198, as I've mentioned there, for me this option will work fine. I had some concerns about the design, but of course it is up to you.

Btw, I think #198 can be closed if we continue discussion here

denisbredikhin avatar Oct 24 '25 13:10 denisbredikhin

When AddGlobalOptions is called, it immediately performs parsing (regardless of order, searching through all of args) and returns the removed items in ref args. Therefore, using GlobalOptions will decrease performance. However, in practical terms, it's not significant enough to worry about, so I think this degradation is acceptable.

Yes, I agree. I checked the specifications in System.CommandLine as well, and it seems appropriate to display them mixed in with the command's options.

Why not group them into a single ConfigureGlobalOptions method where you can get all of the defined options and process them together more effiencetly. Something like:

var app = ConsoleApp.Create()
     .ConfigureGlobalOptions(args, x =>
      {
            x.Add<bool>("--no-color", "Don't colorize output.");
            x.Add<string>("--prefix-output", "Prefix output with level.");
            x.Add<bool>( "--dry-run");
       }) // option 1: individual add statements and process to a dictionary or something?
     .ConfigureGlobalOptions<GlobalOptions>(args) // option 2: use an object for definitions and bind directly
     .ConfigureLogging(opts, loggingBuilder =>
     {
          if (opts.Verbose)
          {
              loggingBuilder.SetMinimumLevel(LogLevel.Trace);
          }
      }
     .ConfigureServices(opts, services =>
     {
           services.AddSomeService(opts.Options["no-color"]
     });
app.Run(args);

record GlobalOptions(bool Verbose, bool NoColor, string PrefixOutput, bool DryRun);

MatthewMi11er avatar Oct 26 '25 19:10 MatthewMi11er

If we parse immediately before Run, exceptions from parse failures will propagate through, resulting in behavior that differs from user expectations. So I think we should stop parsing immediately.

I think option 1 approach (ConfigureGlobalOptions) looks promising, but GlobalOptions would become a DI-only feature. Well, I guess that can't be helped...

neuecc avatar Oct 27 '25 04:10 neuecc

Proposal updated.

var app = ConsoleApp.Create();

// Func<GlobalOptionsBuilder, object>
app.ConfigureGlobalOptions(builder =>
{
    var verbose = builder.AddOptionalGlobalOptions<bool>("-v|--verbose");
    var noColor = builder.AddOptionalGlobalOptions<bool>("--no-color", "Don't colorize output.");
    var prefixOutput = builder.AddRequiredGlobalOptions<string>("--prefix-output", "Prefix output with level.");
    var dryRun = builder.AddOptionalGlobalOptions<bool>("--dry-run");

    return new GlobalOptions(verbose, noColor, prefixOutput, dryRun);
});

// ConfigureServices/Logging receives ConsoleAppContext
app.ConfigureServices((context, x) =>
{
    var globalOptions = (GlobalOptions)context.GlobalOptions;

    // typed DI
    x.AddSingleton<GlobalOptions>(globalOptions);

    // variable for setup other DI
    x.AddLogging(l =>
    {
        var console = l.AddSimpleConsole();
        if (globalOptions.Verbose)
        {
            console.SetMinimumLevel(LogLevel.Trace);
        }
    });
});

app.Run(args);

record GlobalOptions(bool Verbose, bool NoColor, string PrefixOutput, bool DryRun);

Add an object GlobalOptions property to ConsoleAppContext. Also add overloads for ConfigureServices/ConfigureLogging that receive ConsoleAppContext.

Since ConsoleAppContext can already be received in methods and filters, GlobalOptions can be used in all places (even when not using DI).

neuecc avatar Oct 27 '25 12:10 neuecc

v5.7.0 support it. thank you for advices.

neuecc avatar Nov 06 '25 09:11 neuecc