Skip to content

Added support for SubOption parsers. Implements #17 #29

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

Closed
wants to merge 3 commits into from
Closed
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
78 changes: 76 additions & 2 deletions src/libcmdline/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,28 @@ private static PropertyInfo GetProperty(object target, out Type concreteType)
return pairZero.Left;
}
}

/// <summary>
/// Models a category of options that are separate from the main options.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public sealed class SubOptionAttribute : Attribute
{
/// <summary>
/// Name of suboption as identified by the command line arguments.
/// <example>MyProgram.exe suboptionName --arg1 --arg2</example>
/// </summary>
public string Name { get; private set; }

/// <summary>
/// Create a new SubOption identified by the given name.
/// </summary>
/// <param name="name">Name of subcommand.</param>
public SubOptionAttribute(string name)
{
Name = name;
}
}
#endregion

#region Core
Expand Down Expand Up @@ -853,6 +875,22 @@ public static OptionMap CreateMap(object target, CommandLineParserSettings setti
map[pair.Right.UniqueName] = new OptionInfo(pair.Right, pair.Left);
}

map.SubOptions = new Dictionary<string, PropertyInfo>();
var subOptions = ReflectionUtil.RetrievePropertyList<SubOptionAttribute>(target);

foreach (var subOption in subOptions)
{
if (subOption.Left.PropertyType.GetConstructor(Type.EmptyTypes) == null &&
subOption.Left.GetValue(target, null) == null)
{
throw new CommandLineParserException(String.Format(
"Type {0} must have parameterless constructor or " +
"already be initialized to be used as a suboption.",
subOption.Left.PropertyType));
}
map.SubOptions.Add(subOption.Right.Name, subOption.Left);
}

map.RawOptions = target;

return map;
Expand Down Expand Up @@ -1131,6 +1169,8 @@ public OptionInfo this[string key]
}
}

public Dictionary<string, PropertyInfo> SubOptions { get; set; }

internal object RawOptions { private get; set; }

public bool EnforceRules()
Expand Down Expand Up @@ -1782,6 +1822,38 @@ private bool DoParseArguments(string[] args, object options)
if ((result & ParserState.MoveOnNextElement) == ParserState.MoveOnNextElement)
arguments.MoveNext();
}
else if (optionMap.SubOptions.ContainsKey(argument))
{

var prop = optionMap.SubOptions[argument];
var subOptions = prop.GetValue(options, null) ?? Activator.CreateInstance(prop.PropertyType);

var subOptionMap = OptionInfo.CreateMap(subOptions, _settings);
subOptionMap.SetDefaults();

while (arguments.MoveNext())
{
argument = arguments.Current;
var subparser = ArgumentParser.Create(argument, _settings.IgnoreUnknownArguments);
if (subparser == null) // Done parsing
{
arguments.MovePrevious();
break;
}

var result = subparser.Parse(arguments, subOptionMap, subOptions);
if ((result & ParserState.Failure) == ParserState.Failure)
{
SetPostParsingStateIfNeeded(subOptions, subparser.PostParsingState);
hadError = true;
continue;
}

if ((result & ParserState.MoveOnNextElement) == ParserState.MoveOnNextElement)
arguments.MoveNext();
}
prop.SetValue(options, subOptions, null);
}
else if (target.IsValueListDefined)
{
if (!target.AddValueItemIfAllowed(argument))
Expand Down Expand Up @@ -1869,9 +1941,11 @@ public static IList<Pair<PropertyInfo, TAttribute>> RetrievePropertyList<TAttrib
var setMethod = property.GetSetMethod();
if (setMethod != null && !setMethod.IsStatic)
{
var attribute = Attribute.GetCustomAttribute(property, typeof(TAttribute), false);
if (attribute != null)
var attributes = Attribute.GetCustomAttributes(property, typeof(TAttribute), false);
foreach (var attribute in attributes)
{
list.Add(new Pair<PropertyInfo, TAttribute>(property, (TAttribute)attribute));
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/tests/CommandLine.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Compile Include="Mocks\SimpleOptionsWithSubOptions.cs" />
<Compile Include="Mocks\SimpleOptionsWithSuboptionWithMultipleAliases.cs" />
<Compile Include="Mocks\SimpleOptionWithInvalidSuboption.cs" />
<Compile Include="Mocks\SimpleOptionWithMultipleSubOptions.cs" />
<Compile Include="Mocks\SimpleSubOptions.cs" />
<Compile Include="Mocks\SimpleSuboptionWithNoDefaultConstructor.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="BaseFixture.cs" />
<Compile Include="CommandLineParserBaseFixture.cs" />
Expand Down
19 changes: 19 additions & 0 deletions src/tests/Mocks/SimpleOptionWithInvalidSuboption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

namespace CommandLine.Tests.Mocks
{
class SimpleOptionWithInvalidSuboption : OptionsBase
{
public SimpleOptionWithInvalidSuboption()
{

}

public SimpleOptionWithInvalidSuboption(int i)
{
Opt = new SimpleSuboptionWithNoDefaultConstructor(i);
}

[SubOption("opt")]
public SimpleSuboptionWithNoDefaultConstructor Opt { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/tests/Mocks/SimpleOptionWithMultipleSubOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

namespace CommandLine.Tests.Mocks
{
class SimpleOptionWithMultipleSubOptions : OptionsBase
{
[SubOption("first")]
public SimpleSubOptions Sub1 { get; set; }

[SubOption("second")]
public SimpleSubOptions Sub2 { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/tests/Mocks/SimpleOptionsWithSubOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

namespace CommandLine.Tests.Mocks
{
class SimpleOptionsWithSubOption : OptionsBase
{
[Option("s", "string")]
public string StringValue { get; set; }

[SubOption("suboption")]
public SimpleSubOptions SubOptions { get; set; }
}
}
10 changes: 10 additions & 0 deletions src/tests/Mocks/SimpleOptionsWithSuboptionWithMultipleAliases.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

namespace CommandLine.Tests.Mocks
{
class SimpleOptionsWithSuboptionWithMultipleAliases : OptionsBase
{
[SubOption("co")]
[SubOption("checkout")]
public SimpleSubOptions SubOpt { get; set; }
}
}
9 changes: 9 additions & 0 deletions src/tests/Mocks/SimpleSubOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

namespace CommandLine.Tests.Mocks
{
class SimpleSubOptions : OptionsBase
{
[Option("i", "int")]
public int IntegerValue { get; set; }
}
}
16 changes: 16 additions & 0 deletions src/tests/Mocks/SimpleSuboptionWithNoDefaultConstructor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

namespace CommandLine.Tests.Mocks
{
class SimpleSuboptionWithNoDefaultConstructor : OptionsBase
{
[Option("i", null)]
public int SomethingElse { get; set; }

public int Value { get; private set; }

public SimpleSuboptionWithNoDefaultConstructor(int val)
{
Value = val;
}
}
}
71 changes: 71 additions & 0 deletions src/tests/Parser/CommandLineParserFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,77 @@ public void ParseOptionsWithDefaultArray()
options.DoubleArrayValue.Should().Equal(new double[] { 1.1, 2.2, 3.3 });
}

[Test]
public void ParseOptionsWithSubOptionAsFirstArgument()
{
var options = new SimpleOptionsWithSubOption();
Result = base.Parser.ParseArguments(new string[] { "suboption", "--int", "3" }, options);

ResultShouldBeTrue();

options.SubOptions.IntegerValue.Should().Equal(3);
}

[Test]
public void ParseOptionsWithOwnOptionsBeforeSuboption()
{
var options = new SimpleOptionsWithSubOption();
Result = base.Parser.ParseArguments(new string[]{ "--string", "val", "suboption", "--int", "3" }, options);

ResultShouldBeTrue();

options.StringValue.Should().Equal("val");
options.SubOptions.IntegerValue.Should().Equal(3);
}

[Test]
public void ParseOptionsWithMultipleSuboptions()
{
var options = new SimpleOptionWithMultipleSubOptions();
Result = base.Parser.ParseArguments(new string[]{ "first", "--int", "2", "second", "--int", "3" }, options);

ResultShouldBeTrue();

options.Sub1.IntegerValue.Should().Equal(2);
options.Sub2.IntegerValue.Should().Equal(3);
}

[Test]
[ExpectedException(typeof(CommandLineParserException))]
public void ParseOptionsWithSuboptionThatHasNoDefaultConstructorMustFail()
{
var options = new SimpleOptionWithInvalidSuboption();
Result = base.Parser.ParseArguments(new string[] { "opt", "-i", "10" }, options);

ResultShouldBeFalse();
}

[Test]
public void ParseOptionsWithInitializedSuboptionThatHasNoDefaultConstructor()
{
var options = new SimpleOptionWithInvalidSuboption(5);
Result = base.Parser.ParseArguments(new string[] { "opt", "-i", "10" }, options);

ResultShouldBeTrue();

options.Opt.SomethingElse.Should().Equal(10);
options.Opt.Value.Should().Equal(5);
}

[Test]
public void ParseOptionsWithMultipleAliases()
{
var options = new SimpleOptionsWithSuboptionWithMultipleAliases();

Result = base.Parser.ParseArguments(new string[] {"co", "-i", "10"}, options);
ResultShouldBeTrue();
options.SubOpt.IntegerValue.Should().Equal(10);

Result = base.Parser.ParseArguments(new string[] {"co", "-i", "3"}, options);
ResultShouldBeTrue();
options.SubOpt.IntegerValue.Should().Equal(3);
}

[Test]
[ExpectedException(typeof(CommandLineParserException))]
public void ParseOptionsWithBadDefaults()
Expand Down