-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathOptionSet.cs
More file actions
234 lines (220 loc) · 9.37 KB
/
OptionSet.cs
File metadata and controls
234 lines (220 loc) · 9.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
using BytecodeApi.Extensions;
using BytecodeApi.IO;
using System.Collections;
using System.Diagnostics;
using System.Runtime.Versioning;
namespace BytecodeApi.CommandLineParser;
/// <summary>
/// Represents a set of commandline options that can be used to parse a given commandline.
/// </summary>
[DebuggerDisplay($"{nameof(OptionSet)}: OptionPrefix = {{OptionPrefix}}, OptionAlternativePrefix = {{OptionAlternativePrefix}}, Options: {{Count}}")]
public sealed class OptionSet : ICollection<Option>
{
private readonly List<Option> Options;
/// <summary>
/// Gets the prefix <see cref="string" /> for commandline options. This is typically a dash or a slash character.
/// </summary>
public string OptionPrefix { get; }
/// <summary>
/// Gets the prefix <see cref="string" /> for the alternative commandline options. This is typically a double dash.
/// </summary>
public string OptionAlternativePrefix { get; }
/// <summary>
/// Gets a value indicating whether the commandline parsing ignores character casing.
/// </summary>
public bool OptionPrefixIgnoreCase { get; }
/// <summary>
/// Gets the number of elements contained in the <see cref="OptionSet" />.
/// </summary>
public int Count => Options.Count;
/// <summary>
/// Gets a value indicating whether the <see cref="OptionSet" /> is read-only.
/// </summary>
public bool IsReadOnly => false;
/// <summary>
/// Initializes a new instance of the <see cref="OptionSet" /> class with the specified prefixes.
/// <para>Example: "-" and "--", or "/" and "--"</para>
/// </summary>
/// <param name="optionPrefix">The prefix <see cref="string" /> for commandline options. This is typically a dash or a slash character.</param>
/// <param name="optionAlternativePrefix">The prefix <see cref="string" /> for the alternative commandline options. This is typically a double dash.</param>
public OptionSet(string optionPrefix, string optionAlternativePrefix) : this(optionPrefix, optionAlternativePrefix, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OptionSet" /> class with the specified prefixes.
/// <para>Example: "-" and "--", or "/" and "--"</para>
/// </summary>
/// <param name="optionPrefix">The prefix <see cref="string" /> for commandline options. This is typically a dash or a slash character.</param>
/// <param name="optionAlternativePrefix">The prefix <see cref="string" /> for the alternative commandline options. This is typically a double dash.</param>
/// <param name="optionPrefixIgnoreCase"><see langword="true" /> to ignore character casing during commandline parsing.</param>
public OptionSet(string optionPrefix, string optionAlternativePrefix, bool optionPrefixIgnoreCase)
{
Check.ArgumentNull(optionPrefix);
Check.ArgumentEx.StringNotEmpty(optionPrefix);
Check.Argument(optionPrefix.All(c => c.IsSymbol() || c.IsPunctuation()), nameof(optionPrefix), "String must contain only symbols or punctuation characters.");
Check.ArgumentNull(optionAlternativePrefix);
Check.ArgumentEx.StringNotEmpty(optionAlternativePrefix);
Check.Argument(optionAlternativePrefix.All(c => c.IsSymbol() || c.IsPunctuation()), nameof(optionAlternativePrefix), "String must contain only symbols or punctuation characters.");
Check.Argument(optionPrefix != optionAlternativePrefix, nameof(optionAlternativePrefix), "Option prefix and alternative option prefix must not be identical.");
OptionPrefix = optionPrefix;
OptionAlternativePrefix = optionAlternativePrefix;
OptionPrefixIgnoreCase = optionPrefixIgnoreCase;
Options = [];
}
/// <summary>
/// Creates a new <see cref="Option" /> and adds it to this <see cref="OptionSet" />.
/// </summary>
/// <param name="arguments">A collection of strings that defines what arguments apply to the new <see cref="Option" />.</param>
/// <returns>
/// A reference to this instance after the operation has completed.
/// </returns>
public OptionSet Add(params string[] arguments)
{
return Add(new Option(arguments));
}
/// <summary>
/// Creates a new <see cref="Option" /> and adds it to this <see cref="OptionSet" />.
/// </summary>
/// <param name="arguments">A collection of strings that defines what arguments apply to the new <see cref="Option" />.</param>
/// <param name="alternatives">A collection of strings that defines what arguments apply to the new <see cref="Option" /> alternatively.</param>
/// <returns>
/// A reference to this instance after the operation has completed.
/// </returns>
public OptionSet Add(string[] arguments, string[]? alternatives)
{
return Add(new Option(arguments, alternatives));
}
/// <summary>
/// Adds the specified <see cref="Option" /> to this <see cref="OptionSet" />.
/// </summary>
/// <param name="option">The <see cref="Option" /> to be added.</param>
/// <returns>
/// A reference to this instance after the operation has completed.
/// </returns>
public OptionSet Add(Option option)
{
Check.ArgumentNull(option);
Check.Argument(Options.SelectMany(o => o.Arguments).Intersect(option.Arguments).None(), nameof(option), "An option with this argument was already added.");
Check.Argument(Options.SelectMany(o => o.Alternatives).Intersect(option.Alternatives).None(), nameof(option), "An option with this argument alternative was already added.");
Options.Add(option);
return this;
}
/// <summary>
/// Parses a commandline arguments and returns a new <see cref="ParsedOptionSet" /> with the result.
/// </summary>
/// <param name="args">An array of <see cref="string" /> objects with the commandline arguments, excluding the executable filename.</param>
/// <returns>
/// A new <see cref="ParsedOptionSet" /> with the parsed commandline.
/// </returns>
public ParsedOptionSet Parse(string[] args)
{
Check.ArgumentNull(args);
Check.ArgumentEx.ArrayValuesNotNull(args);
List<string> parsedArguments = [];
List<ParsedOption> parsedOptions = [];
Option? currentOption = null;
List<string> currentOptionValues = [];
for (int i = 0; i < args.Length; i++)
{
StringComparison comparison = OptionPrefixIgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
Option? option = Options.FirstOrDefault(o =>
o.Arguments.Any(a => args[i].Equals(OptionPrefix + a, comparison)) ||
o.Alternatives.Any(a => args[i].Equals(OptionAlternativePrefix + a, comparison)));
if (option == null)
{
if (currentOption == null)
{
parsedArguments.Add(args[i]);
}
else
{
currentOptionValues.Add(args[i]);
}
}
else
{
AddToCurrentOption();
currentOption = option;
}
}
AddToCurrentOption();
return new(parsedArguments, parsedOptions);
void AddToCurrentOption()
{
if (currentOption != null)
{
parsedOptions.Add(new(currentOption, currentOptionValues.ToArray()));
currentOptionValues.Clear();
}
}
}
/// <summary>
/// Parses a commandline <see cref="string" /> and returns a new <see cref="ParsedOptionSet" /> with the result.
/// </summary>
/// <param name="commandLine">The commandline <see cref="string" />, where each argument is separated with spaces, excluding the executable filename. Arguments containing spaces should be quoted.</param>
/// <returns>
/// A new <see cref="ParsedOptionSet" /> with the parsed commandline.
/// </returns>
[SupportedOSPlatform("windows")]
public ParsedOptionSet Parse(string commandLine)
{
Check.ArgumentNull(commandLine);
return Parse(CommandLine.GetArguments(commandLine));
}
void ICollection<Option>.Add(Option item)
{
Add(item);
}
/// <summary>
/// Removes the first occurrence of a specific <see cref="Option" /> from the <see cref="OptionSet" />.
/// </summary>
/// <param name="item">The <see cref="Option" /> to remove from the <see cref="OptionSet" />.</param>
/// <returns>
/// <see langword="true" />, if <paramref name="item" /> is successfully removed;
/// otherwise, <see langword="false" />.
/// This method also returns <see langword="false" />, if <paramref name="item" /> was not found in the <see cref="OptionSet" />.
/// </returns>
public bool Remove(Option item)
{
return Options.Remove(item);
}
/// <summary>
/// Removes all elements from the <see cref="OptionSet" />.
/// </summary>
public void Clear()
{
Options.Clear();
}
/// <summary>
/// Determines whether an element is in the <see cref="OptionSet" />.
/// </summary>
/// <param name="item">The <see cref="Option" /> to locate in the <see cref="OptionSet" />.</param>
/// <returns>
/// <see langword="true" />, if <paramref name="item" /> is found in the <see cref="OptionSet" />;
/// otherwise, <see langword="false" />.
/// </returns>
public bool Contains(Option item)
{
return Options.Contains(item);
}
void ICollection<Option>.CopyTo(Option[] array, int arrayIndex)
{
Check.ArgumentNull(array);
Check.IndexOutOfRange(arrayIndex, array.Length - Count + 1);
Options.CopyTo(array, arrayIndex);
}
/// <summary>
/// Returns an enumerator that iterates through the <see cref="OptionSet" />.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the <see cref="OptionSet" />.
/// </returns>
public IEnumerator<Option> GetEnumerator()
{
return Options.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Options.GetEnumerator();
}
}