Skip to content
Merged
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 @@ -15,7 +15,7 @@ internal static class PropertyInfoEnumerableExtensions
return properties.GroupBy(t => t.GetCustomAttribute<OptionGroupAttribute>());
}

internal static T? FindOptionGroupAttributeWithBestParameters<T>(
internal static T? FindOptionGroupSingleAttributeWithBestParameters<T>(
this IEnumerable<PropertyInfo> properties,
string groupId)
where T : OptionGroupBaseAttribute
Expand All @@ -28,20 +28,22 @@ internal static class PropertyInfoEnumerableExtensions
.Where(t => t is not null && t.Id == groupId)
.ToList()!;

bool isGroupWithInfoExists = candidates.Any(t =>
{
return !string.IsNullOrEmpty(t.Header)
|| !string.IsNullOrEmpty(t.Description);
});
return properties.FindOptionGroupAttributeWithBestParameters(candidates);
}

if (!isGroupWithInfoExists)
return candidates.FirstOrDefault();
internal static T? FindOptionGroupMultipleAttributeWithBestParameters<T>(
this IEnumerable<PropertyInfo> properties,
string groupId)
where T : OptionGroupBaseAttribute
{
ExtendedArgumentNullException.ThrowIfNull(properties, nameof(properties));
ExtendedArgumentNullException.ThrowIfNull(groupId, nameof(groupId));

T? bestGroupByHeader = candidates
.FirstOrDefault(t => !string.IsNullOrEmpty(t.Header));
List<T> candidates = [.. properties
.SelectMany(t => t.GetCustomAttributes<T>())
.Where(t => t is not null && t.Id == groupId)];

return bestGroupByHeader ?? candidates
.FirstOrDefault(t => !string.IsNullOrEmpty(t.Description));
return properties.FindOptionGroupAttributeWithBestParameters(candidates);
}

internal static Dictionary<PropertyInfo, ICommonOption> CreateOptions(
Expand All @@ -55,4 +57,30 @@ internal static Dictionary<PropertyInfo, ICommonOption> CreateOptions(
t => t,
t => t.CreateOption(source) ?? throw new UnsupportedOptionConfigException(null, t));
}

private static T? FindOptionGroupAttributeWithBestParameters<T>(
this IEnumerable<PropertyInfo> properties,
IEnumerable<T> candidates)
where T : OptionGroupBaseAttribute
{
ExtendedArgumentNullException.ThrowIfNull(properties, nameof(properties));
ExtendedArgumentNullException.ThrowIfNull(candidates, nameof(candidates));

candidates = [.. candidates];

bool isGroupWithInfoExists = candidates.Any(t =>
{
return !string.IsNullOrEmpty(t.Header)
|| !string.IsNullOrEmpty(t.Description);
});

if (!isGroupWithInfoExists)
return candidates.FirstOrDefault();

T? bestGroupByHeader = candidates
.FirstOrDefault(t => !string.IsNullOrEmpty(t.Header));

return bestGroupByHeader ?? candidates
.FirstOrDefault(t => !string.IsNullOrEmpty(t.Description));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NetArgumentParser.Options;
Expand All @@ -7,29 +8,38 @@ namespace NetArgumentParser.Attributes.Extensions;

internal static class PropertyInfoExtensions
{
internal static bool HasAttribute<T>(this PropertyInfo propertyInfo)
internal static bool HasSingleAttribute<T>(this PropertyInfo propertyInfo)
where T : Attribute
{
ExtendedArgumentNullException.ThrowIfNull(propertyInfo, nameof(propertyInfo));
return propertyInfo.GetCustomAttribute<T>() is not null;
}

internal static bool HasMultipleAttribute<T>(this PropertyInfo propertyInfo)
where T : Attribute
{
ExtendedArgumentNullException.ThrowIfNull(propertyInfo, nameof(propertyInfo));

IEnumerable<T> attributes = propertyInfo.GetCustomAttributes<T>();
return attributes is not null && attributes.Any();
}

internal static bool HasOptionAttribute(this PropertyInfo propertyInfo)
{
ExtendedArgumentNullException.ThrowIfNull(propertyInfo, nameof(propertyInfo));
return propertyInfo.HasAttribute<CommonOptionAttribute>();
return propertyInfo.HasSingleAttribute<CommonOptionAttribute>();
}

internal static bool HasOptionGroupAttribute(this PropertyInfo propertyInfo)
{
ExtendedArgumentNullException.ThrowIfNull(propertyInfo, nameof(propertyInfo));
return propertyInfo.HasAttribute<OptionGroupAttribute>();
return propertyInfo.HasSingleAttribute<OptionGroupAttribute>();
}

internal static bool HasMutuallyExclusiveOptionGroupAttribute(this PropertyInfo propertyInfo)
{
ExtendedArgumentNullException.ThrowIfNull(propertyInfo, nameof(propertyInfo));
return propertyInfo.HasAttribute<MutuallyExclusiveOptionGroupAttribute>();
return propertyInfo.HasMultipleAttribute<MutuallyExclusiveOptionGroupAttribute>();
}

internal static bool HasSubcommandAttribute(this PropertyInfo propertyInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace NetArgumentParser.Attributes;

[AttributeUsage(
AttributeTargets.Property,
AllowMultiple = false,
AllowMultiple = true,
Inherited = false)
]
public sealed class MutuallyExclusiveOptionGroupAttribute : OptionGroupBaseAttribute
Expand Down
16 changes: 10 additions & 6 deletions Core/NetArgumentParser/Generators/ArgumentParserGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ protected virtual void ConfigureOptionGroups(
if (attribute is not null)
{
attribute = optionMap.Keys
.FindOptionGroupAttributeWithBestParameters<OptionGroupAttribute>(attribute.Id);
.FindOptionGroupSingleAttributeWithBestParameters<OptionGroupAttribute>(attribute.Id);
}

OptionGroup<ICommonOption>? optionGroup = attribute is not null
Expand All @@ -96,14 +96,18 @@ protected virtual void ConfigureMutuallyExclusiveOptionGroups(
ExtendedArgumentNullException.ThrowIfNull(argumentParser, nameof(argumentParser));
ExtendedArgumentNullException.ThrowIfNull(rootQuantum, nameof(rootQuantum));

IEnumerable<KeyValuePair<PropertyInfo, ICommonOption>> optionsWithGroup =
IEnumerable<KeyValuePair<PropertyInfo, ICommonOption>> optionsWithPropertyInfo =
rootQuantum.FindOptions(t => t.Key.HasMutuallyExclusiveOptionGroupAttribute(), true);

var mutuallyExclusiveOptionGroups = optionsWithGroup.GroupBy(t =>
var optionInfoWithGroupAttribute = optionsWithPropertyInfo.SelectMany(kv =>
{
return t.Key.GetCustomAttribute<MutuallyExclusiveOptionGroupAttribute>();
return kv.Key
.GetCustomAttributes<MutuallyExclusiveOptionGroupAttribute>()
.Select(attr => new { OptionInfo = kv, GroupAttribute = attr });
});

var mutuallyExclusiveOptionGroups = optionInfoWithGroupAttribute.GroupBy(t => t.GroupAttribute);

foreach (var group in mutuallyExclusiveOptionGroups)
{
MutuallyExclusiveOptionGroupAttribute? attribute = group.Key;
Expand All @@ -113,13 +117,13 @@ protected virtual void ConfigureMutuallyExclusiveOptionGroups(

MutuallyExclusiveOptionGroupAttribute? attributeWithBestParameters = rootQuantum.Options
.Select(t => t.Key)
.FindOptionGroupAttributeWithBestParameters<MutuallyExclusiveOptionGroupAttribute>(
.FindOptionGroupMultipleAttributeWithBestParameters<MutuallyExclusiveOptionGroupAttribute>(
attribute.Id);

if (attributeWithBestParameters is not null)
attribute = attributeWithBestParameters;

IEnumerable<ICommonOption> groupOptions = group.Select(t => t.Value);
IEnumerable<ICommonOption> groupOptions = group.Select(t => t.OptionInfo.Value);

_ = argumentParser.AddMutuallyExclusiveOptionGroup(
attribute.Header,
Expand Down
39 changes: 25 additions & 14 deletions Documentation/ParserGenerationUsingAttributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ internal class CustomParserConfig
}
```

Attributes for mutually exclusive groups are set in a similar manner. But unlike regular groups, mutually exclusive groups can contain options from different levels of subcommands.
Attributes for mutually exclusive groups are set in a similar manner using `MutuallyExclusiveOptionGroup` attribute. But unlike regular groups, mutually exclusive groups can contain options from different levels of subcommands. In addition, one property can have multiple `MutuallyExclusiveOptionGroup` attributes.

```cs
[ParserConfig]
Expand All @@ -274,8 +274,29 @@ internal class CustomParserConfig
[MutuallyExclusiveOptionGroup("id", "", "")]
public bool ShowSecondName { get; set; }

[ValueOption<double>("scale")]
[MutuallyExclusiveOptionGroup(
$"{nameof(ScaleFactor)}-{nameof(NewWidth)}",
$"{nameof(ScaleFactor)}-{nameof(NewWidth)}",
$"{nameof(ScaleFactor)} cannot be used with {nameof(NewWidth)}")
]
[MutuallyExclusiveOptionGroup(
$"{nameof(ScaleFactor)}-{nameof(NewHeight)}",
$"{nameof(ScaleFactor)}-{nameof(NewHeight)}",
$"{nameof(ScaleFactor)} cannot be used with {nameof(NewHeight)}")
]
public double ScaleFactor { get; set; }

[ValueOption<int>("width")]
[MutuallyExclusiveOptionGroup($"{nameof(ScaleFactor)}-{nameof(NewWidth)}", "", "")]
public int NewWidth { get; set; }

[ValueOption<int>("height")]
[MutuallyExclusiveOptionGroup($"{nameof(ScaleFactor)}-{nameof(NewHeight)}", "", "")]
public int NewHeight { get; set; }

[Subcommand("status", "description")]
public StatusSubcommand Status { get; }
public StatusSubcommand Status { get; } = new();
}

internal class StatusSubcommand
Expand All @@ -293,27 +314,17 @@ Subcommands can be configured using `SubcommandAttribute` attribute. The corresp
[ParserConfig]
internal class CustomParserConfig
{
public CustomParserConfig()
{
Status = new();
}

[Subcommand("status", "description")]
public StatusSubcommand Status { get; }
public StatusSubcommand Status { get; } = new();
}

internal class StatusSubcommand
{
public StatusSubcommand()
{
Update = new();
}

[CounterOption("verbosity", "v")]
public int Verbosity { get; set; }

[Subcommand("update", "description")]
public UpdateSubcommand Update { get; }
public UpdateSubcommand Update { get; } = new();
}

internal class UpdateSubcommand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,15 @@
};
}

_ = parser.Parse(["--help"]);
try
{
_ = parser.Parse(args);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return;
}

#pragma warning disable
[ParserConfig]
Expand Down Expand Up @@ -229,7 +237,35 @@ internal class UpdateSubcommand
{
[FlagOption("remote", "r")]
public bool Remote { get; set; }

[ValueOption<double>("scale")]
[MutuallyExclusiveOptionGroup(
$"{nameof(ScaleFactor)}-{nameof(NewWidth)}",
$"{nameof(ScaleFactor)}-{nameof(NewWidth)}",
$"{nameof(ScaleFactor)} cannot be used with {nameof(NewWidth)}")
]
[MutuallyExclusiveOptionGroup(
$"{nameof(ScaleFactor)}-{nameof(NewHeight)}",
$"{nameof(ScaleFactor)}-{nameof(NewHeight)}",
$"{nameof(ScaleFactor)} cannot be used with {nameof(NewHeight)}")
]
public double ScaleFactor { get; set; }

[ValueOption<int>("width")]
[MutuallyExclusiveOptionGroup($"{nameof(ScaleFactor)}-{nameof(NewWidth)}", "", "")]
public int NewWidth { get; set; }

[ValueOption<int>("height")]
[MutuallyExclusiveOptionGroup($"{nameof(ScaleFactor)}-{nameof(NewHeight)}", "", "")]
public int NewHeight { get; set; }
}

internal record Point(double X, double Y, double Z);
#pragma warning restore

/*
./NetArgumentParser.Examples.ParserGenerationUsingAttributes
--files 1.txt 2.txt 3.txt
--mode Create
status update --scale 0.5
*/
66 changes: 66 additions & 0 deletions Tests/NetArgumentParser.Tests/ArgumentParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,72 @@ public void Parse_MutuallyExclusiveOptions_ThrowsException()
Assert.Null(ex);
}

[Fact]
public void Parse_PairedMutuallyExclusiveOptions_ThrowsException()
{
var arguments = new string[]
{
"--scale", "0.5",
"-b", "15",
"--height", "1080",
"-d", "10",
"--width", "1920"
};

var scaleOption = new ValueOption<double>("scale", string.Empty);
var widthOption = new ValueOption<double>("width", string.Empty);
var heightOption = new ValueOption<int>("height", string.Empty);

var scaleWidthGroupOptions = new List<ICommonOption>() { scaleOption, widthOption };
var scaleHeightGroupOptions = new List<ICommonOption>() { scaleOption, heightOption };

var options = new ICommonOption[]
{
scaleOption,
new ValueOption<double>(string.Empty, "b"),
widthOption,
new ValueOption<int>(string.Empty, "d"),
heightOption
};

var parser = new ArgumentParser()
{
UseDefaultHelpOption = false
};

parser.AddOptions(options);

MutuallyExclusiveOptionGroup<ICommonOption> scaleWidthGroup =
parser.AddMutuallyExclusiveOptionGroup("scaleWidth", null, scaleWidthGroupOptions);

MutuallyExclusiveOptionGroup<ICommonOption> scaleHeightGroup =
parser.AddMutuallyExclusiveOptionGroup("scaleHeight", null, scaleHeightGroupOptions);

void VerifyConflict(ICommonOption expectedExistingOption, ICommonOption expectedNewOption)
{
var exception = Assert.Throws<MutuallyExclusiveOptionsFoundException>(() =>
{
_ = parser.Parse(arguments);
});

Assert.Equal(expectedExistingOption, exception.ExistingOption);
Assert.Equal(expectedNewOption, exception.NewOption);
}

VerifyConflict(scaleOption, heightOption);

scaleHeightGroup.RemoveOption(heightOption);
parser.ResetOptionsHandledState();

VerifyConflict(scaleOption, widthOption);

scaleWidthGroup.RemoveOption(widthOption);
parser.ResetOptionsHandledState();

Exception? ex = Record.Exception(() => parser.Parse(arguments));
Assert.Null(ex);
}

[Fact]
public void Parse_SeveralArguments_ArgumentsParseResultIsCorrect()
{
Expand Down
Loading
Loading