Skip to content

Commit 2a02d39

Browse files
authored
Allow user-defined options to shadow implicit ones (--help, --version) (#159)
1 parent c40b4f3 commit 2a02d39

File tree

6 files changed

+183
-25
lines changed

6 files changed

+183
-25
lines changed

CliFx.Tests/HelpTextSpecs.cs

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public async Task I_can_request_the_help_text_by_running_the_application_without
3434
}
3535

3636
[Fact]
37-
public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option()
37+
public async Task I_can_request_the_help_text_by_running_the_application_with_the_implicit_help_option()
3838
{
3939
// Arrange
4040
var commandType = DynamicCommandBuilder.Compile(
@@ -65,7 +65,7 @@ public class DefaultCommand : ICommand
6565
}
6666

6767
[Fact]
68-
public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option_even_if_the_default_command_is_not_defined()
68+
public async Task I_can_request_the_help_text_by_running_the_application_with_the_implicit_help_option_even_if_the_default_command_is_not_defined()
6969
{
7070
// Arrange
7171
var commandTypes = DynamicCommandBuilder.CompileMany(
@@ -102,7 +102,7 @@ public class NamedChildCommand : ICommand
102102
}
103103

104104
[Fact]
105-
public async Task I_can_request_the_help_text_for_a_specific_command_by_running_the_application_and_specifying_its_name_with_the_help_option()
105+
public async Task I_can_request_the_help_text_for_a_specific_command_by_running_the_application_and_specifying_its_name_with_the_implicit_help_option()
106106
{
107107
// Arrange
108108
var commandTypes = DynamicCommandBuilder.CompileMany(
@@ -147,7 +147,7 @@ public class NamedChildCommand : ICommand
147147
}
148148

149149
[Fact]
150-
public async Task I_can_request_the_help_text_for_a_specific_nested_command_by_running_the_application_and_specifying_its_name_with_the_help_option()
150+
public async Task I_can_request_the_help_text_for_a_specific_nested_command_by_running_the_application_and_specifying_its_name_with_the_implicit_help_option()
151151
{
152152
// Arrange
153153
var commandTypes = DynamicCommandBuilder.CompileMany(
@@ -476,7 +476,7 @@ public class Command : ICommand
476476
}
477477

478478
[Fact]
479-
public async Task I_can_request_the_help_text_to_see_the_help_and_version_options()
479+
public async Task I_can_request_the_help_text_to_see_the_help_and_implicit_version_options()
480480
{
481481
// Arrange
482482
var commandType = DynamicCommandBuilder.Compile(
@@ -515,7 +515,7 @@ public class Command : ICommand
515515
}
516516

517517
[Fact]
518-
public async Task I_can_request_the_help_text_on_a_named_command_to_see_the_help_option()
518+
public async Task I_can_request_the_help_text_on_a_named_command_to_see_the_implicit_help_option()
519519
{
520520
// Arrange
521521
var commandType = DynamicCommandBuilder.Compile(
@@ -974,7 +974,7 @@ public class SecondCommandSecondChildCommand : ICommand
974974
}
975975

976976
[Fact]
977-
public async Task I_can_request_the_version_text_by_running_the_application_with_the_version_option()
977+
public async Task I_can_request_the_version_text_by_running_the_application_with_the_implicit_version_option()
978978
{
979979
// Arrange
980980
var application = new CliApplicationBuilder()
@@ -992,4 +992,72 @@ public async Task I_can_request_the_version_text_by_running_the_application_with
992992
var stdOut = FakeConsole.ReadOutputString();
993993
stdOut.Trim().Should().Be("v6.9");
994994
}
995+
996+
[Fact]
997+
public async Task I_cannot_request_the_help_text_by_running_the_application_with_the_implicit_help_option_if_there_is_an_option_with_the_same_identifier()
998+
{
999+
// Arrange
1000+
var commandType = DynamicCommandBuilder.Compile(
1001+
// lang=csharp
1002+
"""
1003+
[Command]
1004+
public class DefaultCommand : ICommand
1005+
{
1006+
[CommandOption("help", 'h')]
1007+
public string? Foo { get; init; }
1008+
1009+
public ValueTask ExecuteAsync(IConsole console) => default;
1010+
}
1011+
"""
1012+
);
1013+
1014+
var application = new CliApplicationBuilder()
1015+
.AddCommand(commandType)
1016+
.UseConsole(FakeConsole)
1017+
.SetDescription("This will be in help text")
1018+
.Build();
1019+
1020+
// Act
1021+
var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>());
1022+
1023+
// Assert
1024+
exitCode.Should().Be(0);
1025+
1026+
var stdOut = FakeConsole.ReadOutputString();
1027+
stdOut.Should().NotContain("This will be in help text");
1028+
}
1029+
1030+
[Fact]
1031+
public async Task I_cannot_request_the_version_text_by_running_the_application_with_the_implicit_version_option_if_there_is_an_option_with_the_same_identifier()
1032+
{
1033+
// Arrange
1034+
var commandType = DynamicCommandBuilder.Compile(
1035+
// lang=csharp
1036+
"""
1037+
[Command]
1038+
public class DefaultCommand : ICommand
1039+
{
1040+
[CommandOption("version")]
1041+
public string? Foo { get; init; }
1042+
1043+
public ValueTask ExecuteAsync(IConsole console) => default;
1044+
}
1045+
"""
1046+
);
1047+
1048+
var application = new CliApplicationBuilder()
1049+
.AddCommand(commandType)
1050+
.SetVersion("v6.9")
1051+
.UseConsole(FakeConsole)
1052+
.Build();
1053+
1054+
// Act
1055+
var exitCode = await application.RunAsync(["--version"], new Dictionary<string, string>());
1056+
1057+
// Assert
1058+
exitCode.Should().Be(0);
1059+
1060+
var stdOut = FakeConsole.ReadOutputString();
1061+
stdOut.Trim().Should().NotBe("v6.9");
1062+
}
9951063
}

CliFx.Tests/OptionBindingSpecs.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,86 @@ public ValueTask ExecuteAsync(IConsole console)
587587
stdOut.Trim().Should().Be("-13");
588588
}
589589

590+
[Fact]
591+
public async Task I_can_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_help_option_and_get_the_correct_value()
592+
{
593+
// Arrange
594+
var commandType = DynamicCommandBuilder.Compile(
595+
// lang=csharp
596+
"""
597+
[Command]
598+
public class Command : ICommand
599+
{
600+
[CommandOption("help", 'h')]
601+
public string? Foo { get; init; }
602+
603+
public ValueTask ExecuteAsync(IConsole console)
604+
{
605+
console.WriteLine(Foo);
606+
return default;
607+
}
608+
}
609+
"""
610+
);
611+
612+
var application = new CliApplicationBuilder()
613+
.AddCommand(commandType)
614+
.UseConsole(FakeConsole)
615+
.Build();
616+
617+
// Act
618+
var exitCode = await application.RunAsync(
619+
["--help", "me"],
620+
new Dictionary<string, string>()
621+
);
622+
623+
// Assert
624+
exitCode.Should().Be(0);
625+
626+
var stdOut = FakeConsole.ReadOutputString();
627+
stdOut.Trim().Should().Be("me");
628+
}
629+
630+
[Fact]
631+
public async Task I_can_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_version_option_and_get_the_correct_value()
632+
{
633+
// Arrange
634+
var commandType = DynamicCommandBuilder.Compile(
635+
// lang=csharp
636+
"""
637+
[Command]
638+
public class Command : ICommand
639+
{
640+
[CommandOption("version")]
641+
public string? Foo { get; init; }
642+
643+
public ValueTask ExecuteAsync(IConsole console)
644+
{
645+
console.WriteLine(Foo);
646+
return default;
647+
}
648+
}
649+
"""
650+
);
651+
652+
var application = new CliApplicationBuilder()
653+
.AddCommand(commandType)
654+
.UseConsole(FakeConsole)
655+
.Build();
656+
657+
// Act
658+
var exitCode = await application.RunAsync(
659+
["--version", "1.2.0"],
660+
new Dictionary<string, string>()
661+
);
662+
663+
// Assert
664+
exitCode.Should().Be(0);
665+
666+
var stdOut = FakeConsole.ReadOutputString();
667+
stdOut.Trim().Should().Be("1.2.0");
668+
}
669+
590670
[Fact]
591671
public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument()
592672
{

CliFx/CliApplication.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.Linq;
5-
using System.Runtime.InteropServices;
65
using System.Threading.Tasks;
76
using CliFx.Exceptions;
87
using CliFx.Formatting;
@@ -43,14 +42,14 @@ private bool IsPreviewModeEnabled(CommandInput commandInput) =>
4342
Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified;
4443

4544
private bool ShouldShowHelpText(CommandSchema commandSchema, CommandInput commandInput) =>
46-
commandSchema.IsHelpOptionAvailable && commandInput.IsHelpOptionSpecified
45+
commandSchema.IsImplicitHelpOptionAvailable && commandInput.IsHelpOptionSpecified
4746
||
4847
// Show help text also if the fallback default command is executed without any arguments
4948
commandSchema == FallbackDefaultCommand.Schema
5049
&& !commandInput.HasArguments;
5150

5251
private bool ShouldShowVersionText(CommandSchema commandSchema, CommandInput commandInput) =>
53-
commandSchema.IsVersionOptionAvailable && commandInput.IsVersionOptionSpecified;
52+
commandSchema.IsImplicitVersionOptionAvailable && commandInput.IsVersionOptionSpecified;
5453

5554
private async ValueTask PromptDebuggerAsync()
5655
{

CliFx/Input/OptionInput.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ internal class OptionInput(string identifier, IReadOnlyList<string> values)
99

1010
public IReadOnlyList<string> Values { get; } = values;
1111

12-
public bool IsHelpOption => OptionSchema.HelpOption.MatchesIdentifier(Identifier);
12+
public bool IsHelpOption => OptionSchema.ImplicitHelpOption.MatchesIdentifier(Identifier);
1313

14-
public bool IsVersionOption => OptionSchema.VersionOption.MatchesIdentifier(Identifier);
14+
public bool IsVersionOption => OptionSchema.ImplicitVersionOption.MatchesIdentifier(Identifier);
1515

1616
public string GetFormattedIdentifier() =>
1717
Identifier switch

CliFx/Schema/CommandSchema.cs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ IReadOnlyList<OptionSchema> options
2828

2929
public bool IsDefault => string.IsNullOrWhiteSpace(Name);
3030

31-
public bool IsHelpOptionAvailable => Options.Contains(OptionSchema.HelpOption);
31+
public bool IsImplicitHelpOptionAvailable => Options.Contains(OptionSchema.ImplicitHelpOption);
3232

33-
public bool IsVersionOptionAvailable => Options.Contains(OptionSchema.VersionOption);
33+
public bool IsImplicitVersionOptionAvailable =>
34+
Options.Contains(OptionSchema.ImplicitVersionOption);
3435

3536
public bool MatchesName(string? name) =>
3637
!string.IsNullOrWhiteSpace(Name)
@@ -74,10 +75,6 @@ public static bool IsCommandType(Type type) =>
7475
var name = attribute?.Name?.Trim();
7576
var description = attribute?.Description?.Trim();
7677

77-
var implicitOptionSchemas = string.IsNullOrWhiteSpace(name)
78-
? new[] { OptionSchema.HelpOption, OptionSchema.VersionOption }
79-
: new[] { OptionSchema.HelpOption };
80-
8178
var properties = type
8279
// Get properties directly on the command type
8380
.GetProperties()
@@ -103,11 +100,25 @@ p.GetMethod is not null
103100
.WhereNotNull()
104101
.ToArray();
105102

106-
var optionSchemas = properties
107-
.Select(OptionSchema.TryResolve)
108-
.WhereNotNull()
109-
.Concat(implicitOptionSchemas)
110-
.ToArray();
103+
var optionSchemas = properties.Select(OptionSchema.TryResolve).WhereNotNull().ToList();
104+
105+
// Include implicit options, if appropriate
106+
var isImplicitHelpOptionAvailable =
107+
// If the command implements its own help option, don't include the implicit one
108+
!optionSchemas.Any(o => o.MatchesShortName('h') || o.MatchesName("help"));
109+
110+
if (isImplicitHelpOptionAvailable)
111+
optionSchemas.Add(OptionSchema.ImplicitHelpOption);
112+
113+
var isImplicitVersionOptionAvailable =
114+
// Only the default command can have the version option
115+
string.IsNullOrWhiteSpace(name)
116+
&&
117+
// If the command implements its own version option, don't include the implicit one
118+
!optionSchemas.Any(o => o.MatchesName("version"));
119+
120+
if (isImplicitVersionOptionAvailable)
121+
optionSchemas.Add(OptionSchema.ImplicitVersionOption);
111122

112123
return new CommandSchema(type, name, description, parameterSchemas, optionSchemas);
113124
}

CliFx/Schema/OptionSchema.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ internal partial class OptionSchema
103103

104104
internal partial class OptionSchema
105105
{
106-
public static OptionSchema HelpOption { get; } =
106+
public static OptionSchema ImplicitHelpOption { get; } =
107107
new(
108108
NullPropertyDescriptor.Instance,
109109
"help",
@@ -115,7 +115,7 @@ internal partial class OptionSchema
115115
Array.Empty<Type>()
116116
);
117117

118-
public static OptionSchema VersionOption { get; } =
118+
public static OptionSchema ImplicitVersionOption { get; } =
119119
new(
120120
NullPropertyDescriptor.Instance,
121121
"version",

0 commit comments

Comments
 (0)