Skip to content

Commit 31a2e5d

Browse files
committed
feat: Add HiddenAttribute to hide specific command/parameter
1 parent 00f2995 commit 31a2e5d

File tree

5 files changed

+150
-6
lines changed

5 files changed

+150
-6
lines changed

src/ConsoleAppFramework/Command.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public record class Command
1818
{
1919
public required bool IsAsync { get; init; } // Task or Task<int>
2020
public required bool IsVoid { get; init; } // void or int
21+
public required bool IsHidden { get; init; } // Hide help from command list
2122

2223
public bool IsRootCommand => Name == "";
2324
public required string Name { get; init; }
@@ -153,6 +154,7 @@ public record class CommandParameter
153154
public required IgnoreEquality<WellKnownTypes> WellKnownTypes { get; init; }
154155
public required bool IsNullableReference { get; init; }
155156
public required bool IsParams { get; init; }
157+
public required bool IsHidden { get; init; } // Hide command parameter help
156158
public required string Name { get; init; }
157159
public required string OriginalParameterName { get; init; }
158160
public required bool HasDefaultValue { get; init; }

src/ConsoleAppFramework/CommandHelpBuilder.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ static string BuildHelpMessageCore(Command command, bool showCommandName, bool s
6464
if (definition.Options.Any())
6565
{
6666
var hasArgument = definition.Options.Any(x => x.Index.HasValue);
67-
var hasOptions = definition.Options.Any(x => !x.Index.HasValue);
67+
var hasNoHiddenOptions = definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden);
6868

6969
if (hasArgument)
7070
{
7171
sb.AppendLine();
7272
sb.AppendLine(BuildArgumentsMessage(definition));
7373
}
7474

75-
if (hasOptions)
75+
if (hasNoHiddenOptions)
7676
{
7777
sb.AppendLine();
7878
sb.AppendLine(BuildOptionsMessage(definition));
@@ -102,7 +102,7 @@ static string BuildUsageMessage(CommandHelpDefinition definition, bool showComma
102102
sb.Append(" [arguments...]");
103103
}
104104

105-
if (definition.Options.Any(x => !x.Index.HasValue))
105+
if (definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden))
106106
{
107107
sb.Append(" [options...]");
108108
}
@@ -160,6 +160,7 @@ static string BuildOptionsMessage(CommandHelpDefinition definition)
160160
{
161161
var optionsFormatted = definition.Options
162162
.Where(x => !x.Index.HasValue)
163+
.Where(x => !x.IsHidden)
163164
.Select(x => (Options: string.Join("|", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}{(x.IsParams ? "..." : "")}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue))
164165
.ToArray();
165166

@@ -215,6 +216,7 @@ static string BuildOptionsMessage(CommandHelpDefinition definition)
215216
static string BuildMethodListMessage(IEnumerable<Command> commands, out int maxWidth)
216217
{
217218
var formatted = commands
219+
.Where(x => !x.IsHidden)
218220
.Select(x =>
219221
{
220222
return (Command: x.Name, x.Description);
@@ -279,6 +281,7 @@ static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor)
279281
var description = item.Description;
280282
var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean;
281283
var isParams = item.IsParams;
284+
var isHidden = item.IsHidden;
282285

283286
var defaultValue = default(string);
284287
if (item.HasDefaultValue)
@@ -300,7 +303,7 @@ static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor)
300303
}
301304

302305
var paramTypeName = item.ToTypeShortString();
303-
parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams));
306+
parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams, isHidden));
304307
}
305308

306309
var commandName = descriptor.Name;
@@ -336,9 +339,10 @@ class CommandOptionHelpDefinition
336339
public bool IsRequired => DefaultValue == null && !IsParams;
337340
public bool IsFlag { get; }
338341
public bool IsParams { get; }
342+
public bool IsHidden { get; }
339343
public string FormattedValueTypeName => "<" + ValueTypeName + ">";
340344

341-
public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams)
345+
public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams, bool isHidden)
342346
{
343347
Options = options;
344348
Description = description;
@@ -347,6 +351,7 @@ public CommandOptionHelpDefinition(string[] options, string description, string
347351
Index = index;
348352
IsFlag = isFlag;
349353
IsParams = isParams;
354+
IsHidden = isHidden;
350355
}
351356
}
352357
}

src/ConsoleAppFramework/ConsoleAppBaseCode.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ public CommandAttribute(string command)
122122
}
123123
}
124124
125+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
126+
internal sealed class HiddenAttribute : Attribute
127+
{
128+
}
129+
125130
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
126131
internal sealed class RegisterCommandsAttribute : Attribute
127132
{

src/ConsoleAppFramework/Parser.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,11 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag
283283
}
284284
}
285285

286-
var hasParams = x.Modifiers.Any(x => x.IsKind(SyntaxKind.ParamsKeyword));
286+
var hasParams = x.Modifiers.Any(x => x.IsKind(SyntaxKind.ParamsKeyword));
287+
288+
var isHidden = x.AttributeLists
289+
.SelectMany(x => x.Attributes)
290+
.Any(x => model.GetTypeInfo(x).Type?.Name == "HiddenAttribute");
287291

288292
var customParserType = x.AttributeLists.SelectMany(x => x.Attributes)
289293
.Select(x =>
@@ -360,6 +364,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag
360364
IsNullableReference = isNullableReference,
361365
IsConsoleAppContext = isConsoleAppContext,
362366
IsParams = hasParams,
367+
IsHidden = isHidden,
363368
Type = new EquatableTypeSymbol(type.Type!),
364369
Location = x.GetLocation(),
365370
HasDefaultValue = hasDefault,
@@ -381,6 +386,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag
381386
Name = commandName,
382387
IsAsync = isAsync,
383388
IsVoid = isVoid,
389+
IsHidden = false, // Anonymous lambda don't support attribute.
384390
Parameters = parameters,
385391
MethodKind = MethodKind.Lambda,
386392
Description = "",
@@ -472,6 +478,8 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag
472478
return null;
473479
}
474480

481+
var isHiddenCommand = methodSymbol.GetAttributes().Any(x => x.AttributeClass?.Name == "HiddenAttribute");
482+
475483
var methodFilters = methodSymbol.GetAttributes()
476484
.Where(x => x.AttributeClass?.Name == "ConsoleAppFilterAttribute")
477485
.Select(x =>
@@ -516,6 +524,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag
516524
var hasValidation = x.GetAttributes().Any(x => x.AttributeClass?.GetBaseTypes().Any(y => y.Name == "ValidationAttribute") ?? false);
517525
var isCancellationToken = SymbolEqualityComparer.Default.Equals(x.Type, wellKnownTypes.CancellationToken);
518526
var isConsoleAppContext = x.Type!.Name == "ConsoleAppContext";
527+
var isHiddenParameter = x.GetAttributes().Any(x => x.AttributeClass?.Name == "HiddenAttribute");
519528

520529
string description = "";
521530
string[] aliases = [];
@@ -547,6 +556,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag
547556
IsNullableReference = isNullableReference,
548557
IsConsoleAppContext = isConsoleAppContext,
549558
IsParams = x.IsParams,
559+
IsHidden = isHiddenParameter,
550560
Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(),
551561
Type = new EquatableTypeSymbol(x.Type),
552562
HasDefaultValue = x.HasExplicitDefaultValue,
@@ -567,6 +577,7 @@ internal class Parser(ConsoleAppFrameworkGeneratorOptions generatorOptions, Diag
567577
Name = commandName,
568578
IsAsync = isAsync,
569579
IsVoid = isVoid,
580+
IsHidden = isHiddenCommand,
570581
Parameters = parameters,
571582
MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method,
572583
Description = summary,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
namespace ConsoleAppFramework.GeneratorTests;
2+
3+
public class HiddenAtttributeTest(ITestOutputHelper output)
4+
{
5+
VerifyHelper verifier = new(output, "CAF");
6+
7+
[Fact]
8+
public void VerifyHiddenOptions_Lambda()
9+
{
10+
var code =
11+
"""
12+
ConsoleApp.Run(args, (int x, [Hidden]int y) => { });
13+
""";
14+
15+
// Verify Hidden options is not shown on command help.
16+
verifier.Execute(code, args: "--help", expected:
17+
"""
18+
Usage: [options...] [-h|--help] [--version]
19+
20+
Options:
21+
--x <int> (Required)
22+
23+
""");
24+
}
25+
26+
[Fact]
27+
public void VerifyHiddenCommands_Class()
28+
{
29+
var code =
30+
"""
31+
var builder = ConsoleApp.Create();
32+
builder.Add<Commands>();
33+
await builder.RunAsync(args);
34+
35+
public class Commands
36+
{
37+
[Hidden]
38+
public void Command1() { Console.Write("command1"); }
39+
40+
public void Command2() { Console.Write("command2"); }
41+
42+
[Hidden]
43+
public void Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); }
44+
}
45+
""";
46+
47+
// Verify hidden command is not shown on root help commands.
48+
verifier.Execute(code, args: "--help", expected:
49+
"""
50+
Usage: [command] [-h|--help] [--version]
51+
52+
Commands:
53+
command2
54+
55+
""");
56+
57+
// Verify Hidden command help is shown when explicitly specify command name.
58+
verifier.Execute(code, args: "command1 --help", expected:
59+
"""
60+
Usage: command1 [-h|--help] [--version]
61+
62+
""");
63+
64+
verifier.Execute(code, args: "command2 --help", expected:
65+
"""
66+
Usage: command2 [-h|--help] [--version]
67+
68+
""");
69+
70+
verifier.Execute(code, args: "command3 --help", expected:
71+
"""
72+
Usage: command3 [options...] [-h|--help] [--version]
73+
74+
Options:
75+
--x <int> (Required)
76+
77+
""");
78+
79+
// Verify commands involations
80+
verifier.Execute(code, args: "command1", "command1");
81+
verifier.Execute(code, args: "command2", "command2");
82+
verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2");
83+
}
84+
85+
[Fact]
86+
public void VerifyHiddenCommands_LocalFunctions()
87+
{
88+
var code =
89+
"""
90+
var builder = ConsoleApp.Create();
91+
92+
builder.Add("", () => { Console.Write("root"); });
93+
builder.Add("command1", Command1);
94+
builder.Add("command2", Command2);
95+
builder.Add("command3", Command3);
96+
builder.Run(args);
97+
98+
[Hidden]
99+
static void Command1() { Console.Write("command1"); }
100+
101+
static void Command2() { Console.Write("command2"); }
102+
103+
[Hidden]
104+
static void Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); }
105+
""";
106+
107+
verifier.Execute(code, args: "--help", expected:
108+
"""
109+
Usage: [command] [-h|--help] [--version]
110+
111+
Commands:
112+
command2
113+
114+
""");
115+
116+
// Verify commands can be invoked.
117+
verifier.Execute(code, args: "command1", expected: "command1");
118+
verifier.Execute(code, args: "command2", expected: "command2");
119+
verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2");
120+
}
121+
}

0 commit comments

Comments
 (0)