-
Notifications
You must be signed in to change notification settings - Fork 385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
More design questions #2046
Comments
I need access to the values of other command-line options in completions and validation, but apparently not in parsing and default values. The main scenario for default values would be something like |
My understanding is that Argument.Name is culture-invariant for binding against names from reflection, and Argument.HelpName is localised for help. But, perhaps it would be better to have a string BindingName property dedicated to binding, not corresponding to a parameter of the constructor. |
Yes please. The current AddGlobalOption method mutates the Option by setting IsGlobal so it is not possible to add the same Option as a global option of one command tree and as a local option in another command. Making IsGlobal public would make this obvious rather than a surprise. |
No, this presupposes that invocation is to continue after the directive. Which is not true for the |
The generic methods are nice to use with type deduction; please keep them. The non-generic methods might be useful for some reflection scenario where the caller does not know the type at compile time. However, if they were removed, then those applications could still call FindResultFor(argument_or_option)?.GetValueOrDefault<object?>() for the same effect. This would not convert the argument but then again the current non-generic methods don't do that either. So I think they are OK to remove. |
Since there was a request for feedback on Twitter, here it is. I am using this library in production and while it's great and I am sure it is very powerful and can do a million things, in 99% of cases you just want to get the damn command line values, no fancy stuff. Maybe I am misunderstanding the library somehow but when I implemented it, what felt like it should be a simple task was not straightforward and that someone forgot about the most common scenario while designing for very complex scenarios. It took quite some doing and a lot of code (that looks intimidating to a newbie) to understand how to arrive at a simple GetParsedValue< T >(paramName) way of using the library. It involved setting up a custom handler (a method that has 20 overloads), then use an "InvocationContext", whatever that is, to get to the actual parameters. If there is an easier way to achieve this way of using the library, it is sure hidden well. Also, just a personal opinion, but the suggested way in the docs to bind parameters with SetHandler< T1, T2, T3.... >, in the beta version, supports just 8 parameters, and what happens when you need more than whatever the arbitrary limit is? Total rewrite! Since a command line invocation is by definition a one-time occurrence, why isn't there a top-level, quick, discoverable way to just get a parsed parameter value by name, like System.CommandLine.Current.GetValue< T >(paramName)? |
Instead of |
This is certainly something people can do, but the default is then unknowable without actually running the command. Experiences like validation, help messages, dry runs, completions, etc. are stronger if this is modeled in the parser layer. |
This was the original intent but this implementation is no longer used in the core library. |
I would tend to say yes but for invocation, use a common approach that could also potentially be applied to certain options that have "command-like" behaviors, such as |
Ordinal and cardinal numbers… |
Many people share this opinion. We're going to remove those overloads and provide a source generator-based approach to simplify users' code. In the meantime, if you choose the |
I think the concern with "arity" isn't that it's incorrect, but that it's not a word most people understand. Is there a simpler term we could use, using more commonly-known words? Something like It's also important not to confuse people with how this relates to the parsed value. It's possible to allow a single occurrence but parse it as an array, e.g. parsing new Argument<int[]>(parse: r => r.Tokens.Single().Value.Split(',').Select(int.Parse).ToArray())
{
Arity = ArgumentArity.ExactlyOne
} It's also possible to allow many occurrences but parse them into a scalar value, e.g. parsing new Argument<int>(parse: r => r.Tokens.Count)
{
Arity = ArgumentArity.OneOrMore
} |
Yes, |
What about something like |
|
If they're not matched, then the token type is likely misleading. |
I have recently created a small cmdline app using System.CommandLine and I must admit it was not really fun to write.
In general, I found the library not very friendly to "just create a small command-line app", where I would expect a simple experience with an easy to discover happy path. I know that this library also handle very complex cases, and that you could basically recreate the git command-line this way, but I was hoping there was a way to still be declarative (no magic) and compact / easy to learn (maybe think asp.net core minimal api?) PS: I suppose there was certainly other ways to make this code more compact or pleasant to read. Feel free to suggest. I have also created an alternative implementation with some extensions here to illustrate what could be done. I also know that these suggestions are certainly posted too late, but anyway, in case it could help. |
#1787 added InvocationContext.GetValue methods, which should be easier to discover. |
They are not too late, we are in a middle of re-design right now. |
ExecutionInvocationContextGetCancellationToken()
LinkTokenThere is no need to link the tokens, #2044 removes this method (and keeps things simple). Once it's removed, the ExitCodeSimilarly to ConsoleWe can't introduce this abstraction right now. BindingContextI know that DI can be very useful, but IMO S.CL should not be allowing its users to register new services in its own container. It's just not the scope of this library and I don't really see a good justification for that. The best we can do is extend configuration with HelpBuilderWe allow the users to customize help in CommandLineBuilder. Once ParseResultThis is the most important thing that has all we need. If we remove the properties mentioned above, We should consider removing the whole type and making ParserExtensionsThis type creates a feeling that Parser parser = new CommandLineBuilder(command).Build();;
parser.InvokeAsync(args);
public static class ParserExtensions
{
public static int Invoke(
this Parser parser,
string[] args,
IConsole? console = null) =>
parser.Parse(args).Invoke(console);
} Once we make it clear that these are two separate things, it's easier to show that parsing is synchronous, while execution can be both sync and async. IMO we should distinguish 3 steps:
CommandLineConfiguration config = new CommandLineConfigurationBuilder()
.EnableDirectives()
.EnablePosixBundling()
.UseVersionOption()
.UseHelp()
.ToConfig();
// I am not sure if we need Parser, we can just use command.Parse.
// It's just easy to discover (I've built a command, now I just use it).
// And it also does not require us to provide Executor for execution.
ParseResult parseResult = rootCommand.Parse(args, config);
int exitCode = await parseResult.InvokeAsync(); // invokes the handler As I wrote creating config should be optional: ParseResult parseResult = rootCommand.Parse(args); // uses default config
int exitCode = await parseResult.InvokeAsync(); HandlerOur user study shows that our users are confused with so many During a design session with @jonsequitur we came to the conclusion that we should most likely have only one Since we do want to support both sync and async execution, we need two. Based on what I wrote above, each handler should accept public class Command
{
public void SetHandler(Func<ParseResult, CancellationToken, Task<int>> asyncHandler);
public void SetHandler(Func<ParseResult, int> syncHandler);
} By doing that we could remove ParseResult.GetValue(alias)Another common feedback is lack of
We definitely want to add it. The only concerns I have is argument name (for arguments it can be only name, for options a name or alias) and duplicate aliases handling. class ParseResult
{
T? GetValue(string nameOrAlias);
} All of that would allow us to turn the following sample: class Program
{
static int Main(string[] args)
{
Option<bool> boolOption = new(new[] { "--flag", "-b" }, "Bool option");
Option<string> stringOption = new(new[] { "--text", "-s" }, "String option");
RootCommand command = new()
{
boolOption,
stringOption
};
command.SetHandler((invocationContext) =>
{
Console.WriteLine($"Bool option: {invocationContext.GetValue(boolOption)}");
Console.WriteLine($"String option: {invocationContext.GetValue(stringOption)}");
invocationContext.ExitCode = 0;
});
Parser parser = new CommandLineBuilder(command).Build();
return parser.Invoke(args);
}
} into: class Program
{
static int Main(string[] args)
{
RootCommand command = new()
{
new Option<bool>(new[] { "--flag", "-b" }, "Bool option"),
new Option<string>(new[] { "--text", "-s" }, "String option")
};
command.SetHandler(parseResult =>
{
Console.WriteLine($"Bool option: {parseResult.GetValue<bool>("--flag")}");
Console.WriteLine($"String option: {parseResult.GetValue<string>("--text")}");
return 0;
});
ParseResult parseResult = command.Parse(args);
return parseResult.Invoke();
}
} If we expose a possibility to set handler in class Program
{
static int Main(string[] args)
=> new Command(Handler)
{
new Option<bool>(new[] { "--flag", "-b" }, "Bool option"),
new Option<string>(new[] { "--text", "-s" }, "String option")
}
.Parse(args)
.Invoke();
static int Handler(ParseResult parseResult)
{
Console.WriteLine($"Bool option: {parseResult.GetValue<bool>("--flag")}");
Console.WriteLine($"String option: {parseResult.GetValue<string>("--text")}");
return 0;
}
} And with the upcoming source-generator based solution: class Program
{
static int Main(bool flag, string text)
{
Console.WriteLine($"Bool option: {flag}");
Console.WriteLine($"String option: {text}");
return 0;
}
} |
One of the things we most need help on is a new name for Arity. What about splitting it into two values. What would their names be?
How bad would it be if someone changed the argument result?
I agree we could simplify some things on Argument creation. But it important to maintain a way to have the default depend on the state of the world at invocation. The common case is
The problem with type specific validators comes from the fact we treat that and DirectoryInfo complex types specially in the core. Other special types are likely to be recognized only in higher layers.
I do think we need a name for commands and options.
TreatUnmatchedTokenAsErrors needs to be command speciifc. For example, the .NET CLI has this false only for selected commands.
I like the suggestion of the instance parse with configuration.
Two points on RootCommand. We have limited user studies so far, but some signal that devs think differently about the root-the entry point. I would like to explore this before changing. The other is that I would like to continue to have helper methods for ExecutablePath and ExecutableName. These may be short, but they are not obvious. We could consider putting this on command and then looking up the tree and reporting the root.
The "Enable..." stuff should be replaced with the new executable approach.
Localization resources: We need some mechanism for users to customize the validation messages. Other messages are cool to lock. If this includes validation messages, lets consider further. The issue is that this would interleave with user's mesages, so maybe allow the message to change, and by implication the localization.
I think we should rethink CommandLineBuilder with the new execution approach and configuration.
Agree that we need to see if people still need middleware. I think we can accomodate this differently
I think we have to postpone a discussion on Help. It is a large discussion. I would like to simplify the core to just a super simple API and move Help into a separate package. There is too much opinion in what we have and I am worried that the changing the display will be considered breaking.
One of our user studies perceived the root as the parser. A thing which contained commands, and commands were things typed after the executable name.
Directives are likely to be caught in the execuation net |
Would it be an error to supply both a sync and an async handler. That might be good but it would take us from requiring both to allowing only one. Might be appropriate, but lets consider whether there are cases where people may have implemented both.
I believe we should keep a name for all symbols (including directives if they become symbols). As this question brings up, we need to be able to find them in code. |
With regard to argument arity/cardinality: Arguments with a This leads to the in my opinion akward situation regarding code readability/expressivity where for example My apologies if this is on the radar of team already and has been mentioned/discussed elsewhere. |
I would prefer
|
You are right. I was assuming Arity was used for argument position. |
It proves that the current name is not intuitive. |
Regarding |
This is a really good point. We've discussed this a bit and it's likely we could make this more consistent. It might require a source generator, depending on whether the needed APIs support trimming. I see the ambiguity here is being pretty similar to the confusion between Ideally the name will make this separation of concepts as clear as possible. @reduckted, I worry |
Include Is the enum type going to have a |
Naming things before drinking enough coffee is hard.
That might make sense. The guidance has been to avoid single-word type names so we focused on those. But I think that would be more consistent. |
@adamsitnik do you mean: remove the I think that's a good idea. |
I am looking at what type I need to use to invoke on, and I'm surprised this is the one. I think CommandLineBuilder builder = new CommandLineBuilder(rootCommand);
CommandLine commandLine = builder.Build();
commandLine.Invoke(args); This @adamsitnik @jonsequitur thoughts? |
CA1724: Type names should not match namespaces, assuming the namespace remains System.CommandLine. |
Ah, that is the reason. I think it would be good to change the names so the primary type to invoke on isn't named |
Users who don't need to configure anything in CommandLineConfiguration can already call CommandExtensions.Invoke(this Command, string, IConsole? = null), or similar for string[]. |
There's also #1905 for moving all methods from CommandExtensions into Command itself. |
Even when the default configuration works for me and I can call This is subjective. |
We are soon going to have another internal round of design discussion for System.CommandLine. I am sharing the list of open questions here so everyone can share their feedback.
Symbols
Symbol
Description
Symbol.Description does not need to be
virtual
: #2045Every symbol can provide the description in the
ctor
.Argument
HelpName
When Argument is parsed, we use its position rather than name. But when we want to refer to it, for example when displaying help, it may need a name.
Currently the type has both
Name
andHelpName
. Why doesArgument
need both?Arity
Can anyone provide a better name?
Parse(string), Parse(string[])
Both methods are used only for testing purposes in System.CommandLine. They don't provide any possibility to customize the configuraiton used for parsing. They can be easily replaced with one liner:
I believe that we should remove them or made them internal.
Validators
Currently a validator is an
Action<ArgumentResult>
. What I don't like about this design is the fact that it allows for modificaitons of theArgumentResult
.Example:
It's hard to propose a better design, as some validators need more than just
SymbolResult.Tokens
. Example: validator may want to callGetValueOrDefault
to validate parsed value.Argument
ctors
Argument<T>
provides multiple ways of setting default value: asT
,Func<T>
andFunc<ArgumentResult, T>
What I don't like is the fact that the last ctor requires the user to define whether given
Func<ArgumentResult, T>
is a default value provider or a custom parser. It's confusing.My understanding is that we need custom parser for complex types and default values for all kinds of types. IMO the latter is way more common than the first.
Do we really need a default value provider that uses
ArgumentResult
as an input?Do we really need the default value to be lazily provided?
How about having just one ctor:
SetDefaultValue, SetDefaultValueFactory
Same as above, there are 3 different ways of setting default value:
I would stick with two:
AcceptLegalFilePathsOnly, AcceptLegalFileNamesOnly
These instance methods are usefull only for argument of
string
,FileInfo
and arrays of these two.Currently they can be used for all kinds of
T
:Does anyone have a better idea how to make sure they don't get suggested/used for
T
other than the ones it makes sense for?We could of course introduce them as extension method of
Argument<string>
,Argument<string[]>
,Argument<FileInfo>
,Argument<FileInfo[]>
, but I doubt it would be accepted by BCL review board.IdentifierSymbol
From my point of view the existence of this type is an implementation detail (it's a Symbol with Aliases).
I need to discuss this case with Jeremy.
Option
ArgumentHelpName
Similar to
Argument.Name
: the users have the possibility to setName
forOption
. Do we really need this property? We use aliases for parsing and it seems like Name is exactly what we need for help.IsGlobal
This property is currently
internal
, but we should expose it.We need a better name. Ideas?
Parse(string), Parse(string[])
Same as
Arugment.Parse
: do we really need these methods to be public?Option
ctors
Same as for
Argument<T>
: we need a better way to distinguish default value provider and custom parser.Command
Command.Children
Command exposes
Argument
,Options
, andSubcommands
properties.It also implements
IEnumerable<Symbol>
andAdd(Symbol)
to support a very common duck typing C# syntax:Do we really need to expose
IEnumerable<Symbol> Children
on top of that? It seems redundant to me.AddGlobalOption
If we made
Option.IsGlobal
public, we can remove this method, so users can do:TreatUnmatchedTokensAsErrors
Does it make sense to make this setting specific to given command? Should it become a part of
CommandLineConfiguration
?Parse
Command has no
Parse
instance methods, but it does have an extension method.IMO we should make it an instance method with optional
CommandLineConfiguration
argument to make it easier to discover the configurability of parsing:RootCommand
Currently it exposes only two public methods:
This information can be easily obtained by System.CommandLine users (by using
System.Environment
class).Currently the only difference with
Command
is that it does not allow for setting custom alias (which should be ultra rare). Could this be achieved without a dedicatedRootCommand
type? I want to reduce the number of concepts the users need to get familiar with. Better perf (fewer types to load and JIT) is just a side effect of that,Parsing
CommandLineConfiguration
This type provides multiple internal properites and some public properties. All of them are readonly. Should we make them mutable to avoid the need of having a builder type?
EnableEnvironmentVariableDirective, EnableSuggestDirective, EnableDirectives
If we expose all the internal properties, are we going to introduce a new property for every new directive?
What are the alternatives other than creating dedicated directive types and adding
Command.Directives
property?EnableTokenReplacement
Why is it enabled by default?
TokenReplacer
Why do we need custom token replacers? What do users customize other than supporting comments in the response files?
Perhaps we should expose an enumeration and provide default implementation?
What I don't like about the current design is that
InvokeAsync
performs parsing which can perform a blocking call when reading the response file:command-line-api/src/System.CommandLine/Parsing/StringExtensions.cs
Line 386 in 1bc03fe
If we just provide an enumeration, it would be easier to do the right thing (call async APIs for async invocation, pass CancellationToken etc).
LocalizationResources
In BCL,
System.*
libraries that need localization provide their own set of resource files, configure automation with https://github.com/dotnet/arcade/blob/main/Documentation/OneLocBuild.md and don't expose a possibility to customize the transaltions. This type needs to be removed / made internal.Please see #2041 for more details.
CommandLineBuilder
The type name is confusing to me. It builds a parser and configuration and I would expect it to be called
CommandLineConfigurationBuilder
. But perhaps we can remove it if we makeCommandLineConfiguration
mutable?It defines almost no public methods, but
CommandLineBuilderExtensions
provides plenty of its extensions.Properties are not always enough
Not every configuration can be expressed with a single property. Some methods allow for additional customization.
For example
UseParseErrorReporting
allows for enabling the parse error reporting and setting a custom exit code for parsing errors:RegisterWithDotnetSuggest
The current design has plenty of flaws. One of them is major perf hit for default config (creating files and starting new process to register).
We most likely need a separate meeting dedicated to dotnet suggest. Who should be invited?
UseDefaults
Before we find a proper solution for
RegisterWithDotnetSuggest
, can we disable it by default?UseExceptionHandler
Curent API:
In my opinion exception handler should always return an int. It would make the config simpler and easier to discover the possibility to set custom exit code.
AddMiddleware
Middleware is VERY powerfull. After #2043 we are close to having 0 middleware by default (it's expensive).
I need to make a research and see why our users provide custom middleware. Just to make sure it's not just a mechanism for workarounding our limitations like missing configuraiton etc.
UseHelp
I've not analysed this part of S.CL yet. I will have comments in the near future.
Parser
First of all I believe that this type is simply hard to discover. The only state it has is
CommandLineConfiguration
. To makeCommandLineConfiguration
more discoverable I would makeParser
static with just one method:We also need to rename it to
CliParser
to avoid conflicts with dozen other parsers ;)ParseResult
Directives
Currently the only way for the users to implement custom directives is enabling directives parsing in the config and using this property to get parsed output.
The problem is that if the users want to perform any custom action based on the directive input like setting custom culture:
[culture pl]
they need to use Middleware (expensive).Should we expose a dedicated
Directive
symbol type?UnmatchedTokens
The
Tokens
property returns a collection ofToken
type.UnmatchedTokens
returns a collection ofstring
s. Why don't we expose full information?GetValue
Do we need both the generic and non-generic overloads?
SymbolResult
SymbolResult and it's entire family LGTM after recent changes.
The text was updated successfully, but these errors were encountered: