diff --git a/src/libcmdline/CommandLine.cs b/src/libcmdline/CommandLine.cs
index 7128663b..45ca2f19 100644
--- a/src/libcmdline/CommandLine.cs
+++ b/src/libcmdline/CommandLine.cs
@@ -370,6 +370,28 @@ private static PropertyInfo GetProperty(object target, out Type concreteType)
return pairZero.Left;
}
}
+
+ ///
+ /// Models a category of options that are separate from the main options.
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
+ public sealed class SubOptionAttribute : Attribute
+ {
+ ///
+ /// Name of suboption as identified by the command line arguments.
+ /// MyProgram.exe suboptionName --arg1 --arg2
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// Create a new SubOption identified by the given name.
+ ///
+ /// Name of subcommand.
+ public SubOptionAttribute(string name)
+ {
+ Name = name;
+ }
+ }
#endregion
#region Core
@@ -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();
+ var subOptions = ReflectionUtil.RetrievePropertyList(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;
@@ -1131,6 +1169,8 @@ public OptionInfo this[string key]
}
}
+ public Dictionary SubOptions { get; set; }
+
internal object RawOptions { private get; set; }
public bool EnforceRules()
@@ -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))
@@ -1869,9 +1941,11 @@ public static IList> RetrievePropertyList(property, (TAttribute)attribute));
+ }
}
}
}
diff --git a/src/tests/CommandLine.Tests.csproj b/src/tests/CommandLine.Tests.csproj
index 0e3e2dbb..a362e5db 100644
--- a/src/tests/CommandLine.Tests.csproj
+++ b/src/tests/CommandLine.Tests.csproj
@@ -40,6 +40,12 @@
+
+
+
+
+
+
diff --git a/src/tests/Mocks/SimpleOptionWithInvalidSuboption.cs b/src/tests/Mocks/SimpleOptionWithInvalidSuboption.cs
new file mode 100644
index 00000000..8c2723de
--- /dev/null
+++ b/src/tests/Mocks/SimpleOptionWithInvalidSuboption.cs
@@ -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; }
+ }
+}
diff --git a/src/tests/Mocks/SimpleOptionWithMultipleSubOptions.cs b/src/tests/Mocks/SimpleOptionWithMultipleSubOptions.cs
new file mode 100644
index 00000000..8e77eb27
--- /dev/null
+++ b/src/tests/Mocks/SimpleOptionWithMultipleSubOptions.cs
@@ -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; }
+ }
+}
diff --git a/src/tests/Mocks/SimpleOptionsWithSubOptions.cs b/src/tests/Mocks/SimpleOptionsWithSubOptions.cs
new file mode 100644
index 00000000..00692ec9
--- /dev/null
+++ b/src/tests/Mocks/SimpleOptionsWithSubOptions.cs
@@ -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; }
+ }
+}
diff --git a/src/tests/Mocks/SimpleOptionsWithSuboptionWithMultipleAliases.cs b/src/tests/Mocks/SimpleOptionsWithSuboptionWithMultipleAliases.cs
new file mode 100644
index 00000000..a6176392
--- /dev/null
+++ b/src/tests/Mocks/SimpleOptionsWithSuboptionWithMultipleAliases.cs
@@ -0,0 +1,10 @@
+
+namespace CommandLine.Tests.Mocks
+{
+ class SimpleOptionsWithSuboptionWithMultipleAliases : OptionsBase
+ {
+ [SubOption("co")]
+ [SubOption("checkout")]
+ public SimpleSubOptions SubOpt { get; set; }
+ }
+}
diff --git a/src/tests/Mocks/SimpleSubOptions.cs b/src/tests/Mocks/SimpleSubOptions.cs
new file mode 100644
index 00000000..297c42c5
--- /dev/null
+++ b/src/tests/Mocks/SimpleSubOptions.cs
@@ -0,0 +1,9 @@
+
+namespace CommandLine.Tests.Mocks
+{
+ class SimpleSubOptions : OptionsBase
+ {
+ [Option("i", "int")]
+ public int IntegerValue { get; set; }
+ }
+}
diff --git a/src/tests/Mocks/SimpleSuboptionWithNoDefaultConstructor.cs b/src/tests/Mocks/SimpleSuboptionWithNoDefaultConstructor.cs
new file mode 100644
index 00000000..35328efe
--- /dev/null
+++ b/src/tests/Mocks/SimpleSuboptionWithNoDefaultConstructor.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/tests/Parser/CommandLineParserFixture.cs b/src/tests/Parser/CommandLineParserFixture.cs
index 951888f4..dd1c9986 100644
--- a/src/tests/Parser/CommandLineParserFixture.cs
+++ b/src/tests/Parser/CommandLineParserFixture.cs
@@ -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()