Skip to content

Commit 17476ab

Browse files
authored
fix: make UnrecognizedArgumentHandling per command scope (#371)
1 parent e556ca6 commit 17476ab

File tree

7 files changed

+91
-40
lines changed

7 files changed

+91
-40
lines changed

src/CommandLineUtils/Attributes/CommandAttribute.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,13 @@ public string? Name
9393
/// <summary>
9494
/// Set the behavior for how to handle unrecognized arguments.
9595
/// </summary>
96-
public UnrecognizedArgumentHandling UnrecognizedArgumentHandling { get; set; } = UnrecognizedArgumentHandling.Throw;
96+
public UnrecognizedArgumentHandling UnrecognizedArgumentHandling
97+
{
98+
get => _unrecognizedArgumentHandling ?? UnrecognizedArgumentHandling.Throw;
99+
set => _unrecognizedArgumentHandling = value;
100+
}
101+
102+
private UnrecognizedArgumentHandling? _unrecognizedArgumentHandling;
97103

98104
/// <summary>
99105
/// Allow '--' to be used to stop parsing arguments.
@@ -172,7 +178,6 @@ internal void Configure(CommandLineApplication app)
172178
app.FullName = FullName;
173179
app.ResponseFileHandling = ResponseFileHandling;
174180
app.ShowInHelpText = ShowInHelpText;
175-
app.UnrecognizedArgumentHandling = UnrecognizedArgumentHandling;
176181
app.OptionsComparison = OptionsComparison;
177182
app.ValueParsers.ParseCulture = ParseCulture;
178183
app.UsePagerForHelpText = UsePagerForHelpText;
@@ -181,6 +186,11 @@ internal void Configure(CommandLineApplication app)
181186
{
182187
app.ClusterOptions = _clusterOptions.Value;
183188
}
189+
190+
if (_unrecognizedArgumentHandling.HasValue)
191+
{
192+
app.UnrecognizedArgumentHandling = _unrecognizedArgumentHandling.Value;
193+
}
184194
}
185195
}
186196
}

src/CommandLineUtils/CommandLineApplication.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ internal CommandLineApplication(
122122
SetContext(context);
123123
_services = new Lazy<IServiceProvider>(() => new ServiceProvider(this));
124124
ValueParsers = parent?.ValueParsers ?? new ValueParserProvider();
125-
_parserConfig = parent?._parserConfig ?? new ParserConfig();
125+
_parserConfig = new ParserConfig();
126126
_clusterOptions = parent?._clusterOptions;
127127
UsePagerForHelpText = parent?.UsePagerForHelpText ?? false;
128128

@@ -255,7 +255,7 @@ public CommandOption? OptionHelp
255255
/// </summary>
256256
public UnrecognizedArgumentHandling UnrecognizedArgumentHandling
257257
{
258-
get => _parserConfig.UnrecognizedArgumentHandling;
258+
get => _parserConfig.UnrecognizedArgumentHandling ?? Parent?.UnrecognizedArgumentHandling ?? UnrecognizedArgumentHandling.Throw;
259259
set => _parserConfig.UnrecognizedArgumentHandling = value;
260260
}
261261

@@ -356,10 +356,23 @@ public bool ClusterOptions
356356
/// </example>
357357
public char[] OptionNameValueSeparators
358358
{
359-
get => _parserConfig.OptionNameValueSeparators;
360-
set => _parserConfig.OptionNameValueSeparators = value;
359+
get => _parserConfig.OptionNameValueSeparators ?? Parent?.OptionNameValueSeparators ?? new[] { ' ', ':', '=' };
360+
set
361+
{
362+
if (value is null)
363+
{
364+
throw new ArgumentNullException(nameof(value));
365+
}
366+
else if (value.Length == 0)
367+
{
368+
throw new ArgumentException(Strings.IsEmptyArray, nameof(value));
369+
}
370+
_parserConfig.OptionNameValueSeparators = value;
371+
}
361372
}
362373

374+
internal bool OptionNameAndValueCanBeSpaceSeparated => Array.IndexOf(this.OptionNameValueSeparators, ' ') >= 0;
375+
363376
/// <summary>
364377
/// Gets the default value parser provider.
365378
/// <para>
@@ -728,7 +741,7 @@ public ParseResult Parse(params string[] args)
728741

729742
args ??= Util.EmptyArray<string>();
730743

731-
var processor = new CommandLineProcessor(this, _parserConfig, args);
744+
var processor = new CommandLineProcessor(this, args);
732745
var result = processor.Process();
733746
result.SelectedCommand.HandleParseResult(result);
734747
return result;

src/CommandLineUtils/Internal/CommandLineProcessor.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ namespace McMaster.Extensions.CommandLineUtils
1414
internal sealed class CommandLineProcessor
1515
{
1616
private readonly CommandLineApplication _initialCommand;
17-
private readonly ParserConfig _config;
1817
private readonly ArgumentEnumerator _enumerator;
1918

2019
private CommandLineApplication _currentCommand
@@ -31,12 +30,10 @@ private CommandLineApplication _currentCommand
3130

3231
public CommandLineProcessor(
3332
CommandLineApplication command,
34-
ParserConfig config,
3533
IReadOnlyList<string> arguments)
3634
{
3735
_initialCommand = command;
38-
_config = config ?? throw new ArgumentNullException(nameof(config));
39-
_enumerator = new ArgumentEnumerator(command, _config, arguments ?? new string[0]);
36+
_enumerator = new ArgumentEnumerator(command, arguments ?? new string[0]);
4037
CheckForShortOptionClustering(command);
4138
}
4239

@@ -260,7 +257,7 @@ private bool ProcessOption(OptionArgument arg)
260257
}
261258
else
262259
{
263-
if (_config.OptionNameAndValueCanBeSpaceSeparated)
260+
if (_currentCommand.OptionNameAndValueCanBeSpaceSeparated)
264261
{
265262
if (_enumerator.MoveNext())
266263
{
@@ -334,7 +331,7 @@ private bool ProcessArgumentSeparator()
334331

335332
private bool ProcessUnexpectedArg(string argTypeName, string? argValue = null)
336333
{
337-
switch (_config.UnrecognizedArgumentHandling)
334+
switch (_currentCommand.UnrecognizedArgumentHandling)
338335
{
339336
case UnrecognizedArgumentHandling.Throw:
340337
_currentCommand.ShowHint();
@@ -429,11 +426,9 @@ private sealed class ArgumentEnumerator : IEnumerator<Argument?>
429426
{
430427
private readonly IEnumerator<string> _rawArgEnumerator;
431428
private IEnumerator<string>? _rspEnumerator;
432-
private readonly ParserConfig _config;
433429

434-
public ArgumentEnumerator(CommandLineApplication command, ParserConfig config, IReadOnlyList<string> rawArguments)
430+
public ArgumentEnumerator(CommandLineApplication command, IReadOnlyList<string> rawArguments)
435431
{
436-
_config = config;
437432
CurrentCommand = command;
438433
_rawArgEnumerator = rawArguments.GetEnumerator();
439434
}
@@ -489,15 +484,15 @@ private Argument CreateArgument(string raw)
489484

490485
if (raw[1] != '-')
491486
{
492-
return new OptionArgument(raw, _config.OptionNameValueSeparators, isShortOption: true);
487+
return new OptionArgument(raw, CurrentCommand.OptionNameValueSeparators, isShortOption: true);
493488
}
494489

495490
if (raw.Length == 2)
496491
{
497492
return ArgumentSeparatorArgument.Instance;
498493
}
499494

500-
return new OptionArgument(raw, _config.OptionNameValueSeparators, isShortOption: false);
495+
return new OptionArgument(raw, CurrentCommand.OptionNameValueSeparators, isShortOption: false);
501496
}
502497

503498
private IEnumerator<string> CreateRspParser(string path)
Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
// Copyright (c) Nate McMaster.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
64
namespace McMaster.Extensions.CommandLineUtils
75
{
86
/// <summary>
97
/// Configures the argument parser.
108
/// </summary>
119
internal class ParserConfig
1210
{
13-
private char[] _optionNameValueSeparators = { ' ', ':', '=' };
14-
1511
/// <summary>
1612
/// Characters used to separate the option name from the value.
1713
/// <para>
@@ -25,26 +21,11 @@ internal class ParserConfig
2521
/// <example>
2622
/// Given --name=value, = is the separator.
2723
/// </example>
28-
public char[] OptionNameValueSeparators
29-
{
30-
get => _optionNameValueSeparators;
31-
set
32-
{
33-
_optionNameValueSeparators = value ?? throw new ArgumentNullException(nameof(value));
34-
if (value.Length == 0)
35-
{
36-
throw new ArgumentException(Strings.IsNullOrEmpty, nameof(value));
37-
}
38-
OptionNameAndValueCanBeSpaceSeparated = Array.IndexOf(OptionNameValueSeparators, ' ') >= 0;
39-
}
40-
}
41-
42-
internal bool OptionNameAndValueCanBeSpaceSeparated { get; private set; } = true;
24+
public char[]? OptionNameValueSeparators { get; set; }
4325

4426
/// <summary>
4527
/// Set the behavior for how to handle unrecognized arguments.
4628
/// </summary>
47-
public UnrecognizedArgumentHandling UnrecognizedArgumentHandling { get; set; } =
48-
UnrecognizedArgumentHandling.Throw;
29+
public UnrecognizedArgumentHandling? UnrecognizedArgumentHandling { get; set; }
4930
}
5031
}

src/CommandLineUtils/Properties/Strings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal static class Strings
1616
public const string DefaultVersionTemplate = "--version";
1717
public const string DefaultVersionOptionDescription = "Show version information.";
1818

19-
public const string IsNullOrEmpty = "Value is null or empty.";
19+
public const string IsEmptyArray = "value is an empty array.";
2020

2121
public const string PathMustNotBeRelative = "File path must not be relative.";
2222

test/CommandLineUtils.Tests/CommandLineApplicationOfTTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,28 @@ public ThrowsInCtorClass()
101101
public void OnExecute() { }
102102
}
103103

104+
[Command(UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.StopParsingAndCollect)]
105+
[Subcommand(typeof(FooCommand))]
106+
class NotThrowOnUnrecognizedArgumentClass
107+
{
108+
public void OnExecute() { }
109+
}
110+
111+
[Command]
112+
class FooCommand
113+
{
114+
}
115+
116+
[Fact]
117+
public void AllowNoThrowBehaviorOnUnexpectedOptionWhenHasSubcommand()
118+
{
119+
using var app = new CommandLineApplication<NotThrowOnUnrecognizedArgumentClass>();
120+
app.Conventions.UseDefaultConventions();
121+
122+
// should not throw
123+
app.Execute("--unexpected");
124+
}
125+
104126
[Fact]
105127
public void ItDoesNotInitalizeClassUnlessNecessary()
106128
{

test/CommandLineUtils.Tests/CommandLineApplicationTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,22 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionAfterSubcommand()
494494
Assert.Equal(unexpectedOption, arg);
495495
}
496496

497+
[Fact]
498+
public void AllowNoThrowBehaviorOnUnexpectedOptionWhenHasSubcommand()
499+
{
500+
var app = new CommandLineApplication
501+
{
502+
UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.StopParsingAndCollect
503+
};
504+
app.Command("k", c =>
505+
{
506+
c.UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.Throw;
507+
});
508+
509+
// should not throw
510+
app.Execute("--unexpected");
511+
}
512+
497513
[Fact]
498514
public void CollectUnrecognizedArguments()
499515
{
@@ -754,6 +770,20 @@ public void OptionSeparatorMustNotUseSpace()
754770
Assert.Equal("abc", option.Value());
755771
}
756772

773+
[Fact]
774+
public void AllowSettingOptionNameValueSeparatorsPerCommand()
775+
{
776+
var app = new CommandLineApplication();
777+
778+
app.Command("k", c =>
779+
{
780+
c.Option("-o", "option desc.", CommandOptionType.SingleValue);
781+
c.OptionNameValueSeparators = new[] { '=' };
782+
});
783+
784+
Assert.ThrowsAny<CommandParsingException>(() => app.Parse("k", "-o", "foo"));
785+
}
786+
757787
[Fact]
758788
public void HelpTextIgnoresHiddenItems()
759789
{

0 commit comments

Comments
 (0)