Upstream Tyrrrz/CliFx state of the touched files before 9ef5dcb635
This commit is contained in:
commit
e3e64a40e4
6 files changed
+2165
No files matched your search
@@ -0,0 +1,80 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Generators.Utils;
|
||||
using CliFx.Generators.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CliFx.Generators.Binding;
|
||||
|
||||
internal partial record CommandSymbol(
|
||||
INamedTypeSymbol Type,
|
||||
string? Name,
|
||||
string? Description,
|
||||
IReadOnlyList<CommandInputSymbol> Inputs
|
||||
)
|
||||
{
|
||||
public bool IsDefault => string.IsNullOrWhiteSpace(Name);
|
||||
|
||||
public IReadOnlyList<CommandParameterSymbol> Parameters { get; } =
|
||||
Inputs.OfType<CommandParameterSymbol>().ToArray();
|
||||
|
||||
public IReadOnlyList<CommandOptionSymbol> Options { get; } =
|
||||
Inputs.OfType<CommandOptionSymbol>().ToArray();
|
||||
|
||||
public override string ToString() => !string.IsNullOrWhiteSpace(Name) ? Name : "<default>";
|
||||
}
|
||||
|
||||
internal partial record CommandSymbol
|
||||
{
|
||||
internal static CommandSymbol? TryResolve(INamedTypeSymbol type, DiagnosticReporter diagnostics)
|
||||
{
|
||||
// Must have the [Command] attribute
|
||||
var attribute = type.TryGetAttribute("CliFx.Binding.CommandAttribute");
|
||||
if (attribute is null)
|
||||
{
|
||||
// Shouldn't happen when called by the generator since it filters types by attribute,
|
||||
// so not worth the effort to produce a diagnostic.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Must be partial by itself, along with all containing types
|
||||
foreach (var declaration in type.GetDeclarations().ToArray())
|
||||
{
|
||||
if (!declaration.IsFullyPartial())
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandTypeMustBePartial,
|
||||
declaration.Identifier.GetLocation(),
|
||||
type.Name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Must implement ICommand
|
||||
if (!type.Implements("CliFx.ICommand"))
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandTypeMustImplementICommand,
|
||||
type.Locations.FirstOrDefault(),
|
||||
type.Name
|
||||
);
|
||||
}
|
||||
|
||||
var name =
|
||||
attribute?.NamedArguments.FirstOrDefault(a => a.Key == "Name").Value.Value as string
|
||||
?? attribute
|
||||
?.ConstructorArguments.Where(a => a.Type?.SpecialType == SpecialType.System_String)
|
||||
.Select(a => a.Value as string)
|
||||
.FirstOrDefault();
|
||||
|
||||
var description =
|
||||
attribute?.NamedArguments.FirstOrDefault(a => a.Key == "Description").Value.Value
|
||||
as string;
|
||||
|
||||
var properties = type.GetProperties().ToArray();
|
||||
var parameters = CommandParameterSymbol.Resolve(properties, diagnostics);
|
||||
var options = CommandOptionSymbol.Resolve(properties, diagnostics);
|
||||
|
||||
return new CommandSymbol(type, name, description, [.. parameters, .. options]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Generators.Binding;
|
||||
using CliFx.Generators.Utils;
|
||||
using CliFx.Generators.Utils.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using PowerKit.Extensions;
|
||||
|
||||
namespace CliFx.Generators.Emitters;
|
||||
|
||||
internal static class CommandDescriptor
|
||||
{
|
||||
private static string EmitValidators(
|
||||
IReadOnlyList<INamedTypeSymbol> validatorTypes,
|
||||
string propertyTypeName
|
||||
)
|
||||
{
|
||||
if (validatorTypes.Count == 0)
|
||||
return $"global::System.Array.Empty<global::CliFx.Activation.IInputValidator<{propertyTypeName}>>()";
|
||||
|
||||
// lang=csharp
|
||||
return $$"""
|
||||
new global::CliFx.Activation.IInputValidator<{{propertyTypeName}}>[]
|
||||
{
|
||||
{{string.Join(
|
||||
", ",
|
||||
validatorTypes.Select(v => $"new {v.GetGloballyQualifiedName()}()")
|
||||
)}}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private static string? TryEmitDefaultScalarConverter(ITypeSymbol type)
|
||||
{
|
||||
// Object
|
||||
if (type.SpecialType == SpecialType.System_Object)
|
||||
return "new global::CliFx.Activation.StringScalarInputConverter()";
|
||||
|
||||
// String
|
||||
if (type.SpecialType == SpecialType.System_String)
|
||||
return "new global::CliFx.Activation.StringScalarInputConverter()";
|
||||
|
||||
// Bool
|
||||
if (type.SpecialType == SpecialType.System_Boolean)
|
||||
return "new global::CliFx.Activation.BoolScalarInputConverter()";
|
||||
|
||||
// Enum
|
||||
if (type.TypeKind == TypeKind.Enum)
|
||||
return $"new global::CliFx.Activation.EnumScalarInputConverter<{type.GetGloballyQualifiedName()}>()";
|
||||
|
||||
// Nullable<T>
|
||||
if (type.TryGetNullableUnderlyingType() is { } underlyingType)
|
||||
{
|
||||
var innerConverterCode = TryEmitDefaultScalarConverter(underlyingType);
|
||||
if (innerConverterCode is null)
|
||||
return null;
|
||||
|
||||
return $"global::CliFx.Activation.NullableScalarInputConverter.Create({innerConverterCode})";
|
||||
}
|
||||
|
||||
// Has static Parse(string, IFormatProvider)
|
||||
var parseMethodWithFormatProvider = type.GetMethods()
|
||||
.FirstOrDefault(m =>
|
||||
string.Equals(m.Name, "Parse", StringComparison.Ordinal)
|
||||
&& m.IsStatic
|
||||
&& m.DeclaredAccessibility == Accessibility.Public
|
||||
&& m.Parameters.Length == 2
|
||||
&& m.Parameters[0].Type.SpecialType == SpecialType.System_String
|
||||
&& m.Parameters[1].Type.IsMatchedBy("System.IFormatProvider")
|
||||
&& SymbolEqualityComparer.Default.Equals(m.ReturnType, type)
|
||||
);
|
||||
|
||||
if (parseMethodWithFormatProvider is not null)
|
||||
{
|
||||
return $"""
|
||||
global::CliFx.Activation.DelegateScalarInputConverter.Create(
|
||||
v => {parseMethodWithFormatProvider.GetGloballyQualifiedName()}(
|
||||
v!,
|
||||
global::System.Globalization.CultureInfo.InvariantCulture
|
||||
)
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
// Has static Parse(string)
|
||||
var parseMethod = type.GetMethods()
|
||||
.FirstOrDefault(m =>
|
||||
string.Equals(m.Name, "Parse", StringComparison.Ordinal)
|
||||
&& m.IsStatic
|
||||
&& m.DeclaredAccessibility == Accessibility.Public
|
||||
&& m.Parameters.Length == 1
|
||||
&& m.Parameters[0].Type.SpecialType == SpecialType.System_String
|
||||
&& SymbolEqualityComparer.Default.Equals(m.ReturnType, type)
|
||||
);
|
||||
|
||||
if (parseMethod is not null)
|
||||
{
|
||||
// lang=csharp
|
||||
return $"""
|
||||
global::CliFx.Activation.DelegateScalarInputConverter.Create(
|
||||
v => {parseMethod.GetGloballyQualifiedName()}(v!)
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
// Has ctor(string)
|
||||
if (
|
||||
type is INamedTypeSymbol namedConstructable
|
||||
&& namedConstructable.Constructors.Any(c =>
|
||||
c.DeclaredAccessibility == Accessibility.Public
|
||||
&& c.Parameters.Length == 1
|
||||
&& c.Parameters[0].Type.SpecialType == SpecialType.System_String
|
||||
)
|
||||
)
|
||||
{
|
||||
// lang=csharp
|
||||
return $"""
|
||||
global::CliFx.Activation.DelegateScalarInputConverter.Create(
|
||||
v => new {type.GetGloballyQualifiedName()}(v!)
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? TryEmitDefaultSequenceConverter(
|
||||
ITypeSymbol collectionType,
|
||||
ITypeSymbol elementType
|
||||
)
|
||||
{
|
||||
var elementConverterFragment = TryEmitDefaultScalarConverter(elementType);
|
||||
if (elementConverterFragment is null)
|
||||
return null;
|
||||
|
||||
// Assignable from T[]
|
||||
if (
|
||||
// T[]
|
||||
collectionType is IArrayTypeSymbol
|
||||
|| (
|
||||
collectionType
|
||||
is INamedTypeSymbol
|
||||
{
|
||||
TypeKind: TypeKind.Interface,
|
||||
TypeArguments: [var interfaceElementType],
|
||||
} collectionInterfaceType
|
||||
&& collectionInterfaceType.ConstructedFrom.SpecialType
|
||||
// IEnumerable<T>
|
||||
is SpecialType.System_Collections_Generic_IEnumerable_T
|
||||
// ICollection<T>
|
||||
or SpecialType.System_Collections_Generic_ICollection_T
|
||||
// IList<T>
|
||||
or SpecialType.System_Collections_Generic_IList_T
|
||||
// IReadOnlyCollection<T>
|
||||
or SpecialType.System_Collections_Generic_IReadOnlyCollection_T
|
||||
// IReadOnlyList<T>
|
||||
or SpecialType.System_Collections_Generic_IReadOnlyList_T
|
||||
&& SymbolEqualityComparer.Default.Equals(interfaceElementType, elementType)
|
||||
)
|
||||
)
|
||||
{
|
||||
return $"global::CliFx.Activation.ArraySequenceInputConverter.Create({elementConverterFragment})";
|
||||
}
|
||||
|
||||
// Has ctor(T[])
|
||||
if (
|
||||
collectionType is INamedTypeSymbol collectionNamedType
|
||||
&& collectionNamedType.Constructors.Any(c =>
|
||||
c.DeclaredAccessibility == Accessibility.Public
|
||||
&& c.Parameters.Length == 1
|
||||
&& (
|
||||
// T[]
|
||||
(
|
||||
c.Parameters[0].Type is IArrayTypeSymbol arrayParameterType
|
||||
&& SymbolEqualityComparer.Default.Equals(
|
||||
arrayParameterType.ElementType,
|
||||
elementType
|
||||
)
|
||||
)
|
||||
// Interfaces implemented by T[]
|
||||
|| (
|
||||
c.Parameters[0].Type is INamedTypeSymbol namedParameterType
|
||||
&& namedParameterType.ConstructedFrom.SpecialType
|
||||
// IEnumerable<T>
|
||||
is SpecialType.System_Collections_Generic_IEnumerable_T
|
||||
// ICollection<T>
|
||||
or SpecialType.System_Collections_Generic_ICollection_T
|
||||
// IList<T>
|
||||
or SpecialType.System_Collections_Generic_IList_T
|
||||
// IReadOnlyCollection<T>
|
||||
or SpecialType.System_Collections_Generic_IReadOnlyCollection_T
|
||||
// IReadOnlyList<T>
|
||||
or SpecialType.System_Collections_Generic_IReadOnlyList_T
|
||||
&& SymbolEqualityComparer.Default.Equals(
|
||||
namedParameterType.TypeArguments[0],
|
||||
elementType
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
{
|
||||
// lang=csharp
|
||||
return $"""
|
||||
global::CliFx.Activation.DelegateSequenceInputConverter.Create(
|
||||
global::CliFx.Activation.ArraySequenceInputConverter.Create({elementConverterFragment}),
|
||||
vs => new {collectionType.GetGloballyQualifiedName()}(vs)
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? TryEmitConverter(
|
||||
INamedTypeSymbol? userConverterType,
|
||||
IPropertySymbol property
|
||||
)
|
||||
{
|
||||
if (userConverterType is not null)
|
||||
return $"new {userConverterType.GetGloballyQualifiedName()}()";
|
||||
|
||||
// Sequence-based property
|
||||
if (
|
||||
property.Type.SpecialType != SpecialType.System_String
|
||||
&& property.Type.TryGetEnumerableUnderlyingType() is { } elementType
|
||||
)
|
||||
{
|
||||
return TryEmitDefaultSequenceConverter(property.Type, elementType);
|
||||
}
|
||||
|
||||
// Scalar property
|
||||
return TryEmitDefaultScalarConverter(property.Type);
|
||||
}
|
||||
|
||||
private static string? TryEmitCommandParameterDescriptor(
|
||||
CommandSymbol command,
|
||||
CommandParameterSymbol parameter,
|
||||
DiagnosticReporter diagnostics
|
||||
)
|
||||
{
|
||||
var converterFragment = TryEmitConverter(parameter.ConverterType, parameter.Property);
|
||||
if (converterFragment is null)
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandInputConverterNotInferrable,
|
||||
parameter.Property.Locations.FirstOrDefault(),
|
||||
parameter.Property.Name,
|
||||
parameter.Property.Type.GetGloballyQualifiedName()
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// lang=csharp
|
||||
return $$"""
|
||||
new global::CliFx.Binding.CommandParameterDescriptor<{{command.Type.GetGloballyQualifiedName()}}, {{parameter.Property.Type.GetGloballyQualifiedName()}}>(
|
||||
new global::CliFx.Binding.PropertyDescriptor<{{command.Type.GetGloballyQualifiedName()}}, {{parameter.Property.Type.GetGloballyQualifiedName()}}>(
|
||||
{{CSharp.Encode(parameter.Property.Name)}},
|
||||
c => c.{{parameter.Property.Name}},
|
||||
(c, v) => c.{{parameter.Property.Name}} = v
|
||||
),
|
||||
{{parameter.Order}},
|
||||
{{CSharp.Encode(parameter.Name)}},
|
||||
{{parameter.IsRequired.ToString().ToLowerInvariant()}},
|
||||
{{CSharp.Encode(parameter.Description)}},
|
||||
{{converterFragment}},
|
||||
{{EmitValidators(
|
||||
parameter.ValidatorTypes,
|
||||
parameter.Property.Type.GetGloballyQualifiedName()
|
||||
)}}
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
private static string? TryEmitCommandOptionDescriptor(
|
||||
CommandSymbol command,
|
||||
CommandOptionSymbol option,
|
||||
DiagnosticReporter diagnostics
|
||||
)
|
||||
{
|
||||
var converterFragment = TryEmitConverter(option.ConverterType, option.Property);
|
||||
if (converterFragment is null)
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandInputConverterNotInferrable,
|
||||
option.Property.Locations.FirstOrDefault(),
|
||||
option.Property.Name,
|
||||
option.Property.Type.GetGloballyQualifiedName()
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// lang=csharp
|
||||
return $$"""
|
||||
new global::CliFx.Binding.CommandOptionDescriptor<{{command.Type.GetGloballyQualifiedName()}}, {{option.Property.Type.GetGloballyQualifiedName()}}>(
|
||||
new global::CliFx.Binding.PropertyDescriptor<{{command.Type.GetGloballyQualifiedName()}}, {{option.Property.Type.GetGloballyQualifiedName()}}>(
|
||||
{{CSharp.Encode(option.Property.Name)}},
|
||||
c => c.{{option.Property.Name}},
|
||||
(c, v) => c.{{option.Property.Name}} = v
|
||||
),
|
||||
{{CSharp.Encode(option.Name)}},
|
||||
{{CSharp.Encode(option.ShortName)}},
|
||||
{{CSharp.Encode(option.EnvironmentVariable)}},
|
||||
{{option.IsRequired.ToString().ToLowerInvariant()}},
|
||||
{{CSharp.Encode(option.Description)}},
|
||||
{{converterFragment}},
|
||||
{{EmitValidators(
|
||||
option.ValidatorTypes,
|
||||
option.Property.Type.GetGloballyQualifiedName()
|
||||
)}}
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
private static string TryEmitCommandHelpOptionDescriptor(
|
||||
CommandSymbol command,
|
||||
DiagnosticReporter diagnostics
|
||||
)
|
||||
{
|
||||
// Check if the user already implemented a custom help option
|
||||
if (command.Type.Implements("CliFx.ICommandWithHelpOption"))
|
||||
{
|
||||
// Ensure that the user bound their IsHelpRequested property to an option
|
||||
var isHelpRequestedProperty = command
|
||||
.Type.GetProperties()
|
||||
.FirstOrDefault(p =>
|
||||
string.Equals(p.Name, "IsHelpRequested", StringComparison.Ordinal)
|
||||
);
|
||||
|
||||
var hasOptionBinding =
|
||||
isHelpRequestedProperty?.TryGetAttribute("CliFx.Binding.CommandOptionAttribute")
|
||||
is not null;
|
||||
|
||||
if (!hasOptionBinding)
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandHelpOptionPropertyMustBeBound,
|
||||
isHelpRequestedProperty?.Locations.FirstOrDefault()
|
||||
?? command.Type.Locations.FirstOrDefault(),
|
||||
"IsHelpRequested"
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the help option is being shadowed by an existing option
|
||||
var shadowingOptionByName = command.Options.FirstOrDefault(o =>
|
||||
string.Equals(o.Name, "help", StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
var shadowingOptionByShortName = command.Options.FirstOrDefault(o => o.ShortName == 'h');
|
||||
|
||||
var shadowingOption = shadowingOptionByShortName ?? shadowingOptionByName;
|
||||
|
||||
if (shadowingOption is not null)
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandOptionShadowsConventionalHelpOption,
|
||||
shadowingOption.Property.Locations.FirstOrDefault(),
|
||||
shadowingOption.Property.Name,
|
||||
shadowingOptionByName is not null ? "name" : "short name"
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// lang=csharp
|
||||
return $$"""
|
||||
new global::CliFx.Binding.CommandOptionDescriptor<{{command.Type.GetGloballyQualifiedName()}}, bool>(
|
||||
new global::CliFx.Binding.PropertyDescriptor<{{command.Type.GetGloballyQualifiedName()}}, bool>(
|
||||
{{CSharp.Encode("IsHelpRequested")}},
|
||||
c => c.IsHelpRequested,
|
||||
(c, v) => c.IsHelpRequested = v
|
||||
),
|
||||
{{CSharp.Encode("help")}},
|
||||
{{CSharp.Encode('h')}},
|
||||
null,
|
||||
false,
|
||||
{{CSharp.Encode("Shows help text.")}},
|
||||
new global::CliFx.Activation.BoolScalarInputConverter(),
|
||||
global::System.Array.Empty<global::CliFx.Activation.IInputValidator<bool>>()
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
private static string TryEmitCommandVersionOptionDescriptor(
|
||||
CommandSymbol command,
|
||||
DiagnosticReporter diagnostics
|
||||
)
|
||||
{
|
||||
// Check if the user already implemented a custom version option
|
||||
if (command.Type.Implements("CliFx.ICommandWithVersionOption"))
|
||||
{
|
||||
var isVersionRequestedProperty = command
|
||||
.Type.GetProperties()
|
||||
.FirstOrDefault(p =>
|
||||
string.Equals(p.Name, "IsVersionRequested", StringComparison.Ordinal)
|
||||
);
|
||||
|
||||
var hasOptionBinding =
|
||||
isVersionRequestedProperty?.TryGetAttribute("CliFx.Binding.CommandOptionAttribute")
|
||||
is not null;
|
||||
|
||||
if (!hasOptionBinding)
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandVersionOptionPropertyMustBeBound,
|
||||
isVersionRequestedProperty?.Locations.FirstOrDefault()
|
||||
?? command.Type.Locations.FirstOrDefault(),
|
||||
"IsVersionRequested"
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only default commands get the conventional version option
|
||||
if (!command.IsDefault)
|
||||
return null;
|
||||
|
||||
// Check if the version option is being shadowed by an existing option
|
||||
var shadowingOptionByName = command.Options.FirstOrDefault(o =>
|
||||
string.Equals(o.Name, "version", StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
if (shadowingOptionByName is not null)
|
||||
{
|
||||
diagnostics.Report(
|
||||
DiagnosticDescriptors.CommandOptionShadowsConventionalVersionOption,
|
||||
shadowingOptionByName.Property.Locations.FirstOrDefault(),
|
||||
shadowingOptionByName.Property.Name,
|
||||
"name"
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// lang=csharp
|
||||
return $$"""
|
||||
new global::CliFx.Binding.CommandOptionDescriptor<{{command.Type.GetGloballyQualifiedName()}}, bool>(
|
||||
new global::CliFx.Binding.PropertyDescriptor<{{command.Type.GetGloballyQualifiedName()}}, bool>(
|
||||
{{CSharp.Encode("IsVersionRequested")}},
|
||||
c => c.IsVersionRequested,
|
||||
(c, v) => c.IsVersionRequested = v
|
||||
),
|
||||
{{CSharp.Encode("version")}},
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
{{CSharp.Encode("Shows version information.")}},
|
||||
new global::CliFx.Activation.BoolScalarInputConverter(),
|
||||
global::System.Array.Empty<global::CliFx.Activation.IInputValidator<bool>>()
|
||||
)
|
||||
""";
|
||||
}
|
||||
|
||||
private static string? TryEmitCommandInputDescriptor(
|
||||
CommandSymbol command,
|
||||
CommandInputSymbol input,
|
||||
DiagnosticReporter diagnostics
|
||||
) =>
|
||||
input switch
|
||||
{
|
||||
CommandParameterSymbol parameter => TryEmitCommandParameterDescriptor(
|
||||
command,
|
||||
parameter,
|
||||
diagnostics
|
||||
),
|
||||
CommandOptionSymbol option => TryEmitCommandOptionDescriptor(
|
||||
command,
|
||||
option,
|
||||
diagnostics
|
||||
),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unknown input type '{input.GetType().Name}'."
|
||||
),
|
||||
};
|
||||
|
||||
internal static string Emit(CommandSymbol command, DiagnosticReporter diagnostics) =>
|
||||
// lang=csharp
|
||||
$$"""
|
||||
// <auto-generated />
|
||||
|
||||
{{(command.Type.TryGetNamespaceName() is { } namespaceName
|
||||
? $"namespace {namespaceName};"
|
||||
: ""
|
||||
)}}
|
||||
|
||||
{{string.Concat(command.Type.GetContainingTypes().Reverse().Select(t =>
|
||||
// lang=csharp
|
||||
$$"""
|
||||
partial class {{t.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}}
|
||||
{
|
||||
"""
|
||||
))}}
|
||||
|
||||
partial class {{command.Type.Name}}
|
||||
{
|
||||
/// <summary>
|
||||
/// Generated command descriptor for <see cref="{{Xml.Escape(command.Type.GetGloballyQualifiedName())}}" /> ({{Xml.Escape(command.ToString())}}).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
{{string.Join(
|
||||
Environment.NewLine,
|
||||
command.Inputs.Select(i =>
|
||||
// lang=xml
|
||||
$"""
|
||||
/// <item>
|
||||
/// <see cref="{Xml.Escape(i.Property.GetGloballyQualifiedName())}" /> ({Xml.Escape(i.ToString())})
|
||||
/// </item>
|
||||
"""
|
||||
)
|
||||
).NullIfWhiteSpace() ?? "///"}}
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static global::CliFx.Binding.CommandDescriptor Descriptor { get; } =
|
||||
new global::CliFx.Binding.CommandDescriptor<{{command.Type.GetGloballyQualifiedName()}}>(
|
||||
{{CSharp.Encode(command.Name)}},
|
||||
{{CSharp.Encode(command.Description)}},
|
||||
new global::CliFx.Binding.CommandInputDescriptor[]
|
||||
{
|
||||
{{string.Join(",", command.Inputs
|
||||
.Select(i => TryEmitCommandInputDescriptor(
|
||||
command,
|
||||
i,
|
||||
diagnostics
|
||||
))
|
||||
.Append(
|
||||
TryEmitCommandHelpOptionDescriptor(command, diagnostics) is {} helpOptionDescriptorFragment
|
||||
? helpOptionDescriptorFragment
|
||||
: null
|
||||
)
|
||||
.Append(
|
||||
TryEmitCommandVersionOptionDescriptor(command, diagnostics) is {} versionOptionDescriptorFragment
|
||||
? versionOptionDescriptorFragment
|
||||
: null
|
||||
)
|
||||
.WhereNotNull()
|
||||
)}}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
{{(!command.Type.Implements("CliFx.ICommandWithHelpOption")
|
||||
? // lang=csharp
|
||||
$$"""
|
||||
partial class {{command.Type.Name}} : global::CliFx.ICommandWithHelpOption
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsHelpRequested { get; set; }
|
||||
}
|
||||
"""
|
||||
: ""
|
||||
)}}
|
||||
|
||||
{{(command.IsDefault && !command.Type.Implements("CliFx.ICommandWithVersionOption")
|
||||
? // lang=csharp
|
||||
$$"""
|
||||
partial class {{command.Type.Name}} : global::CliFx.ICommandWithVersionOption
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsVersionRequested { get; set; }
|
||||
}
|
||||
"""
|
||||
: ""
|
||||
)}}
|
||||
|
||||
{{string.Concat(command.Type.GetContainingTypes().Reverse().Select(_ => "}"))}}
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,984 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliFx.Tests.Utils.Extensions;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace CliFx.Tests;
|
||||
|
||||
public class HelpSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput)
|
||||
{
|
||||
[Fact]
|
||||
public async Task I_can_request_help_by_passing_the_conventional_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.SetDescription("This will be in the help text")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().Contain("This will be in the help text");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_by_passing_the_manually_bound_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class Command : ICommand, ICommandWithHelpOption
|
||||
{
|
||||
[CommandOption("help", 'h', Description = "Custom help text.")]
|
||||
public bool IsHelpRequested { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ContainAllInOrder("OPTIONS", "-h", "--help", "Custom help text.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_by_passing_no_arguments_if_the_default_command_is_not_registered()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.UseConsole(FakeConsole)
|
||||
.SetDescription("This will be in the help text")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync([], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().Contain("This will be in the help text");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_for_a_specific_named_command_by_passing_its_name_and_the_conventional_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd", Description = "Description of a named command.")]
|
||||
public partial class NamedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd child")]
|
||||
public partial class NamedChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["cmd", "--help"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().Contain("Description of a named command.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_for_a_specific_nested_named_command_by_passing_its_name_and_the_conventional_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd")]
|
||||
public partial class NamedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd child", Description = "Description of a named child command.")]
|
||||
public partial class NamedChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["cmd", "sub", "--help"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().Contain("Description of a named child command.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_by_passing_invalid_arguments()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommand(NoOpCommand.Descriptor)
|
||||
.UseConsole(FakeConsole)
|
||||
.SetDescription("This will be in the help text")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["invalid-command", "--invalid-option"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().NotBe(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().Contain("This will be in the help text");
|
||||
|
||||
var stdErr = FakeConsole.ReadErrorString();
|
||||
stdErr.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_application_title_description_and_version()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.UseConsole(FakeConsole)
|
||||
.SetTitle("App title")
|
||||
.SetDescription("App description")
|
||||
.SetVersion("App version")
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ContainAll("App title", "App description", "App version");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_command_description()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command(Description = "Description of the default command.")]
|
||||
public partial class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ContainAllInOrder("DESCRIPTION", "Description of the default command.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_usage_format_of_a_named_command()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class DefaultCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd")]
|
||||
public partial class NamedCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Should().ContainAllInOrder("USAGE", "[command]", "...");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_usage_format_of_all_parameters()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public required string Foo { get; set; }
|
||||
|
||||
[CommandParameter(1)]
|
||||
public required string Bar { get; set; }
|
||||
|
||||
[CommandParameter(2)]
|
||||
public required string MultiWordName123 { get; set; }
|
||||
|
||||
[CommandParameter(3)]
|
||||
public required IReadOnlyList<string> Baz { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder("USAGE", "<foo>", "<bar>", "<multi-word-name123>", "<baz...>");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_usage_format_of_all_required_options()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public required string Foo { get; set; }
|
||||
|
||||
[CommandOption("bar")]
|
||||
public string? Bar { get; set; }
|
||||
|
||||
[CommandOption("baz")]
|
||||
public required IReadOnlyList<string> Baz { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder("USAGE", "--foo <value>", "--baz <values...>", "[options]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_list_of_all_inputs()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "foo", Description = "Description of foo.")]
|
||||
public string? Foo { get; set; }
|
||||
|
||||
[CommandOption("bar", Description = "Description of bar.")]
|
||||
public string? Bar { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"PARAMETERS",
|
||||
"foo",
|
||||
"Description of foo.",
|
||||
"OPTIONS",
|
||||
"--bar",
|
||||
"Description of bar."
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_list_of_all_inputs_including_inherited_ones()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public abstract class GrandParentCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "foo", Description = "Description of foo.")]
|
||||
public string? Foo { get; set; }
|
||||
|
||||
public abstract ValueTask ExecuteAsync(IConsole console);
|
||||
}
|
||||
|
||||
public abstract class ParentCommand : GrandParentCommand
|
||||
{
|
||||
[CommandOption("bar", Description = "Description of bar.")]
|
||||
public string? Bar { get; set; }
|
||||
}
|
||||
|
||||
[Command]
|
||||
public partial class Command : ParentCommand
|
||||
{
|
||||
[CommandOption("baz", Description = "Description of baz.")]
|
||||
public string? Baz { get; set; }
|
||||
|
||||
public override ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"PARAMETERS",
|
||||
"foo",
|
||||
"Description of foo.",
|
||||
"OPTIONS",
|
||||
"--baz",
|
||||
"Description of baz.",
|
||||
"--bar",
|
||||
"Description of bar."
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_conventional_help_and_version_options()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"OPTIONS",
|
||||
"-h",
|
||||
"--help",
|
||||
"Shows help text",
|
||||
"--version",
|
||||
"Shows version information"
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_on_a_named_command_to_see_the_conventional_help_option()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command("cmd")]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(
|
||||
["cmd", "--help"],
|
||||
new Dictionary<string, string>()
|
||||
);
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
|
||||
stdOut.Should().ContainAllInOrder("OPTIONS", "-h", "--help", "Shows help text");
|
||||
stdOut.Should().NotContainAny("--version", "Shows version information");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_list_of_valid_values_for_inputs_bound_to_enum_properties()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public enum CustomEnum { One, Two, Three }
|
||||
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public CustomEnum Foo { get; set; }
|
||||
|
||||
[CommandOption("bar")]
|
||||
public CustomEnum Bar { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"PARAMETERS",
|
||||
"foo",
|
||||
"Choices:",
|
||||
"One",
|
||||
"Two",
|
||||
"Three",
|
||||
"OPTIONS",
|
||||
"--bar",
|
||||
"Choices:",
|
||||
"One",
|
||||
"Two",
|
||||
"Three"
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_list_of_valid_values_for_inputs_bound_to_nullable_enum_properties()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public enum CustomEnum { One, Two, Three }
|
||||
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
[CommandParameter(0)]
|
||||
public CustomEnum? Foo { get; set; }
|
||||
|
||||
[CommandOption("bar")]
|
||||
public CustomEnum? Bar { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"PARAMETERS",
|
||||
"foo",
|
||||
"Choices:",
|
||||
"One",
|
||||
"Two",
|
||||
"Three",
|
||||
"OPTIONS",
|
||||
"--bar",
|
||||
"Choices:",
|
||||
"One",
|
||||
"Two",
|
||||
"Three"
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_environment_variables_of_options_that_use_them_as_fallback()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public enum CustomEnum { One, Two, Three }
|
||||
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
|
||||
public CustomEnum Foo { get; set; }
|
||||
|
||||
[CommandOption("bar", EnvironmentVariable = "ENV_BAR")]
|
||||
public CustomEnum Bar { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"OPTIONS",
|
||||
"--foo",
|
||||
"Environment variable:",
|
||||
"ENV_FOO",
|
||||
"--bar",
|
||||
"Environment variable:",
|
||||
"ENV_BAR"
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_default_values_of_non_required_options()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
public enum CustomEnum { One, Two, Three }
|
||||
|
||||
[Command]
|
||||
public partial class Command : ICommand
|
||||
{
|
||||
[CommandOption("foo")]
|
||||
public object? Foo { get; set; } = 42;
|
||||
|
||||
[CommandOption("bar")]
|
||||
public string? Bar { get; set; } = "hello";
|
||||
|
||||
[CommandOption("baz")]
|
||||
public IReadOnlyList<string>? Baz { get; set; } = new[] {"one", "two", "three"};
|
||||
|
||||
[CommandOption("qwe")]
|
||||
public bool Qwe { get; set; } = true;
|
||||
|
||||
[CommandOption("qop")]
|
||||
public int? Qop { get; set; } = 1337;
|
||||
|
||||
[CommandOption("zor")]
|
||||
public TimeSpan Zor { get; set; } = TimeSpan.FromMinutes(123);
|
||||
|
||||
[CommandOption("lol")]
|
||||
public CustomEnum Lol { get; set; } = CustomEnum.Two;
|
||||
|
||||
[CommandOption("hmm")]
|
||||
public required string Hmm { get; set; } = "not printed";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"OPTIONS",
|
||||
"--foo",
|
||||
"Default:",
|
||||
"42",
|
||||
"--bar",
|
||||
"Default:",
|
||||
"hello",
|
||||
"--baz",
|
||||
"Default:",
|
||||
"one",
|
||||
"two",
|
||||
"three",
|
||||
"--qwe",
|
||||
"Default:",
|
||||
"true",
|
||||
"--qop",
|
||||
"Default:",
|
||||
"1337",
|
||||
"--zor",
|
||||
"Default:",
|
||||
"02:03:00",
|
||||
"--lol",
|
||||
"Default:",
|
||||
"Two"
|
||||
);
|
||||
|
||||
stdOut.Should().NotContain("not printed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_list_of_all_immediate_child_commands()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command("cmd1", Description = "Description of one command.")]
|
||||
public partial class FirstCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd2", Description = "Description of another command.")]
|
||||
public partial class SecondCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd2 child", Description = "Description of another command's child command.")]
|
||||
public partial class SecondCommandChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd3 child", Description = "Description of an orphaned command.")]
|
||||
public partial class ThirdCommandChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"COMMANDS",
|
||||
"cmd1",
|
||||
"Description of one command.",
|
||||
"cmd2",
|
||||
"Description of another command.",
|
||||
// "cmd2 child" will appear as an immediate command because it does not
|
||||
// have a more specific parent.
|
||||
"cmd3 child",
|
||||
"Description of an orphaned command."
|
||||
);
|
||||
|
||||
// "cmd2 child" will still appear in the list of "cmd2" subcommands,
|
||||
// but its description will not be visible.
|
||||
stdOut.Should().NotContain("Description of another command's child command.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_help_to_see_the_list_of_all_immediate_grand_child_commands()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command("cmd1")]
|
||||
public partial class FirstCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd1 child1")]
|
||||
public partial class FirstCommandFirstChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd2")]
|
||||
public partial class SecondCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd2 child11")]
|
||||
public partial class SecondCommandFirstChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
|
||||
[Command("cmd2 child2")]
|
||||
public partial class SecondCommandSecondChildCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut
|
||||
.Should()
|
||||
.ContainAllInOrder(
|
||||
"COMMANDS",
|
||||
"cmd1",
|
||||
"Subcommands:",
|
||||
"cmd1 child1",
|
||||
"cmd2",
|
||||
"Subcommands:",
|
||||
"cmd2 child1",
|
||||
"cmd2 child2"
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_version_information_by_passing_the_conventional_version_option()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommand(NoOpCommand.Descriptor)
|
||||
.SetVersion("v6.9")
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--version"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("v6.9");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task I_can_request_version_information_by_passing_the_manually_bound_version_option()
|
||||
{
|
||||
// Arrange
|
||||
var application = new CommandLineApplicationBuilder()
|
||||
.AddCommands(
|
||||
CommandCompiler.Compile(
|
||||
// lang=csharp
|
||||
"""
|
||||
[Command]
|
||||
public partial class Command : ICommand, ICommandWithVersionOption
|
||||
{
|
||||
[CommandOption("version", Description = "Custom version text.")]
|
||||
public bool IsVersionRequested { get; set; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||
}
|
||||
"""
|
||||
)
|
||||
)
|
||||
.SetVersion("v1.0")
|
||||
.UseConsole(FakeConsole)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var exitCode = await application.RunAsync(["--version"], new Dictionary<string, string>());
|
||||
|
||||
// Assert
|
||||
exitCode.Should().Be(0);
|
||||
|
||||
var stdOut = FakeConsole.ReadOutputString();
|
||||
stdOut.Trim().Should().Be("v1.0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace CliFx.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// Binds a type to a command.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class CommandAttribute(string? name) : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandAttribute" />.
|
||||
/// </summary>
|
||||
public CommandAttribute()
|
||||
: this(null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Command name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Command can have no name, in which case it's treated as the application's default (i.e., root) command.
|
||||
/// Only one default command is allowed to be registered in an application.
|
||||
/// All commands registered in an application must have unique names (comparison IS NOT case-sensitive).
|
||||
/// </remarks>
|
||||
public string? Name { get; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Command description.
|
||||
/// Used for display purposes in the help text.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace CliFx.Binding;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a binding between a CLR type and a command.
|
||||
/// </summary>
|
||||
public class CommandDescriptor(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
Type type,
|
||||
string? name,
|
||||
string? description,
|
||||
IReadOnlyList<CommandInputDescriptor> inputs
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// CLR type of the command.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
public Type Type { get; } = type;
|
||||
|
||||
/// <inheritdoc cref="CommandAttribute.Name" />
|
||||
public string? Name { get; } = name;
|
||||
|
||||
internal bool IsDefault => string.IsNullOrWhiteSpace(Name);
|
||||
|
||||
/// <inheritdoc cref="CommandAttribute.Description" />
|
||||
public string? Description { get; } = description;
|
||||
|
||||
/// <summary>
|
||||
/// Inputs (parameters and options) of the command;
|
||||
/// </summary>
|
||||
public IReadOnlyList<CommandInputDescriptor> Inputs { get; } = inputs;
|
||||
|
||||
internal IReadOnlyList<CommandParameterDescriptor> Parameters { get; } =
|
||||
inputs.OfType<CommandParameterDescriptor>().ToArray();
|
||||
|
||||
internal IReadOnlyList<CommandOptionDescriptor> Options { get; } =
|
||||
inputs.OfType<CommandOptionDescriptor>().ToArray();
|
||||
|
||||
internal bool MatchesName(string? name) =>
|
||||
!string.IsNullOrWhiteSpace(Name)
|
||||
? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase)
|
||||
: string.IsNullOrWhiteSpace(name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => !string.IsNullOrWhiteSpace(Name) ? Name : "<default>";
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CommandDescriptor" />
|
||||
/// <remarks>
|
||||
/// Generic version used by source-generated code for static type references and AOT compatibility.
|
||||
/// </remarks>
|
||||
public class CommandDescriptor<
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
|
||||
TCommand
|
||||
>(string? name, string? description, IReadOnlyList<CommandInputDescriptor> inputs)
|
||||
: CommandDescriptor(typeof(TCommand), name, description, inputs)
|
||||
where TCommand : ICommand;
|
||||
@@ -0,0 +1,432 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using CliFx.Binding;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace CliFx.Help;
|
||||
|
||||
internal class HelpWriter(HelpContext context, ConsoleWriter consoleWriter)
|
||||
: FormattedConsoleWriter(consoleWriter)
|
||||
{
|
||||
private void WriteHeader(string text)
|
||||
{
|
||||
Write(ConsoleColor.White, text.ToUpperInvariant());
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
private void WriteCommandInvocation()
|
||||
{
|
||||
Write(context.Metadata.ExecutableName);
|
||||
|
||||
// Command name
|
||||
if (!string.IsNullOrWhiteSpace(context.Command.Name))
|
||||
{
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, context.Command.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCommandUsage()
|
||||
{
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Usage");
|
||||
|
||||
// Current command
|
||||
{
|
||||
WriteHorizontalMargin();
|
||||
|
||||
WriteCommandInvocation();
|
||||
Write(' ');
|
||||
|
||||
// Parameters
|
||||
foreach (var parameter in context.Command.Parameters.OrderBy(p => p.Order))
|
||||
{
|
||||
Write(ConsoleColor.DarkCyan, parameter.ToString(includeKind: false));
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Required options
|
||||
foreach (var option in context.Command.Options.Where(o => o.IsRequired))
|
||||
{
|
||||
Write(
|
||||
ConsoleColor.Yellow,
|
||||
option.ToString(
|
||||
includeKind: false,
|
||||
includeBothIdentifiers: false,
|
||||
includeValuePlaceholder: true
|
||||
)
|
||||
);
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Placeholder for non-required options
|
||||
if (context.Command.Options.Any(o => !o.IsRequired))
|
||||
{
|
||||
Write(ConsoleColor.Yellow, "[options]");
|
||||
}
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
// Child commands
|
||||
if (context.Root.GetChildCommands(context.Command.Name).Any())
|
||||
{
|
||||
WriteHorizontalMargin();
|
||||
|
||||
WriteCommandInvocation();
|
||||
Write(' ');
|
||||
|
||||
// Placeholder for child command
|
||||
Write(ConsoleColor.Cyan, "[command]");
|
||||
Write(' ');
|
||||
|
||||
// Placeholder for other arguments
|
||||
Write("...");
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCommandDescription()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(context.Command.Description))
|
||||
return;
|
||||
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Description");
|
||||
|
||||
WriteHorizontalMargin();
|
||||
|
||||
Write(context.Command.Description);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
private void WriteCommandInputDefaultValue(CommandInputDescriptor input)
|
||||
{
|
||||
var defaultValue = context.CommandDefaultValues.GetValueOrDefault(input);
|
||||
if (defaultValue is null)
|
||||
return;
|
||||
|
||||
// Normalize to an array to process both scalar and sequence default values uniformly
|
||||
var defaultValues =
|
||||
defaultValue is not string && defaultValue is IEnumerable defaultValueAsEnumerable
|
||||
? defaultValueAsEnumerable.Cast<object>().ToArray()
|
||||
: [defaultValue];
|
||||
|
||||
// Only strings, chars, bools, and types that implement IFormattable have
|
||||
// meaningful ToString() representations.
|
||||
if (!defaultValues.All(v => v is string or char or bool or IFormattable))
|
||||
return;
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
foreach (var value in defaultValues)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
Write(ConsoleColor.White, "Default: ");
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(", ");
|
||||
}
|
||||
|
||||
Write(ConsoleColor.DarkGray, '<');
|
||||
Write(
|
||||
value switch
|
||||
{
|
||||
// Lowercase bools look better
|
||||
bool boolValue => boolValue.ToString().ToLowerInvariant(),
|
||||
IFormattable formattable => formattable.ToString(
|
||||
null,
|
||||
CultureInfo.InvariantCulture
|
||||
),
|
||||
_ => value.ToString(),
|
||||
}
|
||||
);
|
||||
Write(ConsoleColor.DarkGray, '>');
|
||||
}
|
||||
|
||||
if (!isFirst)
|
||||
Write('.');
|
||||
}
|
||||
|
||||
private void WriteCommandParameters()
|
||||
{
|
||||
if (!context.Command.Parameters.Any())
|
||||
return;
|
||||
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Parameters");
|
||||
|
||||
foreach (var parameter in context.Command.Parameters.OrderBy(p => p.Order))
|
||||
{
|
||||
if (parameter.IsRequired)
|
||||
{
|
||||
Write(ConsoleColor.Red, "* ");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteHorizontalMargin();
|
||||
}
|
||||
|
||||
Write(ConsoleColor.DarkCyan, $"{parameter.Name}");
|
||||
|
||||
WriteColumnMargin();
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(parameter.Description))
|
||||
{
|
||||
Write(parameter.Description);
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Valid values
|
||||
var validValues = parameter.Property.TryGetValidValues() ?? [];
|
||||
if (validValues.Any())
|
||||
{
|
||||
Write(ConsoleColor.White, "Choices: ");
|
||||
|
||||
var isFirst = true;
|
||||
foreach (var validValue in validValues)
|
||||
{
|
||||
if (validValue is null)
|
||||
continue;
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(", ");
|
||||
}
|
||||
|
||||
Write('"');
|
||||
Write(validValue.ToString());
|
||||
Write('"');
|
||||
}
|
||||
|
||||
Write('.');
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Default value
|
||||
if (!parameter.IsRequired)
|
||||
{
|
||||
WriteCommandInputDefaultValue(parameter);
|
||||
}
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCommandOptions()
|
||||
{
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Options");
|
||||
|
||||
foreach (var option in context.Command.Options.OrderByDescending(o => o.IsRequired))
|
||||
{
|
||||
if (option.IsRequired)
|
||||
{
|
||||
Write(ConsoleColor.Red, "* ");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteHorizontalMargin();
|
||||
}
|
||||
|
||||
// Short name
|
||||
if (option.ShortName is not null)
|
||||
{
|
||||
Write(ConsoleColor.Yellow, $"-{option.ShortName}");
|
||||
}
|
||||
|
||||
// Separator
|
||||
if (!string.IsNullOrWhiteSpace(option.Name) && option.ShortName is not null)
|
||||
{
|
||||
Write('|');
|
||||
}
|
||||
|
||||
// Name
|
||||
if (!string.IsNullOrWhiteSpace(option.Name))
|
||||
{
|
||||
Write(ConsoleColor.Yellow, $"--{option.Name}");
|
||||
}
|
||||
|
||||
WriteColumnMargin();
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(option.Description))
|
||||
{
|
||||
Write(option.Description);
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Valid values
|
||||
var validValues = option.Property.TryGetValidValues() ?? [];
|
||||
if (validValues.Any())
|
||||
{
|
||||
Write(ConsoleColor.White, "Choices: ");
|
||||
|
||||
var isFirst = true;
|
||||
foreach (var validValue in validValues)
|
||||
{
|
||||
if (validValue is null)
|
||||
continue;
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(", ");
|
||||
}
|
||||
|
||||
Write('"');
|
||||
Write(validValue.ToString());
|
||||
Write('"');
|
||||
}
|
||||
|
||||
Write('.');
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Environment variable
|
||||
if (!string.IsNullOrWhiteSpace(option.EnvironmentVariable))
|
||||
{
|
||||
Write(ConsoleColor.White, "Environment variable: ");
|
||||
Write(option.EnvironmentVariable);
|
||||
Write('.');
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Default value
|
||||
if (!option.IsRequired)
|
||||
{
|
||||
WriteCommandInputDefaultValue(option);
|
||||
}
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteCommandChildren()
|
||||
{
|
||||
var childCommands = context
|
||||
.Root.GetChildCommands(context.Command.Name)
|
||||
.OrderBy(a => a.Name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (!childCommands.Any())
|
||||
return;
|
||||
|
||||
if (!IsEmpty)
|
||||
WriteVerticalMargin();
|
||||
|
||||
WriteHeader("Commands");
|
||||
|
||||
foreach (var childCommand in childCommands)
|
||||
{
|
||||
// Name
|
||||
WriteHorizontalMargin();
|
||||
Write(
|
||||
ConsoleColor.Cyan,
|
||||
// Trim the name so it's relative to the current command
|
||||
childCommand.Name?.Substring(context.Command.Name?.Length ?? 0).Trim()
|
||||
);
|
||||
|
||||
WriteColumnMargin();
|
||||
|
||||
// Description
|
||||
if (!string.IsNullOrWhiteSpace(childCommand.Description))
|
||||
{
|
||||
Write(childCommand.Description);
|
||||
Write(' ');
|
||||
}
|
||||
|
||||
// Grand-child commands
|
||||
var grandChildCommands = context
|
||||
.Root.GetChildCommands(childCommand.Name)
|
||||
.OrderBy(c => c.Name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
if (grandChildCommands.Any())
|
||||
{
|
||||
Write(ConsoleColor.White, "Subcommands: ");
|
||||
|
||||
var isFirst = true;
|
||||
foreach (var grandChildCommand in grandChildCommands)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(", ");
|
||||
}
|
||||
|
||||
Write(
|
||||
ConsoleColor.Cyan,
|
||||
// Trim the name so it's relative to the current command
|
||||
grandChildCommand.Name?.Substring(context.Command.Name?.Length ?? 0).Trim()
|
||||
);
|
||||
}
|
||||
|
||||
Write('.');
|
||||
}
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
// Child command help tip
|
||||
WriteVerticalMargin();
|
||||
Write("You can run \"");
|
||||
WriteCommandInvocation();
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Cyan, "[command]");
|
||||
Write(' ');
|
||||
Write(ConsoleColor.White, "--help");
|
||||
Write("\" to show help for a specific command.");
|
||||
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
public void WriteHelpText()
|
||||
{
|
||||
// Application info
|
||||
Write(ConsoleColor.White, context.Metadata.Title);
|
||||
Write(' ');
|
||||
Write(ConsoleColor.Yellow, context.Metadata.Version);
|
||||
WriteLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(context.Metadata.Description))
|
||||
{
|
||||
WriteHorizontalMargin();
|
||||
Write(context.Metadata.Description);
|
||||
WriteLine();
|
||||
}
|
||||
|
||||
// Command info
|
||||
WriteCommandUsage();
|
||||
WriteCommandDescription();
|
||||
WriteCommandParameters();
|
||||
WriteCommandOptions();
|
||||
WriteCommandChildren();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user