Skip to content

Fix #2128 #2656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ Arguments:
<Read|ReadWrite|Write> the-root-arg-enum-default-description [default: Read]

Options:
-trna, --the-root-option-no-arg (REQUIRED) the-root-option-no-arg-description
-trondda, --the-root-option-no-description-default-arg [default: the-root-option--no-description-default-arg-value]
-tronda, --the-root-option-no-default-arg <the-root-option-arg-no-default-arg> (REQUIRED) the-root-option-no-default-description
-troda, --the-root-option-default-arg <the-root-option-arg> the-root-option-default-arg-description [default: the-root-option-arg-value]
-troea, --the-root-option-enum-arg <Read|ReadWrite|Write> the-root-option-description [default: Read]
-trorea, --the-root-option-required-enum-arg <Read|ReadWrite|Write> (REQUIRED) the-root-option-description [default: Read]
-tromld, --the-root-option-multi-line-description the-root-option
multi-line
description
-trna, --the-root-option-no-arg (REQUIRED) the-root-option-no-arg-description
-trondda, --the-root-option-no-description-default-arg <the-root-option-no-description-default-arg> [default: the-root-option--no-description-default-arg-value]
-tronda, --the-root-option-no-default-arg <the-root-option-arg-no-default-arg> (REQUIRED) the-root-option-no-default-description
-troda, --the-root-option-default-arg <the-root-option-arg> the-root-option-default-arg-description [default: the-root-option-arg-value]
-troea, --the-root-option-enum-arg <Read|ReadWrite|Write> the-root-option-description [default: Read]
-trorea, --the-root-option-required-enum-arg <Read|ReadWrite|Write> (REQUIRED) the-root-option-description [default: Read]
-tromld, --the-root-option-multi-line-description the-root-option
multi-line
description

Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public void Option_can_customize_displayed_default_value()
_helpBuilder.Write(command, _console);
var expected =
$"Options:{NewLine}" +
$"{_indentation}--the-option{_columnPadding}[default: 42]{NewLine}{NewLine}";
$"{_indentation}--the-option <the-option>{_columnPadding}[default: 42]{NewLine}{NewLine}";

_console.ToString().Should().Contain(expected);
}
Expand Down Expand Up @@ -245,9 +245,9 @@ public void Customize_throws_when_symbol_is_null()


[Theory]
[InlineData(false, false, "--option \\s*description")]
[InlineData(false, false, "--option <option>\\s*description")]
[InlineData(true, false, "custom 1st\\s*description")]
[InlineData(false, true, "--option \\s*custom 2nd")]
[InlineData(false, true, "--option <option>\\s*custom 2nd")]
[InlineData(true, true, "custom 1st\\s*custom 2nd")]
public void Option_can_fallback_to_default_when_customizing(bool conditionA, bool conditionB, string expected)
{
Expand Down
32 changes: 24 additions & 8 deletions src/System.CommandLine.Tests/Help/HelpBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,7 @@ public void Options_section_properly_wraps_description_when_long_default_value_i
new Option<string>("--aaa", "-a")
{
Description = longOptionText,
DefaultValueFactory = (_) => "the quick brown fox jumps over the lazy dog"
DefaultValueFactory = _ => "the quick brown fox jumps over the lazy dog"
},
new Option<string>("-y") { Description = "Option with a short description" },
};
Expand All @@ -1163,9 +1163,9 @@ public void Options_section_properly_wraps_description_when_long_default_value_i
helpBuilder.Write(command, _console);

var expected =
$"{_indentation}-a, --aaa{_columnPadding}The option whose description is long enough that it {NewLine}" +
$"{_indentation} {_columnPadding}wraps to a new line [default: the quick brown fox jumps {NewLine}" +
$"{_indentation} {_columnPadding}over the lazy dog]{NewLine}";
$"{_indentation}-a, --aaa <aaa>{_columnPadding}The option whose description is long enough that {NewLine}" +
$"{_indentation} {_columnPadding}it wraps to a new line [default: the quick brown {NewLine}" +
$"{_indentation} {_columnPadding}fox jumps over the lazy dog]{NewLine}";

_console.ToString().Should().Contain(expected);
}
Expand Down Expand Up @@ -1313,6 +1313,22 @@ public void Option_aliases_are_shown_before_long_names_regardless_of_alphabetica
_console.ToString().Should().Contain("-a, -z, --aaa, --zzz");
}

[Fact] // https://github.com/dotnet/command-line-api/issues/2128
public void Option_argument_name_uses_option_name_when_help_name_is_not_specified()
{
var command = new RootCommand
{
new Option<string>("--name", "-n")
{
Description = "The description"
}
};

_helpBuilder.Write(command, _console);

_console.ToString().Should().Contain("-n, --name <name> The description");
}

[Fact]
public void Help_describes_default_value_for_option_with_argument_having_default_value()
{
Expand Down Expand Up @@ -1341,14 +1357,14 @@ public void Option_arguments_with_default_values_that_are_enumerable_display_pip
{
new Option<List<int>>("--filter-size")
{
DefaultValueFactory = (_) => new List<int> { 0, 2, 4 }
DefaultValueFactory = _ => [0, 2, 4]
}
};

_helpBuilder.Write(command, _console);
var expected =
$"Options:{NewLine}" +
$"{_indentation}--filter-size{_columnPadding}[default: 0|2|4]{NewLine}{NewLine}";
$"{_indentation}--filter-size <filter-size>{_columnPadding}[default: 0|2|4]{NewLine}{NewLine}";

_console.ToString().Should().Contain(expected);
}
Expand All @@ -1360,14 +1376,14 @@ public void Option_arguments_with_default_values_that_are_array_display_pipe_del
{
new Option<string[]>("--prefixes")
{
DefaultValueFactory = (_) => new[]{ "^(TODO|BUG)", "^HACK" }
DefaultValueFactory = _ => new[]{ "^(TODO|BUG)", "^HACK" }
}
};

_helpBuilder.Write(command, _console);
var expected =
$"Options:{NewLine}" +
$"{_indentation}--prefixes{_columnPadding}[default: ^(TODO|BUG)|^HACK]{NewLine}{NewLine}";
$"{_indentation}--prefixes <prefixes>{_columnPadding}[default: ^(TODO|BUG)|^HACK]{NewLine}{NewLine}";

_console.ToString().Should().Contain(expected);
}
Expand Down
4 changes: 2 additions & 2 deletions src/System.CommandLine.Tests/HelpOptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ public void Help_and_version_options_are_displayed_after_other_options_on_root_c
.Should()
.ContainInOrder([
"Options:",
"-i The option",
"-i <i> The option",
"-?, -h, --help Show help and usage information",
"--version Show version information",
"Commands:"
Expand Down Expand Up @@ -356,7 +356,7 @@ public void Help_and_version_options_are_displayed_after_other_options_on_subcom
.Should()
.ContainInOrder([
"Options:",
"-i The option",
"-i <i> The option",
"-?, -h, --help Show help and usage information",
]);
}
Expand Down
106 changes: 93 additions & 13 deletions src/System.CommandLine.Tests/Invocation/InvocationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,35 +265,112 @@ public void Nonterminating_option_action_does_not_short_circuit_command_action()
commandActionWasCalled.Should().BeTrue();
}

[Fact]
public void When_multiple_options_with_actions_are_present_then_only_the_last_one_is_invoked()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task When_multiple_options_with_terminating_actions_are_present_then_only_the_last_one_is_invoked(bool invokeAsync)
{
bool optionAction1WasCalled = false;
bool optionAction2WasCalled = false;
bool optionAction3WasCalled = false;

SynchronousTestAction optionAction1 = new(_ => optionAction1WasCalled = true);
SynchronousTestAction optionAction2 = new(_ => optionAction2WasCalled = true);
SynchronousTestAction optionAction3 = new(_ => optionAction3WasCalled = true);
SynchronousTestAction optionAction1 = new(_ =>
{
optionAction1WasCalled = true;
}, terminating: true);
SynchronousTestAction optionAction2 = new(_ =>
{
optionAction2WasCalled = true;
}, terminating: true);
SynchronousTestAction optionAction3 = new(_ =>
{
optionAction3WasCalled = true;
}, terminating: true);

Command command = new Command("cmd")
var command = new RootCommand
{
new Option<bool>("--1") { Action = optionAction1 },
new Option<bool>("--2") { Action = optionAction2 },
new Option<bool>("--3") { Action = optionAction3 }
Action = new AsynchronousTestAction(_ => {}),
Options =
{
new Option<bool>("--1") { Action = optionAction1 },
new Option<bool>("--2") { Action = optionAction2 },
new Option<bool>("--3") { Action = optionAction3 },
}
};

ParseResult parseResult = command.Parse("cmd --1 true --3 false --2 true");
ParseResult parseResult = command.Parse("--1 --3 --2");

using var _ = new AssertionScope();

parseResult.Action.Should().Be(optionAction2);
parseResult.Invoke().Should().Be(0);

if (invokeAsync)
{
(await parseResult.InvokeAsync()).Should().Be(0);
}
else
{
parseResult.Invoke().Should().Be(0);
}

optionAction1WasCalled.Should().BeFalse();
optionAction2WasCalled.Should().BeTrue();
optionAction3WasCalled.Should().BeFalse();
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task When_multiple_options_with_nonterminating_actions_are_present_then_all_are_invoked(bool invokeAsync)
{
bool optionAction1WasCalled = false;
bool optionAction2WasCalled = false;
bool optionAction3WasCalled = false;
bool commandActionWasCalled = false;

SynchronousTestAction optionAction1 = new(_ =>
{
optionAction1WasCalled = true;
}, terminating: false);
SynchronousTestAction optionAction2 = new(_ =>
{
optionAction2WasCalled = true;
}, terminating: false);
SynchronousTestAction optionAction3 = new(_ =>
{
optionAction3WasCalled = true;
}, terminating: false);

var command = new RootCommand
{
Action = new AsynchronousTestAction(_ => commandActionWasCalled = true),
Options =
{
new Option<bool>("--1") { Action = optionAction1 },
new Option<bool>("--2") { Action = optionAction2 },
new Option<bool>("--3") { Action = optionAction3 },
}
};

ParseResult parseResult = command.Parse("--1 true --3 false --2 true");

using var _ = new AssertionScope();

if (invokeAsync)
{
(await parseResult.InvokeAsync()).Should().Be(0);
}
else
{
parseResult.Invoke().Should().Be(0);
}

optionAction1WasCalled.Should().BeTrue();
optionAction2WasCalled.Should().BeTrue();
optionAction3WasCalled.Should().BeTrue();
commandActionWasCalled.Should().BeTrue();
}

[Fact]
public void Directive_action_takes_precedence_over_option_action()
{
Expand Down Expand Up @@ -327,9 +404,12 @@ public void Directive_action_takes_precedence_over_option_action()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Nontermninating_option_actions_handle_exceptions_and_return_an_error_return_code(bool invokeAsync)
public async Task Nonterminating_option_actions_handle_exceptions_and_return_an_error_return_code(bool invokeAsync)
{
var nonexclusiveAction = new SynchronousTestAction(_ => throw new Exception("oops!"), terminating: false);
var nonexclusiveAction = new SynchronousTestAction(_ =>
{
throw new Exception("oops!");
}, terminating: false);

var command = new RootCommand
{
Expand Down
42 changes: 29 additions & 13 deletions src/System.CommandLine/Help/HelpBuilder.Default.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,44 @@ public static string GetArgumentUsageLabel(Symbol parameter)
_ => throw new InvalidOperationException()
};

static string? GetUsageLabel(string? helpName, Type valueType, List<Func<CompletionContext, IEnumerable<CompletionItem>>> completionSources, Symbol symbol, ArgumentArity arity)
static string? GetUsageLabel(
string? helpName,
Type valueType,
List<Func<CompletionContext, IEnumerable<CompletionItem>>> completionSources,
Symbol symbol,
ArgumentArity arity)
{
// Argument.HelpName is always first choice
if (!string.IsNullOrWhiteSpace(helpName))
{
return $"<{helpName}>";
}
else if (
!(valueType == typeof(bool) || valueType == typeof(bool?))
&& arity.MaximumNumberOfValues > 0 // allowing zero arguments means we don't need to show usage
&& completionSources.Count > 0)
{
IEnumerable<string> completions = symbol
.GetCompletions(CompletionContext.Empty)
.Select(item => item.Label);

string joined = string.Join("|", completions);
if (valueType == typeof(bool) ||
valueType == typeof(bool?) ||
arity.MaximumNumberOfValues <= 0) // allowing zero arguments means we don't need to show usage
{
return null;
}

if (!string.IsNullOrEmpty(joined))
if (completionSources.Count <= 0)
{
if (symbol is Option)
{
return $"<{joined}>";
return $"<{symbol.Name.Trim('-', '/')}>";
}

return null;
}

IEnumerable<string> completions = symbol
.GetCompletions(CompletionContext.Empty)
.Select(item => item.Label);

string joined = string.Join("|", completions);

if (!string.IsNullOrEmpty(joined))
{
return $"<{joined}>";
}

return null;
Expand Down
2 changes: 1 addition & 1 deletion src/System.CommandLine/ParseResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal ParseResult(
}
else
{
Tokens = Array.Empty<Token>();
Tokens = [];
}

CommandLineText = commandLineText;
Expand Down