Skip to content

Commit 82bf18f

Browse files
committed
API review: remove ParserSettings
Will revisit the need for a separate parser settings class in 3.0
1 parent 40457a3 commit 82bf18f

File tree

9 files changed

+146
-152
lines changed

9 files changed

+146
-152
lines changed

src/CommandLineUtils/Attributes/CommandAttribute.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,40 @@ public string Name
100100
/// </summary>
101101
public CultureInfo ParseCulture { get; set; } = CultureInfo.CurrentCulture;
102102

103+
/// <summary>
104+
/// <para>
105+
/// One or more options of <see cref="CommandOptionType.NoValue"/>, followed by at most one option that takes values, should be accepted when grouped behind one '-' delimiter.
106+
/// </para>
107+
/// <para>
108+
/// When true, the following are equivalent.
109+
///
110+
/// <code>
111+
/// -abcXyellow
112+
/// -abcX=yellow
113+
/// -abcX:yellow
114+
/// -abc -X=yellow
115+
/// -ab -cX=yellow
116+
/// -a -b -c -Xyellow
117+
/// -a -b -c -X yellow
118+
/// -a -b -c -X=yellow
119+
/// -a -b -c -X:yellow
120+
/// </code>
121+
/// </para>
122+
/// <para>
123+
/// This defaults to true unless an option with a short name of two or more characters is added.
124+
/// </para>
125+
/// </summary>
126+
/// <remarks>
127+
/// <seealso href="https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html"/>
128+
/// </remarks>
129+
public bool ClusterOptions
130+
{
131+
get => _clusterOptions ?? true;
132+
set => _clusterOptions = value;
133+
}
134+
135+
private bool? _clusterOptions;
136+
103137
internal void Configure(CommandLineApplication app)
104138
{
105139
// this might have been set from SubcommandAttribute
@@ -119,6 +153,11 @@ internal void Configure(CommandLineApplication app)
119153
app.ThrowOnUnexpectedArgument = ThrowOnUnexpectedArgument;
120154
app.OptionsComparison = OptionsComparison;
121155
app.ValueParsers.ParseCulture = ParseCulture;
156+
157+
if (_clusterOptions.HasValue)
158+
{
159+
app.ClusterOptions = _clusterOptions.Value;
160+
}
122161
}
123162
}
124163
}

src/CommandLineUtils/CommandLineApplication.cs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ internal CommandLineApplication(CommandLineApplication parent,
104104
SetContext(context);
105105
_services = new Lazy<IServiceProvider>(() => new ServiceProvider(this));
106106
ValueParsers = parent?.ValueParsers ?? new ValueParserProvider();
107-
ParserSettings = parent?.ParserSettings ?? new ParserSettings();
107+
_clusterOptions = parent?._clusterOptions;
108108

109109
_conventionContext = CreateConventionContext();
110110

@@ -280,6 +280,42 @@ public CommandOption OptionHelp
280280
/// </summary>
281281
public StringComparison OptionsComparison { get; set; }
282282

283+
/// <summary>
284+
/// <para>
285+
/// One or more options of <see cref="CommandOptionType.NoValue"/>, followed by at most one option that takes values, should be accepted when grouped behind one '-' delimiter.
286+
/// </para>
287+
/// <para>
288+
/// When true, the following are equivalent.
289+
///
290+
/// <code>
291+
/// -abcXyellow
292+
/// -abcX=yellow
293+
/// -abcX:yellow
294+
/// -abc -X=yellow
295+
/// -ab -cX=yellow
296+
/// -a -b -c -Xyellow
297+
/// -a -b -c -X yellow
298+
/// -a -b -c -X=yellow
299+
/// -a -b -c -X:yellow
300+
/// </code>
301+
/// </para>
302+
/// <para>
303+
/// This defaults to true unless an option with a short name of two or more characters is added.
304+
/// </para>
305+
/// </summary>
306+
/// <remarks>
307+
/// <seealso href="https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html"/>
308+
/// </remarks>
309+
public bool ClusterOptions
310+
{
311+
get => _clusterOptions ?? true;
312+
set => _clusterOptions = value;
313+
}
314+
315+
private bool? _clusterOptions;
316+
317+
internal bool ClusterOptionsWasSetExplicitly => _clusterOptions.HasValue;
318+
283319
/// <summary>
284320
/// Gets the default value parser provider.
285321
/// <para>
@@ -623,16 +659,25 @@ public ParseResult Parse(params string[] args)
623659
{
624660
args = args ?? new string[0];
625661

626-
var processor = new CommandLineProcessor(this, ParserSettings, args);
662+
var processor = new CommandLineProcessor(this, args);
627663
var result = processor.Process();
628664
result.SelectedCommand.HandleParseResult(result);
629665
return result;
630666
}
631667

632668
/// <summary>
633-
/// Settings to control the behavior of the parser.
669+
/// When an invalid argument is given, make suggestions in the error message
670+
/// about similar, valid commands or options.
671+
/// <para>
672+
/// $ git pshu
673+
/// Specify --help for a list of available options and commands
674+
/// Unrecognized command or argument 'pshu'
675+
///
676+
/// Did you mean this?
677+
/// push
678+
/// </para>
634679
/// </summary>
635-
public ParserSettings ParserSettings { get; }
680+
public bool MakeSuggestionsInErrorMessage { get; set; } = true;
636681

637682
/// <summary>
638683
/// Handle the result of parsing command line arguments.

src/CommandLineUtils/MissingParameterlessConstructorException.cs renamed to src/CommandLineUtils/Errors/MissingParameterlessConstructorException.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
using System;
1+
// Copyright (c) Nate McMaster.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Reflection;
26

37
namespace McMaster.Extensions.CommandLineUtils
48
{
59
/// <summary>
610
/// The exception that is thrown when trying to instantiate a model with no parameterless constructor.
711
/// </summary>
8-
public class MissingParameterlessConstructorException : Exception
12+
public class MissingParameterlessConstructorException : TargetException
913
{
1014
/// <summary>
1115
/// Gets the type that caused the exception.
@@ -17,7 +21,8 @@ public class MissingParameterlessConstructorException : Exception
1721
/// </summary>
1822
/// <param name="type">The type missing a parameterless constructor.</param>
1923
/// <param name="innerException">The original exception.</param>
20-
public MissingParameterlessConstructorException(Type type, Exception innerException) : base($"Class {type.FullName} does not have a parameterless constructor", innerException)
24+
public MissingParameterlessConstructorException(Type type, Exception innerException)
25+
: base($"Class {type.FullName} does not have a parameterless constructor", innerException)
2126
{
2227
Type = type;
2328
}

src/CommandLineUtils/UnrecognizedCommandParsingException.cs renamed to src/CommandLineUtils/Errors/UnrecognizedCommandParsingException.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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;
45
using System.Collections.Generic;
56

67
namespace McMaster.Extensions.CommandLineUtils
@@ -11,23 +12,28 @@ namespace McMaster.Extensions.CommandLineUtils
1112
/// </summary>
1213
public class UnrecognizedCommandParsingException : CommandParsingException
1314
{
14-
1515
/// <summary>
1616
/// Initializes an instance of <see cref="UnrecognizedCommandParsingException"/>.
1717
/// </summary>
1818
/// <param name="command"></param>
19+
/// <param name="nearestMatches">The options or commands that </param>
1920
/// <param name="message"></param>
20-
public UnrecognizedCommandParsingException(CommandLineApplication command, string message) : base(command, message)
21-
{ }
21+
public UnrecognizedCommandParsingException(CommandLineApplication command,
22+
IEnumerable<string> nearestMatches,
23+
string message)
24+
: base(command, message)
25+
{
26+
NearestMatches = nearestMatches ?? throw new ArgumentNullException(nameof(nearestMatches));
27+
}
2228

2329
/// <summary>
24-
/// A list of strings representing suggestions about similar and valid commands or options for the invalid
30+
/// A collection of strings representing suggestions about similar and valid commands or options for the invalid
2531
/// argument that caused this <see cref="UnrecognizedCommandParsingException"/>.
2632
/// </summary>
2733
/// <remarks>
28-
/// This property is set if <see cref="ParserSettings.MakeSuggestionsInErrorMessage"/> is true
34+
/// This property always be empty <see cref="CommandLineApplication.MakeSuggestionsInErrorMessage"/> is false.
2935
/// </remarks>
3036
/// <value>This property get/set the suggestions for an invalid argument.</value>
31-
public List<string> NearestMatches { get; set; }
37+
public IEnumerable<string> NearestMatches { get; }
3238
}
3339
}

src/CommandLineUtils/Internal/CommandLineProcessor.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ namespace McMaster.Extensions.CommandLineUtils
1515
internal sealed class CommandLineProcessor
1616
{
1717
private readonly CommandLineApplication _app;
18-
private readonly ParserSettings _settings;
1918
private readonly CommandLineApplication _initialCommand;
2019
private readonly ParameterEnumerator _enumerator;
2120

@@ -32,30 +31,28 @@ private CommandLineApplication _currentCommand
3231
private CommandArgumentEnumerator _currentCommandArguments;
3332

3433
public CommandLineProcessor(CommandLineApplication command,
35-
ParserSettings settings,
3634
IReadOnlyList<string> arguments)
3735
{
3836
_app = command;
39-
_settings = settings;
4037
_initialCommand = command;
4138
_enumerator = new ParameterEnumerator(arguments ?? new string[0]);
4239

4340
// TODO in 3.0, remove this check, and make ClusterOptions true always
4441
// and make it an error to use short options with multiple characters
4542
var allOptions = command.Commands.SelectMany(c => c.Options).Concat(command.GetOptions());
46-
if (!settings.ClusterOptionsWasSetExplicitly)
43+
if (!command.ClusterOptionsWasSetExplicitly)
4744
{
48-
settings.ClusterOptions = !allOptions.Any(o => o.ShortName != null && o.ShortName.Length > 1);
45+
command.ClusterOptions = !allOptions.Any(o => o.ShortName != null && o.ShortName.Length > 1);
4946
}
5047

51-
if (settings.ClusterOptions)
48+
if (command.ClusterOptions)
5249
{
5350
foreach (var option in allOptions)
5451
{
5552
if (option.ShortName != null && option.ShortName.Length != 1)
5653
{
5754
throw new CommandParsingException(command,
58-
$"The ShortName on CommandOption is too long: '{option.ShortName}'. Short names cannot be more than one character long when {nameof(ParserSettings.ClusterOptions)} is enabled.");
55+
$"The ShortName on CommandOption is too long: '{option.ShortName}'. Short names cannot be more than one character long when {nameof(CommandLineApplication.ClusterOptions)} is enabled.");
5956
}
6057
}
6158
}
@@ -156,7 +153,7 @@ private bool ProcessOption()
156153
var name = arg.Name;
157154
if (arg.Type == ParameterType.ShortOption)
158155
{
159-
if (_settings.ClusterOptions)
156+
if (_currentCommand.ClusterOptions)
160157
{
161158
for (var i = 0; i < arg.Name.Length; i++)
162159
{
@@ -344,16 +341,15 @@ private void HandleUnexpectedArg(string argTypeName, string argValue = null)
344341
_currentCommand.ShowHint();
345342
var value = argValue ?? _enumerator.Current?.Raw;
346343

347-
List<string> suggestions = null;
348-
if (_settings.MakeSuggestionsInErrorMessage && !string.IsNullOrEmpty(value))
344+
var suggestions = Enumerable.Empty<string>();
345+
346+
if (_currentCommand.MakeSuggestionsInErrorMessage && !string.IsNullOrEmpty(value))
349347
{
350348
suggestions = SuggestionCreator.GetTopSuggestions(_currentCommand, value);
351349
}
352350

353-
throw new UnrecognizedCommandParsingException(_currentCommand, $"Unrecognized {argTypeName} '{value}'")
354-
{
355-
NearestMatches = suggestions
356-
};
351+
throw new UnrecognizedCommandParsingException(_currentCommand, suggestions,
352+
$"Unrecognized {argTypeName} '{value}'");
357353
}
358354

359355
// All remaining arguments are stored for further use

src/CommandLineUtils/Internal/StringDistance.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ internal static IEnumerable<string> GetBestMatchesSorted(Func<string, string, in
158158
.Where(candidate => candidate.normalizedDistance >= threshold)
159159
.OrderByDescending(candidate => candidate.normalizedDistance)
160160
.Select(candidate => candidate.stringValue);
161-
162161
}
163162
}
164163
}

src/CommandLineUtils/Internal/SuggestionCreator.cs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,59 @@
44
using System.Collections.Generic;
55
using System.Linq;
66

7-
namespace McMaster.Extensions.CommandLineUtils.Internal
7+
namespace McMaster.Extensions.CommandLineUtils
88
{
99
/// <summary>
1010
/// Creates suggestions based on an input string and a command object.
1111
/// </summary>
1212
internal static class SuggestionCreator
1313
{
14-
1514
/// <summary>
1615
/// Gets a list of suggestions from sub commands and options of <paramref name="command"/> that are likely to
1716
/// fix the invalid argument <paramref name="input"/>
1817
/// </summary>
1918
/// <param name="command"></param>
2019
/// <param name="input"></param>
2120
/// <returns>A list of string with suggestions or null if no suggestions were found</returns>
22-
public static List<string> GetTopSuggestions(CommandLineApplication command, string input)
21+
public static IEnumerable<string> GetTopSuggestions(CommandLineApplication command, string input)
2322
{
2423
var candidates = GetCandidates(command).ToList();
2524

2625
if (candidates.Count == 0)
2726
{
28-
return null;
27+
return Enumerable.Empty<string>();
2928
}
3029

3130
return StringDistance.GetBestMatchesSorted(StringDistance.DamareuLevenshteinDistance,
3231
input,
33-
candidates.Select(c => c.CompareValue),
34-
0.33d)
35-
.ToList();
32+
candidates,
33+
0.33d);
3634
}
3735

38-
private static IEnumerable<Candidate> GetCandidates(CommandLineApplication command)
36+
private static IEnumerable<string> GetCandidates(CommandLineApplication command)
3937
{
4038
foreach (var cmd in command.Commands)
4139
{
42-
yield return new Candidate(cmd.Name, cmd.Name);
40+
yield return cmd.Name;
4341
}
4442

4543
foreach (var option in command.GetOptions().Where(o => o.ShowInHelpText))
4644
{
4745
if (!string.IsNullOrEmpty(option.LongName))
4846
{
49-
yield return new Candidate("--" + option.LongName, option.LongName);
47+
yield return option.LongName;
5048
}
5149

5250
if (!string.IsNullOrEmpty(option.ShortName))
5351
{
54-
yield return new Candidate("-" + option.ShortName, option.ShortName);
52+
yield return option.ShortName;
5553
}
5654

5755
if (!string.IsNullOrEmpty(option.SymbolName))
5856
{
59-
yield return new Candidate("-" + option.SymbolName, option.SymbolName);
57+
yield return option.SymbolName;
6058
}
6159
}
6260
}
63-
64-
private struct Candidate
65-
{
66-
public Candidate(string displayValue, string compareValue)
67-
{
68-
DisplayValue = displayValue;
69-
CompareValue = compareValue;
70-
}
71-
72-
public string DisplayValue { get; }
73-
public string CompareValue { get; }
74-
}
7561
}
7662
}

0 commit comments

Comments
 (0)