// Copyright (c) 2010-2014 SharpDX - Alexandre Mutel // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices; namespace SharpDX { /// /// Reusable, reflection based helper for parsing command line options. /// Greetings to Shawn Hargreaves, original code http://blogs.msdn.com/b/shawnhar/archive/2012/04/20/a-reusable-reflection-based-command-line-parser.aspx /// This is a modified version of command line parser that adds: /// - .NET 2.0 compatible /// - Allow inheritance to simplify declaration /// - Print exe banner, using AssemblyTitle and AssemblyCopyright. /// - Better padding of options, add descriptor and value text overrides. /// - Add support for - and / starting options. /// - Remove usage of ":" to separate option from parsed option /// - Add "<options>" to the Usage when options are defined /// - Add Console Color handling /// /// /// This single file is intended to be directly included in the project that needs to handle command line without requiring any SharpDX assembly dependencies. /// class ConsoleProgram { private const int STD_OUTPUT_HANDLE = -11; private static int hConsoleHandle; private static COORD ConsoleOutputLocation; private static CONSOLE_SCREEN_BUFFER_INFO ConsoleInfo; private static int OriginalColors; private Dictionary optionalOptions = new Dictionary(); private List optionalUsageHelp = new List(); private object optionsObject; private string[] optionNames; private Queue requiredOptions = new Queue(); private List requiredUsageHelp = new List(); static ConsoleProgram() { ConsoleInfo = new CONSOLE_SCREEN_BUFFER_INFO(); ConsoleOutputLocation = new COORD(); hConsoleHandle = GetStdHandle(STD_OUTPUT_HANDLE); GetConsoleScreenBufferInfo(hConsoleHandle, ref ConsoleInfo); OriginalColors = ConsoleInfo.wAttributes; } protected ConsoleProgram(int padOptions = 16) : this(null, padOptions) { } // Constructor. private ConsoleProgram(object optionsObjectArg, int padOptions = 16) { this.optionsObject = optionsObjectArg ?? this; // Reflect to find what command line options are available. foreach (FieldInfo field in optionsObject.GetType().GetFields()) { var option = GetOptionName(field); var optionName = option.Name; if (option.Required) { // Record a required option. requiredOptions.Enqueue(field); requiredUsageHelp.Add(string.Format("<{0}>", option.Name)); } else { // Record an optional option. optionalOptions.Add(optionName, field); if (field.FieldType == typeof (bool)) { optionalUsageHelp.Add(string.Format("/{0,-" + padOptions + "}{1}", optionName, option.Description ?? string.Empty)); } else { optionalUsageHelp.Add(string.Format("/{0,-" + padOptions + "}{1}", string.Format("{0}{1}", optionName, option.Value ?? ""), option.Description ?? string.Empty)); } } } optionNames = new string[optionalOptions.Count]; optionalOptions.Keys.CopyTo(optionNames, 0); Array.Sort(optionNames, (left, right) => -string.Compare(left, right, StringComparison.Ordinal)); if (optionalOptions.Count > 0) { requiredUsageHelp.Insert(0, ""); } } public static void PrintHeader() { Console.WriteLine("{0} - {1}", GetAssemblyTitle(), Assembly.GetEntryAssembly().GetName().Version); Console.WriteLine("{0}", GetAssemblyCopyright()); Console.WriteLine(); } public static bool ParseCommandLine(object options, string[] args, int padOptions = 16) { PrintHeader(); var cmdParser = new ConsoleProgram(options, padOptions); return cmdParser.ParseCommandLine(args); } protected bool ParseCommandLine(string[] args) { // Parse each argument in turn. foreach (string arg in args) { if (!ParseArgument(arg.Trim())) { return false; } } // Make sure we got all the required options. FieldInfo missingRequiredOption = null; foreach (var field in requiredOptions) { if (!IsList(field) || GetList(field).Count == 0) { missingRequiredOption = field; break; } } if (missingRequiredOption != null) { ShowError("Missing argument '{0}'", GetOptionName(missingRequiredOption).Name); return false; } return true; } private bool ParseArgument(string arg) { if (arg.StartsWith("/") || arg.StartsWith("-")) { string name = arg.Substring(1); string value = null; FieldInfo field = null; foreach (var registerName in optionNames) { if (name.StartsWith(registerName, StringComparison.InvariantCultureIgnoreCase)) { field = optionalOptions[registerName]; value = name.Substring(registerName.Length); break; } } if (field == null) { ShowError("Unknown option '{0}'", name); return false; } if (string.IsNullOrEmpty(value)) { value = "true"; } return SetOption(field, value); } else { // Parse a required argument. if (requiredOptions.Count == 0) { ShowError("Too many arguments"); return false; } FieldInfo field = requiredOptions.Peek(); if (!IsList(field)) { requiredOptions.Dequeue(); } return SetOption(field, arg); } } private bool SetOption(FieldInfo field, string value) { try { if (IsList(field)) { // Append this value to a list of options. GetList(field).Add(ChangeType(value, ListElementType(field))); } else { // Set the value of a single option. field.SetValue(optionsObject, ChangeType(value, field.FieldType)); } return true; } catch { ShowError("Invalid value '{0}' for option '{1}'", value, GetOptionName(field).Name); return false; } } private static object ChangeType(string value, Type type) { TypeConverter converter = TypeDescriptor.GetConverter(type); return converter.ConvertFromInvariantString(value); } private static bool IsList(FieldInfo field) { return typeof (IList).IsAssignableFrom(field.FieldType); } private IList GetList(FieldInfo field) { return (IList) field.GetValue(optionsObject); } private static Type ListElementType(FieldInfo field) { foreach (var fieldInterface in field.FieldType.GetInterfaces()) { if (fieldInterface.IsGenericType && fieldInterface.GetGenericTypeDefinition() == typeof (IEnumerable<>)) { return fieldInterface.GetGenericArguments()[0]; } } return null; } private static OptionAttribute GetOptionName(FieldInfo field) { return GetAttribute(field) ?? new OptionAttribute(field.Name); } public void ShowError(string message, params object[] args) { string name = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); ErrorColor(); System.Console.Error.WriteLine(message, args); ResetColor(); System.Console.Error.WriteLine(); System.Console.Out.WriteLine("Usage: {0} {1}", name, string.Join(" ", requiredUsageHelp.ToArray())); if (optionalUsageHelp.Count > 0) { System.Console.Out.WriteLine(); System.Console.Out.WriteLine("Options:"); foreach (string optional in optionalUsageHelp) { System.Console.Out.WriteLine(" {0}", optional); } } } private static T GetAttribute(ICustomAttributeProvider provider) where T : Attribute { var attributes = provider.GetCustomAttributes(typeof (T), false); if (attributes.Length > 0) { return (T)attributes[0]; } return null; } /// /// Gets the assembly title. /// /// The assembly title. private static string GetAssemblyTitle() { var attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyTitleAttribute), false); if (attributes.Length > 0) { var titleAttribute = (AssemblyTitleAttribute)attributes[0]; if (!string.IsNullOrEmpty(titleAttribute.Title)) return titleAttribute.Title; } return Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().CodeBase); } /// /// Gets the assembly title. /// /// The assembly title. private static string GetAssemblyCopyright() { var attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false); if (attributes.Length > 0) { var titleAttribute = (AssemblyCopyrightAttribute)attributes[0]; if (!string.IsNullOrEmpty(titleAttribute.Copyright)) return titleAttribute.Copyright; } return string.Empty; } // Used on optionsObject fields to indicate which options are required. [DllImport("kernel32.dll", EntryPoint = "GetStdHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] private static extern int GetStdHandle(int nStdHandle); [DllImport("kernel32.dll", EntryPoint = "GetConsoleScreenBufferInfo", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] private static extern int GetConsoleScreenBufferInfo(int hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo); [DllImport("kernel32.dll", EntryPoint = "SetConsoleTextAttribute", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] private static extern int SetConsoleTextAttribute(int hConsoleOutput, int wAttributes); public static void ErrorColor() { Color(ConsoleColor.Red | ConsoleColor.Intensity); } public static void Color(ConsoleColor color) { SetConsoleTextAttribute(hConsoleHandle, (int) color); } public static void ResetColor() { SetConsoleTextAttribute(hConsoleHandle, OriginalColors); } #region Nested type: CONSOLE_SCREEN_BUFFER_INFO [StructLayout(LayoutKind.Sequential)] private struct CONSOLE_SCREEN_BUFFER_INFO { public COORD dwSize; public COORD dwCursorPosition; public int wAttributes; public SMALL_RECT srWindow; public COORD dwMaximumWindowSize; } #endregion #region Nested type: COORD [StructLayout(LayoutKind.Sequential)] private struct COORD { private short X; private short Y; } #endregion #region Nested type: OptionAttribute [AttributeUsage(AttributeTargets.Field)] public sealed class OptionAttribute : Attribute { public OptionAttribute(string name) { this.Name = name; } public string Name { get; private set; } public string Description { get; set; } public string Value { get; set; } public bool Required { get; set; } } #endregion #region Nested type: SMALL_RECT [StructLayout(LayoutKind.Sequential)] private struct SMALL_RECT { private short Left; private short Top; private short Right; private short Bottom; } #endregion } /// /// Colors used by /// [Flags] enum ConsoleColor { /// /// Blue foreground color. /// Blue = 0x00000001, /// /// Green foreground color. /// Green = 0x00000002, /// /// Red foreground color. /// Red = 0x00000004, /// /// Intensity foreground color modifier. /// Intensity = 0x00000008, /// /// Blue background color. /// BlueBackground = 0x00000010, /// /// Green background color. /// GreenBackground = 0x00000020, /// /// Red background color. /// RedBackground = 0x00000040, /// /// Intensity background color modifier. /// IntensityBackground = 0x00000080, Black = 0, Cyan = Green | Blue, Magenta = Red | Blue, Yellow = Red | Green, DarkGrey = Red | Green | Blue, LightGrey = Intensity, LightRed = Intensity | Red, LightGreen = Intensity | Green, LightBlue = Intensity | Blue, LightCyan = Intensity | Green | Blue, LightMagenta = Intensity | Red | Blue, LightYellow = Intensity | Red | Green, White = Intensity | Red | Green | Blue, } }