2010-06-03 Jb Evain <jbevain@novell.com>
[mcs.git] / class / Mono.Options / Mono.Options / Options.cs
blobd8e65f303b97c31a2e886a0f1d16869386d2734c
1 //
2 // Options.cs
3 //
4 // Authors:
5 // Jonathan Pryor <jpryor@novell.com>
6 // Federico Di Gregorio <fog@initd.org>
7 //
8 // Copyright (C) 2008 Novell (http://www.novell.com)
9 // Copyright (C) 2009 Federico Di Gregorio.
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
18 //
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
21 //
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 // Compile With:
32 // gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll
33 // gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll
35 // The LINQ version just changes the implementation of
36 // OptionSet.Parse(IEnumerable<string>), and confers no semantic changes.
39 // A Getopt::Long-inspired option parsing library for C#.
41 // NDesk.Options.OptionSet is built upon a key/value table, where the
42 // key is a option format string and the value is a delegate that is
43 // invoked when the format string is matched.
45 // Option format strings:
46 // Regex-like BNF Grammar:
47 // name: .+
48 // type: [=:]
49 // sep: ( [^{}]+ | '{' .+ '}' )?
50 // aliases: ( name type sep ) ( '|' name type sep )*
51 //
52 // Each '|'-delimited name is an alias for the associated action. If the
53 // format string ends in a '=', it has a required value. If the format
54 // string ends in a ':', it has an optional value. If neither '=' or ':'
55 // is present, no value is supported. `=' or `:' need only be defined on one
56 // alias, but if they are provided on more than one they must be consistent.
58 // Each alias portion may also end with a "key/value separator", which is used
59 // to split option values if the option accepts > 1 value. If not specified,
60 // it defaults to '=' and ':'. If specified, it can be any character except
61 // '{' and '}' OR the *string* between '{' and '}'. If no separator should be
62 // used (i.e. the separate values should be distinct arguments), then "{}"
63 // should be used as the separator.
65 // Options are extracted either from the current option by looking for
66 // the option name followed by an '=' or ':', or is taken from the
67 // following option IFF:
68 // - The current option does not contain a '=' or a ':'
69 // - The current option requires a value (i.e. not a Option type of ':')
71 // The `name' used in the option format string does NOT include any leading
72 // option indicator, such as '-', '--', or '/'. All three of these are
73 // permitted/required on any named option.
75 // Option bundling is permitted so long as:
76 // - '-' is used to start the option group
77 // - all of the bundled options are a single character
78 // - at most one of the bundled options accepts a value, and the value
79 // provided starts from the next character to the end of the string.
81 // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'
82 // as '-Dname=value'.
84 // Option processing is disabled by specifying "--". All options after "--"
85 // are returned by OptionSet.Parse() unchanged and unprocessed.
87 // Unprocessed options are returned from OptionSet.Parse().
89 // Examples:
90 // int verbose = 0;
91 // OptionSet p = new OptionSet ()
92 // .Add ("v", v => ++verbose)
93 // .Add ("name=|value=", v => Console.WriteLine (v));
94 // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"});
96 // The above would parse the argument string array, and would invoke the
97 // lambda expression three times, setting `verbose' to 3 when complete.
98 // It would also print out "A" and "B" to standard output.
99 // The returned array would contain the string "extra".
101 // C# 3.0 collection initializers are supported and encouraged:
102 // var p = new OptionSet () {
103 // { "h|?|help", v => ShowHelp () },
104 // };
106 // System.ComponentModel.TypeConverter is also supported, allowing the use of
107 // custom data types in the callback type; TypeConverter.ConvertFromString()
108 // is used to convert the value option to an instance of the specified
109 // type:
111 // var p = new OptionSet () {
112 // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) },
113 // };
115 // Random other tidbits:
116 // - Boolean options (those w/o '=' or ':' in the option format string)
117 // are explicitly enabled if they are followed with '+', and explicitly
118 // disabled if they are followed with '-':
119 // string a = null;
120 // var p = new OptionSet () {
121 // { "a", s => a = s },
122 // };
123 // p.Parse (new string[]{"-a"}); // sets v != null
124 // p.Parse (new string[]{"-a+"}); // sets v != null
125 // p.Parse (new string[]{"-a-"}); // sets v == null
128 using System;
129 using System.Collections;
130 using System.Collections.Generic;
131 using System.Collections.ObjectModel;
132 using System.ComponentModel;
133 using System.Globalization;
134 using System.IO;
135 using System.Runtime.Serialization;
136 using System.Security.Permissions;
137 using System.Text;
138 using System.Text.RegularExpressions;
140 #if LINQ
141 using System.Linq;
142 #endif
144 #if TEST
145 using NDesk.Options;
146 #endif
148 #if NDESK_OPTIONS
149 namespace NDesk.Options
150 #else
151 namespace Mono.Options
152 #endif
154 static class StringCoda {
156 public static IEnumerable<string> WrappedLines (string self, params int[] widths)
158 IEnumerable<int> w = widths;
159 return WrappedLines (self, w);
162 public static IEnumerable<string> WrappedLines (string self, IEnumerable<int> widths)
164 if (widths == null)
165 throw new ArgumentNullException ("widths");
166 return CreateWrappedLinesIterator (self, widths);
169 private static IEnumerable<string> CreateWrappedLinesIterator (string self, IEnumerable<int> widths)
171 if (string.IsNullOrEmpty (self)) {
172 yield return string.Empty;
173 yield break;
175 using (var ewidths = widths.GetEnumerator ()) {
176 bool? hw = null;
177 int width = GetNextWidth (ewidths, int.MaxValue, ref hw);
178 int start = 0, end;
179 do {
180 end = GetLineEnd (start, width, self);
181 char c = self [end-1];
182 if (char.IsWhiteSpace (c))
183 --end;
184 bool needContinuation = end != self.Length && !IsEolChar (c);
185 string continuation = "";
186 if (needContinuation) {
187 --end;
188 continuation = "-";
190 string line = self.Substring (start, end - start) + continuation;
191 yield return line;
192 start = end;
193 if (char.IsWhiteSpace (c))
194 ++start;
195 width = GetNextWidth (ewidths, width, ref hw);
196 } while (end < self.Length);
200 private static int GetNextWidth (IEnumerator<int> ewidths, int curWidth, ref bool? eValid)
202 if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) {
203 curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth;
204 // '.' is any character, - is for a continuation
205 const string minWidth = ".-";
206 if (curWidth < minWidth.Length)
207 throw new ArgumentOutOfRangeException ("widths",
208 string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth));
209 return curWidth;
211 // no more elements, use the last element.
212 return curWidth;
215 private static bool IsEolChar (char c)
217 return !char.IsLetterOrDigit (c);
220 private static int GetLineEnd (int start, int length, string description)
222 int end = System.Math.Min (start + length, description.Length);
223 int sep = -1;
224 for (int i = start; i < end; ++i) {
225 if (description [i] == '\n')
226 return i+1;
227 if (IsEolChar (description [i]))
228 sep = i+1;
230 if (sep == -1 || end == description.Length)
231 return end;
232 return sep;
236 public class OptionValueCollection : IList, IList<string> {
238 List<string> values = new List<string> ();
239 OptionContext c;
241 internal OptionValueCollection (OptionContext c)
243 this.c = c;
246 #region ICollection
247 void ICollection.CopyTo (Array array, int index) {(values as ICollection).CopyTo (array, index);}
248 bool ICollection.IsSynchronized {get {return (values as ICollection).IsSynchronized;}}
249 object ICollection.SyncRoot {get {return (values as ICollection).SyncRoot;}}
250 #endregion
252 #region ICollection<T>
253 public void Add (string item) {values.Add (item);}
254 public void Clear () {values.Clear ();}
255 public bool Contains (string item) {return values.Contains (item);}
256 public void CopyTo (string[] array, int arrayIndex) {values.CopyTo (array, arrayIndex);}
257 public bool Remove (string item) {return values.Remove (item);}
258 public int Count {get {return values.Count;}}
259 public bool IsReadOnly {get {return false;}}
260 #endregion
262 #region IEnumerable
263 IEnumerator IEnumerable.GetEnumerator () {return values.GetEnumerator ();}
264 #endregion
266 #region IEnumerable<T>
267 public IEnumerator<string> GetEnumerator () {return values.GetEnumerator ();}
268 #endregion
270 #region IList
271 int IList.Add (object value) {return (values as IList).Add (value);}
272 bool IList.Contains (object value) {return (values as IList).Contains (value);}
273 int IList.IndexOf (object value) {return (values as IList).IndexOf (value);}
274 void IList.Insert (int index, object value) {(values as IList).Insert (index, value);}
275 void IList.Remove (object value) {(values as IList).Remove (value);}
276 void IList.RemoveAt (int index) {(values as IList).RemoveAt (index);}
277 bool IList.IsFixedSize {get {return false;}}
278 object IList.this [int index] {get {return this [index];} set {(values as IList)[index] = value;}}
279 #endregion
281 #region IList<T>
282 public int IndexOf (string item) {return values.IndexOf (item);}
283 public void Insert (int index, string item) {values.Insert (index, item);}
284 public void RemoveAt (int index) {values.RemoveAt (index);}
286 private void AssertValid (int index)
288 if (c.Option == null)
289 throw new InvalidOperationException ("OptionContext.Option is null.");
290 if (index >= c.Option.MaxValueCount)
291 throw new ArgumentOutOfRangeException ("index");
292 if (c.Option.OptionValueType == OptionValueType.Required &&
293 index >= values.Count)
294 throw new OptionException (string.Format (
295 c.OptionSet.MessageLocalizer ("Missing required value for option '{0}'."), c.OptionName),
296 c.OptionName);
299 public string this [int index] {
300 get {
301 AssertValid (index);
302 return index >= values.Count ? null : values [index];
304 set {
305 values [index] = value;
308 #endregion
310 public List<string> ToList ()
312 return new List<string> (values);
315 public string[] ToArray ()
317 return values.ToArray ();
320 public override string ToString ()
322 return string.Join (", ", values.ToArray ());
326 public class OptionContext {
327 private Option option;
328 private string name;
329 private int index;
330 private OptionSet set;
331 private OptionValueCollection c;
333 public OptionContext (OptionSet set)
335 this.set = set;
336 this.c = new OptionValueCollection (this);
339 public Option Option {
340 get {return option;}
341 set {option = value;}
344 public string OptionName {
345 get {return name;}
346 set {name = value;}
349 public int OptionIndex {
350 get {return index;}
351 set {index = value;}
354 public OptionSet OptionSet {
355 get {return set;}
358 public OptionValueCollection OptionValues {
359 get {return c;}
363 public enum OptionValueType {
364 None,
365 Optional,
366 Required,
369 public abstract class Option {
370 string prototype, description;
371 string[] names;
372 OptionValueType type;
373 int count;
374 string[] separators;
376 protected Option (string prototype, string description)
377 : this (prototype, description, 1)
381 protected Option (string prototype, string description, int maxValueCount)
383 if (prototype == null)
384 throw new ArgumentNullException ("prototype");
385 if (prototype.Length == 0)
386 throw new ArgumentException ("Cannot be the empty string.", "prototype");
387 if (maxValueCount < 0)
388 throw new ArgumentOutOfRangeException ("maxValueCount");
390 this.prototype = prototype;
391 this.names = prototype.Split ('|');
392 this.description = description;
393 this.count = maxValueCount;
394 this.type = ParsePrototype ();
396 if (this.count == 0 && type != OptionValueType.None)
397 throw new ArgumentException (
398 "Cannot provide maxValueCount of 0 for OptionValueType.Required or " +
399 "OptionValueType.Optional.",
400 "maxValueCount");
401 if (this.type == OptionValueType.None && maxValueCount > 1)
402 throw new ArgumentException (
403 string.Format ("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount),
404 "maxValueCount");
405 if (Array.IndexOf (names, "<>") >= 0 &&
406 ((names.Length == 1 && this.type != OptionValueType.None) ||
407 (names.Length > 1 && this.MaxValueCount > 1)))
408 throw new ArgumentException (
409 "The default option handler '<>' cannot require values.",
410 "prototype");
413 public string Prototype {get {return prototype;}}
414 public string Description {get {return description;}}
415 public OptionValueType OptionValueType {get {return type;}}
416 public int MaxValueCount {get {return count;}}
418 public string[] GetNames ()
420 return (string[]) names.Clone ();
423 public string[] GetValueSeparators ()
425 if (separators == null)
426 return new string [0];
427 return (string[]) separators.Clone ();
430 protected static T Parse<T> (string value, OptionContext c)
432 Type tt = typeof (T);
433 bool nullable = tt.IsValueType && tt.IsGenericType &&
434 !tt.IsGenericTypeDefinition &&
435 tt.GetGenericTypeDefinition () == typeof (Nullable<>);
436 Type targetType = nullable ? tt.GetGenericArguments () [0] : typeof (T);
437 TypeConverter conv = TypeDescriptor.GetConverter (targetType);
438 T t = default (T);
439 try {
440 if (value != null)
441 t = (T) conv.ConvertFromString (value);
443 catch (Exception e) {
444 throw new OptionException (
445 string.Format (
446 c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."),
447 value, targetType.Name, c.OptionName),
448 c.OptionName, e);
450 return t;
453 internal string[] Names {get {return names;}}
454 internal string[] ValueSeparators {get {return separators;}}
456 static readonly char[] NameTerminator = new char[]{'=', ':'};
458 private OptionValueType ParsePrototype ()
460 char type = '\0';
461 List<string> seps = new List<string> ();
462 for (int i = 0; i < names.Length; ++i) {
463 string name = names [i];
464 if (name.Length == 0)
465 throw new ArgumentException ("Empty option names are not supported.", "prototype");
467 int end = name.IndexOfAny (NameTerminator);
468 if (end == -1)
469 continue;
470 names [i] = name.Substring (0, end);
471 if (type == '\0' || type == name [end])
472 type = name [end];
473 else
474 throw new ArgumentException (
475 string.Format ("Conflicting option types: '{0}' vs. '{1}'.", type, name [end]),
476 "prototype");
477 AddSeparators (name, end, seps);
480 if (type == '\0')
481 return OptionValueType.None;
483 if (count <= 1 && seps.Count != 0)
484 throw new ArgumentException (
485 string.Format ("Cannot provide key/value separators for Options taking {0} value(s).", count),
486 "prototype");
487 if (count > 1) {
488 if (seps.Count == 0)
489 this.separators = new string[]{":", "="};
490 else if (seps.Count == 1 && seps [0].Length == 0)
491 this.separators = null;
492 else
493 this.separators = seps.ToArray ();
496 return type == '=' ? OptionValueType.Required : OptionValueType.Optional;
499 private static void AddSeparators (string name, int end, ICollection<string> seps)
501 int start = -1;
502 for (int i = end+1; i < name.Length; ++i) {
503 switch (name [i]) {
504 case '{':
505 if (start != -1)
506 throw new ArgumentException (
507 string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
508 "prototype");
509 start = i+1;
510 break;
511 case '}':
512 if (start == -1)
513 throw new ArgumentException (
514 string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
515 "prototype");
516 seps.Add (name.Substring (start, i-start));
517 start = -1;
518 break;
519 default:
520 if (start == -1)
521 seps.Add (name [i].ToString ());
522 break;
525 if (start != -1)
526 throw new ArgumentException (
527 string.Format ("Ill-formed name/value separator found in \"{0}\".", name),
528 "prototype");
531 public void Invoke (OptionContext c)
533 OnParseComplete (c);
534 c.OptionName = null;
535 c.Option = null;
536 c.OptionValues.Clear ();
539 protected abstract void OnParseComplete (OptionContext c);
541 public override string ToString ()
543 return Prototype;
547 [Serializable]
548 public class OptionException : Exception {
549 private string option;
551 public OptionException ()
555 public OptionException (string message, string optionName)
556 : base (message)
558 this.option = optionName;
561 public OptionException (string message, string optionName, Exception innerException)
562 : base (message, innerException)
564 this.option = optionName;
567 protected OptionException (SerializationInfo info, StreamingContext context)
568 : base (info, context)
570 this.option = info.GetString ("OptionName");
573 public string OptionName {
574 get {return this.option;}
577 [SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)]
578 public override void GetObjectData (SerializationInfo info, StreamingContext context)
580 base.GetObjectData (info, context);
581 info.AddValue ("OptionName", option);
585 public delegate void OptionAction<TKey, TValue> (TKey key, TValue value);
587 public class OptionSet : KeyedCollection<string, Option>
589 public OptionSet ()
590 : this (delegate (string f) {return f;})
594 public OptionSet (Converter<string, string> localizer)
596 this.localizer = localizer;
599 Converter<string, string> localizer;
601 public Converter<string, string> MessageLocalizer {
602 get {return localizer;}
605 protected override string GetKeyForItem (Option item)
607 if (item == null)
608 throw new ArgumentNullException ("option");
609 if (item.Names != null && item.Names.Length > 0)
610 return item.Names [0];
611 // This should never happen, as it's invalid for Option to be
612 // constructed w/o any names.
613 throw new InvalidOperationException ("Option has no names!");
616 [Obsolete ("Use KeyedCollection.this[string]")]
617 protected Option GetOptionForName (string option)
619 if (option == null)
620 throw new ArgumentNullException ("option");
621 try {
622 return base [option];
624 catch (KeyNotFoundException) {
625 return null;
629 protected override void InsertItem (int index, Option item)
631 base.InsertItem (index, item);
632 AddImpl (item);
635 protected override void RemoveItem (int index)
637 base.RemoveItem (index);
638 Option p = Items [index];
639 // KeyedCollection.RemoveItem() handles the 0th item
640 for (int i = 1; i < p.Names.Length; ++i) {
641 Dictionary.Remove (p.Names [i]);
645 protected override void SetItem (int index, Option item)
647 base.SetItem (index, item);
648 RemoveItem (index);
649 AddImpl (item);
652 private void AddImpl (Option option)
654 if (option == null)
655 throw new ArgumentNullException ("option");
656 List<string> added = new List<string> (option.Names.Length);
657 try {
658 // KeyedCollection.InsertItem/SetItem handle the 0th name.
659 for (int i = 1; i < option.Names.Length; ++i) {
660 Dictionary.Add (option.Names [i], option);
661 added.Add (option.Names [i]);
664 catch (Exception) {
665 foreach (string name in added)
666 Dictionary.Remove (name);
667 throw;
671 public new OptionSet Add (Option option)
673 base.Add (option);
674 return this;
677 sealed class ActionOption : Option {
678 Action<OptionValueCollection> action;
680 public ActionOption (string prototype, string description, int count, Action<OptionValueCollection> action)
681 : base (prototype, description, count)
683 if (action == null)
684 throw new ArgumentNullException ("action");
685 this.action = action;
688 protected override void OnParseComplete (OptionContext c)
690 action (c.OptionValues);
694 public OptionSet Add (string prototype, Action<string> action)
696 return Add (prototype, null, action);
699 public OptionSet Add (string prototype, string description, Action<string> action)
701 if (action == null)
702 throw new ArgumentNullException ("action");
703 Option p = new ActionOption (prototype, description, 1,
704 delegate (OptionValueCollection v) { action (v [0]); });
705 base.Add (p);
706 return this;
709 public OptionSet Add (string prototype, OptionAction<string, string> action)
711 return Add (prototype, null, action);
714 public OptionSet Add (string prototype, string description, OptionAction<string, string> action)
716 if (action == null)
717 throw new ArgumentNullException ("action");
718 Option p = new ActionOption (prototype, description, 2,
719 delegate (OptionValueCollection v) {action (v [0], v [1]);});
720 base.Add (p);
721 return this;
724 sealed class ActionOption<T> : Option {
725 Action<T> action;
727 public ActionOption (string prototype, string description, Action<T> action)
728 : base (prototype, description, 1)
730 if (action == null)
731 throw new ArgumentNullException ("action");
732 this.action = action;
735 protected override void OnParseComplete (OptionContext c)
737 action (Parse<T> (c.OptionValues [0], c));
741 sealed class ActionOption<TKey, TValue> : Option {
742 OptionAction<TKey, TValue> action;
744 public ActionOption (string prototype, string description, OptionAction<TKey, TValue> action)
745 : base (prototype, description, 2)
747 if (action == null)
748 throw new ArgumentNullException ("action");
749 this.action = action;
752 protected override void OnParseComplete (OptionContext c)
754 action (
755 Parse<TKey> (c.OptionValues [0], c),
756 Parse<TValue> (c.OptionValues [1], c));
760 public OptionSet Add<T> (string prototype, Action<T> action)
762 return Add (prototype, null, action);
765 public OptionSet Add<T> (string prototype, string description, Action<T> action)
767 return Add (new ActionOption<T> (prototype, description, action));
770 public OptionSet Add<TKey, TValue> (string prototype, OptionAction<TKey, TValue> action)
772 return Add (prototype, null, action);
775 public OptionSet Add<TKey, TValue> (string prototype, string description, OptionAction<TKey, TValue> action)
777 return Add (new ActionOption<TKey, TValue> (prototype, description, action));
780 protected virtual OptionContext CreateOptionContext ()
782 return new OptionContext (this);
785 #if LINQ
786 public List<string> Parse (IEnumerable<string> arguments)
788 bool process = true;
789 OptionContext c = CreateOptionContext ();
790 c.OptionIndex = -1;
791 var def = GetOptionForName ("<>");
792 var unprocessed =
793 from argument in arguments
794 where ++c.OptionIndex >= 0 && (process || def != null)
795 ? process
796 ? argument == "--"
797 ? (process = false)
798 : !Parse (argument, c)
799 ? def != null
800 ? Unprocessed (null, def, c, argument)
801 : true
802 : false
803 : def != null
804 ? Unprocessed (null, def, c, argument)
805 : true
806 : true
807 select argument;
808 List<string> r = unprocessed.ToList ();
809 if (c.Option != null)
810 c.Option.Invoke (c);
811 return r;
813 #else
814 public List<string> Parse (IEnumerable<string> arguments)
816 OptionContext c = CreateOptionContext ();
817 c.OptionIndex = -1;
818 bool process = true;
819 List<string> unprocessed = new List<string> ();
820 Option def = Contains ("<>") ? this ["<>"] : null;
821 foreach (string argument in arguments) {
822 ++c.OptionIndex;
823 if (argument == "--") {
824 process = false;
825 continue;
827 if (!process) {
828 Unprocessed (unprocessed, def, c, argument);
829 continue;
831 if (!Parse (argument, c))
832 Unprocessed (unprocessed, def, c, argument);
834 if (c.Option != null)
835 c.Option.Invoke (c);
836 return unprocessed;
838 #endif
840 private static bool Unprocessed (ICollection<string> extra, Option def, OptionContext c, string argument)
842 if (def == null) {
843 extra.Add (argument);
844 return false;
846 c.OptionValues.Add (argument);
847 c.Option = def;
848 c.Option.Invoke (c);
849 return false;
852 private readonly Regex ValueOption = new Regex (
853 @"^(?<flag>--|-|/)(?<name>[^:=]+)((?<sep>[:=])(?<value>.*))?$");
855 protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value)
857 if (argument == null)
858 throw new ArgumentNullException ("argument");
860 flag = name = sep = value = null;
861 Match m = ValueOption.Match (argument);
862 if (!m.Success) {
863 return false;
865 flag = m.Groups ["flag"].Value;
866 name = m.Groups ["name"].Value;
867 if (m.Groups ["sep"].Success && m.Groups ["value"].Success) {
868 sep = m.Groups ["sep"].Value;
869 value = m.Groups ["value"].Value;
871 return true;
874 protected virtual bool Parse (string argument, OptionContext c)
876 if (c.Option != null) {
877 ParseValue (argument, c);
878 return true;
881 string f, n, s, v;
882 if (!GetOptionParts (argument, out f, out n, out s, out v))
883 return false;
885 Option p;
886 if (Contains (n)) {
887 p = this [n];
888 c.OptionName = f + n;
889 c.Option = p;
890 switch (p.OptionValueType) {
891 case OptionValueType.None:
892 c.OptionValues.Add (n);
893 c.Option.Invoke (c);
894 break;
895 case OptionValueType.Optional:
896 case OptionValueType.Required:
897 ParseValue (v, c);
898 break;
900 return true;
902 // no match; is it a bool option?
903 if (ParseBool (argument, n, c))
904 return true;
905 // is it a bundled option?
906 if (ParseBundledValue (f, string.Concat (n + s + v), c))
907 return true;
909 return false;
912 private void ParseValue (string option, OptionContext c)
914 if (option != null)
915 foreach (string o in c.Option.ValueSeparators != null
916 ? option.Split (c.Option.ValueSeparators, StringSplitOptions.None)
917 : new string[]{option}) {
918 c.OptionValues.Add (o);
920 if (c.OptionValues.Count == c.Option.MaxValueCount ||
921 c.Option.OptionValueType == OptionValueType.Optional)
922 c.Option.Invoke (c);
923 else if (c.OptionValues.Count > c.Option.MaxValueCount) {
924 throw new OptionException (localizer (string.Format (
925 "Error: Found {0} option values when expecting {1}.",
926 c.OptionValues.Count, c.Option.MaxValueCount)),
927 c.OptionName);
931 private bool ParseBool (string option, string n, OptionContext c)
933 Option p;
934 string rn;
935 if (n.Length >= 1 && (n [n.Length-1] == '+' || n [n.Length-1] == '-') &&
936 Contains ((rn = n.Substring (0, n.Length-1)))) {
937 p = this [rn];
938 string v = n [n.Length-1] == '+' ? option : null;
939 c.OptionName = option;
940 c.Option = p;
941 c.OptionValues.Add (v);
942 p.Invoke (c);
943 return true;
945 return false;
948 private bool ParseBundledValue (string f, string n, OptionContext c)
950 if (f != "-")
951 return false;
952 for (int i = 0; i < n.Length; ++i) {
953 Option p;
954 string opt = f + n [i].ToString ();
955 string rn = n [i].ToString ();
956 if (!Contains (rn)) {
957 if (i == 0)
958 return false;
959 throw new OptionException (string.Format (localizer (
960 "Cannot bundle unregistered option '{0}'."), opt), opt);
962 p = this [rn];
963 switch (p.OptionValueType) {
964 case OptionValueType.None:
965 Invoke (c, opt, n, p);
966 break;
967 case OptionValueType.Optional:
968 case OptionValueType.Required: {
969 string v = n.Substring (i+1);
970 c.Option = p;
971 c.OptionName = opt;
972 ParseValue (v.Length != 0 ? v : null, c);
973 return true;
975 default:
976 throw new InvalidOperationException ("Unknown OptionValueType: " + p.OptionValueType);
979 return true;
982 private static void Invoke (OptionContext c, string name, string value, Option option)
984 c.OptionName = name;
985 c.Option = option;
986 c.OptionValues.Add (value);
987 option.Invoke (c);
990 private const int OptionWidth = 29;
992 public void WriteOptionDescriptions (TextWriter o)
994 foreach (Option p in this) {
995 int written = 0;
996 if (!WriteOptionPrototype (o, p, ref written))
997 continue;
999 if (written < OptionWidth)
1000 o.Write (new string (' ', OptionWidth - written));
1001 else {
1002 o.WriteLine ();
1003 o.Write (new string (' ', OptionWidth));
1006 bool indent = false;
1007 string prefix = new string (' ', OptionWidth+2);
1008 foreach (string line in GetLines (localizer (GetDescription (p.Description)))) {
1009 if (indent)
1010 o.Write (prefix);
1011 o.WriteLine (line);
1012 indent = true;
1017 bool WriteOptionPrototype (TextWriter o, Option p, ref int written)
1019 string[] names = p.Names;
1021 int i = GetNextOptionIndex (names, 0);
1022 if (i == names.Length)
1023 return false;
1025 if (names [i].Length == 1) {
1026 Write (o, ref written, " -");
1027 Write (o, ref written, names [0]);
1029 else {
1030 Write (o, ref written, " --");
1031 Write (o, ref written, names [0]);
1034 for ( i = GetNextOptionIndex (names, i+1);
1035 i < names.Length; i = GetNextOptionIndex (names, i+1)) {
1036 Write (o, ref written, ", ");
1037 Write (o, ref written, names [i].Length == 1 ? "-" : "--");
1038 Write (o, ref written, names [i]);
1041 if (p.OptionValueType == OptionValueType.Optional ||
1042 p.OptionValueType == OptionValueType.Required) {
1043 if (p.OptionValueType == OptionValueType.Optional) {
1044 Write (o, ref written, localizer ("["));
1046 Write (o, ref written, localizer ("=" + GetArgumentName (0, p.MaxValueCount, p.Description)));
1047 string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0
1048 ? p.ValueSeparators [0]
1049 : " ";
1050 for (int c = 1; c < p.MaxValueCount; ++c) {
1051 Write (o, ref written, localizer (sep + GetArgumentName (c, p.MaxValueCount, p.Description)));
1053 if (p.OptionValueType == OptionValueType.Optional) {
1054 Write (o, ref written, localizer ("]"));
1057 return true;
1060 static int GetNextOptionIndex (string[] names, int i)
1062 while (i < names.Length && names [i] == "<>") {
1063 ++i;
1065 return i;
1068 static void Write (TextWriter o, ref int n, string s)
1070 n += s.Length;
1071 o.Write (s);
1074 private static string GetArgumentName (int index, int maxIndex, string description)
1076 if (description == null)
1077 return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1078 string[] nameStart;
1079 if (maxIndex == 1)
1080 nameStart = new string[]{"{0:", "{"};
1081 else
1082 nameStart = new string[]{"{" + index + ":"};
1083 for (int i = 0; i < nameStart.Length; ++i) {
1084 int start, j = 0;
1085 do {
1086 start = description.IndexOf (nameStart [i], j);
1087 } while (start >= 0 && j != 0 ? description [j++ - 1] == '{' : false);
1088 if (start == -1)
1089 continue;
1090 int end = description.IndexOf ("}", start);
1091 if (end == -1)
1092 continue;
1093 return description.Substring (start + nameStart [i].Length, end - start - nameStart [i].Length);
1095 return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1098 private static string GetDescription (string description)
1100 if (description == null)
1101 return string.Empty;
1102 StringBuilder sb = new StringBuilder (description.Length);
1103 int start = -1;
1104 for (int i = 0; i < description.Length; ++i) {
1105 switch (description [i]) {
1106 case '{':
1107 if (i == start) {
1108 sb.Append ('{');
1109 start = -1;
1111 else if (start < 0)
1112 start = i + 1;
1113 break;
1114 case '}':
1115 if (start < 0) {
1116 if ((i+1) == description.Length || description [i+1] != '}')
1117 throw new InvalidOperationException ("Invalid option description: " + description);
1118 ++i;
1119 sb.Append ("}");
1121 else {
1122 sb.Append (description.Substring (start, i - start));
1123 start = -1;
1125 break;
1126 case ':':
1127 if (start < 0)
1128 goto default;
1129 start = i + 1;
1130 break;
1131 default:
1132 if (start < 0)
1133 sb.Append (description [i]);
1134 break;
1137 return sb.ToString ();
1140 private static IEnumerable<string> GetLines (string description)
1142 return StringCoda.WrappedLines (description,
1143 80 - OptionWidth,
1144 80 - OptionWidth - 2);