dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.38k stars 381 forks source link

[Proposal] ValueSubsystem - default values #2357

Open KathleenDollard opened 6 months ago

KathleenDollard commented 6 months ago

Edit: Updated to TryGetValue and other typos

Default values could be part of the core parser layer or part of extensibility as a subsystem. This design proposes making default values a subsystem. This offers more control to subsystem authors so that they can offer more control to CLI authors.

One compelling case is #1191's case for environment variables defaults that are not shown in help - the name of the environment variable, but not the current value is shown.

That issue also discusses environment variables, at least sometimes and maybe always, being able to fulfill the "required" aspect of validation.

The requirement that help display default values will also test our design for subsystems exchanging information :-)

Default values should be able to contribute to help.

Approaches to default values

Defaults may be explicit values or calculated. Each is valuable in different scenarios. There are additional considerations in some calculated scenarios - such as #1191 's discussion of environment variable handling.

In order to provide maximum flexibility and the potential for an evolving API, default values are a subsystem.

The core parser will provide the default for the type as would be returned by default.

Drawbacks

While this approach has benefits, it also means default values will not be available in the (new) simple parsing that uses the core parser directly.

Background: As discussed in #2338, there are two modes of using System.CommandLine (Powderhouse). It can be used as a full-featured parser with a pipeline of subsystems, or a very simple parser with any subsystems explicitly called. Placing default values into a subsystem means they are only available when that subsystem is called, from a pipeline or explicitly.

Discussing this with someone that expects to use simple parsing, they anticipated they would use nullables to indicate missing values and null coalescing to add defaults.

Placing defaults into the pipeline is consistent with simple parsing being raw access to the Posix parsing portions only.

Details for base subsystem

A base DefaultValueSubsystem will ship with subsystems. This describes it's design. Any evolved default value systems should be derived from it.

Initialize

No action.

GetIsActivated

Always returns true.

Execute

Sets values in the value results (options and arguments).

This means the values in results are changeable outside the parser. The raw converted value may also be retained.

Teardown

No action.

Data

The base DefaultValueSubsystem allows setting of the default value in several ways, such as:

The data stored for each type of default is given below.

Order of evaluation

The DefaultValueSubsystem.Execute method would be the first to execute after parsing and after terminating informational subsystems like help and version. It runs before validation.

The order of individual symbol result evaluation should be considered indeterminate and any dependencies handled via dependent delegates.

Explicit value

This would just be the value held as an object. The DefaultValueSubsystem would include:

void SetExplicit(CliSymbol symbol, object value)
  => SetAnnotation(symbol, DefaultValueAnnotations.Explicit, value);
object GetExplicit(CliSymbol symbol) 
  => TryGetAnnotation<object>(symbol, DefaultValueAnnotations.Explicit, out var value)
            ? value
            : "";
AnnotationAccessor<object> Explicit
  => new (this, DefaultValueAnnotations.Explicit);

to allow setting as:

defaultValueSubsystem.SetExplicit(mySymbol, value);
// or 
mySymbol.With(defaultValue.Explicit, value);

Calculated default

This provides a factory method for calculating the default. The DefaultValueSubsystem would include:

void SetCalculated(CliSymbol symbol, Func<ValueResult, T> factory)
  => SetAnnotation(symbol, DefaultValueAnnotations.Calculated, factory);
Func<ValueResult, T> GetCalculated(CliSymbol symbol) 
  => TryGetAnnotation<Func<ValueResult, T>>(symbol, DefaultValueAnnotations.Calculated, out var value)
            ? value
            :  null;
AnnotationAccessor<Func<ValueResult, T>> Calculated
  => new (this, DefaultValueAnnotations.Calculated);

to allow setting as:

defaultValueSubsystem.SetCalculated(mySymbol, myFunc);
// or 
mySymbol.With(defaultValue.Calculated, myFunc);

Enviroment variable (as a string name) (new, may not be at GA)

This provides an environment variable name, whose value would be the default. The DefaultValueSubsystem would include:

void SetEnviroment(CliSymbol symbol, string name)
  => SetAnnotation(symbol, DefaultValueAnnotations.Enviroment, name);
string GetEnviroment(CliSymbol symbol) 
  => TryGetAnnotation<string>(symbol, DefaultValueAnnotations.Enviroment, out var value)
            ? value
            :  null;
AnnotationAccessor<string> Enviroment
  => new (this, DefaultValueAnnotations.Enviroment);

to allow setting as:

defaultValueSubsystem.SetEnviroment(mySymbol, name);
// or 
mySymbol.With(defaultValue.Enviroment, name);

Dependent defaults

An example of a dependent default is if one option/argument should be relative to another value (default or user entered). For example, a default --end-date might be 5 days after the --start-date. To correctly calculate defaults, the start date's default must be calcualted before the end date.

One of the things custom parsing was used for was dependent delegates. We think this is sufficiently common to provide a direct solution.

Dependent defaults could be modeled as:

The DefaultValueSubsystem would include:

void SetDependentDefault(CliSymbol symbol, DependentDefault dependent)
  => SetAnnotation(symbol, DefaultValueAnnotations.DependentDefault, name);
DependentDefault GetDependentDefault(CliSymbol symbol) 
  => TryGetAnnotation<DependentDefault>(symbol, DefaultValueAnnotations.DependentDefault, out var value)
            ? value
            :  null;
AnnotationAccessor<DependentDefault> DependentDefault
  => new (this, DefaultValueAnnotations.DependentDefault);

to allow setting as:

defaultValueSubsystem.SetDependentDefault(mySymbol, new DependentDefault(...));
// or 
mySymbol.With(defaultValue.DependentDefault, new DependentDefault(...));

Help text

The DefaultValueSubsystem would provide default help text as "defaults to " (localized):

The user may supply explicit help text, which uses the same pattern as the different default patterns, and set via:

defaultValueSubsystem.SetHelpText(mySymbol, text);
// or 
mySymbol.With(defaultValue.HelpText, text);

Explicit help text would be used similar to $"defaults to {text}" (localized)

Future default kinds

Future default kinds might be exciting, because the common cases could be explicit. One common case is adding a value to a set value or dependent value. For example, adding or subtracting a date from the current date or a date in another result.

This may result in generalizing the pattern used in DependentDefault to use a base class which handles HelpText, such as:

"defaults to today + 5" (localized) "defaults to --start-date + 5" (localized)

This would shift the localization burden for the most common cases from the CLI author to the subsystem, which might be us who has the infrastructure.

KathleenDollard commented 6 months ago

Thought on precedence:

I am still not sure what the precedence should be between calculated values, dependent values, and explicit values. However, since environment variables are closer to the user, they should clearly win and it is a clear use case to have both an environment variable name and one of the other approaches.

If we allowed calculated and dependent values to be conditional, then they should win over explicit values.

Otherwise, it feels like a coin flip on this precedence.