Skip to content

Commit adbd9f1

Browse files
committed
Added support for command handlers or command as delegates without the async keyword
- Updated to latest daily build 2.0.0-beta4.24201.1 of System.CommandLine. - Added support for command handlers or command as delegates without the async keyword (which returns Task or Task<int>).
1 parent cbd2e43 commit adbd9f1

File tree

225 files changed

+1005
-246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

225 files changed

+1005
-246
lines changed

docs/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,79 @@ The class that `CliCommand` attribute is applied to,
376376
- will be a root command if the class is not a nested class and `Parent`property is not set.
377377
- will be a sub command if the class is a nested class or `Parent` property is set.
378378

379+
#### Accessing parent commands
380+
381+
Sub-commands can get a reference to the parent command by adding a property of the parent command type.
382+
Alternatively `ParseResult.Bind<TDefinition>` method can be called to manually get reference to a parent command.
383+
Note that binding will be done only once per definition class, so calling this method consecutively
384+
for the same definition class will return the cached result.
385+
386+
```c#
387+
// Sub-commands can get a reference to the parent command by adding a property of the parent command type.
388+
389+
[CliCommand(Description = "A root cli command with children that can access parent commands")]
390+
public class ParentCommandAccessorCliCommand
391+
{
392+
[CliOption(
393+
Description = "This is a global option (Recursive option on the root command), it can appear anywhere on the command line",
394+
Recursive = true)]
395+
public string GlobalOption1 { get; set; } = "DefaultForGlobalOption1";
396+
397+
[CliArgument(Description = "Description for RootArgument1")]
398+
public string RootArgument1 { get; set; }
399+
400+
public void Run(CliContext context)
401+
{
402+
context.ShowValues();
403+
}
404+
405+
[CliCommand(Description = "A nested level 1 sub-command which accesses the root command")]
406+
public class Level1SubCliCommand
407+
{
408+
[CliOption(
409+
Description = "This is global for all sub commands (it can appear anywhere after the level-1 verb)",
410+
Recursive = true)]
411+
public string Level1RecursiveOption1 { get; set; } = "DefaultForLevel1RecusiveOption1";
412+
413+
[CliArgument(Description = "Description for Argument1")]
414+
public string Argument1 { get; set; }
415+
416+
// The parent command gets automatically injected
417+
public ParentCommandAccessorCliCommand RootCommand { get; set; }
418+
419+
public void Run(CliContext context)
420+
{
421+
context.ShowValues();
422+
}
423+
424+
[CliCommand(Description = "A nested level 2 sub-command which accesses its parent commands")]
425+
public class Level2SubCliCommand
426+
{
427+
[CliOption(Description = "Description for Option1")]
428+
public string Option1 { get; set; } = "DefaultForOption1";
429+
430+
[CliArgument(Description = "Description for Argument1")]
431+
public string Argument1 { get; set; }
432+
433+
// All ancestor commands gets injected
434+
public ParentCommandAccessorCliCommand RootCommand { get; set; }
435+
public Level1SubCliCommand ParentCommand { get; set; }
436+
437+
public void Run(CliContext context)
438+
{
439+
context.ShowValues();
440+
441+
Console.WriteLine();
442+
Console.WriteLine(@$"Level1RecursiveOption1 = {ParentCommand.Level1RecursiveOption1}");
443+
Console.WriteLine(@$"parent Argument1 = {ParentCommand.Argument1}");
444+
Console.WriteLine(@$"GlobalOption1 = {RootCommand.GlobalOption1}");
445+
Console.WriteLine(@$"RootArgument1 = {RootCommand.RootArgument1}");
446+
}
447+
}
448+
}
449+
}
450+
```
451+
379452
### Command Inheritance
380453

381454
When you have repeating/common options and arguments for your commands, you can define them once in a base class and then

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<!-- https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#generateassemblyinfo -->
5-
<VersionPrefix>1.8.6</VersionPrefix>
5+
<VersionPrefix>1.8.7</VersionPrefix>
66
<Product>DotMake Command-Line</Product>
77
<Company>DotMake</Company>
88
<!-- Copyright is also used for NuGet metadata -->

src/DotMake.CommandLine.SourceGeneration/CliCommandAsDelegateInfo.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,18 @@ namespace DotMake.CommandLine.SourceGeneration
1010
{
1111
public class CliCommandAsDelegateInfo : CliSymbolInfo, IEquatable<CliCommandAsDelegateInfo>
1212
{
13-
private const string TaskFullName = "System.Threading.Tasks.Task";
14-
private const string TaskIntFullName = "System.Threading.Tasks.Task<System.Int32>";
1513
public static readonly string CliCommandAsDelegateFullName = "DotMake.CommandLine.CliCommandAsDelegate";
1614

1715
public CliCommandAsDelegateInfo(ISymbol symbol, SyntaxNode syntaxNode, SemanticModel semanticModel)
1816
: base(symbol, syntaxNode, semanticModel)
1917
{
2018
Symbol = (IMethodSymbol)symbol;
2119

22-
if (Symbol.IsAsync || Symbol.ReturnType.ToCompareString() is TaskFullName or TaskIntFullName)
20+
if (Symbol.IsAsync || Symbol.ReturnType.IsTask() || Symbol.ReturnType.IsTaskInt())
2321
{
2422
IsAsync = true;
25-
ReturnsVoid = (Symbol.ReturnType.ToCompareString() == TaskFullName);
26-
ReturnsValue = (Symbol.ReturnType is INamedTypeSymbol namedTypeSymbol)
27-
&& namedTypeSymbol.IsGenericType
28-
&& namedTypeSymbol.BaseType?.ToCompareString() == TaskFullName
29-
&& (namedTypeSymbol.TypeArguments.FirstOrDefault().SpecialType == SpecialType.System_Int32);
23+
ReturnsVoid = Symbol.ReturnType.IsTask();
24+
ReturnsValue = Symbol.ReturnType.IsTaskInt();
3025
}
3126
else
3227
{

src/DotMake.CommandLine.SourceGeneration/CliCommandHandlerInfo.cs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
using System;
2-
using System.Linq;
32
using Microsoft.CodeAnalysis;
43

54
namespace DotMake.CommandLine.SourceGeneration
65
{
76
public class CliCommandHandlerInfo : CliSymbolInfo, IEquatable<CliCommandHandlerInfo>
87
{
9-
private const string TaskFullName = "System.Threading.Tasks.Task";
10-
private const string TaskIntFullName = "System.Threading.Tasks.Task<System.Int32>";
118
private const string CliContextFullName = "DotMake.CommandLine.CliContext";
129
public const string DiagnosticName = "CLI command handler";
1310

@@ -17,14 +14,11 @@ public CliCommandHandlerInfo(IMethodSymbol symbol, SyntaxNode syntaxNode, Semant
1714
Symbol = symbol;
1815
Parent = parent;
1916

20-
if (symbol.IsAsync || (Symbol.Name == "RunAsync" && Symbol.ReturnType.ToCompareString() is TaskFullName or TaskIntFullName))
17+
if (symbol.IsAsync || symbol.ReturnType.IsTask() || symbol.ReturnType.IsTaskInt())
2118
{
2219
IsAsync = true;
23-
ReturnsVoid = (symbol.ReturnType.ToCompareString() == TaskFullName);
24-
ReturnsValue = (symbol.ReturnType is INamedTypeSymbol namedTypeSymbol)
25-
&& namedTypeSymbol.IsGenericType
26-
&& namedTypeSymbol.BaseType?.ToCompareString() == TaskFullName
27-
&& (namedTypeSymbol.TypeArguments.FirstOrDefault().SpecialType == SpecialType.System_Int32);
20+
ReturnsVoid = symbol.ReturnType.IsTask();
21+
ReturnsValue = symbol.ReturnType.IsTaskInt();
2822
}
2923
else
3024
{
@@ -78,12 +72,9 @@ private void Analyze()
7872

7973
public static bool HasCorrectName(IMethodSymbol symbol)
8074
{
81-
return symbol.Name switch
82-
{
83-
"Run" => true,
84-
"RunAsync" => symbol.IsAsync || symbol.ReturnType.ToCompareString() is TaskFullName or TaskIntFullName,
85-
_ => false
86-
};
75+
return symbol.IsAsync || symbol.ReturnType.IsTask() || symbol.ReturnType.IsTaskInt()
76+
? (symbol.Name == "RunAsync")
77+
: (symbol.Name == "Run");
8778
}
8879

8980
public void AppendCSharpCallString(CodeStringBuilder sb, string varCliContext = null)

src/DotMake.CommandLine.SourceGeneration/SymbolExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public static class SymbolExtensions
2828
miscellaneousOptions:
2929
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers);
3030

31+
private const string TaskFullName = "System.Threading.Tasks.Task";
32+
3133
/// <summary>
3234
/// Converts the symbol to a string representation, which is suitable for calling the symbol in code.
3335
/// <para>
@@ -137,5 +139,18 @@ public static string CombineNameParts(params string[] nameParts)
137139
{
138140
return string.Join(".", nameParts.Where(n => !string.IsNullOrEmpty(n)));
139141
}
142+
143+
public static bool IsTask(this ITypeSymbol type)
144+
{
145+
return (type.ToCompareString() == TaskFullName);
146+
}
147+
148+
public static bool IsTaskInt(this ITypeSymbol type)
149+
{
150+
return (type is INamedTypeSymbol namedTypeSymbol)
151+
&& namedTypeSymbol.IsGenericType
152+
&& namedTypeSymbol.BaseType?.ToCompareString() == TaskFullName
153+
&& (namedTypeSymbol.TypeArguments.FirstOrDefault().SpecialType == SpecialType.System_Int32);
154+
}
140155
}
141156
}

src/DotMake.CommandLine/DotMake.CommandLine.csproj

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@
2020
<Description>Declarative syntax for System.CommandLine via attributes for easy, fast, strongly-typed (no reflection) usage. Includes a source generator which automagically converts your classes to CLI commands and properties to CLI options or CLI arguments.</Description>
2121
<PackageTags>command-line CLI console System.CommandLine declarative attributes parsing command argument option generator</PackageTags>
2222
<PackageReleaseNotes>
23-
- Added parent command accessor support. Sub-commands can get a reference to the parent command by adding a property of the parent command type.
24-
- ParseResultExtensions.Bind improvement: Binding will be done only once per definition class, so calling this method consecutively for
25-
the same definition class will return the cached result.
23+
- Updated to latest daily build 2.0.0-beta4.24201.1 of System.CommandLine.
24+
- Added support for command handlers or command as delegates without the async keyword (which returns Task or Task&lt;int&gt;).
2625
</PackageReleaseNotes>
2726
</PropertyGroup>
2827

@@ -35,7 +34,7 @@
3534
as it's not on official nuget feed, we directly reference the DLL inside the package so that it's also bundled in our package.
3635
https://blog.maartenballiauw.be/post/2020/04/22/referencing-specific-assembly-nuget-package.html
3736
-->
38-
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.24126.1">
37+
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.24201.1">
3938
<GeneratePathProperty>true</GeneratePathProperty>
4039
<IncludeAssets>None</IncludeAssets>
4140
<ExcludeAssets>All</ExcludeAssets>

src/DotMake.CommandLine/ParseResultExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public static class ParseResultExtensions
1111
{
1212
private static readonly ConcurrentDictionary<ParseResult, ConcurrentDictionary<Type, object>> BindResults = new();
1313

14-
/// <inheritdoc cref = "Bind{TDefinition}" />
14+
/// <inheritdoc cref = "Bind" />
1515
/// <typeparam name="TDefinition"><inheritdoc cref="Cli.GetConfiguration{TDefinition}" path="/typeparam[@name='TDefinition']/node()" /></typeparam>
1616
public static TDefinition Bind<TDefinition>(this ParseResult parseResult)
1717
{

src/TestApp.Nuget/GeneratedFiles/net472/DotMake.CommandLine.SourceGeneration/DotMake.CommandLine.SourceGeneration.CliCommandGenerator/(ModuleInitializerAttribute).g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// <auto-generated />
2-
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
2+
// Generated by DotMake.CommandLine.SourceGeneration v1.8.7.0
33
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
44
// Generation: 1
55

src/TestApp.Nuget/GeneratedFiles/net472/DotMake.CommandLine.SourceGeneration/DotMake.CommandLine.SourceGeneration.CliCommandGenerator/(RequiredMemberAttribute).g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// <auto-generated />
2-
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
2+
// Generated by DotMake.CommandLine.SourceGeneration v1.8.7.0
33
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
44
// Generation: 1
55

src/TestApp.Nuget/GeneratedFiles/net472/DotMake.CommandLine.SourceGeneration/DotMake.CommandLine.SourceGeneration.CliCommandGenerator/ArgumentConverterCliCommandBuilder-3j0trcm.g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// <auto-generated />
2-
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
2+
// Generated by DotMake.CommandLine.SourceGeneration v1.8.7.0
33
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
44
// Generation: 1
55

0 commit comments

Comments
 (0)