Skip to content

Commit 2db5ead

Browse files
committed
add global options, make -h a proper option
1 parent ad1ef32 commit 2db5ead

File tree

10 files changed

+169
-170
lines changed

10 files changed

+169
-170
lines changed

src/System.CommandLine.DragonFruit.Tests/CommandLineTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.CommandLine.Rendering;
5-
using System.Reflection;
65
using System.Threading.Tasks;
76
using FluentAssertions;
87
using Xunit;
@@ -64,7 +63,7 @@ public async Task It_shows_help_text_based_on_XML_documentation_comments()
6463
.Contain("<args> These are arguments")
6564
.And.Contain("Arguments:");
6665
stdOut.Should()
67-
.Contain("--name <name> Specifies the name option")
66+
.ContainAll("--name <name>", "Specifies the name option")
6867
.And.Contain("Options:");
6968
stdOut.Should()
7069
.Contain("Help for the test program");
@@ -88,7 +87,7 @@ public void It_synchronously_shows_help_text_based_on_XML_documentation_comments
8887
.Contain("<args> These are arguments")
8988
.And.Contain("Arguments:");
9089
stdOut.Should()
91-
.Contain("--name <name> Specifies the name option")
90+
.ContainAll("--name <name>","Specifies the name option")
9291
.And.Contain("Options:");
9392
stdOut.Should()
9493
.Contain("Help for the test program");

src/System.CommandLine.Tests/CommandTests.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,44 @@ public void When_multiple_options_are_configured_then_they_must_differ_by_name()
323323
var command = new Command("the-command")
324324
{
325325
new Option("--same")
326-
{
327-
Argument = new Argument<string>()
328-
}
329326
};
330327

331328
command
332-
.Invoking(c => c.Add(new Option("--same")
333-
{
334-
Argument = new Argument<string>()
335-
}))
329+
.Invoking(c => c.Add(new Option("--same")))
330+
.Should()
331+
.Throw<ArgumentException>()
332+
.And
333+
.Message
334+
.Should()
335+
.Be("Alias '--same' is already in use.");
336+
}
337+
338+
[Fact]
339+
public void When_global_options_are_added_then_they_must_differ_from_local_options_by_name()
340+
{
341+
var command = new Command("the-command")
342+
{
343+
new Option("--same")
344+
};
345+
346+
command
347+
.Invoking(c => c.AddGlobalOption(new Option("--same")))
348+
.Should()
349+
.Throw<ArgumentException>()
350+
.And
351+
.Message
352+
.Should()
353+
.Be("Alias '--same' is already in use.");
354+
}
355+
356+
[Fact]
357+
public void When_local_options_are_added_then_they_must_differ_from_global_options_by_name()
358+
{
359+
var command = new Command("the-command");
360+
command.AddGlobalOption(new Option("--same"));
361+
362+
command
363+
.Invoking(c => c.Add(new Option("--same")))
336364
.Should()
337365
.Throw<ArgumentException>()
338366
.And

src/System.CommandLine.Tests/ParserTests.cs

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,26 +1625,6 @@ public void Arguments_can_match_the_aliases_of_sibling_options(string input)
16251625
valueForOption.Should().Be("-y");
16261626
}
16271627

1628-
[Theory]
1629-
[InlineData("/o")]
1630-
[InlineData("-o")]
1631-
[InlineData("--o")]
1632-
[InlineData("/output")]
1633-
[InlineData("-output")]
1634-
[InlineData("--output")]
1635-
public void Option_aliases_can_be_specified_and_are_prefixed_with_defaults(string input)
1636-
{
1637-
var option = new Option(new[] { "output", "o" });
1638-
var configuration = new CommandLineConfiguration(
1639-
new[] { option },
1640-
prefixes: new[] { "-", "--", "/" });
1641-
var parser = new Parser(configuration);
1642-
1643-
ParseResult parseResult = parser.Parse(input);
1644-
parseResult["output"].Should().NotBeNull();
1645-
parseResult["o"].Should().NotBeNull();
1646-
}
1647-
16481628
[Fact]
16491629
public void Option_aliases_do_not_need_to_be_prefixed()
16501630
{
@@ -1655,27 +1635,6 @@ public void Option_aliases_do_not_need_to_be_prefixed()
16551635
result.HasOption(option).Should().BeTrue();
16561636
}
16571637

1658-
[Theory]
1659-
[InlineData("/o")]
1660-
[InlineData("-o")]
1661-
[InlineData("--output")]
1662-
[InlineData("--out")]
1663-
[InlineData("-out")]
1664-
[InlineData("/out")]
1665-
public void Option_aliases_can_be_specified_for_particular_prefixes(string input)
1666-
{
1667-
var option = new Option(new[] { "--output", "-o", "/o", "out" });
1668-
var configuration = new CommandLineConfiguration(
1669-
new[] { option },
1670-
prefixes: new[] { "-", "--", "/" });
1671-
var parser = new Parser(configuration);
1672-
1673-
ParseResult parseResult = parser.Parse(input);
1674-
parseResult["o"].Should().NotBeNull();
1675-
parseResult["out"].Should().NotBeNull();
1676-
parseResult["output"].Should().NotBeNull();
1677-
}
1678-
16791638
[Fact]
16801639
public void Boolean_options_with_no_argument_specified_do_not_match_subsequent_arguments()
16811640
{

src/System.CommandLine.Tests/UseHelpTests.cs

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,6 @@ public async Task UseHelp_interrupts_execution_of_the_specified_command()
5555
wasCalled.Should().BeFalse();
5656
}
5757

58-
[Fact]
59-
public async Task UseHelp_allows_help_for_all_configured_prefixes()
60-
{
61-
var parser =
62-
new CommandLineBuilder()
63-
.AddCommand(new Command("command"))
64-
.UseHelp()
65-
.UsePrefixes(new[] { "~" })
66-
.Build();
67-
68-
await parser.InvokeAsync("command ~help", _console);
69-
70-
_console.Out.ToString().Should().StartWith("Usage:");
71-
}
72-
7358
[Theory]
7459
[InlineData("-h")]
7560
[InlineData("--help")]
@@ -89,12 +74,12 @@ public async Task UseHelp_accepts_default_values(string value)
8974
}
9075

9176
[Fact]
92-
public async Task UseHelp_accepts_collection_of_help_options()
77+
public async Task UseHelp_accepts_custom_aliases()
9378
{
9479
var parser =
9580
new CommandLineBuilder()
9681
.AddCommand(new Command("command"))
97-
.UseHelp(new[] { "~cthulhu" })
82+
.UseHelp(new Option(new[] { "~cthulhu" }))
9883
.Build();
9984

10085
await parser.InvokeAsync("command ~cthulhu", _console);
@@ -120,5 +105,38 @@ public async Task UseHelp_does_not_display_when_option_defined_with_same_alias()
120105

121106
_console.Out.ToString().Should().BeEmpty();
122107
}
108+
109+
[Fact]
110+
public void There_are_no_parse_errors_when_help_is_invoked_on_root_command()
111+
{
112+
var parser = new CommandLineBuilder()
113+
.UseDefaults()
114+
.Build();
115+
116+
var result = parser.Parse("-h");
117+
118+
result.Errors
119+
.Should()
120+
.BeEmpty();
121+
}
122+
123+
[Fact]
124+
public void There_are_no_parse_errors_when_help_is_invoked_on_subcommand()
125+
{
126+
var command = new RootCommand
127+
{
128+
new Command("subcommand")
129+
};
130+
131+
var parser = new CommandLineBuilder(command)
132+
.UseDefaults()
133+
.Build();
134+
135+
var result = parser.Parse("subcommand -h");
136+
137+
result.Errors
138+
.Should()
139+
.BeEmpty();
140+
}
123141
}
124142
}

src/System.CommandLine/Builder/CommandLineBuilder.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,19 @@ public CommandLineBuilder(Command rootCommand = null)
2323

2424
public bool EnablePosixBundling { get; set; } = true;
2525

26-
public IReadOnlyCollection<string> Prefixes { get; set; }
27-
2826
public ResponseFileHandling ResponseFileHandling { get; set; }
2927

3028
internal Func<BindingContext, IHelpBuilder> HelpBuilderFactory { get; set; }
3129

30+
internal Option HelpOption { get; set; }
31+
3232
public Parser Build()
3333
{
3434
var rootCommand = Command;
3535

3636
return new Parser(
3737
new CommandLineConfiguration(
3838
new[] { rootCommand },
39-
prefixes: Prefixes,
4039
enablePosixBundling: EnablePosixBundling,
4140
enableDirectives: EnableDirectives,
4241
validationMessages: ValidationMessages.Instance,

src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs

Lines changed: 18 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -268,50 +268,35 @@ void Default(Exception exception, InvocationContext context)
268268

269269
public static CommandLineBuilder UseHelp(this CommandLineBuilder builder)
270270
{
271-
builder.AddMiddleware(async (context, next) =>
271+
var helpOption = new Option(new []
272272
{
273-
var helpOptionTokens = new HashSet<string>();
274-
var prefixes = context.Parser.Configuration.Prefixes;
275-
if (prefixes == null)
276-
{
277-
helpOptionTokens.Add("-h");
278-
helpOptionTokens.Add("/h");
279-
helpOptionTokens.Add("--help");
280-
helpOptionTokens.Add("-?");
281-
helpOptionTokens.Add("/?");
282-
}
283-
else
284-
{
285-
string[] helpOptionNames = { "help", "h", "?" };
286-
foreach (var helpOption in helpOptionNames)
287-
{
288-
foreach (var prefix in prefixes)
289-
{
290-
helpOptionTokens.Add($"{prefix}{helpOption}");
291-
}
292-
}
293-
}
294-
295-
if (!ShowHelp(context, helpOptionTokens))
296-
{
297-
await next(context);
298-
}
299-
}, MiddlewareOrderInternal.HelpOption);
273+
"-h",
274+
"/h",
275+
"--help",
276+
"-?",
277+
"/?"
278+
});
300279

301-
return builder;
280+
return builder.UseHelp(helpOption);
302281
}
303282

304283
public static CommandLineBuilder UseHelp(
305284
this CommandLineBuilder builder,
306-
IReadOnlyCollection<string> helpOptionTokens)
285+
Option helpOption)
307286
{
287+
builder.HelpOption = helpOption ?? throw new ArgumentNullException(nameof(helpOption));
288+
builder.HelpOption.Argument.Arity = ArgumentArity.Zero;
289+
290+
builder.Command.AddGlobalOption(builder.HelpOption);
291+
308292
builder.AddMiddleware(async (context, next) =>
309293
{
310-
if (!ShowHelp(context, helpOptionTokens))
294+
if (!ShowHelp(context, builder.HelpOption))
311295
{
312296
await next(context);
313297
}
314298
}, MiddlewareOrderInternal.HelpOption);
299+
315300
return builder;
316301
}
317302

@@ -387,13 +372,6 @@ public static CommandLineBuilder UseParseErrorReporting(
387372
return builder;
388373
}
389374

390-
public static TBuilder UsePrefixes<TBuilder>(this TBuilder builder, IReadOnlyCollection<string> prefixes)
391-
where TBuilder : CommandLineBuilder
392-
{
393-
builder.Prefixes = prefixes;
394-
return builder;
395-
}
396-
397375
public static CommandLineBuilder UseSuggestDirective(
398376
this CommandLineBuilder builder)
399377
{
@@ -471,26 +449,15 @@ public static CommandLineBuilder UseVersionOption(
471449

472450
private static bool ShowHelp(
473451
InvocationContext context,
474-
IReadOnlyCollection<string> helpOptionAliases)
452+
IOption helpOption)
475453
{
476-
var lastToken = context.ParseResult.Tokens.LastOrDefault();
477-
478-
if (helpOptionAliases.Contains(lastToken?.Value) &&
479-
!TokenIsDefinedInSyntax())
454+
if (context.ParseResult.FindResultFor(helpOption) != null)
480455
{
481456
context.InvocationResult = new HelpResult();
482457
return true;
483458
}
484459

485460
return false;
486-
487-
bool TokenIsDefinedInSyntax() =>
488-
context.Parser
489-
.Configuration
490-
.Symbols
491-
.FlattenBreadthFirst(s => s.Children)
492-
.SelectMany(s => s.RawAliases)
493-
.Any(helpOptionAliases.Contains);
494461
}
495462
}
496463
}

0 commit comments

Comments
 (0)