Skip to content

Commit

Permalink
Added parent command accessor support
Browse files Browse the repository at this point in the history
- Added parent command accessor support. Sub-commands can get a reference to the parent command by adding a property of the parent command type.
- ParseResultExtensions.Bind improvement: Binding will be done only once per definition class, so calling this method consecutively for
  the same definition class will return the cached result.
  • Loading branch information
calacayir committed Mar 6, 2024
1 parent 5e6b904 commit c1ef7f7
Show file tree
Hide file tree
Showing 220 changed files with 2,655 additions and 1,480 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<!-- https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#generateassemblyinfo -->
<VersionPrefix>1.8.5</VersionPrefix>
<VersionPrefix>1.8.6</VersionPrefix>
<Product>DotMake Command-Line</Product>
<Company>DotMake</Company>
<!-- Copyright is also used for NuGet metadata -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace DotMake.CommandLine
/// <example>
/// <inheritdoc cref="Cli" path="/example/code[@id='gettingStartedDelegate']" />
/// <inheritdoc cref="Cli" path="/example/code[@id='gettingStartedClass']" />
/// <code source="..\TestApp\Commands\ParentCommandAccessorCliCommand.cs" region="ParentCommandAccessorCliCommand" language="cs" />
/// </example>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class CliArgumentAttribute : Attribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ namespace DotMake.CommandLine
/// </para>
/// </summary>
/// <example>
/// <inheritdoc cref="Cli" path="/example/code[@id='gettingStartedDelegate']" />
/// <inheritdoc cref="Cli" path="/example/code[@id='gettingStartedClass']" />
/// <code source="..\TestApp\Commands\RecursiveOptionCliCommand.cs" region="RecursiveOptionCliCommand" language="cs" />
/// <code source="..\TestApp\Commands\ParentCommandAccessorCliCommand.cs" region="ParentCommandAccessorCliCommand" language="cs" />
/// </example>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class CliOptionAttribute : Attribute
Expand Down
142 changes: 78 additions & 64 deletions src/DotMake.CommandLine.SourceGeneration/CliCommandInfo.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,19 @@

namespace DotMake.CommandLine.SourceGeneration
{
public class CliParentCommandRefInfo : CliSymbolInfo, IEquatable<CliParentCommandRefInfo>
public class CliParentCommandAccessorInfo : CliSymbolInfo, IEquatable<CliParentCommandAccessorInfo>
{
public const string DiagnosticName = "CLI parent command reference";
public const string DiagnosticName = "CLI parent command accessor";

public CliParentCommandRefInfo(IPropertySymbol symbol, SyntaxNode syntaxNode, SemanticModel semanticModel,
int parentTreeIndex, CliCommandSettings parentCommandSettings) : base(symbol, syntaxNode, semanticModel)
public CliParentCommandAccessorInfo(ISymbol symbol, SyntaxNode syntaxNode, SemanticModel semanticModel)
: base(symbol, syntaxNode, semanticModel)
{
ParentTreeIndex = parentTreeIndex;
ParentCommandSettings = parentCommandSettings;
Symbol = (IPropertySymbol)symbol;

Analyze();

if (HasProblem)
return;
}

public int ParentTreeIndex { get; }
public CliCommandSettings ParentCommandSettings { get; }
public new IPropertySymbol Symbol => (IPropertySymbol)base.Symbol;
public new IPropertySymbol Symbol;

private void Analyze()
{
Expand All @@ -40,7 +34,7 @@ private void Analyze()
}
}

public bool Equals(CliParentCommandRefInfo other)
public bool Equals(CliParentCommandAccessorInfo other)
{
return base.Equals(other);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public static ITypeSymbol GetUnderlyingTypeIfNullable(this ITypeSymbol type)
}

/// <summary>
/// Gets all own and inherited members (not distinct).
/// Gets all own and then inherited members (not distinct).
/// </summary>
public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol type)
{
Expand Down
15 changes: 0 additions & 15 deletions src/DotMake.CommandLine/Cli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,21 +339,6 @@ public static ParseResult Parse<TDefinition>(string commandLine, CliSettings set
return configuration.Parse(commandLine);
}

/// <summary>
/// Creates a new instance of the command definition class and binds/populates the properties from the parse result.
/// If the command line input is not for the indicated definition class (e.g. it's for a sub-command but not for the indicated root command or vice versa),
/// then the returned instance would be empty (i.e. properties would have default values).
/// </summary>
/// <typeparam name="TDefinition"><inheritdoc cref="GetConfiguration{TDefinition}" path="/typeparam[@name='TDefinition']/node()" /></typeparam>
/// <param name="parseResult">The parse result.</param>
/// <returns>The parsed value or a configured default.</returns>
public static TDefinition Bind<TDefinition>(this ParseResult parseResult)
{
var commandBuilder = CliCommandBuilder.Get<TDefinition>();

return (TDefinition)commandBuilder.Bind(parseResult);
}

private static string[] FixArgs(string[] args)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
Expand Down
52 changes: 0 additions & 52 deletions src/DotMake.CommandLine/CliBindContext.cs

This file was deleted.

35 changes: 18 additions & 17 deletions src/DotMake.CommandLine/CliCommandBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Parsing;
Expand All @@ -14,9 +15,11 @@ namespace DotMake.CommandLine
public abstract class CliCommandBuilder
{
/// <summary>
/// A delegate which is set by the source generator to be called from <see cref="Bind(CliBindContext)"/> method.
/// A delegate which is set by the source generator to be called from <see cref="Bind(ParseResult)"/> method.
/// </summary>
protected Func<CliBindContext, object> BindFunc;
protected Func<ParseResult, object> Binder;

private readonly ConcurrentDictionary<ParseResult, object> bindResults = new();

/// <summary>
/// Initializes a new instance of the <see cref="CliCommandBuilder" /> class.
Expand Down Expand Up @@ -67,28 +70,26 @@ protected CliCommandBuilder()
public abstract CliCommand Build();

/// <summary>
/// Creates a new instance of the definition class and binds/populates the properties from the parse result.
/// Creates a new instance of the command definition class and binds/populates the properties from the parse result.
/// Note that binding will be done only once, so calling this method consecutively will return the cached result.
/// <para>
/// If the command line input is not for this definition class (e.g. it's for a sub-command but not for
/// this root command or vice versa), then the returned instance would be empty (i.e. properties would have default values).
/// </para>
/// </summary>
/// <param name="parseResult">A parse result describing the outcome of the parse operation.</param>
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
public object Bind(ParseResult parseResult)
{
return Bind(new CliBindContext(parseResult));
}

/// <summary>
/// Creates a new instance of the definition class and binds/populates the properties from the parse result.
/// </summary>
/// <param name="cliBindContext">A <see cref="CliBindContext"/> instance to use for the binding operation.</param>
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
public object Bind(CliBindContext cliBindContext)
{
if (BindFunc == null)
throw new Exception("Ensure Build method is called first.");
return bindResults.GetOrAdd(parseResult, pr =>
{
if (Binder == null)
throw new Exception("Binder is not set. Ensure Build method is called first.");

return BindFunc(cliBindContext);
return Binder(pr);
});
}

/// <summary>
/// Gets the command builders that are nested/external children of this command builder.
/// </summary>
Expand Down
5 changes: 3 additions & 2 deletions src/DotMake.CommandLine/DotMake.CommandLine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
<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>
<PackageTags>command-line CLI console System.CommandLine declarative attributes parsing command argument option generator</PackageTags>
<PackageReleaseNotes>
- Added theme support. CliSettings.Theme property can be set to predefined themes Red, DarkRed, Green, DarkGreen, Blue, DarkBlue
or a custom CliTheme. These color and formatting option are mainly used by the help output.
- Added parent command accessor support. Sub-commands can get a reference to the parent command by adding a property of the parent command type.
- ParseResultExtensions.Bind improvement: Binding will be done only once per definition class, so calling this method consecutively for
the same definition class will return the cached result.
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
42 changes: 42 additions & 0 deletions src/DotMake.CommandLine/ParseResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Concurrent;
using System.CommandLine;

namespace DotMake.CommandLine
{
/// <summary>
/// Provides extension methods for <see cref="ParseResult"/>.
/// </summary>
public static class ParseResultExtensions
{
private static readonly ConcurrentDictionary<ParseResult, ConcurrentDictionary<Type, object>> BindResults = new();

/// <inheritdoc cref = "Bind{TDefinition}" />
/// <typeparam name="TDefinition"><inheritdoc cref="Cli.GetConfiguration{TDefinition}" path="/typeparam[@name='TDefinition']/node()" /></typeparam>
public static TDefinition Bind<TDefinition>(this ParseResult parseResult)
{
var commandBuilder = CliCommandBuilder.Get<TDefinition>();

return (TDefinition)commandBuilder.Bind(parseResult);
}

/// <summary>
/// Creates a new instance of the command definition class and binds/populates the properties from the parse result.
/// Note that binding will be done only once per definition class, so calling this method consecutively for
/// the same definition class will return the cached result.
/// <para>
/// If the command line input is not for the indicated definition class (e.g. it's for a sub-command but not for
/// the indicated root command or vice versa), then the returned instance would be empty (i.e. properties would have default values).
/// </para>
/// </summary>
/// <param name="parseResult">A parse result describing the outcome of the parse operation.</param>
/// <param name="definitionType"><inheritdoc cref="Cli.GetConfiguration(Type, CliSettings)" path="/param[@name='definitionType']/node()" /></param>
/// <returns>An instance of the definition class whose properties were bound/populated from the parse result.</returns>
public static object Bind(this ParseResult parseResult, Type definitionType)
{
var commandBuilder = CliCommandBuilder.Get(definitionType);

return commandBuilder.Bind(parseResult);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
// Generation: 1

#if !NET5_0_OR_GREATER
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
// Generation: 1

// Licensed to the .NET Foundation under one or more agreements.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
// Generation: 1

namespace TestApp.Commands.GeneratedCode
Expand Down Expand Up @@ -151,7 +151,7 @@ public override System.CommandLine.CliCommand Build()
rootCommand.Add(child.Build());
}

BindFunc = (parseResult) =>
Binder = (parseResult) =>
{
var targetClass = CreateInstance();

Expand All @@ -166,12 +166,14 @@ public override System.CommandLine.CliCommand Build()
// Set the parsed or default values for the arguments
targetClass.Arg = GetValueForArgument(parseResult, argument0);

// Set the values for the parent command accessors

return targetClass;
};

rootCommand.SetAction(parseResult =>
{
var targetClass = (TestApp.Commands.ArgumentConverterCliCommand) BindFunc(parseResult);
var targetClass = (TestApp.Commands.ArgumentConverterCliCommand) Bind(parseResult);

// Call the command handler
var cliContext = new DotMake.CommandLine.CliContext(parseResult);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
// Generation: 1

namespace TestApp.Commands.GeneratedCode
Expand Down Expand Up @@ -72,7 +72,7 @@ public override System.CommandLine.CliCommand Build()
rootCommand.Add(child.Build());
}

BindFunc = (parseResult) =>
Binder = (parseResult) =>
{
var targetClass = CreateInstance();

Expand All @@ -82,12 +82,14 @@ public override System.CommandLine.CliCommand Build()
// Set the parsed or default values for the arguments
targetClass.Argument1 = GetValueForArgument(parseResult, argument0);

// Set the values for the parent command accessors

return targetClass;
};

rootCommand.SetAction(async (parseResult, cancellationToken) =>
{
var targetClass = (TestApp.Commands.AsyncIntReturnCliCommand) BindFunc(parseResult);
var targetClass = (TestApp.Commands.AsyncIntReturnCliCommand) Bind(parseResult);

// Call the command handler
var cliContext = new DotMake.CommandLine.CliContext(parseResult, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// <auto-generated />
// Generated by DotMake.CommandLine.SourceGeneration v1.8.5.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.8111
// Generated by DotMake.CommandLine.SourceGeneration v1.8.6.0
// Roslyn (Microsoft.CodeAnalysis) v4.900.24.12101
// Generation: 1

namespace TestApp.Commands.GeneratedCode
Expand Down Expand Up @@ -72,7 +72,7 @@ public override System.CommandLine.CliCommand Build()
rootCommand.Add(child.Build());
}

BindFunc = (parseResult) =>
Binder = (parseResult) =>
{
var targetClass = CreateInstance();

Expand All @@ -82,12 +82,14 @@ public override System.CommandLine.CliCommand Build()
// Set the parsed or default values for the arguments
targetClass.Argument1 = GetValueForArgument(parseResult, argument0);

// Set the values for the parent command accessors

return targetClass;
};

rootCommand.SetAction(async (parseResult, cancellationToken) =>
{
var targetClass = (TestApp.Commands.AsyncVoidReturnCliCommand) BindFunc(parseResult);
var targetClass = (TestApp.Commands.AsyncVoidReturnCliCommand) Bind(parseResult);

// Call the command handler
var cliContext = new DotMake.CommandLine.CliContext(parseResult, cancellationToken);
Expand Down
Loading

0 comments on commit c1ef7f7

Please sign in to comment.