Skip to content

Commit

Permalink
Fix help message for nullable options/args
Browse files Browse the repository at this point in the history
  • Loading branch information
mayuki committed Jan 6, 2022
1 parent e42897e commit d42df4f
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/Cocona.Core/Command/CoconaCommandProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ public CommandOptionDescriptor CreateOption(CommandParameterAttributeSet attrSet
var optionName = attrSet.Option?.Name ?? name;
var optionDesc = attrSet.Option?.Description ?? string.Empty;
var optionShortNames = attrSet.Option?.ShortNames ?? Array.Empty<char>();
var optionValueName = attrSet.Option?.ValueName ?? (DynamicListHelper.IsArrayOrEnumerableLike(type) ? DynamicListHelper.GetElementType(type) : type).Name;
var optionValueName = attrSet.Option?.ValueName;
var optionIsHidden = attrSet.Hidden != null;
var optionIsStopParsingOptions = attrSet.Option?.StopParsingOptions ?? false;

Expand Down
8 changes: 7 additions & 1 deletion src/Cocona.Core/Command/CommandArgumentDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ namespace Cocona.Command
public class CommandArgumentDescriptor : ICommandParameterDescriptor
{
public Type ArgumentType { get; }
public Type UnwrappedArgumentType { get; } // ArgumentType = Nullable<bool> --> UnwrappedArgumentType = bool
public string Name { get; }
public int Order { get; }
public string Description { get; }
public CoconaDefaultValue DefaultValue { get; }
public IReadOnlyList<Attribute> ParameterAttributes { get; }

public bool IsEnumerableLike => DynamicListHelper.IsArrayOrEnumerableLike(ArgumentType);
public bool IsEnumerableLike => DynamicListHelper.IsArrayOrEnumerableLike(UnwrappedArgumentType);
public bool IsRequired => !DefaultValue.HasValue;


public CommandArgumentDescriptor(Type argumentType, string name, int order, string description, CoconaDefaultValue defaultValue, IReadOnlyList<Attribute> parameterAttributes)
{
ArgumentType = argumentType ?? throw new ArgumentNullException(nameof(argumentType));
UnwrappedArgumentType = argumentType.IsValueType && argumentType.IsConstructedGenericType && argumentType.GetGenericTypeDefinition() == typeof(Nullable<>)
? argumentType.GetGenericArguments()[0]
: argumentType;

Name = name ?? throw new ArgumentNullException(nameof(name));
Order = order;
Description = description ?? throw new ArgumentNullException(nameof(description));
Expand Down
11 changes: 8 additions & 3 deletions src/Cocona.Core/Command/CommandOptionDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface ICommandOptionDescriptor
public class CommandOptionDescriptor : ICommandOptionDescriptor, ICommandParameterDescriptor
{
public Type OptionType { get; }
public Type UnwrappedOptionType { get; } // OptionType = Nullable<bool> --> UnwrappedType = bool

public string Name { get; }
public IReadOnlyList<char> ShortName { get; }
public string ValueName { get; }
Expand All @@ -27,16 +29,19 @@ public class CommandOptionDescriptor : ICommandOptionDescriptor, ICommandParamet
public CommandOptionFlags Flags { get; }
public bool IsHidden => Flags.HasFlag(CommandOptionFlags.Hidden);
public bool IsRequired => !DefaultValue.HasValue;
public bool IsEnumerableLike => DynamicListHelper.IsArrayOrEnumerableLike(OptionType);

public bool IsEnumerableLike => DynamicListHelper.IsArrayOrEnumerableLike(UnwrappedOptionType);
public CommandOptionDescriptor(Type optionType, string name, IReadOnlyList<char> shortName, string description, CoconaDefaultValue defaultValue, string? valueName, CommandOptionFlags flags, IReadOnlyList<Attribute> parameterAttributes)
{
OptionType = optionType ?? throw new ArgumentNullException(nameof(optionType));
UnwrappedOptionType = optionType.IsValueType && optionType.IsConstructedGenericType && optionType.GetGenericTypeDefinition() == typeof(Nullable<>)
? optionType.GetGenericArguments()[0]
: optionType;

Name = name ?? throw new ArgumentNullException(nameof(name));
ShortName = shortName ?? throw new ArgumentNullException(nameof(shortName));
Description = description ?? throw new ArgumentNullException(nameof(description));
DefaultValue = defaultValue;
ValueName = valueName ?? OptionType.Name;
ValueName = valueName ?? (DynamicListHelper.IsArrayOrEnumerableLike(UnwrappedOptionType) ? DynamicListHelper.GetElementType(UnwrappedOptionType) : UnwrappedOptionType).Name;
Flags = flags;
ParameterAttributes = parameterAttributes;

Expand Down
8 changes: 4 additions & 4 deletions src/Cocona.Core/Help/CoconaCommandHelpProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private string CreateUsageCommandOptionsAndArgs(CommandDescriptor command, IRead
foreach (var opt in command.Options.Where(x => !x.IsHidden))
{
sb.Append(" ");
if (opt.OptionType == typeof(bool))
if (opt.UnwrappedOptionType == typeof(bool))
{
if (opt.DefaultValue.HasValue && opt.DefaultValue.Value != null && opt.DefaultValue.Value.Equals(true))
{
Expand Down Expand Up @@ -230,7 +230,7 @@ private void AddHelpForCommandArguments(HelpMessage help, CommandDescriptor comm
.Select((x, i) =>
new HelpLabelDescriptionListItem(
$"{i}: {x.Name}",
BuildParameterDescription(_localizer.GetArgumentDescription(command, x), x.IsRequired, x.ArgumentType, x.DefaultValue)
BuildParameterDescription(_localizer.GetArgumentDescription(command, x), x.IsRequired, x.UnwrappedArgumentType, x.DefaultValue)
)
)
.ToArray()
Expand All @@ -254,7 +254,7 @@ private void AddHelpForCommandOptions(HelpMessage help, CommandDescriptor comman
x is CommandOptionDescriptor option
? new HelpLabelDescriptionListItem(
BuildParameterLabel(option),
BuildParameterDescription(_localizer.GetOptionDescription(command, x), option.IsRequired, option.OptionType, option.DefaultValue)
BuildParameterDescription(_localizer.GetOptionDescription(command, x), option.IsRequired, option.UnwrappedOptionType, option.DefaultValue)
)
: x is CommandOptionLikeCommandDescriptor optionLikeCommand
? new HelpLabelDescriptionListItem(
Expand All @@ -275,7 +275,7 @@ private string BuildParameterLabel(CommandOptionDescriptor option)
return (option.ShortName.Any() ? string.Join(", ", option.ShortName.Select(x => $"-{x}")) + ", " : "") +
$"--{option.Name}" +
(
option.OptionType == typeof(bool)
option.UnwrappedOptionType == typeof(bool)
? option.DefaultValue.HasValue && option.DefaultValue.Value != null && option.DefaultValue.Value.Equals(true)
? "=<true|false>"
: ""
Expand Down
12 changes: 12 additions & 0 deletions src/Cocona.Core/Internal/NullabilityInfoContextHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ public NullabilityState GetNullabilityState(ParameterInfo parameterInfo)
#else
public NullabilityState GetNullabilityState(ParameterInfo parameterInfo)
{
if (parameterInfo.ParameterType.IsValueType)
{
if (parameterInfo.ParameterType.IsConstructedGenericType && parameterInfo.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return NullabilityState.Nullable;
}
else
{
return NullabilityState.NotNull;
}
}

foreach (var attr in parameterInfo.GetCustomAttributesData())
{
if (attr.AttributeType is { Namespace: "System.Runtime.CompilerServices", Name: "NullableAttribute" })
Expand Down
120 changes: 120 additions & 0 deletions test/Cocona.Test/CommandLine/CoconaCommandLineParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,126 @@ public void ParseCommand_LongOptions_Boolean_NonEqual_Ignored()
parsed.Arguments.Should().NotBeEmpty(); // false
}

[Fact]
public void ParseCommand_LongOptions_NullableBoolean_Equal_False_DefaultTrue()
{
var args = new[] { "--flag=false" };
var parsed = new CoconaCommandLineParser().ParseCommand(
args,
new CommandOptionDescriptor[]
{
CreateCommandOption(typeof(bool?), "flag", new [] { 'f' }, "", new CoconaDefaultValue(null)),
},
new CommandArgumentDescriptor[]
{
}
);
parsed.Should().NotBeNull();
parsed.Options.Should().HaveCount(1);
parsed.Options[0].Value.Should().Be("false");
parsed.Arguments.Should().BeEmpty();
}

[Fact]
public void ParseCommand_LongOptions_NullableBoolean_Equal_Non_True()
{
var args = new[] { "--flag=0" };
var parsed = new CoconaCommandLineParser().ParseCommand(
args,
new CommandOptionDescriptor[]
{
CreateCommandOption(typeof(bool?), "flag", new [] { 'f' }, "", new CoconaDefaultValue(null)),
},
new CommandArgumentDescriptor[]
{
}
);
parsed.Should().NotBeNull();
parsed.Options.Should().HaveCount(1);
parsed.Options[0].Value.Should().Be("false");
parsed.Arguments.Should().BeEmpty();
}

[Fact]
public void ParseCommand_LongOptions_NullableBoolean_Equal_True_DefaultTrue()
{
var args = new[] { "--flag=true" };
var parsed = new CoconaCommandLineParser().ParseCommand(
args,
new CommandOptionDescriptor[]
{
CreateCommandOption(typeof(bool?), "flag", new [] { 'f' }, "", new CoconaDefaultValue(null)),
},
new CommandArgumentDescriptor[]
{
}
);
parsed.Should().NotBeNull();
parsed.Options.Should().HaveCount(1);
parsed.Options[0].Value.Should().Be("true");
parsed.Arguments.Should().BeEmpty();
}

[Fact]
public void ParseCommand_LongOptions_NullableBoolean_Equal_False_DefaultFalse()
{
var args = new[] { "--flag=false" };
var parsed = new CoconaCommandLineParser().ParseCommand(
args,
new CommandOptionDescriptor[]
{
CreateCommandOption(typeof(bool?), "flag", new [] { 'f' }, "", new CoconaDefaultValue(null)),
},
new CommandArgumentDescriptor[]
{
}
);
parsed.Should().NotBeNull();
parsed.Options.Should().HaveCount(1);
parsed.Options[0].Value.Should().Be("false");
parsed.Arguments.Should().BeEmpty();
}

[Fact]
public void ParseCommand_LongOptions_NullableBoolean_Equal_True_DefaultFalse()
{
var args = new[] { "--flag=true" };
var parsed = new CoconaCommandLineParser().ParseCommand(
args,
new CommandOptionDescriptor[]
{
CreateCommandOption(typeof(bool?), "flag", new [] { 'f' }, "", new CoconaDefaultValue(null)),
},
new CommandArgumentDescriptor[]
{
}
);
parsed.Should().NotBeNull();
parsed.Options.Should().HaveCount(1);
parsed.Options[0].Value.Should().Be("true");
parsed.Arguments.Should().BeEmpty();
}

[Fact]
public void ParseCommand_LongOptions_NullableBoolean_NonEqual_Ignored()
{
var args = new[] { "--flag", "false" };
var parsed = new CoconaCommandLineParser().ParseCommand(
args,
new CommandOptionDescriptor[]
{
CreateCommandOption(typeof(bool?), "flag", new [] { 'f' }, "", new CoconaDefaultValue(null)),
},
new CommandArgumentDescriptor[]
{
}
);
parsed.Should().NotBeNull();
parsed.Options.Should().HaveCount(1);
parsed.Options[0].Value.Should().Be("true"); // --flag
parsed.Arguments.Should().NotBeEmpty(); // false
}

[Fact]
public void ParseCommand_ShortOptionWithValue_Arguments()
{
Expand Down
89 changes: 86 additions & 3 deletions test/Cocona.Test/Help/CoconaCommandHelpProviderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ private CommandDescriptor CreateCommand(string name, string description, IComman

private CommandOptionDescriptor CreateCommandOption(Type optionType, string name, IReadOnlyList<char> shortName, string description, CoconaDefaultValue defaultValue, CommandOptionFlags flags = CommandOptionFlags.None)
{
var optionValueName = (DynamicListHelper.IsArrayOrEnumerableLike(optionType) ? DynamicListHelper.GetElementType(optionType) : optionType).Name;

return new CommandOptionDescriptor(optionType, name, shortName, description, defaultValue, optionValueName, flags, Array.Empty<Attribute>());
return new CommandOptionDescriptor(optionType, name, shortName, description, defaultValue, null, flags, Array.Empty<Attribute>());
}

[Fact]
Expand Down Expand Up @@ -271,6 +269,37 @@ command description
".TrimStart());
}

[Fact]
public void CommandHelp_Arguments_Nullable_Rendered()
{
var commandDescriptor = CreateCommand(
"Test",
"command description",
new ICommandParameterDescriptor[]
{
new CommandArgumentDescriptor(typeof(int), "arg0-int-not-null", 0, "Int NotNull", CoconaDefaultValue.None, Array.Empty<Attribute>()),
new CommandArgumentDescriptor(typeof(int?), "arg1-int-nullable", 0, "Int Nullable", new CoconaDefaultValue(null), Array.Empty<Attribute>()),
new CommandArgumentDescriptor(typeof(string), "arg2-string-not-null", 0, "String NotNull", CoconaDefaultValue.None, Array.Empty<Attribute>()),
new CommandArgumentDescriptor(typeof(string), "arg3-string-nullable", 0, "String Nullable", new CoconaDefaultValue(null), Array.Empty<Attribute>()),
}
);

var provider = new CoconaCommandHelpProvider(new FakeApplicationMetadataProvider(), new ServiceCollection().BuildServiceProvider());
var help = provider.CreateCommandHelp(commandDescriptor, Array.Empty<CommandDescriptor>());
var text = new CoconaHelpRenderer().Render(help);
text.Should().Be(@"
Usage: ExeName Test arg0-int-not-null arg1-int-nullable arg2-string-not-null arg3-string-nullable
command description
Arguments:
0: arg0-int-not-null Int NotNull (Required)
1: arg1-int-nullable Int Nullable
2: arg2-string-not-null String NotNull (Required)
3: arg3-string-nullable String Nullable
".TrimStart());
}

[Fact]
public void CreateCommandsIndexHelp_Rendered()
{
Expand Down Expand Up @@ -572,6 +601,60 @@ command description
".TrimStart());
}

[Fact]
public void CommandHelp_Options_NullableBoolean_DefaultFalse_Rendered()
{
var commandDescriptor = CreateCommand(
"Test",
"command description",
new ICommandParameterDescriptor[]
{
CreateCommandOption(typeof(bool?), "flag", new [] { 'f' }, "Boolean option", new CoconaDefaultValue(null)),
}
);

var provider = new CoconaCommandHelpProvider(new FakeApplicationMetadataProvider(), new ServiceCollection().BuildServiceProvider());
var help = provider.CreateCommandHelp(commandDescriptor, Array.Empty<CommandDescriptor>());
var text = new CoconaHelpRenderer().Render(help);
text.Should().Be(@"
Usage: ExeName Test [--flag]
command description
Options:
-f, --flag Boolean option
".TrimStart());
}

[Fact]
public void CommandHelp_Options_Nullable()
{
var commandDescriptor = CreateCommand(
"Test",
"command description",
new ICommandParameterDescriptor[]
{
CreateCommandOption(typeof(string), "nrt", new [] { 'f' }, "Nullable Reference Type", new CoconaDefaultValue(null)),
CreateCommandOption(typeof(bool?), "looooooong-option", new [] { 'l' }, "Long name option", new CoconaDefaultValue(null)),
CreateCommandOption(typeof(int?), "nullable-int", new [] { 'x' }, "Nullable Int", new CoconaDefaultValue(null)),
},
CommandFlags.Primary
);

var provider = new CoconaCommandHelpProvider(new FakeApplicationMetadataProvider(), new ServiceCollection().BuildServiceProvider());
var help = provider.CreateCommandsIndexHelp(new CommandCollection(new[] { commandDescriptor }), Array.Empty<CommandDescriptor>());
var text = new CoconaHelpRenderer().Render(help);
text.Should().Be(@"
Usage: ExeName [--nrt <String>] [--looooooong-option] [--nullable-int <Int32>]
command description
Options:
-f, --nrt <String> Nullable Reference Type
-l, --looooooong-option Long name option
-x, --nullable-int <Int32> Nullable Int
".TrimStart());
}

[Fact]
public void CommandHelp_Options_Hidden_Rendered()
Expand Down

0 comments on commit d42df4f

Please sign in to comment.