-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Make S.CL builder DSLs more transferable #50685
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
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
da15f66
add new project and move some base-layer stuff over
baronfel 8e3eb2e
Replace marker-interface dynamic completion with runtime-data-based v…
baronfel adf8be4
forwarding is also just a set of extensions
baronfel a02d3a3
Documentation links are now a separate feature as well, provided by e…
baronfel 2437237
Fix minor typo
baronfel 44a7ecb
Minor consolidation and cleanup
baronfel 0fd292f
Refactor based on feedback from @MiYanni
baronfel 54020aa
add missing 'isdynamic' from FrameworkOption
baronfel 824484f
guard some concurrent access on the extensions
baronfel 343a51d
formatting feedback
baronfel 731b36a
Merge branch 'main' into more-scl-patterns
marcpopMSFT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
src/Cli/Microsoft.DotNet.Cli.CommandLine/ArgumentBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| using System.CommandLine; | ||
| using System.CommandLine.Completions; | ||
|
|
||
| namespace Microsoft.DotNet.Cli.CommandLine; | ||
|
|
||
| /// <summary> | ||
| /// Extension methods that make it easier to chain argument configuration methods when building arguments. | ||
| /// </summary> | ||
| public static class ArgumentBuilderExtensions | ||
| { | ||
| extension<T>(Argument<T> argument) | ||
| { | ||
| public Argument<T> AddCompletions(Func<CompletionContext, IEnumerable<CompletionItem>> completionSource) | ||
| { | ||
| argument.CompletionSources.Add(completionSource); | ||
| return argument; | ||
| } | ||
| } | ||
| } |
219 changes: 219 additions & 0 deletions
219
src/Cli/Microsoft.DotNet.Cli.CommandLine/ForwardedOptionExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| using System.CommandLine; | ||
| using System.CommandLine.Parsing; | ||
|
|
||
| namespace Microsoft.DotNet.Cli.CommandLine; | ||
|
|
||
| /// <summary> | ||
| /// Extensions for tracking and invoking forwarding functions on options and arguments. | ||
| /// Forwarding functions are used to translate the parsed value of an option or argument | ||
| /// into a set of zero or more string values that will be passed to an inner command. | ||
| /// </summary> | ||
| public static class ForwardedOptionExtensions | ||
| { | ||
| private static readonly Dictionary<Symbol, Func<ParseResult, IEnumerable<string>>> s_forwardingFunctions = []; | ||
| private static readonly Lock s_lock = new(); | ||
|
|
||
| extension(Option option) | ||
| { | ||
| /// <summary> | ||
| /// If this option has a forwarding function, this property will return it; otherwise, it will be null. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This getter is on the untyped Option because much of the _processing_ of option forwarding | ||
| /// is done at the ParseResult level, where we don't have the generic type parameter. | ||
| /// </remarks> | ||
| public Func<ParseResult, IEnumerable<string>>? ForwardingFunction => s_forwardingFunctions.GetValueOrDefault(option); | ||
| } | ||
|
|
||
| extension<TValue>(Option<TValue> option) | ||
| { | ||
| /// <summary> | ||
| /// Internal-only helper function that ensures the provided forwarding function is only called | ||
| /// if the option actually has a value. | ||
| /// </summary> | ||
| private Func<ParseResult, IEnumerable<string>> GetForwardingFunction(Func<TValue, IEnumerable<string>> func) | ||
| { | ||
| return (ParseResult parseResult) => | ||
| { | ||
| if (parseResult.GetResult(option) is OptionResult r) | ||
| { | ||
| if (r.GetValueOrDefault<TValue>() is TValue value) | ||
| { | ||
| return func(value); | ||
| } | ||
| else | ||
| { | ||
| return []; | ||
| } | ||
| } | ||
| return []; | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Internal-only helper function that ensures the provided forwarding function is only called | ||
| /// if the option actually has a value. | ||
| /// </summary> | ||
| private Func<ParseResult, IEnumerable<string>> GetForwardingFunction(Func<TValue, ParseResult, IEnumerable<string>> func) | ||
| { | ||
| return (ParseResult parseResult) => | ||
| { | ||
| if (parseResult.GetResult(option) is OptionResult r) | ||
| { | ||
| if (r.GetValueOrDefault<TValue>() is TValue value) | ||
| { | ||
| return func(value, parseResult); | ||
| } | ||
| else | ||
| { | ||
| return []; | ||
| } | ||
| } | ||
| return []; | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Forwards the option using the provided function to convert the option's value to zero or more string values. | ||
| /// The function will only be called if the option has a value. | ||
| /// </summary> | ||
| public Option<TValue> SetForwardingFunction(Func<TValue?, IEnumerable<string>> func) | ||
| { | ||
| lock (s_lock) | ||
| { | ||
| s_forwardingFunctions[option] = option.GetForwardingFunction(func); | ||
| } | ||
| return option; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Forward the option using the provided function to convert the option's value to a single string value. | ||
| /// The function will only be called if the option has a value. | ||
| /// </summary> | ||
| public Option<TValue> SetForwardingFunction(Func<TValue, string> format) | ||
| { | ||
| lock (s_lock) | ||
| { | ||
| s_forwardingFunctions[option] = option.GetForwardingFunction(o => [format(o)]); | ||
| } | ||
| return option; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Forward the option using the provided function to convert the option's value to a single string value. | ||
| /// The function will only be called if the option has a value. | ||
| /// </summary> | ||
| public Option<TValue> SetForwardingFunction(Func<TValue?, ParseResult, IEnumerable<string>> func) | ||
| { | ||
| lock (s_lock) | ||
| { | ||
| s_forwardingFunctions[option] = option.GetForwardingFunction(func); | ||
| } | ||
| return option; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Forward the option as multiple calculated string values from whatever the option's value is. | ||
| /// </summary> | ||
| /// <param name="format"></param> | ||
| /// <returns></returns> | ||
| public Option<TValue> ForwardAsMany(Func<TValue?, IEnumerable<string>> format) => option.SetForwardingFunction(format); | ||
|
|
||
| /// <summary> | ||
| /// Forward the option as its own name. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public Option<TValue> Forward() => option.SetForwardingFunction((TValue? o) => [option.Name]); | ||
|
|
||
| /// <summary> | ||
| /// Forward the option as a string value. This value will be forwarded as long as the option has a OptionResult - which means that | ||
| /// any implicit value calculation will cause the string value to be forwarded. | ||
| /// </summary> | ||
| public Option<TValue> ForwardAs(string value) => option.SetForwardingFunction((TValue? o) => [value]); | ||
|
|
||
| /// <summary> | ||
| /// Forward the option as a singular calculated string value. | ||
| /// </summary> | ||
| public Option<TValue> ForwardAsSingle(Func<TValue, string> format) => option.SetForwardingFunction(format); | ||
| } | ||
|
|
||
| extension(Option<bool> option) | ||
| { | ||
| /// <summary> | ||
| /// Forward the boolean option as a string value. This value will be forwarded as long as the option has a OptionResult - which means that | ||
| /// any implicit value calculation will cause the string value to be forwarded. For boolean options specifically, if the option is zero arity | ||
| /// and has no default value factory, S.CL will synthesize a true or false value based on whether the option was provided or not, so we need to | ||
| /// add an additional implicit 'value is true' check to prevent accidentally forwarding the option for flags that are absent.. | ||
| /// </summary> | ||
| public Option<bool> ForwardIfEnabled(string value) => option.SetForwardingFunction((bool o) => o ? [value] : []); | ||
| /// <summary> | ||
| /// Forward the boolean option as a string value. This value will be forwarded as long as the option has a OptionResult - which means that | ||
| /// any implicit value calculation will cause the string value to be forwarded. For boolean options specifically, if the option is zero arity | ||
| /// and has no default value factory, S.CL will synthesize a true or false value based on whether the option was provided or not, so we need to | ||
| /// add an additional implicit 'value is true' check to prevent accidentally forwarding the option for flags that are absent.. | ||
| /// </summary> | ||
| public Option<bool> ForwardIfEnabled(string[] value) => option.SetForwardingFunction((bool o) => o ? value : []); | ||
|
|
||
| /// <summary> | ||
| /// Forward the boolean option as a string value. This value will be forwarded as long as the option has a OptionResult - which means that | ||
| /// any implicit value calculation will cause the string value to be forwarded. For boolean options specifically, if the option is zero arity | ||
| /// and has no default value factory, S.CL will synthesize a true or false value based on whether the option was provided or not, so we need to | ||
| /// add an additional implicit 'value is true' check to prevent accidentally forwarding the option for flags that are absent.. | ||
| /// </summary> | ||
| public Option<bool> ForwardAs(string value) => option.ForwardIfEnabled(value); | ||
| } | ||
|
|
||
| extension(Option<IEnumerable<string>> option) | ||
| { | ||
| /// <summary> | ||
| /// Foreach argument in the option's value, yield the <paramref name="alias"/> followed by the argument. | ||
| /// </summary> | ||
| public Option<IEnumerable<string>> ForwardAsManyArgumentsEachPrefixedByOption(string alias) => | ||
| option.ForwardAsMany(o => ForwardedArguments(alias, o)); | ||
| } | ||
|
|
||
| extension(ParseResult parseResult) | ||
| { | ||
| /// <summary> | ||
| /// Calls the forwarding functions for all options that have declared a forwarding function (via <see cref="ForwardedOptionExtensions"/>'s extension members) in the provided <see cref="ParseResult"/>. | ||
| /// </summary> | ||
| /// <param name="parseResult"></param> | ||
| /// <param name="command">If not provided, uses the <see cref="ParseResult.CommandResult" />'s <see cref="CommandResult.Command"/>.</param> | ||
| /// <returns></returns> | ||
| public IEnumerable<string> OptionValuesToBeForwarded(Command? command = null) => | ||
| (command ?? parseResult.CommandResult.Command) | ||
| .Options | ||
| .Select(o => o.ForwardingFunction) | ||
| .SelectMany(f => f is not null ? f(parseResult) : []); | ||
|
|
||
| /// <summary> | ||
| /// Tries to find the first option named <paramref name="alias"/> in <paramref name="command"/>, and if found, | ||
| /// invokes its forwarding function (if any) and returns the result. If no option with that name is found, or if the option | ||
| /// has no forwarding function, returns an empty enumeration. | ||
| /// </summary> | ||
| /// <param name="command"></param> | ||
| /// <param name="alias"></param> | ||
| /// <returns></returns> | ||
| public IEnumerable<string> ForwardedOptionValues(Command command, string alias) | ||
| { | ||
| var func = command.Options? | ||
| .Where(o => | ||
| (o.Name.Equals(alias) || o.Aliases.Contains(alias)) | ||
| && o.ForwardingFunction is not null) | ||
| .FirstOrDefault()?.ForwardingFunction; | ||
| return func?.Invoke(parseResult) ?? []; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// For each argument in <paramref name="arguments"/>, yield the <paramref name="alias"/> followed by the argument. | ||
| /// </summary> | ||
| private static IEnumerable<string> ForwardedArguments(string alias, IEnumerable<string>? arguments) | ||
| { | ||
| foreach (string arg in arguments ?? []) | ||
| { | ||
| yield return alias; | ||
| yield return arg; | ||
| } | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
src/Cli/Microsoft.DotNet.Cli.CommandLine/Microsoft.DotNet.Cli.CommandLine.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>$(SdkTargetFramework)</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <StrongNameKeyId>MicrosoftAspNetCore</StrongNameKeyId> | ||
| <SignAssembly>true</SignAssembly> | ||
| <PublicSign Condition=" '$([MSBuild]::IsOSPlatform(`Windows`))' == 'false' ">true</PublicSign> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="System.CommandLine" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="../../System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj" /> | ||
| </ItemGroup> | ||
| </Project> |
46 changes: 46 additions & 0 deletions
46
src/Cli/Microsoft.DotNet.Cli.CommandLine/OptionBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| using System.CommandLine; | ||
| using System.CommandLine.Completions; | ||
|
|
||
| namespace Microsoft.DotNet.Cli.CommandLine; | ||
|
|
||
| /// <summary> | ||
| /// Extension methods that make it easier to chain option configuration methods when building options. | ||
| /// </summary> | ||
| public static class OptionBuilderExtensions | ||
| { | ||
| extension<T>(T option) where T : Option | ||
| { | ||
| /// <summary> | ||
| /// Forces an option that represents a collection-type to only allow a single | ||
| /// argument per instance of the option. This means that you'd have to | ||
| /// use the option multiple times to pass multiple values. | ||
| /// This prevents ambiguity in parsing when argument tokens may appear after the option. | ||
| /// </summary> | ||
| /// <typeparam name="T"></typeparam> | ||
| /// <param name="option"></param> | ||
| /// <returns></returns> | ||
| public T AllowSingleArgPerToken() | ||
| { | ||
| option.AllowMultipleArgumentsPerToken = false; | ||
| return option; | ||
| } | ||
|
|
||
|
|
||
| public T AggregateRepeatedTokens() | ||
| { | ||
| option.AllowMultipleArgumentsPerToken = true; | ||
| return option; | ||
| } | ||
|
|
||
| public T Hide() | ||
| { | ||
| option.Hidden = true; | ||
| return option; | ||
| } | ||
| public T AddCompletions(Func<CompletionContext, IEnumerable<CompletionItem>> completionSource) | ||
| { | ||
| option.CompletionSources.Add(completionSource); | ||
| return option; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Microsoft.Dotnet.Cli.CommandLine | ||
|
|
||
| This project contains extensions and utilities for building command line applications. | ||
|
|
||
| These extensions are layered on top of core System.CommandLine concepts and types, and | ||
| do not directly reference concepts that are specific to the `dotnet` CLI. We hope that | ||
| these would be published separately as a NuGet package for use by other command line | ||
| applications in the future. | ||
|
|
||
| From a layering perspective, everything that is specific to the `dotnet` CLI should | ||
| be in the `src/Cli/dotnet` or `src/Cli/Microsoft.DotNet.Cli.Utils` projects, which | ||
| reference this project. Keep this one generally-speaking clean. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.