diff --git a/src/Spectre.Console.Cli.Tests/CommandOptionAttributeTests.Deprecation.cs b/src/Spectre.Console.Cli.Tests/CommandOptionAttributeTests.Deprecation.cs new file mode 100644 index 0000000..4e9bb52 --- /dev/null +++ b/src/Spectre.Console.Cli.Tests/CommandOptionAttributeTests.Deprecation.cs @@ -0,0 +1,33 @@ + +namespace Spectre.Console.Tests.Unit.Cli.Annotations; + +public sealed partial class CommandOptionAttributeTests +{ + [Fact] + public void Should_Write_Deprecation_Warning() + { + //Given, When + var fixture = new CommandAppTester(); + fixture.Configure(configurator => configurator.AddCommand("cmd")); + var result = fixture.Run("cmd", "-d", "yes"); + + // Then + result.Output.ShouldContain("Warning"); + result.Output.ShouldContain("This option is deprecated and subject to removal."); + } + + private sealed class DeprecatedOptionSettings : CommandSettings + { + [CommandOption("-d|--deprecated ")] + [Obsolete("This option is deprecated and subject to removal.")] + public string? Deprecated { get; set; } + } + + private sealed class DeprecatedOptionCommand : Command + { + protected override int Execute(CommandContext context, DeprecatedOptionSettings settings, CancellationToken cancellationToken) + { + return 0; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs b/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs index 2cc9af6..348088c 100644 --- a/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs +++ b/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs @@ -7,6 +7,9 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso var lookup = new CommandValueLookup(); var binder = new CommandValueBinder(lookup); + // Track which deprecated options we've warned about to avoid spamming the console. + var warnedDeprecatedOptions = new HashSet(StringComparer.Ordinal); + CommandValidator.ValidateRequiredParameters(tree); while (tree != null) @@ -56,6 +59,41 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso // Process mapped parameters. foreach (var mapped in tree.Mapped) { + if (mapped.Parameter is CommandOption commandOption) + { + string? deprecationMessage = null; + try + { + var prop = mapped.Parameter.Property; + var obsoleteAttr = prop.GetCustomAttribute(false); + if (obsoleteAttr is not null && !string.IsNullOrWhiteSpace(obsoleteAttr.Message)) + { + deprecationMessage = obsoleteAttr.Message; + } + } + catch + { + // Just consume so we do not block binding + } + + var isDeprecated = deprecationMessage != null; + var optionName = commandOption.LongNames.Count > 0 ? commandOption.LongNames[0] + : commandOption.ShortNames.Count > 0 ? commandOption.ShortNames[0] + : commandOption.GetOptionName(); + if (isDeprecated && warnedDeprecatedOptions.Add(optionName)) + { + try + { + if (resolver.Resolve(typeof(IAnsiConsole)) is IAnsiConsole console) + { + var msg = deprecationMessage ?? $"Option '{optionName}' is deprecated."; + console.MarkupLine($"[yellow]Warning: {msg.EscapeMarkup()}[/]"); + } + } + catch { } + } + } + if (mapped.Parameter.WantRawValue) { // Just try to assign the raw value.