· 7 years ago · Sep 26, 2018, 12:20 PM
1namespace dedup
2{
3 using System;
4 using System.Collections.Generic;
5 using System.IO;
6 using System.Linq;
7
8 public static class dedup
9 {
10 private readonly static string ProgramName = AppDomain.CurrentDomain.FriendlyName;
11 private static int _dirsCreated = 0;
12 private static int _filesCopied = 0;
13 private static int _filesSkipped = 0;
14 private static int _fileErrors = 0;
15 private static int _dirErrors = 0;
16 private static Dictionary<long, List<FileInfo>> _list = new Dictionary<long, List<FileInfo>>();
17 private const int _bufferSize = 2048;
18 private readonly static string[] _sizes = { "B", "KB", "MB", "GB", "TB" };
19 private static byte[] _source = new byte[_bufferSize];
20 private static byte[] _target = new byte[_bufferSize];
21
22 static void Message(string message)
23 {
24 Console.WriteLine(ProgramName + " : " + message);
25 }
26
27 static void ShowHelp(OptionSet options, string message = null)
28 {
29 Console.WriteLine(Environment.NewLine + ProgramName + " usage:");
30 Console.WriteLine(Environment.NewLine + ProgramName + " - s <source directory> -t <target directory> [-h]");
31 Console.WriteLine("Replicate subdirectory structure and copy unique files.");
32 Console.WriteLine();
33 Console.WriteLine("Options:");
34 options.WriteOptionDescriptions(Console.Out);
35
36 if (message != null)
37 {
38 Console.WriteLine(Environment.NewLine + ProgramName + " : " + message);
39 }
40 }
41
42 static void Main(string[] args)
43 {
44 string sourceDirectory = null;
45 string targetDirectory = null;
46 bool help = false;
47
48 OptionSet options = new OptionSet()
49 .Add("s|source=", "the source directory.", delegate (string v) { sourceDirectory = v; })
50 .Add("t|trget=", "the target directory.", delegate (string v) { targetDirectory = v; })
51 .Add("h|?|help", "show this help text and exit.", delegate (string v) { help = true; });
52
53 try
54 {
55 List<string> extra = options.Parse(args);
56
57 if (extra.Count > 0)
58 {
59 ShowHelp(options, "Extra options: " + string.Join(" ", extra));
60 return;
61 }
62 }
63 catch (OptionException e)
64 {
65 ShowHelp(options, e.Message);
66 return;
67 }
68
69 if (help)
70 {
71 ShowHelp(options);
72 return;
73 }
74
75 if (sourceDirectory == null || string.IsNullOrEmpty(sourceDirectory))
76 {
77 ShowHelp(options, "Use -s to specify source directory.");
78 return;
79 }
80
81 if (!Directory.Exists(sourceDirectory))
82 {
83 ShowHelp(options, "-s : " + sourceDirectory + " does not exist or is not a directory.");
84 return;
85
86 }
87
88 if (targetDirectory == null || string.IsNullOrEmpty(targetDirectory))
89 {
90 ShowHelp(options, "Use -t to specify target directory.");
91 return;
92 }
93
94 if (!Directory.Exists(targetDirectory))
95 {
96 string message = null;
97
98 try
99 {
100 DirectoryInfo dir = Directory.CreateDirectory(targetDirectory);
101
102 if (!dir.Exists)
103 {
104 message = "Failed to create target subdirectory " + targetDirectory;
105 }
106 }
107 catch (Exception ex)
108 {
109 message = ex.Message;
110 }
111
112 if (message != null)
113 {
114 Message(message);
115 return;
116 }
117 }
118
119 Recurse(new DirectoryInfo(sourceDirectory), new DirectoryInfo(targetDirectory));
120 Message("Subdirectories created: " + _dirsCreated);
121 Message("Errors creating subdirectories: " + _dirErrors);
122 Message("Files copied: " + _filesCopied);
123 Message("Files skipped: " + _filesSkipped);
124 Message("Errors copying files: " + _fileErrors);
125 }
126
127 private static bool BinariesMatch(FileInfo first, FileInfo second)
128 {
129 if (first.FullName == second.FullName)
130 {
131 Message("Unexpected condition.");
132 return false;
133 }
134
135 using (FileStream fiFirst = new FileStream(first.FullName, FileMode.Open))
136 {
137 using (FileStream fiSecond = new FileStream(second.FullName, FileMode.Open))
138 {
139 while (true)
140 {
141 int firstCount = fiFirst.Read(_source, 0, _bufferSize);
142 int secondCount = fiSecond.Read(_target, 0, _bufferSize);
143
144 if (firstCount != secondCount)
145 {
146 continue; // no match
147 }
148
149 if (firstCount == 0)
150 {
151 return true;
152 }
153
154 //TODO: replace with memcmp
155 if (!_source.Take(firstCount).SequenceEqual(_source.Take(secondCount)))
156 {
157 continue;
158 }
159 }
160 }
161 }
162 }
163
164 private static void Recurse(DirectoryInfo sourceDirectory, DirectoryInfo targetDirectory)
165 {
166 Message("Processing " + sourceDirectory.FullName);
167
168 foreach (FileInfo file in sourceDirectory.GetFiles())
169 {
170 if (file.Attributes.HasFlag(FileAttributes.Hidden))
171 {
172 continue;
173 }
174
175 if (file.Attributes.HasFlag(FileAttributes.ReadOnly))
176 {
177 Message("Clear readonly attribute for " + file.FullName);
178 File.SetAttributes(file.FullName, file.Attributes & ~FileAttributes.ReadOnly);
179 }
180
181 bool exists = false;
182
183 if (_list.ContainsKey(file.Length))
184 {
185 foreach (FileInfo check in _list[file.Length])
186 {
187 if (BinariesMatch(file, check))
188 {
189 exists = true;
190 break;
191 }
192 }
193 }
194
195 if (exists)
196 {
197 Message("Skip " + file.FullName);
198 _filesSkipped++;
199 }
200 else
201 {
202 string message = null;
203
204 try
205 {
206 string path = Path.Combine(targetDirectory.FullName, file.Name);
207 File.Copy(file.FullName, path);
208
209 if (!File.Exists(path))
210 {
211 message = "Unable to copy " + file.FullName + " to " + path;
212 }
213
214 _filesCopied++;
215
216 if (!_list.ContainsKey(file.Length))
217 {
218 _list[file.Length] = new List<FileInfo>();
219 }
220
221 _list[file.Length].Add(file);
222 }
223 catch (Exception ex)
224 {
225 message = ex.Message;
226 }
227
228 if (message != null)
229 {
230 _fileErrors++;
231
232 Message(message);
233 //TODO: STDERR? Exit? Exception?
234 }
235 }
236 }
237
238 foreach(DirectoryInfo sourceSubDir in sourceDirectory.GetDirectories())
239 {
240 string targetSubDir = Path.Combine(targetDirectory.FullName, sourceSubDir.Name);
241
242 if (!Directory.Exists(targetSubDir))
243 {
244 string message = null;
245
246 try
247 {
248 DirectoryInfo dir = Directory.CreateDirectory(targetSubDir);
249
250 if (!dir.Exists)
251 {
252 message = "Unable to create subdirectory " + targetSubDir;
253 }
254 else
255 {
256 _dirsCreated++;
257 }
258 }
259 catch(Exception ex)
260 {
261 message = ex.Message;
262 }
263
264 if (message != null)
265 {
266 Message(message);
267 _dirErrors++;
268 }
269 }
270
271 Recurse(sourceSubDir, new DirectoryInfo(targetSubDir));
272 }
273 }
274 }
275}
276
277//
278// Options.cs
279//
280// Authors:
281// Jonathan Pryor <jpryor@novell.com>, <Jonathan.Pryor@microsoft.com>
282// Federico Di Gregorio <fog@initd.org>
283// Rolf Bjarne Kvinge <rolf@xamarin.com>
284//
285// Copyright (C) 2008 Novell (http://www.novell.com)
286// Copyright (C) 2009 Federico Di Gregorio.
287// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
288// Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com)
289//
290// Permission is hereby granted, free of charge, to any person obtaining
291// a copy of this software and associated documentation files (the
292// "Software"), to deal in the Software without restriction, including
293// without limitation the rights to use, copy, modify, merge, publish,
294// distribute, sublicense, and/or sell copies of the Software, and to
295// permit persons to whom the Software is furnished to do so, subject to
296// the following conditions:
297//
298// The above copyright notice and this permission notice shall be
299// included in all copies or substantial portions of the Software.
300//
301// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
302// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
303// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
304// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
305// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
306// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
307// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
308//
309
310// Compile With:
311// mcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll -t:library
312// mcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll -t:library
313//
314// The LINQ version just changes the implementation of
315// OptionSet.Parse(IEnumerable<string>), and confers no semantic changes.
316
317//
318// A Getopt::Long-inspired option parsing library for C#.
319//
320// Mono.Options.OptionSet is built upon a key/value table, where the
321// key is a option format string and the value is a delegate that is
322// invoked when the format string is matched.
323//
324// Option format strings:
325// Regex-like BNF Grammar:
326// name: .+
327// type: [=:]
328// sep: ( [^{}]+ | '{' .+ '}' )?
329// aliases: ( name type sep ) ( '|' name type sep )*
330//
331// Each '|'-delimited name is an alias for the associated action. If the
332// format string ends in a '=', it has a required value. If the format
333// string ends in a ':', it has an optional value. If neither '=' or ':'
334// is present, no value is supported. `=' or `:' need only be defined on one
335// alias, but if they are provided on more than one they must be consistent.
336//
337// Each alias portion may also end with a "key/value separator", which is used
338// to split option values if the option accepts > 1 value. If not specified,
339// it defaults to '=' and ':'. If specified, it can be any character except
340// '{' and '}' OR the *string* between '{' and '}'. If no separator should be
341// used (i.e. the separate values should be distinct arguments), then "{}"
342// should be used as the separator.
343//
344// Options are extracted either from the current option by looking for
345// the option name followed by an '=' or ':', or is taken from the
346// following option IFF:
347// - The current option does not contain a '=' or a ':'
348// - The current option requires a value (i.e. not a Option type of ':')
349//
350// The `name' used in the option format string does NOT include any leading
351// option indicator, such as '-', '--', or '/'. All three of these are
352// permitted/required on any named option.
353//
354// Option bundling is permitted so long as:
355// - '-' is used to start the option group
356// - all of the bundled options are a single character
357// - at most one of the bundled options accepts a value, and the value
358// provided starts from the next character to the end of the string.
359//
360// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'
361// as '-Dname=value'.
362//
363// Option processing is disabled by specifying "--". All options after "--"
364// are returned by OptionSet.Parse() unchanged and unprocessed.
365//
366// Unprocessed options are returned from OptionSet.Parse().
367//
368// Examples:
369// int verbose = 0;
370// OptionSet p = new OptionSet ()
371// .Add ("v", v => ++verbose)
372// .Add ("name=|value=", v => Console.WriteLine (v));
373// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"});
374//
375// The above would parse the argument string array, and would invoke the
376// lambda expression three times, setting `verbose' to 3 when complete.
377// It would also print out "A" and "B" to standard output.
378// The returned array would contain the string "extra".
379//
380// C# 3.0 collection initializers are supported and encouraged:
381// var p = new OptionSet () {
382// { "h|?|help", v => ShowHelp () },
383// };
384//
385// System.ComponentModel.TypeConverter is also supported, allowing the use of
386// custom data types in the callback type; TypeConverter.ConvertFromString()
387// is used to convert the value option to an instance of the specified
388// type:
389//
390// var p = new OptionSet () {
391// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) },
392// };
393//
394// Random other tidbits:
395// - Boolean options (those w/o '=' or ':' in the option format string)
396// are explicitly enabled if they are followed with '+', and explicitly
397// disabled if they are followed with '-':
398// string a = null;
399// var p = new OptionSet () {
400// { "a", s => a = s },
401// };
402// p.Parse (new string[]{"-a"}); // sets v != null
403// p.Parse (new string[]{"-a+"}); // sets v != null
404// p.Parse (new string[]{"-a-"}); // sets v == null
405//
406
407//
408// Mono.Options.CommandSet allows easily having separate commands and
409// associated command options, allowing creation of a *suite* along the
410// lines of **git**(1), **svn**(1), etc.
411//
412// CommandSet allows intermixing plain text strings for `--help` output,
413// Option values -- as supported by OptionSet -- and Command instances,
414// which have a name, optional help text, and an optional OptionSet.
415//
416// var suite = new CommandSet ("suite-name") {
417// // Use strings and option values, as with OptionSet
418// "usage: suite-name COMMAND [OPTIONS]+",
419// { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 },
420// // Commands may also be specified
421// new Command ("command-name", "command help") {
422// Options = new OptionSet {/*...*/},
423// Run = args => { /*...*/},
424// },
425// new MyCommandSubclass (),
426// };
427// return suite.Run (new string[]{...});
428//
429// CommandSet provides a `help` command, and forwards `help COMMAND`
430// to the registered Command instance by invoking Command.Invoke()
431// with `--help` as an option.
432//
433
434using System;
435using System.Collections;
436using System.Collections.Generic;
437using System.Collections.ObjectModel;
438using System.ComponentModel;
439using System.Globalization;
440using System.IO;
441#if PCL
442using System.Reflection;
443#else
444using System.Runtime.Serialization;
445using System.Security.Permissions;
446#endif
447using System.Text;
448using System.Text.RegularExpressions;
449
450#if LINQ
451using System.Linq;
452#endif
453
454#if TEST
455using NDesk.Options;
456#endif
457
458#if PCL
459using MessageLocalizerConverter = System.Func<string, string>;
460#else
461using MessageLocalizerConverter = System.Converter<string, string>;
462#endif
463
464#if NDESK_OPTIONS
465namespace NDesk.Options
466#else
467namespace dedup
468#endif
469{
470 static class StringCoda
471 {
472
473 public static IEnumerable<string> WrappedLines(string self, params int[] widths)
474 {
475 IEnumerable<int> w = widths;
476 return WrappedLines(self, w);
477 }
478
479 public static IEnumerable<string> WrappedLines(string self, IEnumerable<int> widths)
480 {
481 if (widths == null)
482 throw new ArgumentNullException("widths");
483 return CreateWrappedLinesIterator(self, widths);
484 }
485
486 private static IEnumerable<string> CreateWrappedLinesIterator(string self, IEnumerable<int> widths)
487 {
488 if (string.IsNullOrEmpty(self))
489 {
490 yield return string.Empty;
491 yield break;
492 }
493 using (IEnumerator<int> ewidths = widths.GetEnumerator())
494 {
495 bool? hw = null;
496 int width = GetNextWidth(ewidths, int.MaxValue, ref hw);
497 int start = 0, end;
498 do
499 {
500 end = GetLineEnd(start, width, self);
501 char c = self[end - 1];
502 if (char.IsWhiteSpace(c))
503 --end;
504 bool needContinuation = end != self.Length && !IsEolChar(c);
505 string continuation = "";
506 if (needContinuation)
507 {
508 --end;
509 continuation = "-";
510 }
511 string line = self.Substring(start, end - start) + continuation;
512 yield return line;
513 start = end;
514 if (char.IsWhiteSpace(c))
515 ++start;
516 width = GetNextWidth(ewidths, width, ref hw);
517 } while (start < self.Length);
518 }
519 }
520
521 private static int GetNextWidth(IEnumerator<int> ewidths, int curWidth, ref bool? eValid)
522 {
523 if (!eValid.HasValue || (eValid.HasValue && eValid.Value))
524 {
525 curWidth = (eValid = ewidths.MoveNext()).Value ? ewidths.Current : curWidth;
526 // '.' is any character, - is for a continuation
527 const string minWidth = ".-";
528 if (curWidth < minWidth.Length)
529 throw new ArgumentOutOfRangeException("widths",
530 string.Format("Element must be >= {0}, was {1}.", minWidth.Length, curWidth));
531 return curWidth;
532 }
533 // no more elements, use the last element.
534 return curWidth;
535 }
536
537 private static bool IsEolChar(char c)
538 {
539 return !char.IsLetterOrDigit(c);
540 }
541
542 private static int GetLineEnd(int start, int length, string description)
543 {
544 int end = System.Math.Min(start + length, description.Length);
545 int sep = -1;
546 for (int i = start; i < end; ++i)
547 {
548 if (description[i] == '\n')
549 return i + 1;
550 if (IsEolChar(description[i]))
551 sep = i + 1;
552 }
553 if (sep == -1 || end == description.Length)
554 return end;
555 return sep;
556 }
557 }
558
559 public class OptionValueCollection : IList, IList<string>
560 {
561
562 List<string> values = new List<string>();
563 OptionContext c;
564
565 internal OptionValueCollection(OptionContext c)
566 {
567 this.c = c;
568 }
569
570 #region ICollection
571 void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); }
572 bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } }
573 object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } }
574 #endregion
575
576 #region ICollection<T>
577 public void Add(string item) { values.Add(item); }
578 public void Clear() { values.Clear(); }
579 public bool Contains(string item) { return values.Contains(item); }
580 public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); }
581 public bool Remove(string item) { return values.Remove(item); }
582 public int Count { get { return values.Count; } }
583 public bool IsReadOnly { get { return false; } }
584 #endregion
585
586 #region IEnumerable
587 IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); }
588 #endregion
589
590 #region IEnumerable<T>
591 public IEnumerator<string> GetEnumerator() { return values.GetEnumerator(); }
592 #endregion
593
594 #region IList
595 int IList.Add(object value) { return (values as IList).Add(value); }
596 bool IList.Contains(object value) { return (values as IList).Contains(value); }
597 int IList.IndexOf(object value) { return (values as IList).IndexOf(value); }
598 void IList.Insert(int index, object value) { (values as IList).Insert(index, value); }
599 void IList.Remove(object value) { (values as IList).Remove(value); }
600 void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); }
601 bool IList.IsFixedSize { get { return false; } }
602 object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } }
603 #endregion
604
605 #region IList<T>
606 public int IndexOf(string item) { return values.IndexOf(item); }
607 public void Insert(int index, string item) { values.Insert(index, item); }
608 public void RemoveAt(int index) { values.RemoveAt(index); }
609
610 private void AssertValid(int index)
611 {
612 if (c.Option == null)
613 throw new InvalidOperationException("OptionContext.Option is null.");
614 if (index >= c.Option.MaxValueCount)
615 throw new ArgumentOutOfRangeException("index");
616 if (c.Option.OptionValueType == OptionValueType.Required &&
617 index >= values.Count)
618 throw new OptionException(string.Format(
619 c.OptionSet.MessageLocalizer("Missing required value for option '{0}'."), c.OptionName),
620 c.OptionName);
621 }
622
623 public string this[int index]
624 {
625 get
626 {
627 AssertValid(index);
628 return index >= values.Count ? null : values[index];
629 }
630 set
631 {
632 values[index] = value;
633 }
634 }
635 #endregion
636
637 public List<string> ToList()
638 {
639 return new List<string>(values);
640 }
641
642 public string[] ToArray()
643 {
644 return values.ToArray();
645 }
646
647 public override string ToString()
648 {
649 return string.Join(", ", values.ToArray());
650 }
651 }
652
653 public class OptionContext
654 {
655 private Option option;
656 private string name;
657 private int index;
658 private OptionSet set;
659 private OptionValueCollection c;
660
661 public OptionContext(OptionSet set)
662 {
663 this.set = set;
664 this.c = new OptionValueCollection(this);
665 }
666
667 public Option Option
668 {
669 get { return option; }
670 set { option = value; }
671 }
672
673 public string OptionName
674 {
675 get { return name; }
676 set { name = value; }
677 }
678
679 public int OptionIndex
680 {
681 get { return index; }
682 set { index = value; }
683 }
684
685 public OptionSet OptionSet
686 {
687 get { return set; }
688 }
689
690 public OptionValueCollection OptionValues
691 {
692 get { return c; }
693 }
694 }
695
696 public enum OptionValueType
697 {
698 None,
699 Optional,
700 Required,
701 }
702
703 public abstract class Option
704 {
705 string prototype, description;
706 string[] names;
707 OptionValueType type;
708 int count;
709 string[] separators;
710 bool hidden;
711
712 protected Option(string prototype, string description)
713 : this(prototype, description, 1, false)
714 {
715 }
716
717 protected Option(string prototype, string description, int maxValueCount)
718 : this(prototype, description, maxValueCount, false)
719 {
720 }
721
722 protected Option(string prototype, string description, int maxValueCount, bool hidden)
723 {
724 if (prototype == null)
725 throw new ArgumentNullException("prototype");
726 if (prototype.Length == 0)
727 throw new ArgumentException("Cannot be the empty string.", "prototype");
728 if (maxValueCount < 0)
729 throw new ArgumentOutOfRangeException("maxValueCount");
730
731 this.prototype = prototype;
732 this.description = description;
733 this.count = maxValueCount;
734 this.names = (this is OptionSet.Category)
735 // append GetHashCode() so that "duplicate" categories have distinct
736 // names, e.g. adding multiple "" categories should be valid.
737 ? new[] { prototype + this.GetHashCode() }
738 : prototype.Split('|');
739
740 if (this is OptionSet.Category || this is CommandOption)
741 return;
742
743 this.type = ParsePrototype();
744 this.hidden = hidden;
745
746 if (this.count == 0 && type != OptionValueType.None)
747 throw new ArgumentException(
748 "Cannot provide maxValueCount of 0 for OptionValueType.Required or " +
749 "OptionValueType.Optional.",
750 "maxValueCount");
751 if (this.type == OptionValueType.None && maxValueCount > 1)
752 throw new ArgumentException(
753 string.Format("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount),
754 "maxValueCount");
755 if (Array.IndexOf(names, "<>") >= 0 &&
756 ((names.Length == 1 && this.type != OptionValueType.None) ||
757 (names.Length > 1 && this.MaxValueCount > 1)))
758 throw new ArgumentException(
759 "The default option handler '<>' cannot require values.",
760 "prototype");
761 }
762
763 public string Prototype { get { return prototype; } }
764 public string Description { get { return description; } }
765 public OptionValueType OptionValueType { get { return type; } }
766 public int MaxValueCount { get { return count; } }
767 public bool Hidden { get { return hidden; } }
768
769 public string[] GetNames()
770 {
771 return (string[])names.Clone();
772 }
773
774 public string[] GetValueSeparators()
775 {
776 if (separators == null)
777 return new string[0];
778 return (string[])separators.Clone();
779 }
780
781 protected static T Parse<T>(string value, OptionContext c)
782 {
783 Type tt = typeof(T);
784#if PCL
785 TypeInfo ti = tt.GetTypeInfo ();
786#else
787 Type ti = tt;
788#endif
789 bool nullable =
790 ti.IsValueType &&
791 ti.IsGenericType &&
792 !ti.IsGenericTypeDefinition &&
793 ti.GetGenericTypeDefinition() == typeof(Nullable<>);
794#if PCL
795 Type targetType = nullable ? tt.GenericTypeArguments [0] : tt;
796#else
797 Type targetType = nullable ? tt.GetGenericArguments()[0] : tt;
798#endif
799 T t = default(T);
800 try
801 {
802 if (value != null)
803 {
804#if PCL
805 if (targetType.GetTypeInfo ().IsEnum)
806 t = (T) Enum.Parse (targetType, value, true);
807 else
808 t = (T) Convert.ChangeType (value, targetType);
809#else
810 TypeConverter conv = TypeDescriptor.GetConverter(targetType);
811 t = (T)conv.ConvertFromString(value);
812#endif
813 }
814 }
815 catch (Exception e)
816 {
817 throw new OptionException(
818 string.Format(
819 c.OptionSet.MessageLocalizer("Could not convert string `{0}' to type {1} for option `{2}'."),
820 value, targetType.Name, c.OptionName),
821 c.OptionName, e);
822 }
823 return t;
824 }
825
826 internal string[] Names { get { return names; } }
827 internal string[] ValueSeparators { get { return separators; } }
828
829 static readonly char[] NameTerminator = new char[] { '=', ':' };
830
831 private OptionValueType ParsePrototype()
832 {
833 char type = '\0';
834 List<string> seps = new List<string>();
835 for (int i = 0; i < names.Length; ++i)
836 {
837 string name = names[i];
838 if (name.Length == 0)
839 throw new ArgumentException("Empty option names are not supported.", "prototype");
840
841 int end = name.IndexOfAny(NameTerminator);
842 if (end == -1)
843 continue;
844 names[i] = name.Substring(0, end);
845 if (type == '\0' || type == name[end])
846 type = name[end];
847 else
848 throw new ArgumentException(
849 string.Format("Conflicting option types: '{0}' vs. '{1}'.", type, name[end]),
850 "prototype");
851 AddSeparators(name, end, seps);
852 }
853
854 if (type == '\0')
855 return OptionValueType.None;
856
857 if (count <= 1 && seps.Count != 0)
858 throw new ArgumentException(
859 string.Format("Cannot provide key/value separators for Options taking {0} value(s).", count),
860 "prototype");
861 if (count > 1)
862 {
863 if (seps.Count == 0)
864 this.separators = new string[] { ":", "=" };
865 else if (seps.Count == 1 && seps[0].Length == 0)
866 this.separators = null;
867 else
868 this.separators = seps.ToArray();
869 }
870
871 return type == '=' ? OptionValueType.Required : OptionValueType.Optional;
872 }
873
874 private static void AddSeparators(string name, int end, ICollection<string> seps)
875 {
876 int start = -1;
877 for (int i = end + 1; i < name.Length; ++i)
878 {
879 switch (name[i])
880 {
881 case '{':
882 if (start != -1)
883 throw new ArgumentException(
884 string.Format("Ill-formed name/value separator found in \"{0}\".", name),
885 "prototype");
886 start = i + 1;
887 break;
888 case '}':
889 if (start == -1)
890 throw new ArgumentException(
891 string.Format("Ill-formed name/value separator found in \"{0}\".", name),
892 "prototype");
893 seps.Add(name.Substring(start, i - start));
894 start = -1;
895 break;
896 default:
897 if (start == -1)
898 seps.Add(name[i].ToString());
899 break;
900 }
901 }
902 if (start != -1)
903 throw new ArgumentException(
904 string.Format("Ill-formed name/value separator found in \"{0}\".", name),
905 "prototype");
906 }
907
908 public void Invoke(OptionContext c)
909 {
910 OnParseComplete(c);
911 c.OptionName = null;
912 c.Option = null;
913 c.OptionValues.Clear();
914 }
915
916 protected abstract void OnParseComplete(OptionContext c);
917
918 internal void InvokeOnParseComplete(OptionContext c)
919 {
920 OnParseComplete(c);
921 }
922
923 public override string ToString()
924 {
925 return Prototype;
926 }
927 }
928
929 public abstract class ArgumentSource
930 {
931
932 protected ArgumentSource()
933 {
934 }
935
936 public abstract string[] GetNames();
937 public abstract string Description { get; }
938 public abstract bool GetArguments(string value, out IEnumerable<string> replacement);
939
940#if !PCL || NETSTANDARD1_3
941 public static IEnumerable<string> GetArgumentsFromFile(string file)
942 {
943 return GetArguments(File.OpenText(file), true);
944 }
945#endif
946
947 public static IEnumerable<string> GetArguments(TextReader reader)
948 {
949 return GetArguments(reader, false);
950 }
951
952 // Cribbed from mcs/driver.cs:LoadArgs(string)
953 static IEnumerable<string> GetArguments(TextReader reader, bool close)
954 {
955 try
956 {
957 StringBuilder arg = new StringBuilder();
958
959 string line;
960 while ((line = reader.ReadLine()) != null)
961 {
962 int t = line.Length;
963
964 for (int i = 0; i < t; i++)
965 {
966 char c = line[i];
967
968 if (c == '"' || c == '\'')
969 {
970 char end = c;
971
972 for (i++; i < t; i++)
973 {
974 c = line[i];
975
976 if (c == end)
977 break;
978 arg.Append(c);
979 }
980 }
981 else if (c == ' ')
982 {
983 if (arg.Length > 0)
984 {
985 yield return arg.ToString();
986 arg.Length = 0;
987 }
988 }
989 else
990 arg.Append(c);
991 }
992 if (arg.Length > 0)
993 {
994 yield return arg.ToString();
995 arg.Length = 0;
996 }
997 }
998 }
999 finally
1000 {
1001 if (close)
1002 reader.Dispose();
1003 }
1004 }
1005 }
1006
1007#if !PCL || NETSTANDARD1_3
1008 public class ResponseFileSource : ArgumentSource
1009 {
1010
1011 public override string[] GetNames()
1012 {
1013 return new string[] { "@file" };
1014 }
1015
1016 public override string Description
1017 {
1018 get { return "Read response file for more options."; }
1019 }
1020
1021 public override bool GetArguments(string value, out IEnumerable<string> replacement)
1022 {
1023 if (string.IsNullOrEmpty(value) || !value.StartsWith("@"))
1024 {
1025 replacement = null;
1026 return false;
1027 }
1028 replacement = ArgumentSource.GetArgumentsFromFile(value.Substring(1));
1029 return true;
1030 }
1031 }
1032#endif
1033
1034#if !PCL
1035 [Serializable]
1036#endif
1037 public class OptionException : Exception
1038 {
1039 private string option;
1040
1041 public OptionException()
1042 {
1043 }
1044
1045 public OptionException(string message, string optionName)
1046 : base(message)
1047 {
1048 this.option = optionName;
1049 }
1050
1051 public OptionException(string message, string optionName, Exception innerException)
1052 : base(message, innerException)
1053 {
1054 this.option = optionName;
1055 }
1056
1057#if !PCL
1058 protected OptionException(SerializationInfo info, StreamingContext context)
1059 : base(info, context)
1060 {
1061 this.option = info.GetString("OptionName");
1062 }
1063#endif
1064
1065 public string OptionName
1066 {
1067 get { return this.option; }
1068 }
1069
1070#if !PCL
1071#pragma warning disable 618 // SecurityPermissionAttribute is obsolete
1072 [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
1073#pragma warning restore 618
1074 public override void GetObjectData(SerializationInfo info, StreamingContext context)
1075 {
1076 base.GetObjectData(info, context);
1077 info.AddValue("OptionName", option);
1078 }
1079#endif
1080 }
1081
1082 public delegate void OptionAction<TKey, TValue>(TKey key, TValue value);
1083
1084 public class OptionSet : KeyedCollection<string, Option>
1085 {
1086 public OptionSet()
1087 : this(null)
1088 {
1089 }
1090
1091 public OptionSet(MessageLocalizerConverter localizer)
1092 {
1093 this.roSources = new ReadOnlyCollection<ArgumentSource>(sources);
1094 this.localizer = localizer;
1095 if (this.localizer == null)
1096 {
1097 this.localizer = delegate (string f) {
1098 return f;
1099 };
1100 }
1101 }
1102
1103 MessageLocalizerConverter localizer;
1104
1105 public MessageLocalizerConverter MessageLocalizer
1106 {
1107 get { return localizer; }
1108 internal set { localizer = value; }
1109 }
1110
1111 List<ArgumentSource> sources = new List<ArgumentSource>();
1112 ReadOnlyCollection<ArgumentSource> roSources;
1113
1114 public ReadOnlyCollection<ArgumentSource> ArgumentSources
1115 {
1116 get { return roSources; }
1117 }
1118
1119
1120 protected override string GetKeyForItem(Option item)
1121 {
1122 if (item == null)
1123 throw new ArgumentNullException("option");
1124 if (item.Names != null && item.Names.Length > 0)
1125 return item.Names[0];
1126 // This should never happen, as it's invalid for Option to be
1127 // constructed w/o any names.
1128 throw new InvalidOperationException("Option has no names!");
1129 }
1130
1131 [Obsolete("Use KeyedCollection.this[string]")]
1132 protected Option GetOptionForName(string option)
1133 {
1134 if (option == null)
1135 throw new ArgumentNullException("option");
1136 try
1137 {
1138 return base[option];
1139 }
1140 catch (KeyNotFoundException)
1141 {
1142 return null;
1143 }
1144 }
1145
1146 protected override void InsertItem(int index, Option item)
1147 {
1148 base.InsertItem(index, item);
1149 AddImpl(item);
1150 }
1151
1152 protected override void RemoveItem(int index)
1153 {
1154 Option p = Items[index];
1155 base.RemoveItem(index);
1156 // KeyedCollection.RemoveItem() handles the 0th item
1157 for (int i = 1; i < p.Names.Length; ++i)
1158 {
1159 Dictionary.Remove(p.Names[i]);
1160 }
1161 }
1162
1163 protected override void SetItem(int index, Option item)
1164 {
1165 base.SetItem(index, item);
1166 AddImpl(item);
1167 }
1168
1169 private void AddImpl(Option option)
1170 {
1171 if (option == null)
1172 throw new ArgumentNullException("option");
1173 List<string> added = new List<string>(option.Names.Length);
1174 try
1175 {
1176 // KeyedCollection.InsertItem/SetItem handle the 0th name.
1177 for (int i = 1; i < option.Names.Length; ++i)
1178 {
1179 Dictionary.Add(option.Names[i], option);
1180 added.Add(option.Names[i]);
1181 }
1182 }
1183 catch (Exception)
1184 {
1185 foreach (string name in added)
1186 Dictionary.Remove(name);
1187 throw;
1188 }
1189 }
1190
1191 public OptionSet Add(string header)
1192 {
1193 if (header == null)
1194 throw new ArgumentNullException("header");
1195 Add(new Category(header));
1196 return this;
1197 }
1198
1199 internal sealed class Category : Option
1200 {
1201
1202 // Prototype starts with '=' because this is an invalid prototype
1203 // (see Option.ParsePrototype(), and thus it'll prevent Category
1204 // instances from being accidentally used as normal options.
1205 public Category(string description)
1206 : base("=:Category:= " + description, description)
1207 {
1208 }
1209
1210 protected override void OnParseComplete(OptionContext c)
1211 {
1212 throw new NotSupportedException("Category.OnParseComplete should not be invoked.");
1213 }
1214 }
1215
1216
1217 public new OptionSet Add(Option option)
1218 {
1219 base.Add(option);
1220 return this;
1221 }
1222
1223 sealed class ActionOption : Option
1224 {
1225 Action<OptionValueCollection> action;
1226
1227 public ActionOption(string prototype, string description, int count, Action<OptionValueCollection> action)
1228 : this(prototype, description, count, action, false)
1229 {
1230 }
1231
1232 public ActionOption(string prototype, string description, int count, Action<OptionValueCollection> action, bool hidden)
1233 : base(prototype, description, count, hidden)
1234 {
1235 if (action == null)
1236 throw new ArgumentNullException("action");
1237 this.action = action;
1238 }
1239
1240 protected override void OnParseComplete(OptionContext c)
1241 {
1242 action(c.OptionValues);
1243 }
1244 }
1245
1246 public OptionSet Add(string prototype, Action<string> action)
1247 {
1248 return Add(prototype, null, action);
1249 }
1250
1251 public OptionSet Add(string prototype, string description, Action<string> action)
1252 {
1253 return Add(prototype, description, action, false);
1254 }
1255
1256 public OptionSet Add(string prototype, string description, Action<string> action, bool hidden)
1257 {
1258 if (action == null)
1259 throw new ArgumentNullException("action");
1260 Option p = new ActionOption(prototype, description, 1,
1261 delegate (OptionValueCollection v) { action(v[0]); }, hidden);
1262 base.Add(p);
1263 return this;
1264 }
1265
1266 public OptionSet Add(string prototype, OptionAction<string, string> action)
1267 {
1268 return Add(prototype, null, action);
1269 }
1270
1271 public OptionSet Add(string prototype, string description, OptionAction<string, string> action)
1272 {
1273 return Add(prototype, description, action, false);
1274 }
1275
1276 public OptionSet Add(string prototype, string description, OptionAction<string, string> action, bool hidden)
1277 {
1278 if (action == null)
1279 throw new ArgumentNullException("action");
1280 Option p = new ActionOption(prototype, description, 2,
1281 delegate (OptionValueCollection v) { action(v[0], v[1]); }, hidden);
1282 base.Add(p);
1283 return this;
1284 }
1285
1286 sealed class ActionOption<T> : Option
1287 {
1288 Action<T> action;
1289
1290 public ActionOption(string prototype, string description, Action<T> action)
1291 : base(prototype, description, 1)
1292 {
1293 if (action == null)
1294 throw new ArgumentNullException("action");
1295 this.action = action;
1296 }
1297
1298 protected override void OnParseComplete(OptionContext c)
1299 {
1300 action(Parse<T>(c.OptionValues[0], c));
1301 }
1302 }
1303
1304 sealed class ActionOption<TKey, TValue> : Option
1305 {
1306 OptionAction<TKey, TValue> action;
1307
1308 public ActionOption(string prototype, string description, OptionAction<TKey, TValue> action)
1309 : base(prototype, description, 2)
1310 {
1311 if (action == null)
1312 throw new ArgumentNullException("action");
1313 this.action = action;
1314 }
1315
1316 protected override void OnParseComplete(OptionContext c)
1317 {
1318 action(
1319 Parse<TKey>(c.OptionValues[0], c),
1320 Parse<TValue>(c.OptionValues[1], c));
1321 }
1322 }
1323
1324 public OptionSet Add<T>(string prototype, Action<T> action)
1325 {
1326 return Add(prototype, null, action);
1327 }
1328
1329 public OptionSet Add<T>(string prototype, string description, Action<T> action)
1330 {
1331 return Add(new ActionOption<T>(prototype, description, action));
1332 }
1333
1334 public OptionSet Add<TKey, TValue>(string prototype, OptionAction<TKey, TValue> action)
1335 {
1336 return Add(prototype, null, action);
1337 }
1338
1339 public OptionSet Add<TKey, TValue>(string prototype, string description, OptionAction<TKey, TValue> action)
1340 {
1341 return Add(new ActionOption<TKey, TValue>(prototype, description, action));
1342 }
1343
1344 public OptionSet Add(ArgumentSource source)
1345 {
1346 if (source == null)
1347 throw new ArgumentNullException("source");
1348 sources.Add(source);
1349 return this;
1350 }
1351
1352 protected virtual OptionContext CreateOptionContext()
1353 {
1354 return new OptionContext(this);
1355 }
1356
1357 public List<string> Parse(IEnumerable<string> arguments)
1358 {
1359 if (arguments == null)
1360 throw new ArgumentNullException("arguments");
1361 OptionContext c = CreateOptionContext();
1362 c.OptionIndex = -1;
1363 bool process = true;
1364 List<string> unprocessed = new List<string>();
1365 Option def = Contains("<>") ? this["<>"] : null;
1366 ArgumentEnumerator ae = new ArgumentEnumerator(arguments);
1367 foreach (string argument in ae)
1368 {
1369 ++c.OptionIndex;
1370 if (argument == "--")
1371 {
1372 process = false;
1373 continue;
1374 }
1375 if (!process)
1376 {
1377 Unprocessed(unprocessed, def, c, argument);
1378 continue;
1379 }
1380 if (AddSource(ae, argument))
1381 continue;
1382 if (!Parse(argument, c))
1383 Unprocessed(unprocessed, def, c, argument);
1384 }
1385 if (c.Option != null)
1386 c.Option.Invoke(c);
1387 return unprocessed;
1388 }
1389
1390 class ArgumentEnumerator : IEnumerable<string>
1391 {
1392 List<IEnumerator<string>> sources = new List<IEnumerator<string>>();
1393
1394 public ArgumentEnumerator(IEnumerable<string> arguments)
1395 {
1396 sources.Add(arguments.GetEnumerator());
1397 }
1398
1399 public void Add(IEnumerable<string> arguments)
1400 {
1401 sources.Add(arguments.GetEnumerator());
1402 }
1403
1404 public IEnumerator<string> GetEnumerator()
1405 {
1406 do
1407 {
1408 IEnumerator<string> c = sources[sources.Count - 1];
1409 if (c.MoveNext())
1410 yield return c.Current;
1411 else
1412 {
1413 c.Dispose();
1414 sources.RemoveAt(sources.Count - 1);
1415 }
1416 } while (sources.Count > 0);
1417 }
1418
1419 IEnumerator IEnumerable.GetEnumerator()
1420 {
1421 return GetEnumerator();
1422 }
1423 }
1424
1425 bool AddSource(ArgumentEnumerator ae, string argument)
1426 {
1427 foreach (ArgumentSource source in sources)
1428 {
1429 IEnumerable<string> replacement;
1430 if (!source.GetArguments(argument, out replacement))
1431 continue;
1432 ae.Add(replacement);
1433 return true;
1434 }
1435 return false;
1436 }
1437
1438 private static bool Unprocessed(ICollection<string> extra, Option def, OptionContext c, string argument)
1439 {
1440 if (def == null)
1441 {
1442 extra.Add(argument);
1443 return false;
1444 }
1445 c.OptionValues.Add(argument);
1446 c.Option = def;
1447 c.Option.Invoke(c);
1448 return false;
1449 }
1450
1451 private readonly Regex ValueOption = new Regex(
1452 @"^(?<flag>--|-|/)(?<name>[^:=]+)((?<sep>[:=])(?<value>.*))?$");
1453
1454 protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value)
1455 {
1456 if (argument == null)
1457 throw new ArgumentNullException("argument");
1458
1459 flag = name = sep = value = null;
1460 Match m = ValueOption.Match(argument);
1461 if (!m.Success)
1462 {
1463 return false;
1464 }
1465 flag = m.Groups["flag"].Value;
1466 name = m.Groups["name"].Value;
1467 if (m.Groups["sep"].Success && m.Groups["value"].Success)
1468 {
1469 sep = m.Groups["sep"].Value;
1470 value = m.Groups["value"].Value;
1471 }
1472 return true;
1473 }
1474
1475 protected virtual bool Parse(string argument, OptionContext c)
1476 {
1477 if (c.Option != null)
1478 {
1479 ParseValue(argument, c);
1480 return true;
1481 }
1482
1483 string f, n, s, v;
1484 if (!GetOptionParts(argument, out f, out n, out s, out v))
1485 return false;
1486
1487 Option p;
1488 if (Contains(n))
1489 {
1490 p = this[n];
1491 c.OptionName = f + n;
1492 c.Option = p;
1493 switch (p.OptionValueType)
1494 {
1495 case OptionValueType.None:
1496 c.OptionValues.Add(n);
1497 c.Option.Invoke(c);
1498 break;
1499 case OptionValueType.Optional:
1500 case OptionValueType.Required:
1501 ParseValue(v, c);
1502 break;
1503 }
1504 return true;
1505 }
1506 // no match; is it a bool option?
1507 if (ParseBool(argument, n, c))
1508 return true;
1509 // is it a bundled option?
1510 if (ParseBundledValue(f, string.Concat(n + s + v), c))
1511 return true;
1512
1513 return false;
1514 }
1515
1516 private void ParseValue(string option, OptionContext c)
1517 {
1518 if (option != null)
1519 foreach (string o in c.Option.ValueSeparators != null
1520 ? option.Split(c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None)
1521 : new string[] { option })
1522 {
1523 c.OptionValues.Add(o);
1524 }
1525 if (c.OptionValues.Count == c.Option.MaxValueCount ||
1526 c.Option.OptionValueType == OptionValueType.Optional)
1527 c.Option.Invoke(c);
1528 else if (c.OptionValues.Count > c.Option.MaxValueCount)
1529 {
1530 throw new OptionException(localizer(string.Format(
1531 "Error: Found {0} option values when expecting {1}.",
1532 c.OptionValues.Count, c.Option.MaxValueCount)),
1533 c.OptionName);
1534 }
1535 }
1536
1537 private bool ParseBool(string option, string n, OptionContext c)
1538 {
1539 Option p;
1540 string rn;
1541 if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') &&
1542 Contains((rn = n.Substring(0, n.Length - 1))))
1543 {
1544 p = this[rn];
1545 string v = n[n.Length - 1] == '+' ? option : null;
1546 c.OptionName = option;
1547 c.Option = p;
1548 c.OptionValues.Add(v);
1549 p.Invoke(c);
1550 return true;
1551 }
1552 return false;
1553 }
1554
1555 private bool ParseBundledValue(string f, string n, OptionContext c)
1556 {
1557 if (f != "-")
1558 return false;
1559 for (int i = 0; i < n.Length; ++i)
1560 {
1561 Option p;
1562 string opt = f + n[i].ToString();
1563 string rn = n[i].ToString();
1564 if (!Contains(rn))
1565 {
1566 if (i == 0)
1567 return false;
1568 throw new OptionException(string.Format(localizer(
1569 "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null);
1570 }
1571 p = this[rn];
1572 switch (p.OptionValueType)
1573 {
1574 case OptionValueType.None:
1575 Invoke(c, opt, n, p);
1576 break;
1577 case OptionValueType.Optional:
1578 case OptionValueType.Required:
1579 {
1580 string v = n.Substring(i + 1);
1581 c.Option = p;
1582 c.OptionName = opt;
1583 ParseValue(v.Length != 0 ? v : null, c);
1584 return true;
1585 }
1586 default:
1587 throw new InvalidOperationException("Unknown OptionValueType: " + p.OptionValueType);
1588 }
1589 }
1590 return true;
1591 }
1592
1593 private static void Invoke(OptionContext c, string name, string value, Option option)
1594 {
1595 c.OptionName = name;
1596 c.Option = option;
1597 c.OptionValues.Add(value);
1598 option.Invoke(c);
1599 }
1600
1601 private const int OptionWidth = 29;
1602 private const int Description_FirstWidth = 80 - OptionWidth;
1603 private const int Description_RemWidth = 80 - OptionWidth - 2;
1604
1605 static readonly string CommandHelpIndentStart = new string(' ', OptionWidth);
1606 static readonly string CommandHelpIndentRemaining = new string(' ', OptionWidth + 2);
1607
1608 public void WriteOptionDescriptions(TextWriter o)
1609 {
1610 foreach (Option p in this)
1611 {
1612 int written = 0;
1613
1614 if (p.Hidden)
1615 continue;
1616
1617 Category c = p as Category;
1618 if (c != null)
1619 {
1620 WriteDescription(o, p.Description, "", 80, 80);
1621 continue;
1622 }
1623 CommandOption co = p as CommandOption;
1624 if (co != null)
1625 {
1626 WriteCommandDescription(o, co.Command, co.CommandName);
1627 continue;
1628 }
1629
1630 if (!WriteOptionPrototype(o, p, ref written))
1631 continue;
1632
1633 if (written < OptionWidth)
1634 o.Write(new string(' ', OptionWidth - written));
1635 else
1636 {
1637 o.WriteLine();
1638 o.Write(new string(' ', OptionWidth));
1639 }
1640
1641 WriteDescription(o, p.Description, new string(' ', OptionWidth + 2),
1642 Description_FirstWidth, Description_RemWidth);
1643 }
1644
1645 foreach (ArgumentSource s in sources)
1646 {
1647 string[] names = s.GetNames();
1648 if (names == null || names.Length == 0)
1649 continue;
1650
1651 int written = 0;
1652
1653 Write(o, ref written, " ");
1654 Write(o, ref written, names[0]);
1655 for (int i = 1; i < names.Length; ++i)
1656 {
1657 Write(o, ref written, ", ");
1658 Write(o, ref written, names[i]);
1659 }
1660
1661 if (written < OptionWidth)
1662 o.Write(new string(' ', OptionWidth - written));
1663 else
1664 {
1665 o.WriteLine();
1666 o.Write(new string(' ', OptionWidth));
1667 }
1668
1669 WriteDescription(o, s.Description, new string(' ', OptionWidth + 2),
1670 Description_FirstWidth, Description_RemWidth);
1671 }
1672 }
1673
1674 internal void WriteCommandDescription(TextWriter o, Command c, string commandName)
1675 {
1676 var name = new string(' ', 8) + (commandName ?? c.Name);
1677 if (name.Length < OptionWidth - 1)
1678 {
1679 WriteDescription(o, name + new string(' ', OptionWidth - name.Length) + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth);
1680 }
1681 else
1682 {
1683 WriteDescription(o, name, "", 80, 80);
1684 WriteDescription(o, CommandHelpIndentStart + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth);
1685 }
1686 }
1687
1688 void WriteDescription(TextWriter o, string value, string prefix, int firstWidth, int remWidth)
1689 {
1690 bool indent = false;
1691 foreach (string line in GetLines(localizer(GetDescription(value)), firstWidth, remWidth))
1692 {
1693 if (indent)
1694 o.Write(prefix);
1695 o.WriteLine(line);
1696 indent = true;
1697 }
1698 }
1699
1700 bool WriteOptionPrototype(TextWriter o, Option p, ref int written)
1701 {
1702 string[] names = p.Names;
1703
1704 int i = GetNextOptionIndex(names, 0);
1705 if (i == names.Length)
1706 return false;
1707
1708 if (names[i].Length == 1)
1709 {
1710 Write(o, ref written, " -");
1711 Write(o, ref written, names[0]);
1712 }
1713 else
1714 {
1715 Write(o, ref written, " --");
1716 Write(o, ref written, names[0]);
1717 }
1718
1719 for (i = GetNextOptionIndex(names, i + 1);
1720 i < names.Length; i = GetNextOptionIndex(names, i + 1))
1721 {
1722 Write(o, ref written, ", ");
1723 Write(o, ref written, names[i].Length == 1 ? "-" : "--");
1724 Write(o, ref written, names[i]);
1725 }
1726
1727 if (p.OptionValueType == OptionValueType.Optional ||
1728 p.OptionValueType == OptionValueType.Required)
1729 {
1730 if (p.OptionValueType == OptionValueType.Optional)
1731 {
1732 Write(o, ref written, localizer("["));
1733 }
1734 Write(o, ref written, localizer("=" + GetArgumentName(0, p.MaxValueCount, p.Description)));
1735 string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0
1736 ? p.ValueSeparators[0]
1737 : " ";
1738 for (int c = 1; c < p.MaxValueCount; ++c)
1739 {
1740 Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description)));
1741 }
1742 if (p.OptionValueType == OptionValueType.Optional)
1743 {
1744 Write(o, ref written, localizer("]"));
1745 }
1746 }
1747 return true;
1748 }
1749
1750 static int GetNextOptionIndex(string[] names, int i)
1751 {
1752 while (i < names.Length && names[i] == "<>")
1753 {
1754 ++i;
1755 }
1756 return i;
1757 }
1758
1759 static void Write(TextWriter o, ref int n, string s)
1760 {
1761 n += s.Length;
1762 o.Write(s);
1763 }
1764
1765 static string GetArgumentName(int index, int maxIndex, string description)
1766 {
1767 var matches = Regex.Matches(description ?? "", @"(?<=(?<!\{)\{)[^{}]*(?=\}(?!\}))"); // ignore double braces
1768 string argName = "";
1769 foreach (Match match in matches)
1770 {
1771 var parts = match.Value.Split(':');
1772 // for maxIndex=1 it can be {foo} or {0:foo}
1773 if (maxIndex == 1)
1774 {
1775 argName = parts[parts.Length - 1];
1776 }
1777 // look for {i:foo} if maxIndex > 1
1778 if (maxIndex > 1 && parts.Length == 2 &&
1779 parts[0] == index.ToString(CultureInfo.InvariantCulture))
1780 {
1781 argName = parts[1];
1782 }
1783 }
1784
1785 if (string.IsNullOrEmpty(argName))
1786 {
1787 argName = maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1788 }
1789 return argName;
1790 }
1791
1792 private static string GetDescription(string description)
1793 {
1794 if (description == null)
1795 return string.Empty;
1796 StringBuilder sb = new StringBuilder(description.Length);
1797 int start = -1;
1798 for (int i = 0; i < description.Length; ++i)
1799 {
1800 switch (description[i])
1801 {
1802 case '{':
1803 if (i == start)
1804 {
1805 sb.Append('{');
1806 start = -1;
1807 }
1808 else if (start < 0)
1809 start = i + 1;
1810 break;
1811 case '}':
1812 if (start < 0)
1813 {
1814 if ((i + 1) == description.Length || description[i + 1] != '}')
1815 throw new InvalidOperationException("Invalid option description: " + description);
1816 ++i;
1817 sb.Append("}");
1818 }
1819 else
1820 {
1821 sb.Append(description.Substring(start, i - start));
1822 start = -1;
1823 }
1824 break;
1825 case ':':
1826 if (start < 0)
1827 goto default;
1828 start = i + 1;
1829 break;
1830 default:
1831 if (start < 0)
1832 sb.Append(description[i]);
1833 break;
1834 }
1835 }
1836 return sb.ToString();
1837 }
1838
1839 private static IEnumerable<string> GetLines(string description, int firstWidth, int remWidth)
1840 {
1841 return StringCoda.WrappedLines(description, firstWidth, remWidth);
1842 }
1843 }
1844
1845 public class Command
1846 {
1847 public string Name { get; }
1848 public string Help { get; }
1849
1850 public OptionSet Options { get; set; }
1851 public Action<IEnumerable<string>> Run { get; set; }
1852
1853 public CommandSet CommandSet { get; internal set; }
1854
1855 public Command(string name, string help = null)
1856 {
1857 if (string.IsNullOrEmpty(name))
1858 throw new ArgumentNullException(nameof(name));
1859
1860 Name = NormalizeCommandName(name);
1861 Help = help;
1862 }
1863
1864 static string NormalizeCommandName(string name)
1865 {
1866 var value = new StringBuilder(name.Length);
1867 var space = false;
1868 for (int i = 0; i < name.Length; ++i)
1869 {
1870 if (!char.IsWhiteSpace(name, i))
1871 {
1872 space = false;
1873 value.Append(name[i]);
1874 }
1875 else if (!space)
1876 {
1877 space = true;
1878 value.Append(' ');
1879 }
1880 }
1881 return value.ToString();
1882 }
1883
1884 public virtual int Invoke(IEnumerable<string> arguments)
1885 {
1886 var rest = Options?.Parse(arguments) ?? arguments;
1887 Run?.Invoke(rest);
1888 return 0;
1889 }
1890 }
1891
1892 class CommandOption : Option
1893 {
1894 public Command Command { get; }
1895 public string CommandName { get; }
1896
1897 // Prototype starts with '=' because this is an invalid prototype
1898 // (see Option.ParsePrototype(), and thus it'll prevent Category
1899 // instances from being accidentally used as normal options.
1900 public CommandOption(Command command, string commandName = null, bool hidden = false)
1901 : base("=:Command:= " + (commandName ?? command?.Name), (commandName ?? command?.Name), maxValueCount: 0, hidden: hidden)
1902 {
1903 if (command == null)
1904 throw new ArgumentNullException(nameof(command));
1905 Command = command;
1906 CommandName = commandName ?? command.Name;
1907 }
1908
1909 protected override void OnParseComplete(OptionContext c)
1910 {
1911 throw new NotSupportedException("CommandOption.OnParseComplete should not be invoked.");
1912 }
1913 }
1914
1915 class HelpOption : Option
1916 {
1917 Option option;
1918 CommandSet commands;
1919
1920 public HelpOption(CommandSet commands, Option d)
1921 : base(d.Prototype, d.Description, d.MaxValueCount, d.Hidden)
1922 {
1923 this.commands = commands;
1924 this.option = d;
1925 }
1926
1927 protected override void OnParseComplete(OptionContext c)
1928 {
1929 commands.showHelp = true;
1930
1931 option?.InvokeOnParseComplete(c);
1932 }
1933 }
1934
1935 class CommandOptionSet : OptionSet
1936 {
1937 CommandSet commands;
1938
1939 public CommandOptionSet(CommandSet commands, MessageLocalizerConverter localizer)
1940 : base(localizer)
1941 {
1942 this.commands = commands;
1943 }
1944
1945 protected override void SetItem(int index, Option item)
1946 {
1947 if (ShouldWrapOption(item))
1948 {
1949 base.SetItem(index, new HelpOption(commands, item));
1950 return;
1951 }
1952 base.SetItem(index, item);
1953 }
1954
1955 bool ShouldWrapOption(Option item)
1956 {
1957 if (item == null)
1958 return false;
1959 var help = item as HelpOption;
1960 if (help != null)
1961 return false;
1962 foreach (var n in item.Names)
1963 {
1964 if (n == "help")
1965 return true;
1966 }
1967 return false;
1968 }
1969
1970 protected override void InsertItem(int index, Option item)
1971 {
1972 if (ShouldWrapOption(item))
1973 {
1974 base.InsertItem(index, new HelpOption(commands, item));
1975 return;
1976 }
1977 base.InsertItem(index, item);
1978 }
1979 }
1980
1981 public class CommandSet : KeyedCollection<string, Command>
1982 {
1983 readonly string suite;
1984
1985 OptionSet options;
1986 TextWriter outWriter;
1987 TextWriter errorWriter;
1988
1989 internal List<CommandSet> NestedCommandSets;
1990
1991 internal HelpCommand help;
1992
1993 internal bool showHelp;
1994
1995 internal OptionSet Options => options;
1996
1997#if !PCL || NETSTANDARD1_3
1998 public CommandSet(string suite, MessageLocalizerConverter localizer = null)
1999 : this(suite, Console.Out, Console.Error, localizer)
2000 {
2001 }
2002#endif
2003
2004 public CommandSet(string suite, TextWriter output, TextWriter error, MessageLocalizerConverter localizer = null)
2005 {
2006 if (suite == null)
2007 throw new ArgumentNullException(nameof(suite));
2008 if (output == null)
2009 throw new ArgumentNullException(nameof(output));
2010 if (error == null)
2011 throw new ArgumentNullException(nameof(error));
2012
2013 this.suite = suite;
2014 options = new CommandOptionSet(this, localizer);
2015 outWriter = output;
2016 errorWriter = error;
2017 }
2018
2019 public string Suite => suite;
2020 public TextWriter Out => outWriter;
2021 public TextWriter Error => errorWriter;
2022 public MessageLocalizerConverter MessageLocalizer => options.MessageLocalizer;
2023
2024 protected override string GetKeyForItem(Command item)
2025 {
2026 return item?.Name;
2027 }
2028
2029 public new CommandSet Add(Command value)
2030 {
2031 if (value == null)
2032 throw new ArgumentNullException(nameof(value));
2033 AddCommand(value);
2034 options.Add(new CommandOption(value));
2035 return this;
2036 }
2037
2038 void AddCommand(Command value)
2039 {
2040 if (value.CommandSet != null && value.CommandSet != this)
2041 {
2042 throw new ArgumentException("Command instances can only be added to a single CommandSet.", nameof(value));
2043 }
2044 value.CommandSet = this;
2045 if (value.Options != null)
2046 {
2047 value.Options.MessageLocalizer = options.MessageLocalizer;
2048 }
2049
2050 base.Add(value);
2051
2052 help = help ?? value as HelpCommand;
2053 }
2054
2055 public CommandSet Add(string header)
2056 {
2057 options.Add(header);
2058 return this;
2059 }
2060
2061 public CommandSet Add(Option option)
2062 {
2063 options.Add(option);
2064 return this;
2065 }
2066
2067 public CommandSet Add(string prototype, Action<string> action)
2068 {
2069 options.Add(prototype, action);
2070 return this;
2071 }
2072
2073 public CommandSet Add(string prototype, string description, Action<string> action)
2074 {
2075 options.Add(prototype, description, action);
2076 return this;
2077 }
2078
2079 public CommandSet Add(string prototype, string description, Action<string> action, bool hidden)
2080 {
2081 options.Add(prototype, description, action, hidden);
2082 return this;
2083 }
2084
2085 public CommandSet Add(string prototype, OptionAction<string, string> action)
2086 {
2087 options.Add(prototype, action);
2088 return this;
2089 }
2090
2091 public CommandSet Add(string prototype, string description, OptionAction<string, string> action)
2092 {
2093 options.Add(prototype, description, action);
2094 return this;
2095 }
2096
2097 public CommandSet Add(string prototype, string description, OptionAction<string, string> action, bool hidden)
2098 {
2099 options.Add(prototype, description, action, hidden);
2100 return this;
2101 }
2102
2103 public CommandSet Add<T>(string prototype, Action<T> action)
2104 {
2105 options.Add(prototype, null, action);
2106 return this;
2107 }
2108
2109 public CommandSet Add<T>(string prototype, string description, Action<T> action)
2110 {
2111 options.Add(prototype, description, action);
2112 return this;
2113 }
2114
2115 public CommandSet Add<TKey, TValue>(string prototype, OptionAction<TKey, TValue> action)
2116 {
2117 options.Add(prototype, action);
2118 return this;
2119 }
2120
2121 public CommandSet Add<TKey, TValue>(string prototype, string description, OptionAction<TKey, TValue> action)
2122 {
2123 options.Add(prototype, description, action);
2124 return this;
2125 }
2126
2127 public CommandSet Add(ArgumentSource source)
2128 {
2129 options.Add(source);
2130 return this;
2131 }
2132
2133 public CommandSet Add(CommandSet nestedCommands)
2134 {
2135 if (nestedCommands == null)
2136 throw new ArgumentNullException(nameof(nestedCommands));
2137
2138 if (NestedCommandSets == null)
2139 {
2140 NestedCommandSets = new List<CommandSet>();
2141 }
2142
2143 if (!AlreadyAdded(nestedCommands))
2144 {
2145 NestedCommandSets.Add(nestedCommands);
2146 foreach (var o in nestedCommands.options)
2147 {
2148 if (o is CommandOption c)
2149 {
2150 options.Add(new CommandOption(c.Command, $"{nestedCommands.Suite} {c.CommandName}"));
2151 }
2152 else
2153 {
2154 options.Add(o);
2155 }
2156 }
2157 }
2158
2159 nestedCommands.options = this.options;
2160 nestedCommands.outWriter = this.outWriter;
2161 nestedCommands.errorWriter = this.errorWriter;
2162
2163 return this;
2164 }
2165
2166 bool AlreadyAdded(CommandSet value)
2167 {
2168 if (value == this)
2169 return true;
2170 if (NestedCommandSets == null)
2171 return false;
2172 foreach (var nc in NestedCommandSets)
2173 {
2174 if (nc.AlreadyAdded(value))
2175 return true;
2176 }
2177 return false;
2178 }
2179
2180 public IEnumerable<string> GetCompletions(string prefix = null)
2181 {
2182 string rest;
2183 ExtractToken(ref prefix, out rest);
2184
2185 foreach (var command in this)
2186 {
2187 if (command.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
2188 {
2189 yield return command.Name;
2190 }
2191 }
2192
2193 if (NestedCommandSets == null)
2194 yield break;
2195
2196 foreach (var subset in NestedCommandSets)
2197 {
2198 if (subset.Suite.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
2199 {
2200 foreach (var c in subset.GetCompletions(rest))
2201 {
2202 yield return $"{subset.Suite} {c}";
2203 }
2204 }
2205 }
2206 }
2207
2208 static void ExtractToken(ref string input, out string rest)
2209 {
2210 rest = "";
2211 input = input ?? "";
2212
2213 int top = input.Length;
2214 for (int i = 0; i < top; i++)
2215 {
2216 if (char.IsWhiteSpace(input[i]))
2217 continue;
2218
2219 for (int j = i; j < top; j++)
2220 {
2221 if (char.IsWhiteSpace(input[j]))
2222 {
2223 rest = input.Substring(j).Trim();
2224 input = input.Substring(i, j).Trim();
2225 return;
2226 }
2227 }
2228 rest = "";
2229 if (i != 0)
2230 input = input.Substring(i).Trim();
2231 return;
2232 }
2233 }
2234
2235 public int Run(IEnumerable<string> arguments)
2236 {
2237 if (arguments == null)
2238 throw new ArgumentNullException(nameof(arguments));
2239
2240 this.showHelp = false;
2241 if (help == null)
2242 {
2243 help = new HelpCommand();
2244 AddCommand(help);
2245 }
2246 Action<string> setHelp = v => showHelp = v != null;
2247 if (!options.Contains("help"))
2248 {
2249 options.Add("help", "", setHelp, hidden: true);
2250 }
2251 if (!options.Contains("?"))
2252 {
2253 options.Add("?", "", setHelp, hidden: true);
2254 }
2255 var extra = options.Parse(arguments);
2256 if (extra.Count == 0)
2257 {
2258 if (showHelp)
2259 {
2260 return help.Invoke(extra);
2261 }
2262 Out.WriteLine(options.MessageLocalizer($"Use `{Suite} help` for usage."));
2263 return 1;
2264 }
2265 var command = GetCommand(extra);
2266 if (command == null)
2267 {
2268 help.WriteUnknownCommand(extra[0]);
2269 return 1;
2270 }
2271 if (showHelp)
2272 {
2273 if (command.Options?.Contains("help") ?? true)
2274 {
2275 extra.Add("--help");
2276 return command.Invoke(extra);
2277 }
2278 command.Options.WriteOptionDescriptions(Out);
2279 return 0;
2280 }
2281 return command.Invoke(extra);
2282 }
2283
2284 internal Command GetCommand(List<string> extra)
2285 {
2286 return TryGetLocalCommand(extra) ?? TryGetNestedCommand(extra);
2287 }
2288
2289 Command TryGetLocalCommand(List<string> extra)
2290 {
2291 var name = extra[0];
2292 if (Contains(name))
2293 {
2294 extra.RemoveAt(0);
2295 return this[name];
2296 }
2297 for (int i = 1; i < extra.Count; ++i)
2298 {
2299 name = name + " " + extra[i];
2300 if (!Contains(name))
2301 continue;
2302 extra.RemoveRange(0, i + 1);
2303 return this[name];
2304 }
2305 return null;
2306 }
2307
2308 Command TryGetNestedCommand(List<string> extra)
2309 {
2310 if (NestedCommandSets == null)
2311 return null;
2312
2313 var nestedCommands = NestedCommandSets.Find(c => c.Suite == extra[0]);
2314 if (nestedCommands == null)
2315 return null;
2316
2317 var extraCopy = new List<string>(extra);
2318 extraCopy.RemoveAt(0);
2319 if (extraCopy.Count == 0)
2320 return null;
2321
2322 var command = nestedCommands.GetCommand(extraCopy);
2323 if (command != null)
2324 {
2325 extra.Clear();
2326 extra.AddRange(extraCopy);
2327 return command;
2328 }
2329 return null;
2330 }
2331 }
2332
2333 public class HelpCommand : Command
2334 {
2335 public HelpCommand()
2336 : base("help", help: "Show this message and exit")
2337 {
2338 }
2339
2340 public override int Invoke(IEnumerable<string> arguments)
2341 {
2342 var extra = new List<string>(arguments ?? new string[0]);
2343 var _ = CommandSet.Options.MessageLocalizer;
2344 if (extra.Count == 0)
2345 {
2346 CommandSet.Options.WriteOptionDescriptions(CommandSet.Out);
2347 return 0;
2348 }
2349 var command = CommandSet.GetCommand(extra);
2350 if (command == this || extra.Contains("--help"))
2351 {
2352 CommandSet.Out.WriteLine(_($"Usage: {CommandSet.Suite} COMMAND [OPTIONS]"));
2353 CommandSet.Out.WriteLine(_($"Use `{CommandSet.Suite} help COMMAND` for help on a specific command."));
2354 CommandSet.Out.WriteLine();
2355 CommandSet.Out.WriteLine(_($"Available commands:"));
2356 CommandSet.Out.WriteLine();
2357 var commands = GetCommands();
2358 commands.Sort((x, y) => string.Compare(x.Key, y.Key, StringComparison.OrdinalIgnoreCase));
2359 foreach (var c in commands)
2360 {
2361 if (c.Key == "help")
2362 {
2363 continue;
2364 }
2365 CommandSet.Options.WriteCommandDescription(CommandSet.Out, c.Value, c.Key);
2366 }
2367 CommandSet.Options.WriteCommandDescription(CommandSet.Out, CommandSet.help, "help");
2368 return 0;
2369 }
2370 if (command == null)
2371 {
2372 WriteUnknownCommand(extra[0]);
2373 return 1;
2374 }
2375 if (command.Options != null)
2376 {
2377 command.Options.WriteOptionDescriptions(CommandSet.Out);
2378 return 0;
2379 }
2380 return command.Invoke(new[] { "--help" });
2381 }
2382
2383 List<KeyValuePair<string, Command>> GetCommands()
2384 {
2385 var commands = new List<KeyValuePair<string, Command>>();
2386
2387 foreach (var c in CommandSet)
2388 {
2389 commands.Add(new KeyValuePair<string, Command>(c.Name, c));
2390 }
2391
2392 if (CommandSet.NestedCommandSets == null)
2393 return commands;
2394
2395 foreach (var nc in CommandSet.NestedCommandSets)
2396 {
2397 AddNestedCommands(commands, "", nc);
2398 }
2399
2400 return commands;
2401 }
2402
2403 void AddNestedCommands(List<KeyValuePair<string, Command>> commands, string outer, CommandSet value)
2404 {
2405 foreach (var v in value)
2406 {
2407 commands.Add(new KeyValuePair<string, Command>($"{outer}{value.Suite} {v.Name}", v));
2408 }
2409 if (value.NestedCommandSets == null)
2410 return;
2411 foreach (var nc in value.NestedCommandSets)
2412 {
2413 AddNestedCommands(commands, $"{outer}{value.Suite} ", nc);
2414 }
2415 }
2416
2417 internal void WriteUnknownCommand(string unknownCommand)
2418 {
2419 CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Unknown command: {unknownCommand}"));
2420 CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Use `{CommandSet.Suite} help` for usage."));
2421 }
2422 }
2423}