Skip to content

Commit 3f19037

Browse files
authored
Fix optional bool option with 'null' value and DefaultValue configured is not parsed correctly (#39)
* #35 'null' value not respected when DefaultValue is configured. * Fix string resolver always being able to parse * Better exception message * Only found options can throw OptionParseException * Fix codefactor issue * Split Parsing into seperate methods * Extend parsing with defaults test
1 parent 49d6d80 commit 3f19037

File tree

5 files changed

+126
-45
lines changed

5 files changed

+126
-45
lines changed

CommandLineParser.Tests/CommandLineParserTests.cs

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -194,29 +194,72 @@ public void ParseEnumInArguments(string[] args, bool hasErrors, EnumOption enumO
194194
}
195195

196196
[Theory]
197-
[InlineData(new[] { "app.exe", "-1", "message1", "-2", "-3" }, "message1", "message2", "message3")]
198-
[InlineData(new[] { "app.exe", "-1", "-2", "message2", "-3" }, "message1", "message2", "message3")]
199-
[InlineData(new[] { "app.exe", "-1", "-2", "-3" }, "message1", "message2", "message3")]
200-
[InlineData(new[] { "-1", "message1", "-2", "-3" }, "message1", "message2", "message3")]
201-
[InlineData(new[] { "-1", "-2", "message2", "-3" }, "message1", "message2", "message3")]
202-
[InlineData(new[] { "-1", "-2", "-3" }, "message1", "message2", "message3")]
203-
public void ParseWithDefaults(string[] args, string result1, string result2, string result3)
197+
// string
198+
[InlineData(typeof(string), new[] { "-1", "message1", "-2", "-3" }, "default", "message1", "default", "default")]
199+
[InlineData(typeof(string), new[] { "-1", "-2", "message2", "-3" }, "default", "default", "message2", "default")]
200+
[InlineData(typeof(string), new[] { "-1", "-2", "-3" }, "default", "default", "default", "default")]
201+
// bool
202+
[InlineData(typeof(bool), new[] { "-1", "false", "-2", "-3" }, false, false, true, true)]
203+
[InlineData(typeof(bool), new[] { "-1", "-2", "false", "-3" }, false, true, false, true)]
204+
[InlineData(typeof(bool), new[] { "-1", "-2", "-3" }, false, true, true, true)]
205+
//// int
206+
[InlineData(typeof(int), new[] { "-1", "5", "-2", "-3" }, 0, 5, 0, 0)]
207+
[InlineData(typeof(int), new[] { "-1", "-2", "5", "-3" }, 0, 0, 5, 0)]
208+
[InlineData(typeof(int), new[] { "-1", "-2", "-3" }, 0, 0, 0, 0)]
209+
// double
210+
[InlineData(typeof(double), new[] { "-1", "0.5", "-2", "-3" }, 0.1, 0.5, 0.1, 0.1)]
211+
[InlineData(typeof(double), new[] { "-1", "-2", "0.5", "-3" }, 0.1, 0.1, 0.5, 0.1)]
212+
[InlineData(typeof(double), new[] { "-1", "-2", "-3" }, 0.1, 0.1, 0.1, 0.1)]
213+
//// enum
214+
[InlineData(typeof(EnumOption), new[] { "-1", "Opt1", "-2", "-3" }, EnumOption.Opt1, EnumOption.Opt1, "message2", "message3")]
215+
[InlineData(typeof(EnumOption), new[] { "-1", "-2", "Opt1", "-3" }, EnumOption.Opt1, "message1", EnumOption.Opt1, "message3")]
216+
[InlineData(typeof(EnumOption), new[] { "-1", "-2", "-3" }, EnumOption.Opt1, "message1", "message2", "message3")]
217+
public void ParseWithDefaults(Type type, string[] args, object defaultValue, object result1, object result2, object result3)
204218
{
205-
var parser = new CommandLineParser<OptionsWithThreeParams>();
219+
if (type == typeof(bool))
220+
{
221+
Test<bool>();
222+
}
223+
else if (type == typeof(string))
224+
{
225+
Test<string>();
226+
}
227+
else if (type == typeof(int))
228+
{
229+
Test<int>();
230+
}
231+
else if (type == typeof(double))
232+
{
233+
Test<double>();
234+
}
235+
else if (type == typeof(Enum))
236+
{
237+
Test<Enum>();
238+
}
239+
240+
void Test<T>()
241+
{
242+
TestParsingWithDefaults<T>(args, (T)defaultValue, (T)result1, (T)result2, (T)result3);
243+
}
244+
}
245+
246+
private void TestParsingWithDefaults<T>(string[] args, T defaultValue, T result1, T result2, T result3)
247+
{
248+
var parser = new CommandLineParser<OptionsWithThreeParams<T>>();
206249

207250
parser.Configure(opt => opt.Option1)
208251
.Name("1")
209-
.Default(result1)
252+
.Default(defaultValue)
210253
.Required();
211254

212255
parser.Configure(opt => opt.Option2)
213256
.Name("2")
214-
.Default(result2)
257+
.Default(defaultValue)
215258
.Required();
216259

217260
parser.Configure(opt => opt.Option3)
218261
.Name("3")
219-
.Default(result3)
262+
.Default(defaultValue)
220263
.Required();
221264

222265
var parsed = parser.Parse(args);
@@ -341,6 +384,32 @@ public void BoolResolverSpecialCaseParsesCorrectly(string[] args, bool expected)
341384
Assert.Equal(expected, result.Result.Option2);
342385
}
343386

387+
#region Issue_35_Bool_Option_Not_Parsed_Correctly
388+
389+
[Theory]
390+
[InlineData(new string[] { "-v", "command" }, true)]
391+
[InlineData(new string[] { "command", "-v" }, true)]
392+
public void BoolResolverSpecialCaseParsesCorrectlyWithDefaultValueAndNotBeingSpecified(string[] args, bool expected)
393+
{
394+
var parser = new CommandLineParser<Model_Issue_35>();
395+
396+
parser.AddCommand().Name("command");
397+
398+
var result = parser.Parse(args);
399+
400+
result.AssertNoErrors();
401+
402+
Assert.Equal(expected, result.Result.VerbA);
403+
}
404+
405+
private class Model_Issue_35
406+
{
407+
[Name("v", "verb"), Description("Print usage"), DefaultValue(false)]
408+
public bool VerbA { get; set; }
409+
}
410+
411+
#endregion
412+
344413
[Fact]
345414
public void ConfigureTests()
346415
{
@@ -400,11 +469,11 @@ public class EnumOptions
400469
public EnumOption EnumOption { get; set; }
401470
}
402471

403-
private class OptionsWithThreeParams
472+
private class OptionsWithThreeParams<T>
404473
{
405-
public string Option1 { get; set; }
406-
public string Option2 { get; set; }
407-
public string Option3 { get; set; }
474+
public T Option1 { get; set; }
475+
public T Option2 { get; set; }
476+
public T Option3 { get; set; }
408477
}
409478
}
410479
}

CommandLineParser/CommandLineParser`TOption.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,13 @@ private void ParseCommands(IList<Exception> errors, ParseResult<TOption> result,
330330

331331
var cmdParseResult = cmd.Parse(argumentManager);
332332

333+
if (result.HelpRequested)
334+
break;
335+
333336
if (cmdParseResult.HasErrors)
334337
errors.Add(new CommandParseException(cmd, cmdParseResult.Errors));
335338

336339
result.MergeResult(cmdParseResult);
337-
338-
if (result.HelpRequested)
339-
break;
340340
}
341341
}
342342

@@ -357,13 +357,14 @@ private void ParseOptions(IList<Exception> errors, ParseResult<TOption> result,
357357

358358
continue;
359359
}
360-
else if (!model.HasValue && option.HasDefault)
360+
else if ((!found && !model.HasValue && option.HasDefault) ||
361+
(found && !option.CanParse(model) && option.HasDefault))
361362
{
362363
option.UseDefault();
363364

364365
continue;
365366
}
366-
else if (!option.CanParse(model))
367+
else if (found && !option.CanParse(model))
367368
{
368369
errors.Add(new OptionParseException(option, model));
369370

CommandLineParser/Core/Command/CommandLineCommand`TOption`TCommandOption.cs

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -98,29 +98,17 @@ public override ICommandParserResult Parse(IArgumentManager argumentManager)
9898
var result = new CommandParserResult(this);
9999
var errors = new List<Exception>();
100100

101-
foreach (var cmd in m_commands)
102-
{
103-
if (!argumentManager.TryGetValue(cmd, out ArgumentModel model))
104-
{
105-
if (cmd.IsRequired)
106-
errors.Add(new CommandNotFoundException(cmd));
107-
108-
result.MergeResult(new CommandNotFoundParserResult(cmd));
109-
110-
continue;
111-
}
112-
113-
var cmdParseResult = cmd.Parse(argumentManager);
101+
ParseCommands(errors, result, argumentManager);
114102

115-
if (cmdParseResult.HelpRequested)
116-
return result;
103+
ParseOptions(errors, result, argumentManager);
117104

118-
if (cmdParseResult.HasErrors)
119-
errors.Add(new CommandParseException(cmd, cmdParseResult.Errors));
105+
result.MergeResult(errors);
120106

121-
result.MergeResult(cmdParseResult);
122-
}
107+
return result;
108+
}
123109

110+
private void ParseOptions(IList<Exception> errors, CommandParserResult result, IArgumentManager argumentManager)
111+
{
124112
foreach (var o in m_options)
125113
{
126114
var option = o.Value;
@@ -136,13 +124,14 @@ public override ICommandParserResult Parse(IArgumentManager argumentManager)
136124

137125
continue;
138126
}
139-
else if (!model.HasValue && option.HasDefault)
127+
else if ((!found && !model.HasValue && option.HasDefault) ||
128+
(found && !option.CanParse(model) && option.HasDefault))
140129
{
141130
option.UseDefault();
142131

143132
continue;
144133
}
145-
else if (!option.CanParse(model))
134+
else if (found && !option.CanParse(model))
146135
{
147136
errors.Add(new OptionParseException(option, model));
148137

@@ -151,10 +140,32 @@ public override ICommandParserResult Parse(IArgumentManager argumentManager)
151140

152141
option.Parse(model);
153142
}
143+
}
154144

155-
result.MergeResult(errors);
145+
private void ParseCommands(IList<Exception> errors, CommandParserResult result, IArgumentManager argumentManager)
146+
{
147+
foreach (var cmd in m_commands)
148+
{
149+
if (!argumentManager.TryGetValue(cmd, out ArgumentModel model))
150+
{
151+
if (cmd.IsRequired)
152+
errors.Add(new CommandNotFoundException(cmd));
156153

157-
return result;
154+
result.MergeResult(new CommandNotFoundParserResult(cmd));
155+
156+
continue;
157+
}
158+
159+
var cmdParseResult = cmd.Parse(argumentManager);
160+
161+
if (cmdParseResult.HelpRequested)
162+
break;
163+
164+
if (cmdParseResult.HasErrors)
165+
errors.Add(new CommandParseException(cmd, cmdParseResult.Errors));
166+
167+
result.MergeResult(cmdParseResult);
168+
}
158169
}
159170

160171
private bool HelpRequested(CommandParserResult result, CommandLineOptionBase option, ArgumentModel model)

CommandLineParser/Core/Exceptions/OptionParseException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private static string CreateMessage(ICommandLineOption option, ArgumentModel arg
3131
string shortName = hasShort ? option.ShortName : string.Empty;
3232
string longName = hasLong ? option.LongName : string.Empty;
3333

34-
return $"Unable to parse option {shortName}{hasBothSeperator}{longName} value '{argModel.Value}' is invalid!";
34+
return $"Unable to parse option '{shortName}{hasBothSeperator}{longName}' value '{argModel.Value}' is invalid!";
3535
}
3636
}
3737
}

CommandLineParser/Core/Parsing/Resolvers/StringResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace MatthiWare.CommandLine.Core.Parsing.Resolvers
55
{
66
internal class StringResolver : ArgumentResolver<string>
77
{
8-
public override bool CanResolve(ArgumentModel model) => true;
8+
public override bool CanResolve(ArgumentModel model) => model.HasValue;
99

1010
public override string Resolve(ArgumentModel model) => model.HasValue ? model.Value : null;
1111
}

0 commit comments

Comments
 (0)