Upstream Tyrrrz/CliFx state of the touched files before 9ef5dcb635

This commit is contained in:
upstream committed 2026-08-28 10:12:58 +02:00
commit e3e64a40e4
6 files changed
+2165

No files matched your search

+32
View File
@@ -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; }
}
+62
View File
@@ -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;
+432
View File
@@ -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();
}
}