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()