//
// Authors:
// Jonathan Pryor <jpryor@novell.com>
+// Federico Di Gregorio <fog@initd.org>
+// Rolf Bjarne Kvinge <rolf@xamarin.com>
//
// Copyright (C) 2008 Novell (http://www.novell.com)
+// Copyright (C) 2009 Federico Di Gregorio.
+// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
//
// Compile With:
-// gmcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll
-// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll
+// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll
+// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll
//
// The LINQ version just changes the implementation of
// OptionSet.Parse(IEnumerable<string>), and confers no semantic changes.
//
// A Getopt::Long-inspired option parsing library for C#.
//
-// Mono.Options.OptionSet is built upon a key/value table, where the
+// NDesk.Options.OptionSet is built upon a key/value table, where the
// key is a option format string and the value is a delegate that is
// invoked when the format string is matched.
//
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
+#if PCL
+using System.Reflection;
+#else
using System.Security.Permissions;
+#endif
using System.Text;
using System.Text.RegularExpressions;
#endif
#if TEST
-using Mono.Options;
+using NDesk.Options;
#endif
-namespace Mono.Options {
+#if PCL
+using MessageLocalizerConverter = System.Func<string, string>;
+#else
+using MessageLocalizerConverter = System.Converter<string, string>;
+#endif
+
+#if NDESK_OPTIONS
+namespace NDesk.Options
+#else
+namespace Mono.Options
+#endif
+{
+ static class StringCoda {
+
+ public static IEnumerable<string> WrappedLines (string self, params int[] widths)
+ {
+ IEnumerable<int> w = widths;
+ return WrappedLines (self, w);
+ }
+
+ public static IEnumerable<string> WrappedLines (string self, IEnumerable<int> widths)
+ {
+ if (widths == null)
+ throw new ArgumentNullException ("widths");
+ return CreateWrappedLinesIterator (self, widths);
+ }
+
+ private static IEnumerable<string> CreateWrappedLinesIterator (string self, IEnumerable<int> widths)
+ {
+ if (string.IsNullOrEmpty (self)) {
+ yield return string.Empty;
+ yield break;
+ }
+ using (IEnumerator<int> ewidths = widths.GetEnumerator ()) {
+ bool? hw = null;
+ int width = GetNextWidth (ewidths, int.MaxValue, ref hw);
+ int start = 0, end;
+ do {
+ end = GetLineEnd (start, width, self);
+ char c = self [end-1];
+ if (char.IsWhiteSpace (c))
+ --end;
+ bool needContinuation = end != self.Length && !IsEolChar (c);
+ string continuation = "";
+ if (needContinuation) {
+ --end;
+ continuation = "-";
+ }
+ string line = self.Substring (start, end - start) + continuation;
+ yield return line;
+ start = end;
+ if (char.IsWhiteSpace (c))
+ ++start;
+ width = GetNextWidth (ewidths, width, ref hw);
+ } while (start < self.Length);
+ }
+ }
+
+ private static int GetNextWidth (IEnumerator<int> ewidths, int curWidth, ref bool? eValid)
+ {
+ if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) {
+ curWidth = (eValid = ewidths.MoveNext ()).Value ? ewidths.Current : curWidth;
+ // '.' is any character, - is for a continuation
+ const string minWidth = ".-";
+ if (curWidth < minWidth.Length)
+ throw new ArgumentOutOfRangeException ("widths",
+ string.Format ("Element must be >= {0}, was {1}.", minWidth.Length, curWidth));
+ return curWidth;
+ }
+ // no more elements, use the last element.
+ return curWidth;
+ }
+
+ private static bool IsEolChar (char c)
+ {
+ return !char.IsLetterOrDigit (c);
+ }
+
+ private static int GetLineEnd (int start, int length, string description)
+ {
+ int end = System.Math.Min (start + length, description.Length);
+ int sep = -1;
+ for (int i = start; i < end; ++i) {
+ if (description [i] == '\n')
+ return i+1;
+ if (IsEolChar (description [i]))
+ sep = i+1;
+ }
+ if (sep == -1 || end == description.Length)
+ return end;
+ return sep;
+ }
+ }
public class OptionValueCollection : IList, IList<string> {
OptionValueType type;
int count;
string[] separators;
+ bool hidden;
protected Option (string prototype, string description)
- : this (prototype, description, 1)
+ : this (prototype, description, 1, false)
{
}
protected Option (string prototype, string description, int maxValueCount)
+ : this (prototype, description, maxValueCount, false)
+ {
+ }
+
+ protected Option (string prototype, string description, int maxValueCount, bool hidden)
{
if (prototype == null)
throw new ArgumentNullException ("prototype");
throw new ArgumentOutOfRangeException ("maxValueCount");
this.prototype = prototype;
- this.names = prototype.Split ('|');
this.description = description;
this.count = maxValueCount;
+ this.names = (this is OptionSet.Category)
+ // append GetHashCode() so that "duplicate" categories have distinct
+ // names, e.g. adding multiple "" categories should be valid.
+ ? new[]{prototype + this.GetHashCode ()}
+ : prototype.Split ('|');
+
+ if (this is OptionSet.Category)
+ return;
+
this.type = ParsePrototype ();
+ this.hidden = hidden;
if (this.count == 0 && type != OptionValueType.None)
throw new ArgumentException (
public string Description {get {return description;}}
public OptionValueType OptionValueType {get {return type;}}
public int MaxValueCount {get {return count;}}
+ public bool Hidden {get {return hidden;}}
public string[] GetNames ()
{
protected static T Parse<T> (string value, OptionContext c)
{
- TypeConverter conv = TypeDescriptor.GetConverter (typeof (T));
+ Type tt = typeof (T);
+#if PCL
+ TypeInfo ti = tt.GetTypeInfo ();
+#else
+ Type ti = tt;
+#endif
+ bool nullable =
+ ti.IsValueType &&
+ ti.IsGenericType &&
+ !ti.IsGenericTypeDefinition &&
+ ti.GetGenericTypeDefinition () == typeof (Nullable<>);
+#if PCL
+ Type targetType = nullable ? tt.GenericTypeArguments [0] : tt;
+#else
+ Type targetType = nullable ? tt.GetGenericArguments () [0] : tt;
+#endif
T t = default (T);
try {
- if (value != null)
+ if (value != null) {
+#if PCL
+ t = (T) Convert.ChangeType (value, targetType);
+#else
+ TypeConverter conv = TypeDescriptor.GetConverter (targetType);
t = (T) conv.ConvertFromString (value);
+#endif
+ }
}
catch (Exception e) {
throw new OptionException (
string.Format (
c.OptionSet.MessageLocalizer ("Could not convert string `{0}' to type {1} for option `{2}'."),
- value, typeof (T).Name, c.OptionName),
+ value, targetType.Name, c.OptionName),
c.OptionName, e);
}
return t;
}
}
+ public abstract class ArgumentSource {
+
+ protected ArgumentSource ()
+ {
+ }
+
+ public abstract string[] GetNames ();
+ public abstract string Description { get; }
+ public abstract bool GetArguments (string value, out IEnumerable<string> replacement);
+
+#if !PCL
+ public static IEnumerable<string> GetArgumentsFromFile (string file)
+ {
+ return GetArguments (File.OpenText (file), true);
+ }
+#endif
+
+ public static IEnumerable<string> GetArguments (TextReader reader)
+ {
+ return GetArguments (reader, false);
+ }
+
+ // Cribbed from mcs/driver.cs:LoadArgs(string)
+ static IEnumerable<string> GetArguments (TextReader reader, bool close)
+ {
+ try {
+ StringBuilder arg = new StringBuilder ();
+
+ string line;
+ while ((line = reader.ReadLine ()) != null) {
+ int t = line.Length;
+
+ for (int i = 0; i < t; i++) {
+ char c = line [i];
+
+ if (c == '"' || c == '\'') {
+ char end = c;
+
+ for (i++; i < t; i++){
+ c = line [i];
+
+ if (c == end)
+ break;
+ arg.Append (c);
+ }
+ } else if (c == ' ') {
+ if (arg.Length > 0) {
+ yield return arg.ToString ();
+ arg.Length = 0;
+ }
+ } else
+ arg.Append (c);
+ }
+ if (arg.Length > 0) {
+ yield return arg.ToString ();
+ arg.Length = 0;
+ }
+ }
+ }
+ finally {
+ if (close)
+ reader.Dispose ();
+ }
+ }
+ }
+
+#if !PCL
+ public class ResponseFileSource : ArgumentSource {
+
+ public override string[] GetNames ()
+ {
+ return new string[]{"@file"};
+ }
+
+ public override string Description {
+ get {return "Read response file for more options.";}
+ }
+
+ public override bool GetArguments (string value, out IEnumerable<string> replacement)
+ {
+ if (string.IsNullOrEmpty (value) || !value.StartsWith ("@")) {
+ replacement = null;
+ return false;
+ }
+ replacement = ArgumentSource.GetArgumentsFromFile (value.Substring (1));
+ return true;
+ }
+ }
+#endif
+
+#if !PCL
[Serializable]
+#endif
public class OptionException : Exception {
private string option;
this.option = optionName;
}
+#if !PCL
protected OptionException (SerializationInfo info, StreamingContext context)
: base (info, context)
{
this.option = info.GetString ("OptionName");
}
+#endif
public string OptionName {
get {return this.option;}
}
+#if !PCL
+#pragma warning disable 618 // SecurityPermissionAttribute is obsolete
[SecurityPermission (SecurityAction.LinkDemand, SerializationFormatter = true)]
+#pragma warning restore 618
public override void GetObjectData (SerializationInfo info, StreamingContext context)
{
base.GetObjectData (info, context);
info.AddValue ("OptionName", option);
}
+#endif
}
public delegate void OptionAction<TKey, TValue> (TKey key, TValue value);
{
}
- public OptionSet (Converter<string, string> localizer)
+ public OptionSet (MessageLocalizerConverter localizer)
{
this.localizer = localizer;
+ this.roSources = new ReadOnlyCollection<ArgumentSource>(sources);
}
- Converter<string, string> localizer;
+ MessageLocalizerConverter localizer;
- public Converter<string, string> MessageLocalizer {
+ public MessageLocalizerConverter MessageLocalizer {
get {return localizer;}
}
+ List<ArgumentSource> sources = new List<ArgumentSource> ();
+ ReadOnlyCollection<ArgumentSource> roSources;
+
+ public ReadOnlyCollection<ArgumentSource> ArgumentSources {
+ get {return roSources;}
+ }
+
+
protected override string GetKeyForItem (Option item)
{
if (item == null)
protected override void RemoveItem (int index)
{
- base.RemoveItem (index);
Option p = Items [index];
+ base.RemoveItem (index);
// KeyedCollection.RemoveItem() handles the 0th item
for (int i = 1; i < p.Names.Length; ++i) {
Dictionary.Remove (p.Names [i]);
protected override void SetItem (int index, Option item)
{
base.SetItem (index, item);
- RemoveItem (index);
AddImpl (item);
}
}
}
+ public OptionSet Add (string header)
+ {
+ if (header == null)
+ throw new ArgumentNullException ("header");
+ Add (new Category (header));
+ return this;
+ }
+
+ internal sealed class Category : Option {
+
+ // Prototype starts with '=' because this is an invalid prototype
+ // (see Option.ParsePrototype(), and thus it'll prevent Category
+ // instances from being accidentally used as normal options.
+ public Category (string description)
+ : base ("=:Category:= " + description, description)
+ {
+ }
+
+ protected override void OnParseComplete (OptionContext c)
+ {
+ throw new NotSupportedException ("Category.OnParseComplete should not be invoked.");
+ }
+ }
+
+
public new OptionSet Add (Option option)
{
base.Add (option);
Action<OptionValueCollection> action;
public ActionOption (string prototype, string description, int count, Action<OptionValueCollection> action)
- : base (prototype, description, count)
+ : this (prototype, description, count, action, false)
+ {
+ }
+
+ public ActionOption (string prototype, string description, int count, Action<OptionValueCollection> action, bool hidden)
+ : base (prototype, description, count, hidden)
{
if (action == null)
throw new ArgumentNullException ("action");
}
public OptionSet Add (string prototype, string description, Action<string> action)
+ {
+ return Add (prototype, description, action, false);
+ }
+
+ public OptionSet Add (string prototype, string description, Action<string> action, bool hidden)
{
if (action == null)
throw new ArgumentNullException ("action");
Option p = new ActionOption (prototype, description, 1,
- delegate (OptionValueCollection v) { action (v [0]); });
+ delegate (OptionValueCollection v) { action (v [0]); }, hidden);
base.Add (p);
return this;
}
public OptionSet Add (string prototype, string description, OptionAction<string, string> action)
{
+ return Add (prototype, description, action, false);
+ }
+
+ public OptionSet Add (string prototype, string description, OptionAction<string, string> action, bool hidden) {
if (action == null)
throw new ArgumentNullException ("action");
Option p = new ActionOption (prototype, description, 2,
- delegate (OptionValueCollection v) {action (v [0], v [1]);});
+ delegate (OptionValueCollection v) {action (v [0], v [1]);}, hidden);
base.Add (p);
return this;
}
return Add (new ActionOption<TKey, TValue> (prototype, description, action));
}
- protected virtual OptionContext CreateOptionContext ()
+ public OptionSet Add (ArgumentSource source)
{
- return new OptionContext (this);
+ if (source == null)
+ throw new ArgumentNullException ("source");
+ sources.Add (source);
+ return this;
}
-#if LINQ
- public List<string> Parse (IEnumerable<string> arguments)
+ protected virtual OptionContext CreateOptionContext ()
{
- bool process = true;
- OptionContext c = CreateOptionContext ();
- c.OptionIndex = -1;
- var def = GetOptionForName ("<>");
- var unprocessed =
- from argument in arguments
- where ++c.OptionIndex >= 0 && (process || def != null)
- ? process
- ? argument == "--"
- ? (process = false)
- : !Parse (argument, c)
- ? def != null
- ? Unprocessed (null, def, c, argument)
- : true
- : false
- : def != null
- ? Unprocessed (null, def, c, argument)
- : true
- : true
- select argument;
- List<string> r = unprocessed.ToList ();
- if (c.Option != null)
- c.Option.Invoke (c);
- return r;
+ return new OptionContext (this);
}
-#else
+
public List<string> Parse (IEnumerable<string> arguments)
{
+ if (arguments == null)
+ throw new ArgumentNullException ("arguments");
OptionContext c = CreateOptionContext ();
c.OptionIndex = -1;
bool process = true;
List<string> unprocessed = new List<string> ();
Option def = Contains ("<>") ? this ["<>"] : null;
- foreach (string argument in arguments) {
+ ArgumentEnumerator ae = new ArgumentEnumerator (arguments);
+ foreach (string argument in ae) {
++c.OptionIndex;
if (argument == "--") {
process = false;
Unprocessed (unprocessed, def, c, argument);
continue;
}
+ if (AddSource (ae, argument))
+ continue;
if (!Parse (argument, c))
Unprocessed (unprocessed, def, c, argument);
}
c.Option.Invoke (c);
return unprocessed;
}
-#endif
+
+ class ArgumentEnumerator : IEnumerable<string> {
+ List<IEnumerator<string>> sources = new List<IEnumerator<string>> ();
+
+ public ArgumentEnumerator (IEnumerable<string> arguments)
+ {
+ sources.Add (arguments.GetEnumerator ());
+ }
+
+ public void Add (IEnumerable<string> arguments)
+ {
+ sources.Add (arguments.GetEnumerator ());
+ }
+
+ public IEnumerator<string> GetEnumerator ()
+ {
+ do {
+ IEnumerator<string> c = sources [sources.Count-1];
+ if (c.MoveNext ())
+ yield return c.Current;
+ else {
+ c.Dispose ();
+ sources.RemoveAt (sources.Count-1);
+ }
+ } while (sources.Count > 0);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator ()
+ {
+ return GetEnumerator ();
+ }
+ }
+
+ bool AddSource (ArgumentEnumerator ae, string argument)
+ {
+ foreach (ArgumentSource source in sources) {
+ IEnumerable<string> replacement;
+ if (!source.GetArguments (argument, out replacement))
+ continue;
+ ae.Add (replacement);
+ return true;
+ }
+ return false;
+ }
private static bool Unprocessed (ICollection<string> extra, Option def, OptionContext c, string argument)
{
{
if (option != null)
foreach (string o in c.Option.ValueSeparators != null
- ? option.Split (c.Option.ValueSeparators, StringSplitOptions.None)
+ ? option.Split (c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None)
: new string[]{option}) {
c.OptionValues.Add (o);
}
if (i == 0)
return false;
throw new OptionException (string.Format (localizer (
- "Cannot bundle unregistered option '{0}'."), opt), opt);
+ "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null);
}
p = this [rn];
switch (p.OptionValueType) {
}
private const int OptionWidth = 29;
+ private const int Description_FirstWidth = 80 - OptionWidth;
+ private const int Description_RemWidth = 80 - OptionWidth - 2;
public void WriteOptionDescriptions (TextWriter o)
{
foreach (Option p in this) {
int written = 0;
+
+ if (p.Hidden)
+ continue;
+
+ Category c = p as Category;
+ if (c != null) {
+ WriteDescription (o, p.Description, "", 80, 80);
+ continue;
+ }
+
if (!WriteOptionPrototype (o, p, ref written))
continue;
o.Write (new string (' ', OptionWidth));
}
- List<string> lines = GetLines (localizer (GetDescription (p.Description)));
- o.WriteLine (lines [0]);
- string prefix = new string (' ', OptionWidth+2);
- for (int i = 1; i < lines.Count; ++i) {
- o.Write (prefix);
- o.WriteLine (lines [i]);
+ WriteDescription (o, p.Description, new string (' ', OptionWidth+2),
+ Description_FirstWidth, Description_RemWidth);
+ }
+
+ foreach (ArgumentSource s in sources) {
+ string[] names = s.GetNames ();
+ if (names == null || names.Length == 0)
+ continue;
+
+ int written = 0;
+
+ Write (o, ref written, " ");
+ Write (o, ref written, names [0]);
+ for (int i = 1; i < names.Length; ++i) {
+ Write (o, ref written, ", ");
+ Write (o, ref written, names [i]);
+ }
+
+ if (written < OptionWidth)
+ o.Write (new string (' ', OptionWidth - written));
+ else {
+ o.WriteLine ();
+ o.Write (new string (' ', OptionWidth));
}
+
+ WriteDescription (o, s.Description, new string (' ', OptionWidth+2),
+ Description_FirstWidth, Description_RemWidth);
+ }
+ }
+
+ void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth)
+ {
+ bool indent = false;
+ foreach (string line in GetLines (localizer (GetDescription (value)), firstWidth, remWidth)) {
+ if (indent)
+ o.Write (prefix);
+ o.WriteLine (line);
+ indent = true;
}
}
return sb.ToString ();
}
- private static List<string> GetLines (string description)
+ private static IEnumerable<string> GetLines (string description, int firstWidth, int remWidth)
{
- List<string> lines = new List<string> ();
- if (string.IsNullOrEmpty (description)) {
- lines.Add (string.Empty);
- return lines;
- }
- int length = 80 - OptionWidth - 2;
- int start = 0, end;
- do {
- end = GetLineEnd (start, length, description);
- bool cont = false;
- if (end < description.Length) {
- char c = description [end];
- if (c == '-' || (char.IsWhiteSpace (c) && c != '\n'))
- ++end;
- else if (c != '\n') {
- cont = true;
- --end;
- }
- }
- lines.Add (description.Substring (start, end - start));
- if (cont) {
- lines [lines.Count-1] += "-";
- }
- start = end;
- if (start < description.Length && description [start] == '\n')
- ++start;
- } while (end < description.Length);
- return lines;
- }
-
- private static int GetLineEnd (int start, int length, string description)
- {
- int end = System.Math.Min (start + length, description.Length);
- int sep = -1;
- for (int i = start; i < end; ++i) {
- switch (description [i]) {
- case ' ':
- case '\t':
- case '\v':
- case '-':
- case ',':
- case '.':
- case ';':
- sep = i;
- break;
- case '\n':
- return i;
- }
- }
- if (sep == -1 || end == description.Length)
- return end;
- return sep;
+ return StringCoda.WrappedLines (description, firstWidth, remWidth);
}
}
}