-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathOptions.cs
564 lines (523 loc) · 22.4 KB
/
Options.cs
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
/*
* Knox.Options
* This is a Mono.Options semi-compatible library for managing CLI
* arguments and displaying help text for a program. Created as
* Mono.Options has an issue and was requiring significant
* modification to meet my needs. It was quicker to write a new
* version that supported a similar API than to fix the origional.
*
* Copyright © Matthew Knox, Knox Enterprises 2014-Present.
* This code is avalible under the MIT license in the state
* that it was avalible on 05/11/2014 from
* http://opensource.org/licenses/MIT .
*/
#region
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
#endregion
namespace PCRNetworkServer
{
/// <summary>
/// Set of CLI interface
/// </summary>
public class OptionSet : IEnumerable
{
/// <summary>
/// Index of individual options in option set.
/// </summary>
private readonly Dictionary<string, int> _lookupDictionary = new Dictionary<string, int>();
/// <summary>
/// List of options contained in this option set.
/// </summary>
private readonly List<Option> _options = new List<Option>();
private static readonly char[] ArgumentSeparators = { '=', ':' };
private static bool _optionStyleCanChange = true;
private static OptionStyle _optionsStyle = OptionStyle.Nix;
public static OptionStyle OptionsStyle
{
get { return _optionsStyle; }
set
{
if (!_optionStyleCanChange)
{
throw new Exception("After an option has been added the options style cannot be changed.");
}
_optionsStyle = value;
}
}
/// <summary>
/// Option prefixes for use with various option styles.
/// </summary>
private static readonly string[] OptionsPrefixes = { "-", "--", "/", "/" };
/// <summary>
/// Enumerator of this OptionSet
/// </summary>
/// <returns>The enumerator</returns>
public IEnumerator GetEnumerator()
{
return _options.GetEnumerator();
}
/// <summary>
/// Add a cli option to the set.
/// </summary>
/// <param name="cliOptions">The options to associate this option with.</param>
/// <param name="description">Description of this option.</param>
/// <param name="func">Action to run when this option is specified.</param>
/// <param name="conflictSilent">If a cli option has already been specified by a previous option
/// handle the error silently rather than throwing an exception.</param>
public void Add(string cliOptions, string description, Action<string> func, bool conflictSilent = true)
{
var option = new Option(cliOptions, description, func, OptionsStyle);
_options.Add(option);
var ind = _options.Count - 1;
foreach (var opt in option.Arguments)
{
try
{
_lookupDictionary.Add(opt, ind); // add reference for quick lookup
}
catch (Exception e)
{
if (conflictSilent)
{
continue;
}
var opt1 = opt; // remove all instances of this option, as we want to have a good options state
foreach (var op in option.Arguments.TakeWhile(op => op != opt1))
{
_lookupDictionary.Remove(op);
}
_options.Remove(option);
throw new OptionException("Option " + opt + " already specified for another option.", e, opt);
}
}
_optionStyleCanChange = false;
}
/// <summary>
/// Parses a set of arguments into the option equivilents and calls
/// the actions of those options.
/// </summary>
/// <param name="arguments">Arguments to parse.</param>
/// <returns>List of arguments that parsing failed for.</returns>
public List<OptionError> Parse(IEnumerable<string> arguments)
{
var optionsInError = new List<OptionError>();
var temp = new List<string>();
var readForOption = false;
var optionRead = -1;
var enumerable = arguments.ToList();
for (var i = 0; i <= enumerable.Count; i++)
{
if (i == enumerable.Count || enumerable[i].StartsWith(OptionsPrefixes[(int)OptionsStyle]) || !readForOption)
{
if (readForOption)
{
try
{
var arg = temp.Aggregate("", (current, t) => current + (t + " "));
arg = arg.Trim();
if (arg.Length == 0 && _options[optionRead].ExpectsArguments)
{
throw new OptionException("Option " + enumerable[i - 1 - temp.Count] + " expects arguments and none were provided.", arg);
}
_options[optionRead].Action(arg);
}
catch (OptionException e)
{
var err = new OptionError(_options[optionRead], enumerable[i - 1 - temp.Count], temp, e.Message);
optionsInError.Add(err);
}
finally
{
temp.Clear();
}
}
if (i == enumerable.Count)
{
continue;
}
try
{
var i1 = i;
foreach (var separator in ArgumentSeparators.Where(separator => enumerable[i1].Contains(separator)))
{
enumerable.RemoveAt(i);
enumerable.InsertRange(i, enumerable[i].Split(separator));
break;
}
var ind = _lookupDictionary[enumerable[i]];
optionRead = ind;
readForOption = true;
}
catch (Exception)
{
optionsInError.Add(new OptionError(null, enumerable[i], new string[0], "Unknown option: " + enumerable[i]));
readForOption = false;
}
}
else
{
temp.Add(enumerable[i]);
}
}
return optionsInError;
}
/// <summary>
/// Parses a set of arguments into the option equivilents and calls
/// the actions of those options.
/// </summary>
/// <param name="arguments">Arguments to parse.</param>
/// <exception cref="OptionException">On invalid options.</exception>
public void ParseExceptionally(IEnumerable<string> arguments)
{
var result = Parse(arguments);
if (result.Count <= 0) return;
var options = string.Join("\n", result);
var args = new List<string>();
foreach (var optionError in result)
{
args.Add(optionError.InputAs);
args.AddRange(optionError.ProvidedArguments);
}
throw new OptionException(options, args.ToArray());
}
/// <summary>
/// Style of options to use.
/// </summary>
public enum OptionStyle
{
Nix = 0,
Linux = Nix,
Unix = Nix,
Osx = Nix,
Windows = 2
}
/// <summary>
/// Represents an individual option of an OptionSet
/// </summary>
public class Option
{
/// <summary>
/// Creates a new option.
/// </summary>
/// <param name="options">Cli arguments that use this option.</param>
/// <param name="description">Description of this option.</param>
/// <param name="func">Action to perform when this option is specified.</param>
/// <param name="style">Style of option to use.</param>
/// <param name="optionalArgs">If this is true, arguments will be treated as non compulsary ones.</param>
public Option(string options, string description, Action<string> func, OptionStyle style, bool optionalArgs = false)
{
Action = func;
var spl = options.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
Arguments = spl.Select(s => OptionsPrefixes[(int)style + ((s.Length == 1) ? 0 : 1)] + s).ToArray();
var opts = new List<string>();
foreach (Match match in Regex.Matches(description, "{[^}]*}"))
{
var val = match.Value.Substring(1, match.Length - 2);
opts.Add(val.ToUpper());
description = description.Substring(0, match.Index) + val +
description.Substring(match.Index + match.Length);
}
Description = description;
Options = opts.ToArray();
ExpectsArguments = opts.Count > 0 && !optionalArgs;
}
/// <summary>
/// Arguments that this option provides.
/// </summary>
public string[] Arguments { get; private set; }
/// <summary>
/// Words to be displayed as options.
/// </summary>
public string[] Options { get; private set; }
/// <summary>
/// Specifies if this option requires arguments to be passed to it.
/// </summary>
public bool ExpectsArguments { get; private set; }
/// <summary>
/// Description of this option.
/// </summary>
public string Description { get; private set; }
/// <summary>
/// Action to perform when this option is specified.
/// </summary>
public Action<string> Action { get; private set; }
}
#region Help Text
/// <summary>
/// Print help.
/// </summary>
/// <param name="programNameDescription">Decription to accompany the program name.</param>
/// <param name="programSynopsis">Synopsis section of the help.</param>
/// <param name="optionDescriptionPrefix">Text before options.</param>
/// <param name="optionDescriptionPostfix">Text after options.</param>
/// <param name="programAuthor">Author section of the help.</param>
/// <param name="programReportBugs">Bugs section of the help.</param>
/// <param name="programCopyright">Copyright section of the help.</param>
/// <param name="confirm">Halt before continuing execution after printing.</param>
public void ShowHelp(string programNameDescription,
string programSynopsis,
string optionDescriptionPrefix,
string optionDescriptionPostfix,
string programAuthor,
string programReportBugs,
string programCopyright,
bool confirm)
{
WriteProgramName(programNameDescription);
WriteProgramSynopsis(programSynopsis);
WriteOptionDescriptions(this, optionDescriptionPrefix, optionDescriptionPostfix);
WriteProgramAuthor(programAuthor);
WriteProgramReportingBugs(programReportBugs);
WriteProgramCopyrightLicense(programCopyright);
if (confirm)
{
Console.ReadKey(true);
}
}
/// <summary>
/// Print program name and description.
/// </summary>
/// <param name="description">Description to print.</param>
private static void WriteProgramName(string description)
{
var origColour = Console.ForegroundColor;
var appName = AppDomain.CurrentDomain.FriendlyName;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("NAME");
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine('\t' + appName + " - " + description + '\n');
Console.ForegroundColor = origColour;
}
/// <summary>
/// Print the program synopsis.
/// </summary>
/// <param name="synopsis">Synopsis to print.</param>
private static void WriteProgramSynopsis(string synopsis)
{
var origColour = Console.ForegroundColor;
var appName = AppDomain.CurrentDomain.FriendlyName;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("SYNOPSIS");
Console.ForegroundColor = ConsoleColor.Gray;
synopsis = synopsis.Replace("{appName}", appName);
Console.WriteLine('\t' + synopsis + '\n');
Console.ForegroundColor = origColour;
}
/// <summary>
/// Print the program author.
/// </summary>
/// <param name="authorByString">Author string to print.</param>
private static void WriteProgramAuthor(string authorByString)
{
var origColour = Console.ForegroundColor;
var appName = AppDomain.CurrentDomain.FriendlyName;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("AUTHOR");
Console.ForegroundColor = ConsoleColor.Gray;
authorByString = authorByString.Replace("{appName}", appName);
Console.WriteLine('\t' + authorByString + '\n');
Console.ForegroundColor = origColour;
}
/// <summary>
/// Print the program reporting bugs section.
/// </summary>
/// <param name="reportString">Report bugs string.</param>
private static void WriteProgramReportingBugs(string reportString)
{
var origColour = Console.ForegroundColor;
var appName = AppDomain.CurrentDomain.FriendlyName;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("REPORTING BUGS");
Console.ForegroundColor = ConsoleColor.Gray;
reportString = reportString.Replace("{appName}", appName);
var spl = reportString.Split(new[] { "\n", "\r\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var s in spl)
{
Console.WriteLine('\t' + s);
}
Console.WriteLine();
Console.ForegroundColor = origColour;
}
/// <summary>
/// Print the program copyright license.
/// </summary>
/// <param name="copyrightLicense">Copyright license text.</param>
private static void WriteProgramCopyrightLicense(string copyrightLicense)
{
var origColour = Console.ForegroundColor;
var appName = AppDomain.CurrentDomain.FriendlyName;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("COPYRIGHT");
Console.ForegroundColor = ConsoleColor.Gray;
copyrightLicense = copyrightLicense.Replace("{appName}", appName);
var spl = copyrightLicense.Split(new[] { "\n", "\r\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var s in spl)
{
Console.WriteLine('\t' + s);
}
Console.WriteLine();
Console.ForegroundColor = origColour;
}
/// <summary>
/// Prints all the options in an OptionsSet and prefix/postfix text for the description.
/// </summary>
/// <param name="os">OptionsSet to use options from.</param>
/// <param name="prefixText">Text to print before options.</param>
/// <param name="postText">Text to print after options.</param>
private static void WriteOptionDescriptions(OptionSet os, string prefixText, string postText)
{
var origColour = Console.ForegroundColor;
var appName = AppDomain.CurrentDomain.FriendlyName;
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("DESCRIPTION");
Console.ForegroundColor = ConsoleColor.Gray;
if (!string.IsNullOrWhiteSpace(prefixText))
{
prefixText = prefixText.Replace("{appName}", appName);
var spl = prefixText.Split(new[] { "\n", "\r\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var s in spl)
{
Console.WriteLine('\t' + s);
}
}
var buffWid = Console.BufferWidth;
foreach (var p in os._options)
{
Console.Write('\t');
for (var j = 0; j < p.Arguments.Length; j++)
{
Console.Write(p.Arguments[j]);
if (j + 1 != p.Arguments.Length)
{
Console.Write(", ");
}
else
{
if (p.Options.Length > 0)
{
Console.Write('\t');
foreach (var t in p.Options)
{
Console.Write(" [" + t + "]");
}
}
Console.WriteLine();
}
}
Console.Write("\t\t");
var len = buffWid - Console.CursorLeft;
foreach (var l in p.Description.Split(new[] { "\n", "\r\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
)
{
var lenP = 0;
foreach (var w in l.Split(' '))
{
var word = w;
if (lenP != 0 && (lenP + word.Length + 1) > len)
{
if (lenP != len) Console.Write("\n");
Console.Write("\t\t");
lenP = 0;
}
else if (lenP != 0)
{
word = ' ' + word;
}
Console.Write(word);
lenP += word.Length;
}
if (lenP != len) Console.Write("\n");
Console.Write("\t\t");
}
Console.WriteLine();
}
if (!string.IsNullOrWhiteSpace(postText))
{
postText = postText.Replace("{appName}", appName);
var spl = postText.Split(new[] { "\n", "\r\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var s in spl)
{
Console.WriteLine('\t' + s);
}
}
Console.WriteLine();
Console.ForegroundColor = origColour;
}
#endregion
/// <summary>
/// Error container. Provides as much detail as possible about errors that occurred.
/// </summary>
public class OptionError
{
/// <summary>
/// The option the error occurred while parsing. Can be null.
/// </summary>
public Option Option { get; private set; }
/// <summary>
/// The input string that the option was parsed using.
/// </summary>
public string InputAs { get; private set; }
/// <summary>
/// The arguments that were provided with the option.
/// </summary>
public IEnumerable<string> ProvidedArguments { get; private set; }
/// <summary>
/// The error that was incurred while parsing.
/// </summary>
public string Error { get; private set; }
/// <summary>
/// Create a new option error. This will hold information about where and how the error occurred.
/// </summary>
/// <param name="option">The option the error occurred while parsing on. Can be null.</param>
/// <param name="inputAs">The string the user input to get this error.</param>
/// <param name="providedArguments">The arguments provided with the failing option.</param>
/// <param name="error">The error message associated with this error.</param>
internal OptionError(Option option, string inputAs, IEnumerable<string> providedArguments, string error)
{
Option = option;
InputAs = inputAs;
ProvidedArguments = providedArguments;
Error = error;
}
public override string ToString()
{
return Error;
}
}
}
/// <summary>
/// Exception that is thrown when there is an error with the options specified.
/// </summary>
[Serializable]
public class OptionException : Exception
{
/// <summary>
/// Create a new OptionException.
/// </summary>
/// <param name="errorText">The description of this exception.</param>
/// <param name="errorArguments">Arguments that were in error.</param>
public OptionException(string errorText, params string[] errorArguments) : base(errorText)
{
ErrorArguments = errorArguments;
}
/// <summary>
/// Create a new OptionException.
/// </summary>
/// <param name="errorText">The description of this exception.</param>
/// <param name="innerException">The inner exception that caused this one to occur.</param>
/// <param name="errorArguments">Arguments that were in error.</param>
public OptionException(string errorText, Exception innerException, params string[] errorArguments)
: base(errorText, innerException)
{
ErrorArguments = errorArguments;
}
/// <summary>
/// Arguments that were in error.
/// </summary>
public string[] ErrorArguments { get; private set; }
}
}