Skip to content

Commit d33e00d

Browse files
committed
Implement option grouping/clustering fixes #45
1 parent 2010294 commit d33e00d

File tree

6 files changed

+265
-106
lines changed

6 files changed

+265
-106
lines changed

CommandLineParser.Tests/CommandLineParserTests.cs

+16
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ public void OrderAttributeWorks()
4646
Assert.Equal(to, result.Result.To);
4747
}
4848

49+
[Fact]
50+
public void OrderAttributeWorks2()
51+
{
52+
var from = @"path/from/file";
53+
var to = @"path/to/file";
54+
55+
var parser = new CommandLineParser<OrderModel>(Services);
56+
57+
var result = parser.Parse(new string[] { "app.exe", "-r", "5", from, to });
58+
59+
result.AssertNoErrors();
60+
61+
Assert.Equal(from, result.Result.From);
62+
Assert.Equal(to, result.Result.To);
63+
}
64+
4965
[Fact]
5066
public void OrderAttributeInCommandWorks()
5167
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using MatthiWare.CommandLine.Core.Attributes;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
using Xunit;
6+
using Xunit.Abstractions;
7+
8+
namespace MatthiWare.CommandLine.Tests.Parsing
9+
{
10+
public class OptionClusteringTests : TestBase
11+
{
12+
public OptionClusteringTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
13+
{
14+
}
15+
16+
[Fact]
17+
public void ClusterdOptionsAreParsedCorrectly()
18+
{
19+
var parser = new CommandLineParser<ClusteredOptions<bool>>(Services);
20+
21+
var result = parser.Parse(new[] { "app.exe", "-abc" });
22+
23+
result.AssertNoErrors();
24+
25+
var model = result.Result;
26+
27+
Assert.True(model.A);
28+
Assert.True(model.B);
29+
Assert.True(model.C);
30+
}
31+
32+
[Fact]
33+
public void ClusterdOptionsAllSetTheSameValue()
34+
{
35+
var parser = new CommandLineParser<ClusteredOptions<bool>>(Services);
36+
37+
var result = parser.Parse(new[] { "app.exe", "-abc", "false" });
38+
39+
result.AssertNoErrors();
40+
41+
var model = result.Result;
42+
43+
Assert.False(model.A);
44+
Assert.False(model.B);
45+
Assert.False(model.C);
46+
}
47+
48+
[Fact]
49+
public void ClusterdOptionsAreIgnoredWhenRepeated()
50+
{
51+
var parser = new CommandLineParser<ClusteredOptions<bool>>(Services);
52+
53+
var result = parser.Parse(new[] { "app.exe", "-abc", "false", "-abc", "true" });
54+
55+
result.AssertNoErrors();
56+
57+
var model = result.Result;
58+
59+
Assert.False(model.A);
60+
Assert.False(model.B);
61+
Assert.False(model.C);
62+
}
63+
64+
[Fact]
65+
public void ClusterdOptionsInCommandWork()
66+
{
67+
var parser = new CommandLineParser(Services);
68+
69+
parser.AddCommand<ClusteredOptions<bool>>().Name("cmd").Required().OnExecuting((o, model) =>
70+
{
71+
Assert.False(model.A);
72+
Assert.False(model.B);
73+
Assert.False(model.C);
74+
});
75+
76+
var result = parser.Parse(new[] { "app.exe", "cmd", "-abc", "false" });
77+
78+
result.AssertNoErrors();
79+
}
80+
81+
[Fact]
82+
public void ClusterdOptionsInCommandAndReusedInParentWork()
83+
{
84+
var parser = new CommandLineParser<ClusteredOptions<bool>>(Services);
85+
86+
parser.AddCommand<ClusteredOptions<bool>>().Name("cmd").Required().OnExecuting((o, model) =>
87+
{
88+
Assert.False(model.A);
89+
Assert.False(model.B);
90+
Assert.False(model.C);
91+
});
92+
93+
var result = parser.Parse(new[] { "app.exe", "-abc", "cmd", "-abc", "false" });
94+
95+
result.AssertNoErrors();
96+
97+
Assert.True(result.Result.A);
98+
Assert.True(result.Result.B);
99+
Assert.True(result.Result.C);
100+
}
101+
102+
[Fact]
103+
public void ClusterdOptionsInCommandAndReusedInParentWork_String_Version()
104+
{
105+
var parser = new CommandLineParser<ClusteredOptions<string>>(Services);
106+
107+
parser.AddCommand<ClusteredOptions<string>>().Name("cmd").Required().OnExecuting((o, model) =>
108+
{
109+
Assert.Equal("false", model.A);
110+
Assert.Equal("false", model.B);
111+
Assert.Equal("false", model.C);
112+
});
113+
114+
var result = parser.Parse(new[] { "app.exe", "-abc", "works", "cmd", "-abc", "false" });
115+
116+
result.AssertNoErrors();
117+
118+
Assert.Equal("works", result.Result.A);
119+
Assert.Equal("works", result.Result.B);
120+
Assert.Equal("works", result.Result.C);
121+
}
122+
123+
private class ClusteredOptions<T>
124+
{
125+
[Name("a"), Required]
126+
public T A { get; set; }
127+
128+
[Name("b"), Required]
129+
public T B { get; set; }
130+
131+
[Name("c"), Required]
132+
public T C { get; set; }
133+
}
134+
}
135+
}

CommandLineParser.Tests/Utils/ExtensionMethodsTest.cs

-84
This file was deleted.

CommandLineParser/CommandLineParser.xml

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CommandLineParser/Core/Parsing/ArgumentManager.cs

+83-8
Original file line numberDiff line numberDiff line change
@@ -97,20 +97,16 @@ private bool ProcessOption(OptionRecord rec)
9797
{
9898
var foundOption = FindOption(rec);
9999

100-
if (foundOption == null)
100+
if (!foundOption)
101101
{
102102
// In case we have an option named "-1" and int value -1. This causes confusion.
103103
return ProcessCommandOrOptionValue(rec);
104104
}
105105

106-
var argumentModel = new ArgumentModel(rec.Name, rec.Value);
107-
108-
results.Add(foundOption, argumentModel);
109-
110106
return true;
111107
}
112108

113-
private ICommandLineOption FindOption(OptionRecord rec)
109+
private bool FindOption(OptionRecord rec)
114110
{
115111
var context = CurrentContext;
116112

@@ -132,13 +128,77 @@ private ICommandLineOption FindOption(OptionRecord rec)
132128

133129
context.CurrentOption = option;
134130

135-
return option;
131+
var argumentModel = new ArgumentModel(rec.Name, rec.Value);
132+
133+
results.Add(option, argumentModel);
134+
135+
return true;
136+
}
137+
138+
if (ProcessClusteredOptions(context, rec))
139+
{
140+
return true;
136141
}
137142

138143
context = context.Parent;
139144
}
140145

141-
return null;
146+
return false;
147+
}
148+
149+
private bool ProcessClusteredOptions(ProcessingContext context, OptionRecord rec)
150+
{
151+
var tokens = rec.Name.WithoutPreAndPostfixes(options);
152+
153+
var list = new List<ICommandLineOption>();
154+
155+
foreach (var token in tokens)
156+
{
157+
bool found = false;
158+
159+
string key = $"{options.PrefixShortOption}{token}";
160+
161+
foreach (var option in context.CurrentCommand.Options.Where(ValidClusteredOption))
162+
{
163+
if (!option.ShortName.EqualsIgnoreCase(key))
164+
{
165+
continue;
166+
}
167+
168+
var model = new ArgumentModel(key, string.Empty);
169+
170+
list.Add(option);
171+
results.Add(option, model);
172+
173+
found = true;
174+
break;
175+
}
176+
177+
if (!found)
178+
{
179+
return false;
180+
}
181+
}
182+
183+
context.NextArgumentIsForClusteredOptions = true;
184+
context.ProcessedClusteredOptions = list;
185+
186+
return true;
187+
}
188+
189+
private bool ValidClusteredOption(ICommandLineOption option)
190+
{
191+
if (!option.HasShortName)
192+
{
193+
return false;
194+
}
195+
196+
if (results.ContainsKey(option))
197+
{
198+
return false;
199+
}
200+
201+
return true;
142202
}
143203

144204
private bool ProcessCommandOrOptionValue(ArgumentRecord rec)
@@ -161,6 +221,17 @@ private bool ProcessCommandOrOptionValue(ArgumentRecord rec)
161221

162222
while (context != null)
163223
{
224+
if (context.NextArgumentIsForClusteredOptions)
225+
{
226+
foreach (var option in context.ProcessedClusteredOptions)
227+
{
228+
results[option].Value = rec.RawData;
229+
}
230+
231+
context.ProcessedClusteredOptions = null;
232+
context.NextArgumentIsForClusteredOptions = false;
233+
}
234+
164235
if (context.CurrentOption == null)
165236
{
166237
context = context.Parent;
@@ -228,6 +299,8 @@ private class ProcessingContext
228299
public bool HasOrderedOptions { get; }
229300
public bool AllOrderedOptionsProcessed => orderedOptions.Count == 0;
230301
public bool ProcessingOrderedOptions { get; private set; }
302+
public bool NextArgumentIsForClusteredOptions { get; set; }
303+
public List<ICommandLineOption> ProcessedClusteredOptions { get; set; }
231304

232305
public ProcessingContext(ProcessingContext parent, ICommandLineCommandContainer commandContainer, ILogger logger)
233306
{
@@ -257,6 +330,8 @@ public void AssertSafeToSwitchProcessingContext()
257330
{
258331
if (!ProcessingOrderedOptions || AllOrderedOptionsProcessed)
259332
{
333+
ProcessedClusteredOptions = null;
334+
NextArgumentIsForClusteredOptions = false;
260335
return;
261336
}
262337

0 commit comments

Comments
 (0)