Skip to content

Commit c8bcf24

Browse files
feat: add option grouping support for organized help text
Implement argument grouping to organize command options under labeled sections in help output, improving readability for commands with many options. Key features: - ArgumentGroupAttribute: Apply to IArgumentModel classes to set a default group for all options - OptionAttribute.Group: Set group name on individual options - Group inheritance: Nested models inherit parent group unless overridden - Property-level override: Individual options can override model-level group - Alphabetical sorting: Groups displayed in alphabetical order - Backward compatible: Ungrouped options displayed first in help text Changes include: - New ArgumentGroupAttribute for model-level grouping - Group property added to Option and OptionAttribute - Enhanced help text providers (BasicHelpTextProvider, HelpTextProvider) to render grouped options with headers - Comprehensive test coverage for various grouping scenarios including: * Basic option grouping * Model-level grouping with ArgumentGroupAttribute * Property-level overrides * Nested model inheritance and overrides * Mixed options and operands Also removes obsolete LogProvider references from AppRunner.
1 parent d3403c6 commit c8bcf24

File tree

12 files changed

+610
-10
lines changed

12 files changed

+610
-10
lines changed
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
using CommandDotNet.TestTools.Scenarios;
2+
using Xunit;
3+
using Xunit.Abstractions;
4+
5+
namespace CommandDotNet.Tests.FeatureTests.Arguments;
6+
7+
public class ArgumentGroupTests
8+
{
9+
public ArgumentGroupTests(ITestOutputHelper output)
10+
{
11+
Ambient.Output = output;
12+
}
13+
14+
[Fact]
15+
public void BasicHelp_Groups_Options_With_Group_Property()
16+
{
17+
new AppRunner<App>(TestAppSettings.BasicHelp)
18+
.Verify(
19+
new Scenario
20+
{
21+
When = {Args = "GroupedOptions -h"},
22+
Then =
23+
{
24+
Output = @"Usage: testhost.dll GroupedOptions [options]
25+
26+
Options:
27+
--ungrouped An ungrouped option
28+
29+
Database:
30+
31+
--connection Connection string
32+
--timeout Timeout value
33+
34+
Logging:
35+
36+
--logLevel Log level
37+
--logFile Log file path"
38+
}
39+
});
40+
}
41+
42+
[Fact]
43+
public void DetailedHelp_Groups_Options_With_Group_Property()
44+
{
45+
new AppRunner<App>(TestAppSettings.DetailedHelp)
46+
.Verify(
47+
new Scenario
48+
{
49+
When = {Args = "GroupedOptions -h"},
50+
Then =
51+
{
52+
Output = @"Usage: testhost.dll GroupedOptions [options]
53+
54+
Options:
55+
56+
--ungrouped <TEXT>
57+
An ungrouped option
58+
59+
Database:
60+
61+
--connection <TEXT>
62+
Connection string
63+
64+
--timeout <NUMBER>
65+
Timeout value
66+
67+
Logging:
68+
69+
--logLevel <TEXT>
70+
Log level
71+
72+
--logFile <TEXT>
73+
Log file path"
74+
}
75+
});
76+
}
77+
78+
[Fact]
79+
public void Groups_Are_Sorted_Alphabetically()
80+
{
81+
new AppRunner<App>(TestAppSettings.BasicHelp)
82+
.Verify(
83+
new Scenario
84+
{
85+
When = {Args = "AlphabeticalGroups -h"},
86+
Then =
87+
{
88+
Output = @"Usage: testhost.dll AlphabeticalGroups [options]
89+
90+
Options:
91+
--ungrouped Ungrouped option
92+
93+
Alpha:
94+
95+
--alphaOpt Alpha option
96+
97+
Beta:
98+
99+
--betaOpt Beta option
100+
101+
Zeta:
102+
103+
--zetaOpt Zeta option"
104+
}
105+
});
106+
}
107+
108+
[Fact]
109+
public void ArgumentModel_With_ArgumentGroupAttribute_Groups_All_Properties()
110+
{
111+
new AppRunner<App>(TestAppSettings.BasicHelp)
112+
.Verify(
113+
new Scenario
114+
{
115+
When = {Args = "ModelWithGroup -h"},
116+
Then =
117+
{
118+
Output = @"Usage: testhost.dll ModelWithGroup [options]
119+
120+
Options:
121+
--ungrouped Ungrouped option
122+
123+
Server Settings:
124+
125+
--Host Server host
126+
--Port Server port"
127+
}
128+
});
129+
}
130+
131+
[Fact]
132+
public void PropertyLevel_Group_Overrides_Model_Group()
133+
{
134+
new AppRunner<App>(TestAppSettings.BasicHelp)
135+
.Verify(
136+
new Scenario
137+
{
138+
When = {Args = "ModelWithPropertyOverride -h"},
139+
Then =
140+
{
141+
Output = @"Usage: testhost.dll ModelWithPropertyOverride [options]
142+
143+
Options:
144+
--ungrouped Ungrouped option
145+
146+
Database:
147+
148+
--Connection Connection string
149+
150+
Server Settings:
151+
152+
--Host Server host"
153+
}
154+
});
155+
}
156+
157+
[Fact]
158+
public void Nested_Models_Inherit_Parent_Group()
159+
{
160+
new AppRunner<App>(TestAppSettings.BasicHelp)
161+
.Verify(
162+
new Scenario
163+
{
164+
When = {Args = "NestedModelsInheritGroup -h"},
165+
Then =
166+
{
167+
Output = @"Usage: testhost.dll NestedModelsInheritGroup [options]
168+
169+
Server Settings:
170+
171+
--Host Server host
172+
--Port Server port
173+
--Timeout Connection timeout
174+
--Retries Connection retries"
175+
}
176+
});
177+
}
178+
179+
[Fact]
180+
public void Nested_Model_Can_Override_Parent_Group()
181+
{
182+
new AppRunner<App>(TestAppSettings.BasicHelp)
183+
.Verify(
184+
new Scenario
185+
{
186+
When = {Args = "NestedModelOverridesGroup -h"},
187+
Then =
188+
{
189+
Output = @"Usage: testhost.dll NestedModelOverridesGroup [options]
190+
191+
Advanced:
192+
193+
--Timeout Advanced timeout
194+
--Retries Advanced retries
195+
196+
Server Settings:
197+
198+
--Host Server host
199+
--Port Server port"
200+
}
201+
});
202+
}
203+
204+
[Fact]
205+
public void Operands_Are_Not_Grouped()
206+
{
207+
new AppRunner<App>(TestAppSettings.BasicHelp)
208+
.Verify(
209+
new Scenario
210+
{
211+
When = {Args = "MixedOptionsAndOperands -h"},
212+
Then =
213+
{
214+
Output = @"Usage: testhost.dll MixedOptionsAndOperands [options] <file>
215+
216+
Arguments:
217+
file Input file
218+
219+
Options:
220+
--ungrouped Ungrouped option
221+
222+
Processing:
223+
224+
--threads Thread count
225+
--memory Memory limit"
226+
}
227+
});
228+
}
229+
230+
[Fact]
231+
public void Multiple_Options_In_Same_Group()
232+
{
233+
new AppRunner<App>(TestAppSettings.BasicHelp)
234+
.Verify(
235+
new Scenario
236+
{
237+
When = {Args = "MultipleOptionsPerGroup -h"},
238+
Then =
239+
{
240+
Output = @"Usage: testhost.dll MultipleOptionsPerGroup [options]
241+
242+
Security:
243+
244+
--apiKey API key
245+
--apiSecret API secret
246+
--apiEndpoint API endpoint"
247+
}
248+
});
249+
}
250+
251+
private class App
252+
{
253+
public void GroupedOptions(
254+
[Option(Description = "An ungrouped option")] string ungrouped,
255+
[Option(Group = "Database", Description = "Connection string")] string connection,
256+
[Option(Group = "Database", Description = "Timeout value")] int timeout,
257+
[Option(Group = "Logging", Description = "Log level")] string logLevel,
258+
[Option(Group = "Logging", Description = "Log file path")] string logFile)
259+
{ }
260+
261+
public void AlphabeticalGroups(
262+
[Option(Description = "Ungrouped option")] string ungrouped,
263+
[Option(Group = "Zeta", Description = "Zeta option")] string zetaOpt,
264+
[Option(Group = "Alpha", Description = "Alpha option")] string alphaOpt,
265+
[Option(Group = "Beta", Description = "Beta option")] string betaOpt)
266+
{ }
267+
268+
public void ModelWithGroup(
269+
[Option(Description = "Ungrouped option")] string ungrouped,
270+
ServerSettingsModel settings)
271+
{ }
272+
273+
public void ModelWithPropertyOverride(
274+
[Option(Description = "Ungrouped option")] string ungrouped,
275+
ServerSettingsWithOverrideModel settings)
276+
{ }
277+
278+
public void NestedModelsInheritGroup(ServerSettingsWithNestedModel settings)
279+
{ }
280+
281+
public void NestedModelOverridesGroup(ServerSettingsWithNestedOverrideModel settings)
282+
{ }
283+
284+
public void MixedOptionsAndOperands(
285+
[Option(Description = "Ungrouped option")] string ungrouped,
286+
[Option(Group = "Processing", Description = "Thread count")] int threads,
287+
[Option(Group = "Processing", Description = "Memory limit")] int memory,
288+
[Operand(Description = "Input file")] string file)
289+
{ }
290+
291+
public void MultipleOptionsPerGroup(
292+
[Option(Group = "Security", Description = "API key")] string apiKey,
293+
[Option(Group = "Security", Description = "API secret")] string apiSecret,
294+
[Option(Group = "Security", Description = "API endpoint")] string apiEndpoint)
295+
{ }
296+
}
297+
298+
[ArgumentGroup("Server Settings")]
299+
private class ServerSettingsModel : IArgumentModel
300+
{
301+
[Option(Description = "Server host")]
302+
public string? Host { get; set; }
303+
304+
[Option(Description = "Server port")]
305+
public int Port { get; set; }
306+
}
307+
308+
[ArgumentGroup("Server Settings")]
309+
private class ServerSettingsWithOverrideModel : IArgumentModel
310+
{
311+
[Option(Description = "Server host")]
312+
public string? Host { get; set; }
313+
314+
[Option(Group = "Database", Description = "Connection string")]
315+
public string? Connection { get; set; }
316+
}
317+
318+
[ArgumentGroup("Server Settings")]
319+
private class ServerSettingsWithNestedModel : IArgumentModel
320+
{
321+
[Option(Description = "Server host")]
322+
public string? Host { get; set; }
323+
324+
[Option(Description = "Server port")]
325+
public int Port { get; set; }
326+
327+
public ConnectionSettingsModel? Connection { get; set; }
328+
}
329+
330+
// Nested model without ArgumentGroup - inherits from parent
331+
private class ConnectionSettingsModel : IArgumentModel
332+
{
333+
[Option(Description = "Connection timeout")]
334+
public int Timeout { get; set; }
335+
336+
[Option(Description = "Connection retries")]
337+
public int Retries { get; set; }
338+
}
339+
340+
[ArgumentGroup("Server Settings")]
341+
private class ServerSettingsWithNestedOverrideModel : IArgumentModel
342+
{
343+
[Option(Description = "Server host")]
344+
public string? Host { get; set; }
345+
346+
[Option(Description = "Server port")]
347+
public int Port { get; set; }
348+
349+
public AdvancedSettingsModel? Advanced { get; set; }
350+
}
351+
352+
// Nested model with its own ArgumentGroup - overrides parent
353+
[ArgumentGroup("Advanced")]
354+
private class AdvancedSettingsModel : IArgumentModel
355+
{
356+
[Option(Description = "Advanced timeout")]
357+
public int Timeout { get; set; }
358+
359+
[Option(Description = "Advanced retries")]
360+
public int Retries { get; set; }
361+
}
362+
}
363+

CommandDotNet/AppRunner.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using CommandDotNet.Execution;
88
using CommandDotNet.Extensions;
99
using CommandDotNet.Help;
10-
using CommandDotNet.Logging;
1110
using CommandDotNet.Parsing;
1211
using CommandDotNet.Rendering;
1312
using CommandDotNet.Tokens;

0 commit comments

Comments
 (0)