// // NConsoler 0.9.3 // http://nconsoler.csharpus.com // using System; using System.Collections.Generic; using System.Reflection; using System.Diagnostics; using System.Threading; namespace NConsoler { /// /// Entry point for NConsoler applications /// public sealed class Consolery { /// /// Runs an appropriate Action method. /// Uses the class this call lives in as target type and command line arguments from Environment /// public static int Run() { Type declaringType = new StackTrace().GetFrame(1).GetMethod().DeclaringType; string[] args = new string[Environment.GetCommandLineArgs().Length - 1]; new List(Environment.GetCommandLineArgs()).CopyTo(1, args, 0, Environment.GetCommandLineArgs().Length - 1); return Run(declaringType, args); } /// /// Runs an appropriate Action method /// /// Type where to search for Action methods /// Arguments that will be converted to Action method arguments public static int Run(Type targetType, string[] args) { return Run(targetType, args, new ConsoleMessenger()); } /// /// Runs an appropriate Action method /// /// Type where to search for Action methods /// Arguments that will be converted to Action method arguments /// Uses for writing messages instead of Console class methods public static int Run(Type targetType, string[] args, IMessenger messenger) { try { return new Consolery(targetType, args, messenger, false, null).RunAction(); } catch (NConsolerException e) { messenger.Write(e.Message); return 100; } } /// /// Runs an appropriate Action method /// /// Object where to search for Action methods /// Arguments that will be converted to Action method arguments public static int RunInstance(object targetObject, string[] args) { return RunInstance(targetObject, args, new ConsoleMessenger()); } /// /// Runs an appropriate Action method /// /// Object where to search for Action methods /// Arguments that will be converted to Action method arguments /// Uses for writing messages instead of Console class methods public static int RunInstance(object targetObject, string[] args, IMessenger messenger) { try { return new Consolery(targetObject.GetType(), args, messenger, true, targetObject).RunAction(); } catch (NConsolerException e) { messenger.Write(e.Message); return 100; } } /// /// Validates specified type and throws NConsolerException if an error /// /// Type where to search for Action methods public static void Validate(Type targetType) { new Consolery(targetType, new string[] {}, new ConsoleMessenger(), false, null).ValidateMetadata(); } private readonly Type _targetType; private readonly string[] _args; private readonly List _actionMethods = new List(); private readonly IMessenger _messenger; private readonly bool _isInstance; private readonly object _targetObject; private Consolery(Type targetType, string[] args, IMessenger messenger, bool isInstance, object targetObject) { #region Parameter Validation if (targetType == null) { throw new ArgumentNullException("targetType"); } if (args == null) { throw new ArgumentNullException("args"); } if (messenger == null) { throw new ArgumentNullException("messenger"); } if (isInstance && (targetObject == null)) { throw new AbandonedMutexException("targetObject"); } #endregion _targetType = targetType; _args = args; _messenger = messenger; _isInstance = isInstance; _targetObject = targetObject; BindingFlags flags; if(_isInstance) { flags = BindingFlags.Instance; } else { flags = BindingFlags.Static; } MethodInfo[] methods = _targetType.GetMethods(BindingFlags.Public | flags); foreach (MethodInfo method in methods) { object[] attributes = method.GetCustomAttributes(false); foreach (object attribute in attributes) { if (attribute is ActionAttribute) { _actionMethods.Add(method); break; } } } } static object ConvertValue(string value, Type argumentType) { if (argumentType == typeof(int)) { try { return Convert.ToInt32(value); } catch (FormatException) { throw new NConsolerException("Could not convert \"{0}\" to integer", value); } catch (OverflowException) { throw new NConsolerException("Value \"{0}\" is too big or too small", value); } } if (argumentType == typeof(string)) { return value; } if (argumentType == typeof(bool)) { try { return Convert.ToBoolean(value); } catch (FormatException) { throw new NConsolerException("Could not convert \"{0}\" to boolean", value); } } if (argumentType == typeof(string[])) { return value.Split('+'); } if (argumentType == typeof(int[])) { string[] values = value.Split('+'); int[] valuesArray = new int[values.Length]; for (int i = 0; i < values.Length; i++) { valuesArray[i] = (int)ConvertValue(values[i], typeof(int)); } return valuesArray; } if (argumentType == typeof(DateTime)) { return ConvertToDateTime(value); } throw new NConsolerException("Unknown type is used in your method {0}", argumentType.FullName); } private static DateTime ConvertToDateTime(string parameter) { string[] parts = parameter.Split('-'); if (parts.Length != 3) { throw new NConsolerException("Could not convert {0} to Date", parameter); } int day = (int)ConvertValue(parts[0], typeof(int)); int month = (int)ConvertValue(parts[1], typeof(int)); int year = (int)ConvertValue(parts[2], typeof(int)); try { return new DateTime(year, month, day); } catch (ArgumentException) { throw new NConsolerException("Could not convert {0} to Date", parameter); } } private static bool CanBeConvertedToDate(string parameter) { try { ConvertToDateTime(parameter); return true; } catch(NConsolerException) { return false; } } private bool SingleActionWithOnlyOptionalParametersSpecified() { if (IsMulticommand) return false; MethodInfo method = _actionMethods[0]; return OnlyOptionalParametersSpecified(method); } private static bool OnlyOptionalParametersSpecified(MethodBase method) { foreach (ParameterInfo parameter in method.GetParameters()) { if (IsRequired(parameter)) { return false; } } return true; } private int RunAction() { ValidateMetadata(); if (IsHelpRequested()) { PrintUsage(); return 0; } MethodInfo currentMethod = GetCurrentMethod(); if (currentMethod == null) { PrintUsage(); throw new NConsolerException("Unknown subcommand \"{0}\"", _args[0]); } ValidateInput(currentMethod); return InvokeMethod(currentMethod); } private struct ParameterData { public readonly int position; public readonly Type type; public ParameterData(int position, Type type) { this.position = position; this.type = type; } } private static bool IsRequired(ICustomAttributeProvider info) { object[] attributes = info.GetCustomAttributes(typeof(ParameterAttribute), false); return attributes.Length == 0 || attributes[0].GetType() == typeof(RequiredAttribute); } private static bool IsOptional(ICustomAttributeProvider info) { return !IsRequired(info); } private static OptionalAttribute GetOptional(ICustomAttributeProvider info) { object[] attributes = info.GetCustomAttributes(typeof(OptionalAttribute), false); return (OptionalAttribute)attributes[0]; } private bool IsMulticommand { get { return _actionMethods.Count > 1; } } private bool IsHelpRequested() { return (_args.Length == 0 && !SingleActionWithOnlyOptionalParametersSpecified()) || (_args.Length > 0 && (_args[0] == "/?" || _args[0] == "/help" || _args[0] == "/h" || _args[0] == "help")); } private int InvokeMethod(MethodInfo method) { try { return (int)method.Invoke(_isInstance ? _targetObject : null, BuildParameterArray(method)); } catch (TargetInvocationException e) { if (e.InnerException != null) { throw new NConsolerException(e.InnerException.Message, e); } throw; } } private object[] BuildParameterArray(MethodInfo method) { int argumentIndex = IsMulticommand ? 1 : 0; List parameterValues = new List(); Dictionary aliases = new Dictionary(); foreach (ParameterInfo info in method.GetParameters()) { if (IsRequired(info)) { parameterValues.Add(ConvertValue(_args[argumentIndex], info.ParameterType)); } else { OptionalAttribute optional = GetOptional(info); foreach (string altName in optional.AltNames) { aliases.Add(altName.ToLower(), new ParameterData(parameterValues.Count, info.ParameterType)); } aliases.Add(info.Name.ToLower(), new ParameterData(parameterValues.Count, info.ParameterType)); parameterValues.Add(optional.Default); } argumentIndex++; } foreach (string optionalParameter in OptionalParameters(method)) { string name = ParameterName(optionalParameter); string value = ParameterValue(optionalParameter); parameterValues[aliases[name].position] = ConvertValue(value, aliases[name].type); } return parameterValues.ToArray(); } private IEnumerable OptionalParameters(MethodInfo method) { int firstOptionalParameterIndex = RequiredParameterCount(method); if (IsMulticommand) { firstOptionalParameterIndex++; } for (int i = firstOptionalParameterIndex; i < _args.Length; i++) { yield return _args[i]; } } private static int RequiredParameterCount(MethodInfo method) { int requiredParameterCount = 0; foreach (ParameterInfo parameter in method.GetParameters()) { if (IsRequired(parameter)) { requiredParameterCount++; } } return requiredParameterCount; } private MethodInfo GetCurrentMethod() { if (!IsMulticommand) { return _actionMethods[0]; } return GetMethodByName(_args[0].ToLower()); } private MethodInfo GetMethodByName(string name) { foreach (MethodInfo method in _actionMethods) { if (method.Name.ToLower() == name) { return method; } } return null; } private void PrintUsage(MethodInfo method) { PrintMethodDescription(method); Dictionary parameters = GetParametersDescriptions(method); PrintUsageExample(method, parameters); PrintParametersDescriptions(parameters); } private void PrintUsageExample(MethodInfo method, IDictionary parameterList) { string subcommand = IsMulticommand ? method.Name.ToLower() + " " : String.Empty; string parameters = String.Join(" ", new List(parameterList.Keys).ToArray()); _messenger.Write("usage: " + ProgramName() + " " + subcommand + parameters); } private void PrintMethodDescription(MethodInfo method) { string description = GetMethodDescription(method); if (description == String.Empty) return; _messenger.Write(description); } private static string GetMethodDescription(MethodInfo method) { object[] attributes = method.GetCustomAttributes(true); foreach (object attribute in attributes) { if (attribute is ActionAttribute) { return ((ActionAttribute)attribute).Description; } } throw new NConsolerException("Method is not marked with an Action attribute"); } private static Dictionary GetParametersDescriptions(MethodInfo method) { Dictionary parameters = new Dictionary(); foreach (ParameterInfo parameter in method.GetParameters()) { object[] parameterAttributes = parameter.GetCustomAttributes(typeof(ParameterAttribute), false); if (parameterAttributes.Length > 0) { string name = GetDisplayName(parameter); ParameterAttribute attribute = (ParameterAttribute)parameterAttributes[0]; parameters.Add(name, attribute.Description); } else { parameters.Add(parameter.Name, String.Empty); } } return parameters; } private void PrintParametersDescriptions(IEnumerable> parameters) { int maxParameterNameLength = MaxKeyLength(parameters); foreach (KeyValuePair pair in parameters) { if (pair.Value != String.Empty) { int difference = maxParameterNameLength - pair.Key.Length + 2; _messenger.Write(" " + pair.Key + new String(' ', difference) + pair.Value); } } } private static int MaxKeyLength(IEnumerable> parameters) { int maxLength = 0; foreach (KeyValuePair pair in parameters) { if (pair.Key.Length > maxLength) { maxLength = pair.Key.Length; } } return maxLength; } private string ProgramName() { Assembly entryAssembly = Assembly.GetEntryAssembly(); if (entryAssembly == null) { return _targetType.Name.ToLower(); } return new AssemblyName(entryAssembly.FullName).Name; } private void PrintUsage() { if (IsMulticommand && !IsSubcommandHelpRequested()) { PrintGeneralMulticommandUsage(); } else if (IsMulticommand && IsSubcommandHelpRequested()) { PrintSubcommandUsage(); } else { PrintUsage(_actionMethods[0]); } } private void PrintSubcommandUsage() { MethodInfo method = GetMethodByName(_args[1].ToLower()); if (method == null) { PrintGeneralMulticommandUsage(); throw new NConsolerException("Unknown subcommand \"{0}\"", _args[0].ToLower()); } PrintUsage(method); } private bool IsSubcommandHelpRequested() { return _args.Length > 0 && _args[0].ToLower() == "help" && _args.Length == 2; } private void PrintGeneralMulticommandUsage() { _messenger.Write( String.Format("usage: {0} [args]", ProgramName())); _messenger.Write( String.Format("Type '{0} help ' for help on a specific subcommand.", ProgramName())); _messenger.Write(String.Empty); _messenger.Write("Available subcommands:"); foreach (MethodInfo method in _actionMethods) { _messenger.Write(method.Name.ToLower() + " " + GetMethodDescription(method)); } } private static string GetDisplayName(ParameterInfo parameter) { if (IsRequired(parameter)) { return parameter.Name; } OptionalAttribute optional = GetOptional(parameter); string parameterName = (optional.AltNames.Length > 0) ? optional.AltNames[0] : parameter.Name; if (parameter.ParameterType != typeof(bool)) { parameterName += ":" + ValueDescription(parameter.ParameterType); } return "[/" + parameterName + "]"; } private static string ValueDescription(Type type) { if (type == typeof(int)) { return "number"; } if (type == typeof(string)) { return "value"; } if (type == typeof(int[])) { return "number[+number]"; } if (type == typeof(string[])) { return "value[+value]"; } if (type == typeof(DateTime)) { return "dd-mm-yyyy"; } throw new ArgumentOutOfRangeException(String.Format("Type {0} is unknown", type.Name)); } #region Validation private void ValidateInput(MethodInfo method) { CheckAllRequiredParametersAreSet(method); CheckOptionalParametersAreNotDuplicated(method); CheckUnknownParametersAreNotPassed(method); } private void CheckAllRequiredParametersAreSet(MethodInfo method) { int minimumArgsLengh = RequiredParameterCount(method); if (IsMulticommand) { minimumArgsLengh++; } if (_args.Length < minimumArgsLengh) { throw new NConsolerException("Not all required parameters are set"); } } private static string ParameterName(string parameter) { if (parameter.StartsWith("/-")) { return parameter.Substring(2).ToLower(); } if (parameter.Contains(":")) { return parameter.Substring(1, parameter.IndexOf(":") - 1).ToLower(); } return parameter.Substring(1).ToLower(); } private static string ParameterValue(string parameter) { if (parameter.StartsWith("/-")) { return "false"; } if (parameter.Contains(":")) { return parameter.Substring(parameter.IndexOf(":") + 1); } return "true"; } private void CheckOptionalParametersAreNotDuplicated(MethodInfo method) { List passedParameters = new List(); foreach (string optionalParameter in OptionalParameters(method)) { if (!optionalParameter.StartsWith("/")) { throw new NConsolerException("Unknown parameter {0}", optionalParameter); } string name = ParameterName(optionalParameter); if (passedParameters.Contains(name)) { throw new NConsolerException("Parameter with name {0} passed two times", name); } passedParameters.Add(name); } } private void CheckUnknownParametersAreNotPassed(MethodInfo method) { List parameterNames = new List(); foreach (ParameterInfo parameter in method.GetParameters()) { if (IsRequired(parameter)) { continue; } parameterNames.Add(parameter.Name.ToLower()); OptionalAttribute optional = GetOptional(parameter); foreach (string altName in optional.AltNames) { parameterNames.Add(altName.ToLower()); } } foreach (string optionalParameter in OptionalParameters(method)) { string name = ParameterName(optionalParameter); if (!parameterNames.Contains(name.ToLower())) { throw new NConsolerException("Unknown parameter name {0}", optionalParameter); } } } private void ValidateMetadata() { CheckAnyActionMethodExists(); IfActionMethodIsSingleCheckMethodHasParameters(); foreach (MethodInfo method in _actionMethods) { CheckActionMethodNamesAreNotReserved(); CheckRequiredAndOptionalAreNotAppliedAtTheSameTime(method); CheckOptionalParametersAreAfterRequiredOnes(method); CheckOptionalParametersDefaultValuesAreAssignableToRealParameterTypes(method); CheckOptionalParametersAltNamesAreNotDuplicated(method); } } private void CheckActionMethodNamesAreNotReserved() { foreach (MethodInfo method in _actionMethods) { if (method.Name.ToLower() == "help") { throw new NConsolerException("Method name \"{0}\" is reserved. Please, choose another name", method.Name); } } } private void CheckAnyActionMethodExists() { if (_actionMethods.Count == 0) { throw new NConsolerException("Can not find any public static method marked with [Action] attribute in type \"{0}\"", _targetType.Name); } } private void IfActionMethodIsSingleCheckMethodHasParameters() { if (_actionMethods.Count == 1 && _actionMethods[0].GetParameters().Length == 0) { throw new NConsolerException("[Action] attribute applied once to the method \"{0}\" without parameters. In this case NConsoler should not be used", _actionMethods[0].Name); } } private static void CheckRequiredAndOptionalAreNotAppliedAtTheSameTime(MethodBase method) { foreach (ParameterInfo parameter in method.GetParameters()) { object[] attributes = parameter.GetCustomAttributes(typeof(ParameterAttribute), false); if (attributes.Length > 1) { throw new NConsolerException("More than one attribute is applied to the parameter \"{0}\" in the method \"{1}\"", parameter.Name, method.Name); } } } private static bool CanBeNull(Type type) { return type == typeof(string) || type == typeof(string[]) || type == typeof(int[]); } private static void CheckOptionalParametersDefaultValuesAreAssignableToRealParameterTypes(MethodBase method) { foreach (ParameterInfo parameter in method.GetParameters()) { if (IsRequired(parameter)) { continue; } OptionalAttribute optional = GetOptional(parameter); if (optional.Default != null && optional.Default.GetType() == typeof(string) && CanBeConvertedToDate(optional.Default.ToString())) { return; } if ((optional.Default == null && !CanBeNull(parameter.ParameterType)) || (optional.Default != null && !optional.Default.GetType().IsAssignableFrom(parameter.ParameterType))) { throw new NConsolerException("Default value for an optional parameter \"{0}\" in method \"{1}\" can not be assigned to the parameter", parameter.Name, method.Name); } } } private static void CheckOptionalParametersAreAfterRequiredOnes(MethodBase method) { bool optionalFound = false; foreach (ParameterInfo parameter in method.GetParameters()) { if (IsOptional(parameter)) { optionalFound = true; } else if (optionalFound) { throw new NConsolerException("It is not allowed to write a parameter with a Required attribute after a parameter with an Optional one. See method \"{0}\" parameter \"{1}\"", method.Name, parameter.Name); } } } private static void CheckOptionalParametersAltNamesAreNotDuplicated(MethodBase method) { List parameterNames = new List(); foreach (ParameterInfo parameter in method.GetParameters()) { if (IsRequired(parameter)) { parameterNames.Add(parameter.Name.ToLower()); } else { if (parameterNames.Contains(parameter.Name.ToLower())) { throw new NConsolerException("Found duplicated parameter name \"{0}\" in method \"{1}\". Please check alt names for optional parameters", parameter.Name, method.Name); } parameterNames.Add(parameter.Name.ToLower()); OptionalAttribute optional = GetOptional(parameter); foreach (string altName in optional.AltNames) { if (parameterNames.Contains(altName.ToLower())) { throw new NConsolerException("Found duplicated parameter name \"{0}\" in method \"{1}\". Please check alt names for optional parameters", altName, method.Name); } parameterNames.Add(altName.ToLower()); } } } } #endregion } /// /// Used for getting messages from NConsoler /// public interface IMessenger { void Write(string message); } /// /// Uses Console class for message output /// public class ConsoleMessenger : IMessenger { public void Write(string message) { Console.WriteLine(message); } } /// /// Every action method should be marked with this attribute /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class ActionAttribute : Attribute { public ActionAttribute() { } public ActionAttribute(string description) { _description = description; } private string _description = String.Empty; /// /// Description is used for help messages /// public string Description { get { return _description; } set { _description = value; } } } /// /// Should not be used directly /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] public class ParameterAttribute : Attribute { private string _description = String.Empty; /// /// Description is used in help message /// public string Description { get { return _description; } set { _description = value; } } protected ParameterAttribute() { } } /// /// Marks an Action method parameter as optional /// public sealed class OptionalAttribute : ParameterAttribute { private string[] _altNames; public string[] AltNames { get { return _altNames; } set { _altNames = value; } } private readonly object _defaultValue; public object Default { get { return _defaultValue; } } /// Default value if client doesn't pass this value /// Aliases for parameter public OptionalAttribute(object defaultValue, params string[] altNames) { _defaultValue = defaultValue; _altNames = altNames; } } /// /// Marks an Action method parameter as required /// public sealed class RequiredAttribute : ParameterAttribute { } /// /// Can be used for safe exception throwing - NConsoler will catch the exception /// public sealed class NConsolerException : Exception { public NConsolerException() { } public NConsolerException(string message, Exception innerException) : base(message, innerException) { } public NConsolerException(string message, params string[] arguments) : base(String.Format(message, arguments)) { } } }