ConsoleAppFramework
ConsoleAppFramework copied to clipboard
[Question] Handling global options in CLI apps with v5 framework
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!
That's a good suggestion, I'd like to consider adding it.
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.
@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.
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.
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).
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.
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.
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
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);
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...
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).
v5.7.0 support it. thank you for advices.