// NAnt - A .NET build tool // Copyright (C) 2001 Gerry Shaw // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // Gert Driesen (gert.driesen@ardatis.com) using System; using System.Collections; using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Security.Permissions; using System.Text.RegularExpressions; namespace NAnt.Core.Util { /// /// Represents a valid command-line argument. /// public class CommandLineArgument { #region Public Instance Constructors public CommandLineArgument(CommandLineArgumentAttribute attribute, PropertyInfo propertyInfo) { _attribute = attribute; _propertyInfo = propertyInfo; _seenValue = false; _elementType = GetElementType(propertyInfo); _argumentType = GetArgumentType(attribute, propertyInfo); if (IsCollection || IsArray) { _collectionValues = new ArrayList(); } else if (IsNameValueCollection) { _valuePairs = new NameValueCollection(); } Debug.Assert(LongName != null && LongName.Length > 0); Debug.Assert((!IsCollection && !IsArray && !IsNameValueCollection) || AllowMultiple, "Collection and array arguments must have allow multiple"); Debug.Assert(!Unique || (IsCollection || IsArray || IsNameValueCollection), "Unique only applicable to collection arguments"); } #endregion Public Instance Constructors #region Public Instance Properties /// /// Gets the property that backs the argument. /// /// /// The property that backs the arguments. /// public PropertyInfo Property { get { return _propertyInfo; } } /// /// Gets the underlying of the argument. /// /// /// The underlying of the argument. /// /// /// If the of the argument is a collection type, /// this property will returns the underlying type of that collection. /// public Type ValueType { get { return IsCollection || IsArray ? _elementType : Type; } } /// /// Gets the long name of the argument. /// /// The long name of the argument. public string LongName { get { if (_attribute != null && _attribute.Name != null) { return _attribute.Name; } else { return _propertyInfo.Name; } } } /// /// Gets the short name of the argument. /// /// The short name of the argument. public string ShortName { get { if (_attribute != null) { return _attribute.ShortName; } else { return null; } } } /// /// Gets the description of the argument. /// /// The description of the argument. public string Description { get { if (_attribute != null) { return _attribute.Description; } else { return null; } } } /// /// Gets a value indicating whether the argument is required. /// /// /// if the argument is required; otherwise, /// . /// public bool IsRequired { get { return 0 != (_argumentType & CommandLineArgumentTypes.Required); } } /// /// Gets a value indicating whether a mathing command-line argument /// was already found. /// /// /// if a matching command-line argument was /// already found; otherwise, . /// public bool SeenValue { get { return _seenValue; } } /// /// Gets a value indicating whether the argument can be specified multiple /// times. /// /// /// if the argument may be specified multiple /// times; otherwise, . /// public bool AllowMultiple { get { return (IsCollection || IsArray || IsNameValueCollection) && (0 != (_argumentType & CommandLineArgumentTypes.Multiple)); } } /// /// Gets a value indicating whether the argument can only be specified once /// with a certain value. /// /// /// if the argument should always have a unique /// value; otherwise, . /// public bool Unique { get { return 0 != (_argumentType & CommandLineArgumentTypes.Unique); } } /// /// Gets the of the property to which the argument /// is applied. /// /// /// The of the property to which the argument is /// applied. /// public Type Type { get { return _propertyInfo.PropertyType; } } /// /// Gets a value indicating whether the argument is collection-based. /// /// /// if the argument is backed by a /// that can be assigned to and is not backed /// by a that can be assigned to /// ; otherwise, . /// public bool IsCollection { get { return IsCollectionType(Type); } } /// /// Gets a value indicating whether the argument is a set of name/value /// pairs. /// /// /// if the argument is backed by a /// that can be assigned to ; otherwise, /// . /// public bool IsNameValueCollection { get { return IsNameValueCollectionType(Type); } } /// /// Gets a value indicating whether the argument is array-based. /// /// /// if the argument is backed by an array; /// otherwise, . /// public bool IsArray { get { return IsArrayType(Type); } } /// /// Gets a value indicating whether the argument is the default argument. /// /// /// if the argument is the default argument; /// otherwise, . /// public bool IsDefault { get { return (_attribute != null && _attribute is DefaultCommandLineArgumentAttribute); } } /// /// Gets a value indicating whether the argument cannot be combined with /// other arguments. /// /// /// if the argument cannot be combined with other /// arguments; otherwise, . /// public bool IsExclusive { get { return 0 != (_argumentType & CommandLineArgumentTypes.Exclusive); } } #endregion Public Instance Properties #region Public Instance Methods /// /// Sets the value of the argument on the specified object. /// /// The object on which the value of the argument should be set. /// The argument is required and no value was specified. /// /// /// The matching property is collection-based, but is not initialized /// and cannot be written to. /// /// -or- /// /// The matching property is collection-based, but has no strongly-typed /// Add method. /// /// -or- /// /// The matching property is collection-based, but the signature of the /// Add method is not supported. /// /// [ReflectionPermission(SecurityAction.Demand, Flags=ReflectionPermissionFlag.NoFlags)] public void Finish(object destination) { if (IsRequired && !SeenValue) { throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, "Missing required argument '-{0}'.", LongName)); } if (IsArray) { _propertyInfo.SetValue(destination, _collectionValues.ToArray(_elementType), BindingFlags.Default, null, null, CultureInfo.InvariantCulture); } else if (IsCollection) { // If value of property is null, create new instance of collection if (_propertyInfo.GetValue(destination, BindingFlags.Default, null, null, CultureInfo.InvariantCulture) == null) { if (!_propertyInfo.CanWrite) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1171") + " but is not initialized and does not allow the" + "collection to be initialized.", LongName)); } object instance = Activator.CreateInstance(_propertyInfo.PropertyType, BindingFlags.Public | BindingFlags.Instance, null, null, CultureInfo.InvariantCulture); _propertyInfo.SetValue(destination, instance, BindingFlags.Default, null, null, CultureInfo.InvariantCulture); } object value = _propertyInfo.GetValue(destination, BindingFlags.Default, null, null, CultureInfo.InvariantCulture); MethodInfo addMethod = null; // Locate Add method with 1 parameter foreach (MethodInfo method in value.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (method.Name == "Add" && method.GetParameters().Length == 1) { ParameterInfo parameter = method.GetParameters()[0]; if (parameter.ParameterType != typeof(object)) { addMethod = method; break; } } } if (addMethod == null) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1169"), LongName)); } else { try { foreach (object item in _collectionValues) { addMethod.Invoke(value, BindingFlags.Default, null, new object[] {item}, CultureInfo.InvariantCulture); } } catch (Exception ex) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1173"), LongName), ex); } } } else if (IsNameValueCollection) { // If value of property is null, create new instance of collection if (_propertyInfo.GetValue(destination, BindingFlags.Default, null, null, CultureInfo.InvariantCulture) == null) { if (!_propertyInfo.CanWrite) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1171") + " but is not initialized and does not allow the" + "collection to be initialized.", LongName)); } object instance = Activator.CreateInstance(_propertyInfo.PropertyType, BindingFlags.Public | BindingFlags.Instance, null, null, CultureInfo.InvariantCulture); _propertyInfo.SetValue(destination, instance, BindingFlags.Default, null, null, CultureInfo.InvariantCulture); } object value = _propertyInfo.GetValue(destination, BindingFlags.Default, null, null, CultureInfo.InvariantCulture); MethodInfo addMethod = null; // Locate Add method with 2 string parameters foreach (MethodInfo method in value.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (method.Name == "Add" && method.GetParameters().Length == 2) { if (method.GetParameters()[0].ParameterType == typeof(string) && method.GetParameters()[1].ParameterType == typeof(string)) { addMethod = method; break; } } } if (addMethod == null) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1169"), LongName)); } else { try { foreach (string key in _valuePairs) { addMethod.Invoke(value, BindingFlags.Default, null, new object[] {key, _valuePairs.Get(key)}, CultureInfo.InvariantCulture); } } catch (Exception ex) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1173"), LongName), ex); } } } else { // this fails on mono if the _argumentValue is null if (_argumentValue != null) { _propertyInfo.SetValue(destination, _argumentValue, BindingFlags.Default, null, null, CultureInfo.InvariantCulture); } } } /// /// Assigns the specified value to the argument. /// /// The value that should be assigned to the argument. /// /// Duplicate argument. /// -or- /// Invalid value. /// public void SetValue(string value) { if (SeenValue && !AllowMultiple) { throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1175"), LongName)); } _seenValue = true; object newValue = ParseValue(ValueType, value); if (IsCollection || IsArray) { if (Unique && _collectionValues.Contains(newValue)) { throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1172"), value, LongName)); } else { _collectionValues.Add(newValue); } } else if (IsNameValueCollection) { // name/value pair is added to collection in ParseValue } else { _argumentValue = newValue; } } #endregion Public Instance Methods #region Private Instance Methods private object ParseValue(Type type, string stringData) { // null is only valid for bool variables // empty string is never valid if ((stringData != null || type == typeof(bool)) && (stringData == null || stringData.Length > 0)) { try { if (type == typeof(string)) { return stringData; } else if (type == typeof(bool)) { if (stringData == null || stringData == "+") { return true; } else if (stringData == "-") { return false; } } else if (IsNameValueCollectionType(type)) { Match match = Regex.Match(stringData, @"(\w+[^=]*)=(\w*.*)"); if (match.Success) { string name = match.Groups[1].Value; string value = match.Groups[2].Value; if (Unique && _valuePairs.Get(name) != null) { // we always assume we're dealing with properties // here to make the message more clear throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1174"), name, LongName)); } _valuePairs.Add(name, value); return _valuePairs; } else { throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1170"), stringData, LongName), new ArgumentException( "Expected name/value pair (=).")); } } else { if (type.IsEnum) { try { return Enum.Parse(type, stringData, true); } catch(ArgumentException ex) { string message = "Invalid value {0} for command-line argument '-{1}'. Valid values are: "; foreach (object value in Enum.GetValues(type)) { message += value.ToString() + ", "; } // strip last , message = message.Substring(0, message.Length - 2) + "."; throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, message, stringData, LongName), ex); } } else { // Make a guess that the there's a public static Parse method on the type of the property // that will take an argument of type string to convert the string to the type // required by the property. System.Reflection.MethodInfo parseMethod = type.GetMethod( "Parse", BindingFlags.Public | BindingFlags.Static, null, CallingConventions.Standard, new Type[] {typeof(string)}, null); if (parseMethod != null) { // Call the Parse method return parseMethod.Invoke(null, BindingFlags.Default, null, new object[] {stringData}, CultureInfo.InvariantCulture); } else if (type.IsClass) { // Search for a constructor that takes a string argument ConstructorInfo stringArgumentConstructor = type.GetConstructor(new Type[] {typeof(string)}); if (stringArgumentConstructor != null) { return stringArgumentConstructor.Invoke( BindingFlags.Default, null, new object[] {stringData}, CultureInfo.InvariantCulture); } } } } } catch (CommandLineArgumentException) { throw; } catch (Exception ex) { throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1170"), stringData, LongName), ex); } } throw new CommandLineArgumentException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1170"), stringData, LongName)); } #endregion Private Instance Methods #region Private Static Methods private static CommandLineArgumentTypes GetArgumentType(CommandLineArgumentAttribute attribute, PropertyInfo propertyInfo) { if (attribute != null) { return attribute.Type; } else if (IsCollectionType(propertyInfo.PropertyType)) { return CommandLineArgumentTypes.MultipleUnique; } else { return CommandLineArgumentTypes.AtMostOnce; } } private static Type GetElementType(PropertyInfo propertyInfo) { Type elementType = null; if (propertyInfo.PropertyType.IsArray) { elementType = propertyInfo.PropertyType.GetElementType(); if (elementType == typeof(object)) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Property {0} is not a strong-typed array.", propertyInfo.Name)); } } else if (typeof(ICollection).IsAssignableFrom(propertyInfo.PropertyType)) { // Locate Add method with 1 parameter foreach (MethodInfo method in propertyInfo.PropertyType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (method.Name == "Add" && method.GetParameters().Length == 1) { ParameterInfo parameter = method.GetParameters()[0]; if (parameter.ParameterType == typeof(object)) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Property {0} is not a strong-typed collection.", propertyInfo.Name)); } else { elementType = parameter.ParameterType; break; } } } if (elementType == null) { throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Invalid commandline argument type for property {0}.", propertyInfo.Name)); } } return elementType; } /// /// Indicates whether the specified is a /// . /// /// /// if can be assigned /// to ; otherwise, . /// private static bool IsNameValueCollectionType(Type type) { return typeof(NameValueCollection).IsAssignableFrom(type); } /// /// Indicates whether the specified is collection-based. /// /// /// if can be assigned /// to and is not backed by a /// that can be assigned to ; /// otherwise, . /// private static bool IsCollectionType(Type type) { return typeof(ICollection).IsAssignableFrom(type) && !IsNameValueCollectionType(type); } /// /// Indicates whether the specified is an array. /// /// /// if is an array; /// otherwise, . /// private static bool IsArrayType(Type type) { return type.IsArray; } #endregion Private Static Methods #region Private Instance Fields private Type _elementType; private bool _seenValue; private CommandLineArgumentTypes _argumentType; private PropertyInfo _propertyInfo; private CommandLineArgumentAttribute _attribute; private object _argumentValue; private ArrayList _collectionValues; private NameValueCollection _valuePairs; #endregion Private Instance Fields } }