5 // Jonathan Pryor <jpryor@novell.com>, <Jonathan.Pryor@microsoft.com>
6 // Federico Di Gregorio <fog@initd.org>
7 // Rolf Bjarne Kvinge <rolf@xamarin.com>
9 // Copyright (C) 2008 Novell (http://www.novell.com)
10 // Copyright (C) 2009 Federico Di Gregorio.
11 // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
12 // Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com)
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
25 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 // mcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll
36 // mcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll
38 // The LINQ version just changes the implementation of
39 // OptionSet.Parse(IEnumerable<string>), and confers no semantic changes.
42 // A Getopt::Long-inspired option parsing library for C#.
44 // Mono.Options.OptionSet is built upon a key/value table, where the
45 // key is a option format string and the value is a delegate that is
46 // invoked when the format string is matched.
48 // Option format strings:
49 // Regex-like BNF Grammar:
52 // sep: ( [^{}]+ | '{' .+ '}' )?
53 // aliases: ( name type sep ) ( '|' name type sep )*
55 // Each '|'-delimited name is an alias for the associated action. If the
56 // format string ends in a '=', it has a required value. If the format
57 // string ends in a ':', it has an optional value. If neither '=' or ':'
58 // is present, no value is supported. `=' or `:' need only be defined on one
59 // alias, but if they are provided on more than one they must be consistent.
61 // Each alias portion may also end with a "key/value separator", which is used
62 // to split option values if the option accepts > 1 value. If not specified,
63 // it defaults to '=' and ':'. If specified, it can be any character except
64 // '{' and '}' OR the *string* between '{' and '}'. If no separator should be
65 // used (i.e. the separate values should be distinct arguments), then "{}"
66 // should be used as the separator.
68 // Options are extracted either from the current option by looking for
69 // the option name followed by an '=' or ':', or is taken from the
70 // following option IFF:
71 // - The current option does not contain a '=' or a ':'
72 // - The current option requires a value (i.e. not a Option type of ':')
74 // The `name' used in the option format string does NOT include any leading
75 // option indicator, such as '-', '--', or '/'. All three of these are
76 // permitted/required on any named option.
78 // Option bundling is permitted so long as:
79 // - '-' is used to start the option group
80 // - all of the bundled options are a single character
81 // - at most one of the bundled options accepts a value, and the value
82 // provided starts from the next character to the end of the string.
84 // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'
87 // Option processing is disabled by specifying "--". All options after "--"
88 // are returned by OptionSet.Parse() unchanged and unprocessed.
90 // Unprocessed options are returned from OptionSet.Parse().
94 // OptionSet p = new OptionSet ()
95 // .Add ("v", v => ++verbose)
96 // .Add ("name=|value=", v => Console.WriteLine (v));
97 // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"});
99 // The above would parse the argument string array, and would invoke the
100 // lambda expression three times, setting `verbose' to 3 when complete.
101 // It would also print out "A" and "B" to standard output.
102 // The returned array would contain the string "extra".
104 // C# 3.0 collection initializers are supported and encouraged:
105 // var p = new OptionSet () {
106 // { "h|?|help", v => ShowHelp () },
109 // System.ComponentModel.TypeConverter is also supported, allowing the use of
110 // custom data types in the callback type; TypeConverter.ConvertFromString()
111 // is used to convert the value option to an instance of the specified
114 // var p = new OptionSet () {
115 // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) },
118 // Random other tidbits:
119 // - Boolean options (those w/o '=' or ':' in the option format string)
120 // are explicitly enabled if they are followed with '+', and explicitly
121 // disabled if they are followed with '-':
123 // var p = new OptionSet () {
124 // { "a", s => a = s },
126 // p.Parse (new string[]{"-a"}); // sets v != null
127 // p.Parse (new string[]{"-a+"}); // sets v != null
128 // p.Parse (new string[]{"-a-"}); // sets v == null
132 // Mono.Options.CommandSet allows easily having separate commands and
133 // associated command options, allowing creation of a *suite* along the
134 // lines of **git**(1), **svn**(1), etc.
136 // CommandSet allows intermixing plain text strings for `--help` output,
137 // Option values -- as supported by OptionSet -- and Command instances,
138 // which have a name, optional help text, and an optional OptionSet.
140 // var suite = new CommandSet ("suite-name") {
141 // // Use strings and option values, as with OptionSet
142 // "usage: suite-name COMMAND [OPTIONS]+",
143 // { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 },
144 // // Commands may also be specified
145 // new Command ("command-name", "command help") {
146 // Options = new OptionSet {/*...*/},
147 // Run = args => { /*...*/},
149 // new MyCommandSubclass (),
151 // return suite.Run (new string[]{...});
153 // CommandSet provides a `help` command, and forwards `help COMMAND`
154 // to the registered Command instance by invoking Command.Invoke()
155 // with `--help` as an option.
159 using System.Collections;
160 using System.Collections.Generic;
161 using System.Collections.ObjectModel;
162 using System.ComponentModel;
163 using System.Globalization;
165 using System.Runtime.Serialization;
167 using System.Reflection;
169 using System.Security.Permissions;
172 using System.Text.RegularExpressions;
183 using MessageLocalizerConverter = System.Func<string, string>;
185 using MessageLocalizerConverter = System.Converter<string, string>;
189 namespace NDesk.Options
191 namespace Mono.Options
194 static class StringCoda {
196 public static IEnumerable<string> WrappedLines (string self, params int[] widths)
198 IEnumerable<int> w = widths;
199 return WrappedLines (self, w);
202 public static IEnumerable<string> WrappedLines (string self, IEnumerable<int> widths)
205 throw new ArgumentNullException ("widths");
206 return CreateWrappedLinesIterator (self, widths);
209 private static IEnumerable<string> CreateWrappedLinesIterator (string self, IEnumerable<int> widths)
211 if (string.IsNullOrEmpty (self)) {
212 yield return string.Empty;
215 using (IEnumerator<int> ewidths = widths.GetEnumerator ()) {
217 int width = GetNextWidth (ewidths, int.MaxValue, ref hw);
220 end = GetLineEnd (start, width, self);
221 char c = self [end-1];
222 if (char.IsWhiteSpace (c))
224 bool needContinuation = end != self.Length && !IsEolChar (c);
225 string continuation = "";
226 if (needContinuation) {
230 string line = self.Substring (start, end - start) + continuation;
233 if (char.IsWhiteSpace (c))
235 width = GetNextWidth (ewidths, width, ref hw);
236 } while (start < self.Length);
240 private static int GetNextWidth (IEnumerator<int> ewidths, int curWidth, ref bool? eValid)
242 if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) {
243 curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth;
244 // '.' is any character, - is for a continuation
245 const string minWidth = ".-";
246 if (curWidth < minWidth.Length)
247 throw new ArgumentOutOfRangeException ("widths",
248 string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth));
251 // no more elements, use the last element.
255 private static bool IsEolChar (char c)
257 return !char.IsLetterOrDigit (c);
260 private static int GetLineEnd (int start, int length, string description)
262 int end = System.Math.Min (start + length, description.Length);
264 for (int i = start; i < end; ++i) {
265 if (description [i] == '\n')
267 if (IsEolChar (description [i]))
270 if (sep == -1 || end == description.Length)
276 public class OptionValueCollection : IList, IList<string> {
278 List<string> values = new List<string> ();
281 internal OptionValueCollection (OptionContext c)
287 void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);}
288 bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}}
289 object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}}
292 #region ICollection<T>
293 public void Add (string item) {values.Add (item);}
294 public void Clear () {values.Clear ();}
295 public bool Contains (string item) {return values.Contains (item);}
296 public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);}
297 public bool Remove (string item) {return values.Remove (item);}
298 public int Count {get {return values.Count;}}
299 public bool IsReadOnly {get {return false;}}
303 IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();}
306 #region IEnumerable<T>
307 public IEnumerator<string> GetEnumerator () {return values.GetEnumerator ();}
311 int IList.Add (object value) {return (values as IList).Add (value);}
312 bool IList.Contains (object value) {return (values as IList).Contains (value);}
313 int IList.IndexOf (object value) {return (values as IList).IndexOf (value);}
314 void IList.Insert (int index, object value) {(values as IList).Insert (index, value);}
315 void IList.Remove (object value) {(values as IList).Remove (value);}
316 void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);}
317 bool IList.IsFixedSize {get {return false;}}
318 object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}}
322 public int IndexOf (string item) {return values.IndexOf (item);}
323 public void Insert (int index, string item) {values.Insert (index, item);}
324 public void RemoveAt (int index) {values.RemoveAt (index);}
326 private void AssertValid (int index)
328 if (c.Option == null)
329 throw new InvalidOperationException ("OptionContext.Option is null.");
330 if (index >= c.Option.MaxValueCount)
331 throw new ArgumentOutOfRangeException ("index");
332 if (c.Option.OptionValueType == OptionValueType.Required &&
333 index >= values.Count)
334 throw new OptionException (string.Format (
335 c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName),
339 public string this [int index] {
342 return index >= values.Count ? null : values [index];
345 values [index] = value;
350 public List<string> ToList ()
352 return new List<string> (values);
355 public string[] ToArray ()
357 return values.ToArray ();
360 public override string ToString ()
362 return string.Join (", ", values.ToArray ());
366 public class OptionContext {
367 private Option option;
370 private OptionSet set;
371 private OptionValueCollection c;
373 public OptionContext (OptionSet set)
376 this.c = new OptionValueCollection (this);
379 public Option Option {
381 set {option = value;}
384 public string OptionName {
389 public int OptionIndex {
394 public OptionSet OptionSet {
398 public OptionValueCollection OptionValues {
403 public enum OptionValueType {
409 public abstract class Option {
410 string prototype, description;
412 OptionValueType type;
417 protected Option (string prototype, string description)
418 : this (prototype, description, 1, false)
422 protected Option (string prototype, string description, int maxValueCount)
423 : this (prototype, description, maxValueCount, false)
427 protected Option (string prototype, string description, int maxValueCount, bool hidden)
429 if (prototype == null)
430 throw new ArgumentNullException ("prototype");
431 if (prototype.Length == 0)
432 throw new ArgumentException ("Cannot be the empty string.", "prototype");
433 if (maxValueCount < 0)
434 throw new ArgumentOutOfRangeException ("maxValueCount");
436 this.prototype = prototype;
437 this.description = description;
438 this.count = maxValueCount;
439 this.names = (this is OptionSet.Category)
440 // append GetHashCode() so that "duplicate" categories have distinct
441 // names, e.g. adding multiple "" categories should be valid.
442 ? new[]{prototype + this.GetHashCode ()}
443 : prototype.Split ('|');
445 if (this is OptionSet.Category || this is CommandOption)
448 this.type = ParsePrototype ();
449 this.hidden = hidden;
451 if (this.count == 0 && type != OptionValueType.None)
452 throw new ArgumentException (
453 "Cannot provide maxValueCount of 0 for OptionValueType.Required or " +
454 "OptionValueType.Optional.",
456 if (this.type == OptionValueType.None && maxValueCount > 1)
457 throw new ArgumentException (
458 string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount),
460 if (Array.IndexOf (names, "<>") >= 0 &&
461 ((names.Length == 1 && this.type != OptionValueType.None) ||
462 (names.Length > 1 && this.MaxValueCount > 1)))
463 throw new ArgumentException (
464 "The default option handler '<>' cannot require values.",
468 public string Prototype {get {return prototype;}}
469 public string Description {get {return description;}}
470 public OptionValueType OptionValueType {get {return type;}}
471 public int MaxValueCount {get {return count;}}
472 public bool Hidden {get {return hidden;}}
474 public string[] GetNames ()
476 return (string[]) names.Clone ();
479 public string[] GetValueSeparators ()
481 if (separators == null)
482 return new string [0];
483 return (string[]) separators.Clone ();
486 protected static T Parse<T> (string value, OptionContext c)
488 Type tt = typeof (T);
490 TypeInfo ti = tt.GetTypeInfo ();
497 !ti.IsGenericTypeDefinition &&
498 ti.GetGenericTypeDefinition () == typeof (Nullable<>);
500 Type targetType = nullable ? tt.GenericTypeArguments [0] : tt;
502 Type targetType = nullable ? tt.GetGenericArguments () [0] : tt;
508 if (targetType.GetTypeInfo ().IsEnum)
509 t = (T) Enum.Parse (targetType, value, true);
511 t = (T) Convert.ChangeType (value, targetType);
513 TypeConverter conv = TypeDescriptor.GetConverter (targetType);
514 t = (T) conv.ConvertFromString (value);
518 catch (Exception e) {
519 throw new OptionException (
521 c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."),
522 value, targetType.Name, c.OptionName),
528 internal string[] Names {get {return names;}}
529 internal string[] ValueSeparators {get {return separators;}}
531 static readonly char[] NameTerminator = new char[]{'=', ':'};
533 private OptionValueType ParsePrototype ()
536 List<string> seps = new List<string> ();
537 for (int i = 0; i < names.Length; ++i) {
538 string name = names [i];
539 if (name.Length == 0)
540 throw new ArgumentException ("Empty option names are not supported.", "prototype");
542 int end = name.IndexOfAny (NameTerminator);
545 names [i] = name.Substring (0, end);
546 if (type == '\0' || type == name [end])
549 throw new ArgumentException (
550 string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]),
552 AddSeparators (name, end, seps);
556 return OptionValueType.None;
558 if (count <= 1 && seps.Count != 0)
559 throw new ArgumentException (
560 string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count),
564 this.separators = new string[]{":", "="};
565 else if (seps.Count == 1 && seps [0].Length == 0)
566 this.separators = null;
568 this.separators = seps.ToArray ();
571 return type == '=' ? OptionValueType.Required : OptionValueType.Optional;
574 private static void AddSeparators (string name, int end, ICollection<string> seps)
577 for (int i = end+1; i < name.Length; ++i) {
581 throw new ArgumentException (
582 string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
588 throw new ArgumentException (
589 string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
591 seps.Add (name.Substring (start, i-start));
596 seps.Add (name [i].ToString ());
601 throw new ArgumentException (
602 string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
606 public void Invoke (OptionContext c)
611 c.OptionValues.Clear ();
614 protected abstract void OnParseComplete (OptionContext c);
616 internal void InvokeOnParseComplete (OptionContext c)
621 public override string ToString ()
627 public abstract class ArgumentSource {
629 protected ArgumentSource ()
633 public abstract string[] GetNames ();
634 public abstract string Description { get; }
635 public abstract bool GetArguments (string value, out IEnumerable<string> replacement);
638 public static IEnumerable<string> GetArgumentsFromFile (string file)
640 return GetArguments (File.OpenText (file), true);
644 public static IEnumerable<string> GetArguments (TextReader reader)
646 return GetArguments (reader, false);
649 // Cribbed from mcs/driver.cs:LoadArgs(string)
650 static IEnumerable<string> GetArguments (TextReader reader, bool close)
653 StringBuilder arg = new StringBuilder ();
656 while ((line = reader.ReadLine ()) != null) {
659 for (int i = 0; i < t; i++) {
662 if (c == '"' || c == '\'') {
665 for (i++; i < t; i++){
672 } else if (c == ' ') {
673 if (arg.Length > 0) {
674 yield return arg.ToString ();
680 if (arg.Length > 0) {
681 yield return arg.ToString ();
694 public class ResponseFileSource : ArgumentSource {
696 public override string[] GetNames ()
698 return new string[]{"@file"};
701 public override string Description {
702 get {return "Read response file for more options.";}
705 public override bool GetArguments (string value, out IEnumerable<string> replacement)
707 if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) {
711 replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1));
720 public class OptionException : Exception {
721 private string option;
723 public OptionException ()
727 public OptionException (string message, string optionName)
730 this.option = optionName;
733 public OptionException (string message, string optionName, Exception innerException)
734 : base (message, innerException)
736 this.option = optionName;
740 protected OptionException (SerializationInfo info, StreamingContext context)
741 : base (info, context)
743 this.option = info.GetString ("OptionName");
747 public string OptionName {
748 get {return this.option;}
752 #pragma warning disable 618 // SecurityPermissionAttribute is obsolete
753 [SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)]
754 #pragma warning restore 618
755 public override void GetObjectData (SerializationInfo info, StreamingContext context)
757 base.GetObjectData (info, context);
758 info.AddValue ("OptionName", option);
763 public delegate void OptionAction<TKey, TValue> (TKey key, TValue value);
765 public class OptionSet : KeyedCollection<string, Option>
772 public OptionSet (MessageLocalizerConverter localizer)
774 this.roSources = new ReadOnlyCollection<ArgumentSource> (sources);
775 this.localizer = localizer;
776 if (this.localizer == null) {
777 this.localizer = delegate (string f) {
783 MessageLocalizerConverter localizer;
785 public MessageLocalizerConverter MessageLocalizer {
786 get {return localizer;}
787 internal set {localizer = value;}
790 List<ArgumentSource> sources = new List<ArgumentSource> ();
791 ReadOnlyCollection<ArgumentSource> roSources;
793 public ReadOnlyCollection<ArgumentSource> ArgumentSources {
794 get {return roSources;}
798 protected override string GetKeyForItem (Option item)
801 throw new ArgumentNullException ("option");
802 if (item.Names != null && item.Names.Length > 0)
803 return item.Names [0];
804 // This should never happen, as it's invalid for Option to be
805 // constructed w/o any names.
806 throw new InvalidOperationException ("Option has no names!");
809 [Obsolete ("Use KeyedCollection.this[string]")]
810 protected Option GetOptionForName (string option)
813 throw new ArgumentNullException ("option");
815 return base [option];
817 catch (KeyNotFoundException) {
822 protected override void InsertItem (int index, Option item)
824 base.InsertItem (index, item);
828 protected override void RemoveItem (int index)
830 Option p = Items [index];
831 base.RemoveItem (index);
832 // KeyedCollection.RemoveItem() handles the 0th item
833 for (int i = 1; i < p.Names.Length; ++i) {
834 Dictionary.Remove (p.Names [i]);
838 protected override void SetItem (int index, Option item)
840 base.SetItem (index, item);
844 private void AddImpl (Option option)
847 throw new ArgumentNullException ("option");
848 List<string> added = new List<string> (option.Names.Length);
850 // KeyedCollection.InsertItem/SetItem handle the 0th name.
851 for (int i = 1; i < option.Names.Length; ++i) {
852 Dictionary.Add (option.Names [i], option);
853 added.Add (option.Names [i]);
857 foreach (string name in added)
858 Dictionary.Remove (name);
863 public OptionSet Add (string header)
866 throw new ArgumentNullException ("header");
867 Add (new Category (header));
871 internal sealed class Category : Option {
873 // Prototype starts with '=' because this is an invalid prototype
874 // (see Option.ParsePrototype(), and thus it'll prevent Category
875 // instances from being accidentally used as normal options.
876 public Category (string description)
877 : base ("=:Category:= " + description, description)
881 protected override void OnParseComplete (OptionContext c)
883 throw new NotSupportedException ("Category.OnParseComplete should not be invoked.");
888 public new OptionSet Add (Option option)
894 sealed class ActionOption : Option {
895 Action<OptionValueCollection> action;
897 public ActionOption (string prototype, string description, int count, Action<OptionValueCollection> action)
898 : this (prototype, description, count, action, false)
902 public ActionOption (string prototype, string description, int count, Action<OptionValueCollection> action, bool hidden)
903 : base (prototype, description, count, hidden)
906 throw new ArgumentNullException ("action");
907 this.action = action;
910 protected override void OnParseComplete (OptionContext c)
912 action (c.OptionValues);
916 public OptionSet Add (string prototype, Action<string> action)
918 return Add (prototype, null, action);
921 public OptionSet Add (string prototype, string description, Action<string> action)
923 return Add (prototype, description, action, false);
926 public OptionSet Add (string prototype, string description, Action<string> action, bool hidden)
929 throw new ArgumentNullException ("action");
930 Option p = new ActionOption (prototype, description, 1,
931 delegate (OptionValueCollection v) { action (v [0]); }, hidden);
936 public OptionSet Add (string prototype, OptionAction<string, string> action)
938 return Add (prototype, null, action);
941 public OptionSet Add (string prototype, string description, OptionAction<string, string> action)
943 return Add (prototype, description, action, false);
946 public OptionSet Add (string prototype, string description, OptionAction<string, string> action, bool hidden) {
948 throw new ArgumentNullException ("action");
949 Option p = new ActionOption (prototype, description, 2,
950 delegate (OptionValueCollection v) {action (v [0], v [1]);}, hidden);
955 sealed class ActionOption<T> : Option {
958 public ActionOption (string prototype, string description, Action<T> action)
959 : base (prototype, description, 1)
962 throw new ArgumentNullException ("action");
963 this.action = action;
966 protected override void OnParseComplete (OptionContext c)
968 action (Parse<T> (c.OptionValues [0], c));
972 sealed class ActionOption<TKey, TValue> : Option {
973 OptionAction<TKey, TValue> action;
975 public ActionOption (string prototype, string description, OptionAction<TKey, TValue> action)
976 : base (prototype, description, 2)
979 throw new ArgumentNullException ("action");
980 this.action = action;
983 protected override void OnParseComplete (OptionContext c)
986 Parse<TKey> (c.OptionValues [0], c),
987 Parse<TValue> (c.OptionValues [1], c));
991 public OptionSet Add<T> (string prototype, Action<T> action)
993 return Add (prototype, null, action);
996 public OptionSet Add<T> (string prototype, string description, Action<T> action)
998 return Add (new ActionOption<T> (prototype, description, action));
1001 public OptionSet Add<TKey, TValue> (string prototype, OptionAction<TKey, TValue> action)
1003 return Add (prototype, null, action);
1006 public OptionSet Add<TKey, TValue> (string prototype, string description, OptionAction<TKey, TValue> action)
1008 return Add (new ActionOption<TKey, TValue> (prototype, description, action));
1011 public OptionSet Add (ArgumentSource source)
1014 throw new ArgumentNullException ("source");
1015 sources.Add (source);
1019 protected virtual OptionContext CreateOptionContext ()
1021 return new OptionContext (this);
1024 public List<string> Parse (IEnumerable<string> arguments)
1026 if (arguments == null)
1027 throw new ArgumentNullException ("arguments");
1028 OptionContext c = CreateOptionContext ();
1030 bool process = true;
1031 List<string> unprocessed = new List<string> ();
1032 Option def = Contains ("<>") ? this ["<>"] : null;
1033 ArgumentEnumerator ae = new ArgumentEnumerator (arguments);
1034 foreach (string argument in ae) {
1036 if (argument == "--") {
1041 Unprocessed (unprocessed, def, c, argument);
1044 if (AddSource (ae, argument))
1046 if (!Parse (argument, c))
1047 Unprocessed (unprocessed, def, c, argument);
1049 if (c.Option != null)
1050 c.Option.Invoke (c);
1054 class ArgumentEnumerator : IEnumerable<string> {
1055 List<IEnumerator<string>> sources = new List<IEnumerator<string>> ();
1057 public ArgumentEnumerator (IEnumerable<string> arguments)
1059 sources.Add (arguments.GetEnumerator ());
1062 public void Add (IEnumerable<string> arguments)
1064 sources.Add (arguments.GetEnumerator ());
1067 public IEnumerator<string> GetEnumerator ()
1070 IEnumerator<string> c = sources [sources.Count-1];
1072 yield return c.Current;
1075 sources.RemoveAt (sources.Count-1);
1077 } while (sources.Count > 0);
1080 IEnumerator IEnumerable.GetEnumerator ()
1082 return GetEnumerator ();
1086 bool AddSource (ArgumentEnumerator ae, string argument)
1088 foreach (ArgumentSource source in sources) {
1089 IEnumerable<string> replacement;
1090 if (!source.GetArguments (argument, out replacement))
1092 ae.Add (replacement);
1098 private static bool Unprocessed (ICollection<string> extra, Option def, OptionContext c, string argument)
1101 extra.Add (argument);
1104 c.OptionValues.Add (argument);
1106 c.Option.Invoke (c);
1110 private readonly Regex ValueOption = new Regex (
1111 @"^(?<flag>--|-|/)(?<name>[^:=]+)((?<sep>[:=])(?<value>.*))?$");
1113 protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value)
1115 if (argument == null)
1116 throw new ArgumentNullException ("argument");
1118 flag = name = sep = value = null;
1119 Match m = ValueOption.Match (argument);
1123 flag = m.Groups ["flag"].Value;
1124 name = m.Groups ["name"].Value;
1125 if (m.Groups ["sep"].Success && m.Groups ["value"].Success) {
1126 sep = m.Groups ["sep"].Value;
1127 value = m.Groups ["value"].Value;
1132 protected virtual bool Parse (string argument, OptionContext c)
1134 if (c.Option != null) {
1135 ParseValue (argument, c);
1140 if (!GetOptionParts (argument, out f, out n, out s, out v))
1146 c.OptionName = f + n;
1148 switch (p.OptionValueType) {
1149 case OptionValueType.None:
1150 c.OptionValues.Add (n);
1151 c.Option.Invoke (c);
1153 case OptionValueType.Optional:
1154 case OptionValueType.Required:
1160 // no match; is it a bool option?
1161 if (ParseBool (argument, n, c))
1163 // is it a bundled option?
1164 if (ParseBundledValue (f, string.Concat (n + s + v), c))
1170 private void ParseValue (string option, OptionContext c)
1173 foreach (string o in c.Option.ValueSeparators != null
1174 ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None)
1175 : new string[]{option}) {
1176 c.OptionValues.Add (o);
1178 if (c.OptionValues.Count == c.Option.MaxValueCount ||
1179 c.Option.OptionValueType == OptionValueType.Optional)
1180 c.Option.Invoke (c);
1181 else if (c.OptionValues.Count > c.Option.MaxValueCount) {
1182 throw new OptionException (localizer (string.Format (
1183 "Error: Found {0} option values when expecting {1}.",
1184 c.OptionValues.Count, c.Option.MaxValueCount)),
1189 private bool ParseBool (string option, string n, OptionContext c)
1193 if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') &&
1194 Contains ((rn = n.Substring (0, n.Length-1)))) {
1196 string v = n [n.Length-1] == '+' ? option : null;
1197 c.OptionName = option;
1199 c.OptionValues.Add (v);
1206 private bool ParseBundledValue (string f, string n, OptionContext c)
1210 for (int i = 0; i < n.Length; ++i) {
1212 string opt = f + n [i].ToString ();
1213 string rn = n [i].ToString ();
1214 if (!Contains (rn)) {
1217 throw new OptionException (string.Format (localizer (
1218 "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null);
1221 switch (p.OptionValueType) {
1222 case OptionValueType.None:
1223 Invoke (c, opt, n, p);
1225 case OptionValueType.Optional:
1226 case OptionValueType.Required: {
1227 string v = n.Substring (i+1);
1230 ParseValue (v.Length != 0 ? v : null, c);
1234 throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType);
1240 private static void Invoke (OptionContext c, string name, string value, Option option)
1242 c.OptionName = name;
1244 c.OptionValues.Add (value);
1248 private const int OptionWidth = 29;
1249 private const int Description_FirstWidth = 80 - OptionWidth;
1250 private const int Description_RemWidth = 80 - OptionWidth - 2;
1252 static readonly string CommandHelpIndentStart = new string (' ', OptionWidth);
1253 static readonly string CommandHelpIndentRemaining = new string (' ', OptionWidth + 2);
1255 public void WriteOptionDescriptions (TextWriter o)
1257 foreach (Option p in this) {
1263 Category c = p as Category;
1265 WriteDescription (o, p.Description, "", 80, 80);
1268 CommandOption co = p as CommandOption;
1270 WriteCommandDescription (o, co.Command);
1274 if (!WriteOptionPrototype (o, p, ref written))
1277 if (written < OptionWidth)
1278 o.Write (new string (' ', OptionWidth - written));
1281 o.Write (new string (' ', OptionWidth));
1284 WriteDescription (o, p.Description, new string (' ', OptionWidth+2),
1285 Description_FirstWidth, Description_RemWidth);
1288 foreach (ArgumentSource s in sources) {
1289 string[] names = s.GetNames ();
1290 if (names == null || names.Length == 0)
1295 Write (o, ref written, " ");
1296 Write (o, ref written, names [0]);
1297 for (int i = 1; i < names.Length; ++i) {
1298 Write (o, ref written, ", ");
1299 Write (o, ref written, names [i]);
1302 if (written < OptionWidth)
1303 o.Write (new string (' ', OptionWidth - written));
1306 o.Write (new string (' ', OptionWidth));
1309 WriteDescription (o, s.Description, new string (' ', OptionWidth+2),
1310 Description_FirstWidth, Description_RemWidth);
1314 internal void WriteCommandDescription (TextWriter o, Command c)
1316 var name = new string (' ', 8) + c.Name;
1317 if (name.Length < OptionWidth - 1) {
1318 WriteDescription (o, name + new string (' ', OptionWidth - name.Length) + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth);
1320 WriteDescription (o, name, "", 80, 80);
1321 WriteDescription (o, CommandHelpIndentStart + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth);
1325 void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth)
1327 bool indent = false;
1328 foreach (string line in GetLines (localizer (GetDescription (value)), firstWidth, remWidth)) {
1336 bool WriteOptionPrototype (TextWriter o, Option p, ref int written)
1338 string[] names = p.Names;
1340 int i = GetNextOptionIndex (names, 0);
1341 if (i == names.Length)
1344 if (names [i].Length == 1) {
1345 Write (o, ref written, " -");
1346 Write (o, ref written, names [0]);
1349 Write (o, ref written, " --");
1350 Write (o, ref written, names [0]);
1353 for ( i = GetNextOptionIndex (names, i+1);
1354 i < names.Length; i = GetNextOptionIndex (names, i+1)) {
1355 Write (o, ref written, ", ");
1356 Write (o, ref written, names [i].Length == 1 ? "-" : "--");
1357 Write (o, ref written, names [i]);
1360 if (p.OptionValueType == OptionValueType.Optional ||
1361 p.OptionValueType == OptionValueType.Required) {
1362 if (p.OptionValueType == OptionValueType.Optional) {
1363 Write (o, ref written, localizer ("["));
1365 Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description)));
1366 string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0
1367 ? p.ValueSeparators [0]
1369 for (int c = 1; c < p.MaxValueCount; ++c) {
1370 Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description)));
1372 if (p.OptionValueType == OptionValueType.Optional) {
1373 Write (o, ref written, localizer ("]"));
1379 static int GetNextOptionIndex (string[] names, int i)
1381 while (i < names.Length && names [i] == "<>") {
1387 static void Write (TextWriter o, ref int n, string s)
1393 private static string GetArgumentName (int index, int maxIndex, string description)
1395 if (description == null)
1396 return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1399 nameStart = new string[]{"{0:", "{"};
1401 nameStart = new string[]{"{" + index + ":"};
1402 for (int i = 0; i < nameStart.Length; ++i) {
1405 start = description.IndexOf (nameStart [i], j);
1406 } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false);
1409 int end = description.IndexOf ("}", start);
1412 return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length);
1414 return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1417 private static string GetDescription (string description)
1419 if (description == null)
1420 return string.Empty;
1421 StringBuilder sb = new StringBuilder (description.Length);
1423 for (int i = 0; i < description.Length; ++i) {
1424 switch (description [i]) {
1435 if ((i+1) == description.Length || description [i+1] != '}')
1436 throw new InvalidOperationException ("Invalid option description: " + description);
1441 sb.Append (description.Substring (start, i - start));
1452 sb.Append (description [i]);
1456 return sb.ToString ();
1459 private static IEnumerable<string> GetLines (string description, int firstWidth, int remWidth)
1461 return StringCoda.WrappedLines (description, firstWidth, remWidth);
1465 public class Command
1467 public string Name {get;}
1468 public string Help {get;}
1470 public OptionSet Options {get; set;}
1471 public Action<IEnumerable<string>> Run {get; set;}
1473 public CommandSet CommandSet {get; internal set;}
1475 public Command (string name, string help = null)
1477 if (string.IsNullOrEmpty (name))
1478 throw new ArgumentNullException (nameof (name));
1484 public virtual int Invoke (IEnumerable<string> arguments)
1486 var rest = Options?.Parse (arguments) ?? arguments;
1492 class CommandOption : Option
1494 public Command Command {get;}
1496 // Prototype starts with '=' because this is an invalid prototype
1497 // (see Option.ParsePrototype(), and thus it'll prevent Category
1498 // instances from being accidentally used as normal options.
1499 public CommandOption (Command command, bool hidden = false)
1500 : base ("=:Command:= " + command?.Name, command?.Name, maxValueCount: 0, hidden: hidden)
1502 if (command == null)
1503 throw new ArgumentNullException (nameof (command));
1507 protected override void OnParseComplete (OptionContext c)
1509 throw new NotSupportedException ("CommandOption.OnParseComplete should not be invoked.");
1513 class HelpOption : Option
1516 CommandSet commands;
1518 public HelpOption (CommandSet commands, Option d)
1519 : base (d.Prototype, d.Description, d.MaxValueCount, d.Hidden)
1521 this.commands = commands;
1525 protected override void OnParseComplete (OptionContext c)
1527 commands.showHelp = true;
1529 option?.InvokeOnParseComplete (c);
1533 class CommandOptionSet : OptionSet
1535 CommandSet commands;
1537 public CommandOptionSet (CommandSet commands, MessageLocalizerConverter localizer)
1540 this.commands = commands;
1543 protected override void SetItem (int index, Option item)
1545 if (ShouldWrapOption (item)) {
1546 base.SetItem (index, new HelpOption (commands, item));
1549 base.SetItem (index, item);
1552 bool ShouldWrapOption (Option item)
1556 var help = item as HelpOption;
1559 foreach (var n in item.Names) {
1566 protected override void InsertItem (int index, Option item)
1568 if (ShouldWrapOption (item)) {
1569 base.InsertItem (index, new HelpOption (commands, item));
1572 base.InsertItem (index, item);
1576 public class CommandSet : KeyedCollection<string, Command>
1578 readonly OptionSet options;
1579 readonly TextWriter outWriter;
1580 readonly TextWriter errorWriter;
1581 readonly string suite;
1585 internal bool showHelp;
1587 internal OptionSet Options => options;
1589 public CommandSet (string suite, MessageLocalizerConverter localizer = null, TextWriter output = null, TextWriter error = null)
1592 throw new ArgumentNullException (nameof (suite));
1594 options = new CommandOptionSet (this, localizer);
1595 outWriter = output ?? Console.Out;
1596 errorWriter = error ?? Console.Error;
1599 public string Suite => suite;
1600 public TextWriter Out => outWriter;
1601 public TextWriter Error => errorWriter;
1602 public MessageLocalizerConverter MessageLocalizer => options.MessageLocalizer;
1604 protected override string GetKeyForItem (Command item)
1609 public new CommandSet Add (Command value)
1612 throw new ArgumentNullException (nameof (value));
1614 options.Add (new CommandOption (value));
1618 void AddCommand (Command value)
1620 if (value.CommandSet != null && value.CommandSet != this) {
1621 throw new ArgumentException ("Command instances can only be added to a single CommandSet.", nameof (value));
1623 value.CommandSet = this;
1624 if (value.Options != null) {
1625 value.Options.MessageLocalizer = options.MessageLocalizer;
1630 help = help ?? value as HelpCommand;
1633 public CommandSet Add (string header)
1635 options.Add (header);
1639 public CommandSet Add (Option option)
1641 options.Add (option);
1645 public CommandSet Add (string prototype, Action<string> action)
1647 options.Add (prototype, action);
1651 public CommandSet Add (string prototype, string description, Action<string> action)
1653 options.Add (prototype, description, action);
1657 public CommandSet Add (string prototype, string description, Action<string> action, bool hidden)
1659 options.Add (prototype, description, action, hidden);
1663 public CommandSet Add (string prototype, OptionAction<string, string> action)
1665 options.Add (prototype, action);
1669 public CommandSet Add (string prototype, string description, OptionAction<string, string> action)
1671 options.Add (prototype, description, action);
1675 public CommandSet Add (string prototype, string description, OptionAction<string, string> action, bool hidden)
1677 options.Add (prototype, description, action, hidden);
1681 public CommandSet Add<T> (string prototype, Action<T> action)
1683 options.Add (prototype, null, action);
1687 public CommandSet Add<T> (string prototype, string description, Action<T> action)
1689 options.Add (prototype, description, action);
1693 public CommandSet Add<TKey, TValue> (string prototype, OptionAction<TKey, TValue> action)
1695 options.Add (prototype, action);
1699 public CommandSet Add<TKey, TValue> (string prototype, string description, OptionAction<TKey, TValue> action)
1701 options.Add (prototype, description, action);
1705 public CommandSet Add (ArgumentSource source)
1707 options.Add (source);
1711 public int Run (IEnumerable<string> arguments)
1713 if (arguments == null)
1714 throw new ArgumentNullException (nameof (arguments));
1716 this.showHelp = false;
1718 help = new HelpCommand ();
1721 Action<string> setHelp = v => showHelp = v != null;
1722 if (!options.Contains ("help")) {
1723 options.Add ("help", "", setHelp, hidden: true);
1725 if (!options.Contains ("?")) {
1726 options.Add ("?", "", setHelp, hidden: true);
1728 var extra = options.Parse (arguments);
1729 if (extra.Count == 0) {
1731 return help.Invoke (extra);
1733 Out.WriteLine (options.MessageLocalizer ($"Use `{Suite} help` for usage."));
1736 var command = Contains (extra [0]) ? this [extra [0]] : null;
1737 if (command == null) {
1738 help.WriteUnknownCommand (extra [0]);
1743 if (command.Options?.Contains ("help") ?? true) {
1744 extra.Add ("--help");
1745 return command.Invoke (extra);
1747 command.Options.WriteOptionDescriptions (Out);
1750 return command.Invoke (extra);
1754 public class HelpCommand : Command
1756 public HelpCommand ()
1757 : base ("help", help: "Show this message and exit")
1761 public override int Invoke (IEnumerable<string> arguments)
1763 var extra = new List<string> (arguments ?? new string [0]);
1764 var _ = CommandSet.Options.MessageLocalizer;
1765 if (extra.Count == 0) {
1766 CommandSet.Options.WriteOptionDescriptions (CommandSet.Out);
1769 var command = CommandSet.Contains (extra [0])
1770 ? CommandSet [extra [0]]
1772 if (command == this || extra [0] == "--help") {
1773 CommandSet.Out.WriteLine (_ ($"Usage: {CommandSet.Suite} COMMAND [OPTIONS]"));
1774 CommandSet.Out.WriteLine (_ ($"Use `{CommandSet.Suite} help COMMAND` for help on a specific command."));
1775 CommandSet.Out.WriteLine ();
1776 CommandSet.Out.WriteLine (_ ($"Available commands:"));
1777 CommandSet.Out.WriteLine ();
1778 foreach (var c in CommandSet) {
1779 CommandSet.Options.WriteCommandDescription (CommandSet.Out, c);
1783 if (command == null) {
1784 WriteUnknownCommand (extra [0]);
1787 if (command.Options != null) {
1788 command.Options.WriteOptionDescriptions (CommandSet.Out);
1791 return command.Invoke (new [] { "--help" });
1794 internal void WriteUnknownCommand (string unknownCommand)
1796 CommandSet.Error.WriteLine (CommandSet.Options.MessageLocalizer ($"{CommandSet.Suite}: Unknown command: {unknownCommand}"));
1797 CommandSet.Error.WriteLine (CommandSet.Options.MessageLocalizer ($"{CommandSet.Suite}: Use `{CommandSet.Suite} help` for usage."));