#region Copyright 2009-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0 /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #endregion using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading; using CSharpTest.Net.Utils; using System.Reflection; using System.Text.RegularExpressions; using System.IO; using CommandTypes = global::CSharpTest.Net.Commands.DefaultCommands; using System.Diagnostics; namespace CSharpTest.Net.Commands { /// /// The primary class involved in providing a command-line interpreter. /// public partial class CommandInterpreter : ICommandInterpreter { readonly Dictionary _commands; readonly Dictionary _options; readonly List _filters; readonly BuiltInCommands _buildInCommands; private ReadNextCharacter _fnNextCh; private ICommandChain _head; private string _prompt; private string _filterPrecedence; /// /// Constructs a command-line interpreter from the objects and/or System.Types provided. /// public CommandInterpreter(params object[] handlers) : this(CommandTypes.Default, handlers) { } /// /// Constructs a command-line interpreter from the objects and/or System.Types provided. /// public CommandInterpreter(DefaultCommands defaultCmds, params object[] handlers) { _head = null; _prompt = "> "; _commands = new Dictionary(StringComparer.OrdinalIgnoreCase); _options = new Dictionary(StringComparer.OrdinalIgnoreCase); _filters = new List(); _fnNextCh = GetNextCharacter; //defaults to { Redirect, then Pipe, then everything else } _filterPrecedence = "<|*"; _buildInCommands = new BuiltInCommands( Command.Make(this, this.GetType().GetMethod("Get")), Command.Make(this, this.GetType().GetMethod("Set", new Type[] { typeof(string), typeof(object), typeof(bool) } )), Command.Make(this, this.GetType().GetMethod("Help", new Type[] { typeof(string), typeof(bool) } )), Option.Make(this, this.GetType().GetProperty("ErrorLevel")), Option.Make(this, this.GetType().GetProperty("Prompt")) ); if (System.Net.HttpListener.IsSupported) _buildInCommands.AddRange(Command.Make(this, this.GetType().GetMethod("HostHttp"))); _buildInCommands.Add(this, defaultCmds); foreach (object o in handlers) AddHandler(o); } #region AddHandler, AddCommand, AddOption /// /// Adds the static methods to the command list, and static properties to the list of /// global options (used with commands set/get) /// public void AddHandler(Type targetObject) { this.AddHandler(targetObject); } /// /// Adds the instance methods to the command list, and instance properties to the list of /// global options (used with commands set/get) /// public void AddHandler(T targetObject) where T : class { BindingFlags flags = BindingFlags.Public | BindingFlags.IgnoreCase; Type type = targetObject as Type; if (type == null) { flags |= BindingFlags.Instance; type = targetObject.GetType(); } else flags |= BindingFlags.Static; MethodInfo[] methods = type.GetMethods(flags | BindingFlags.InvokeMethod); foreach (MethodInfo method in methods) { if (method.IsSpecialName || method.DeclaringType == typeof(Object) || method.GetCustomAttributes(typeof(IgnoreMemberAttribute),true).Length > 0) continue; ICommand command = Command.Make(targetObject, method); if (command is ICommandFilter) AddFilter((ICommandFilter)command); else AddCommand(command); } PropertyInfo[] props = type.GetProperties(flags | BindingFlags.GetProperty | BindingFlags.SetProperty); foreach (PropertyInfo prop in props) { if (!prop.CanRead || !prop.CanWrite || prop.GetIndexParameters().Length > 0 || prop.GetCustomAttributes(typeof(IgnoreMemberAttribute), true).Length > 0) continue; AddOption(Option.Make(targetObject, prop)); } } /// Manually adds a command public void AddCommand(ICommand command) { foreach (string key in command.AllNames) { if (String.IsNullOrEmpty(key)) continue; InterpreterException.Assert(false == _commands.ContainsKey(key), "Command {0} already exists.", key); _commands.Add(key, command); } } /// Manually remove a command public void RemoveCommand(ICommand command) { foreach (string key in command.AllNames) { if (String.IsNullOrEmpty(key)) continue; _commands.Remove(key); } } /// /// Adds a command 'filter' that is called for every command invoked enabling custom processing /// of arguments and pre/post processing. /// public void AddFilter(ICommandFilter filter) { _filters.Remove(filter); _filters.Add(filter); _head = null; } /// Manually adds an option public void AddOption(IOption option) { foreach (string key in option.AllNames) { InterpreterException.Assert(false == _options.ContainsKey(key), "Option {0} already exists.", key); _options.Add(key, option); } } #endregion /// Gets/sets the exit code of the operation/process [Option(Category = "Built-in", Description = "Gets or sets the exit code of the operation.")] public int ErrorLevel { get { return Environment.ExitCode; } set { Environment.ExitCode = value; } } /// Gets/sets the prompt, use "$(OptionName)" to reference options [Option(Category = "Built-in", Description = "Gets or sets the text to display to prompt for input use \"$(OptionName)\" to reference options.")] public string Prompt { get { return _prompt; } set { _prompt = Check.NotNull(value); } } /// /// Lists all the commands that have been added to the interpreter /// public ICommand[] Commands { get { List cmds = new List(); foreach (ICommand item in _commands.Values) if (!cmds.Contains(item)) cmds.Add(item); cmds.Sort(new OrderByName()); return cmds.ToArray(); } } /// /// Lists all the options that have been added to the interpreter, use the set/get commands /// to modify their values. /// public IOption[] Options { get { List opts = new List(); foreach (IOption item in _options.Values) if (!opts.Contains(item)) opts.Add(item); opts.Sort(new OrderByName()); return opts.ToArray(); } } /// Lists all the filters that have been added to the interpreter public ICommandFilter[] Filters { get { List filters = new List(_filters); filters.Sort(new OrderByName()); return filters.ToArray(); } } /// /// Returns true if the command was found and cmd output parameter is set. /// public bool TryGetCommand(string name, out ICommand cmd) { return _commands.TryGetValue(name, out cmd); } /// /// Returns true if the command was found and cmd output parameter is set. /// public bool TryGetOption(string name, out IOption cmd) { return _options.TryGetValue(name, out cmd); } /// Command to get an option value [Command(Category = "Built-in", Description = "Gets a global option by name")] public object Get(string property) { IOption opt; InterpreterException.Assert(_options.TryGetValue(property, out opt), "The option {0} was not found.", property); object value = opt.Value; Console.Out.WriteLine("{0}", value); return value; } /// Command to set the value of an option [IgnoreMember] public void Set(string property, object value) { Set(property, value, false); } /// Command to set the value of an option [Command(Category = "Built-in", Description = "Sets a global option by name or lists options available.")] public void Set([DefaultValue(null)] string property, [DefaultValue(null)] object value, [DefaultValue(false),Description("Read from std::in lines formatted as NAME=VALUE")]bool readInput) { if (readInput) { string line; while (null != (line = Console.In.ReadLine())) Set(line, null, false); return; } if (property == null) { foreach (IOption opt in Options) Console.WriteLine("{0}={1}", opt.DisplayName, opt.Value); return; } else if (value == null && property.IndexOf('=') < 0) { Get(property); return; } else if (value == null) { string[] args = property.Split(new char[] { '=' }, 2); property = args[0].TrimEnd(); value = args[1].TrimStart(); } IOption option; InterpreterException.Assert(_options.TryGetValue(property, out option), "The option {0} was not found.", property); option.Value = value; } /// /// The last link in the command chain /// internal void ProcessCommand(string[] arguments) { if (Check.NotNull(arguments).Length == 0) { Help(null); return; } string commandName = arguments[0]; ICommand command; InterpreterException.Assert(_commands.TryGetValue(commandName, out command), "Invalid command name: {0}", commandName); List args = new List(); for (int i = 1; i < arguments.Length; i++) args.Add(ExpandOptions(arguments[i])); command.Run(this, args.ToArray()); } class QuitException : Exception { } [Command("Quit", "Exit", Visible = false)] private void Quit() { throw new QuitException(); } /// called to handle error events durring processing protected virtual void OnError(Exception error) { Trace.TraceError(error.ToString()); if(error is OperationCanceledException || error is QuitException) {/* Silent */} else Console.Error.WriteLine(error is ApplicationException ? error.Message : error.ToString()); if (ErrorLevel == 0) ErrorLevel = 1; } /// Defines the filter precedence by appearance order of key character public string FilterPrecedence { get { return _filterPrecedence; } set { _filterPrecedence = Check.NotEmpty(value); _head = null; } } /// returns the chained filters private ICommandChain GetHead() { ICommandChain chain = _head; if (chain == null) { chain = new LastFilter(this); List filters = new List(_filters); filters.Sort(PrecedenceOrder); filters.Reverse();//add in reverse order foreach (ICommandFilter filter in filters) chain = new FilterChainItem(this, filter, chain); _head = chain; } return chain; } /// Compares the command filters in order of precendence private int PrecedenceOrder(ICommandFilter x, ICommandFilter y) { int posX = _filterPrecedence.IndexOfAny(x.Keys); posX = posX >= 0 ? posX : int.MaxValue; int posY = _filterPrecedence.IndexOfAny(y.Keys); posY = posY >= 0 ? posY : int.MaxValue; return posX.CompareTo(posY); } /// /// Run the command whos name is the first argument with the remaining arguments provided to the command /// as needed. /// public void Run(params string[] arguments) { try { GetHead().Next(Check.NotNull(arguments)); } catch (System.Threading.ThreadAbortException) { throw; } catch (QuitException) { throw; } catch (Exception e) { OnError(e); } } /// /// Run the command whos name is the first argument with the remaining arguments provided to the command /// as needed. /// public void Run(string[] arguments, TextWriter mapstdout, TextWriter mapstderr, TextReader mapstdin) { TextWriter stdout = ConsoleOutput.Capture(mapstdout); TextWriter stderr = ConsoleError.Capture(mapstderr); TextReader stdin = ConsoleInput.Capture(mapstdin); try { GetHead().Next(Check.NotNull(arguments)); } finally { ConsoleOutput.Restore(mapstdout, stdout); ConsoleError.Restore(mapstderr, stderr); ConsoleInput.Restore(mapstdin, stdin); } } /// /// Runs each line from the reader until EOF, can be used with Console.In /// public void Run(System.IO.TextReader input) { ICommand quit = Command.Make(this, this.GetType().GetMethod("Quit", BindingFlags.NonPublic| BindingFlags.Instance | BindingFlags.InvokeMethod)); AddCommand(quit); try { while (true) { try { Console.Write(ExpandOptions(Prompt)); string nextLine = input.ReadLine(); if (nextLine == null) break; string[] arguments = ArgumentList.Parse(nextLine); Run(arguments); } catch (System.Threading.ThreadAbortException) { throw; } catch (QuitException) { break; } catch (Exception e) { OnError(e); return; } Console.WriteLine(); } } finally { RemoveCommand(quit); } } static readonly Regex _optionName = new Regex(@"(?[\w]+)\)"); /// /// Expands '$(OptionName)' within the input string to the named option's value. /// public string ExpandOptions(string input) { // replaces $(OptionName) with value of OptionName return StringUtils.Transform(input, _optionName, delegate(Match m) { string optionName = m.Groups["Name"].Value; InterpreterException.Assert(_options.ContainsKey(optionName), "Unknown option specified: {0}", optionName); return String.Format("{0}", _options[optionName].Value); } ).Replace("$$", "$"); } /// Default inplementation of get keystroke private Char GetNextCharacter() { ProcessInfo pi = new ProcessInfo(); if (Constants.IsUnitTest) throw new InvalidOperationException(); return Console.ReadKey(true).KeyChar; } /// /// Reads a keystroke, not from the std:in stream, rather from the console or ui. /// public ReadNextCharacter ReadNextCharacter { get { return _fnNextCh; } set { _fnNextCh = Check.NotNull(value); } } /// /// Adds the specified attribute to every command argument by the given name. /// public void AddGlobalArgumentAttribute(string argumentName, Attribute attribute) { foreach (var command in _commands.Values) { foreach (var argument in command.Arguments) { if (argument.DisplayName == argumentName) argument.AddAttribute(attribute); } } } #region ConsoleWriter/ConsoleOutput/ConsoleError/ConsoleInput private abstract class ConsoleWriter : TextWriter { protected abstract TextWriter Writer { get; } public override void Close() { Writer.Close(); } protected override void Dispose(bool disposing) { } public override void Flush() { Writer.Flush(); } public override void Write(char value) { Writer.Write(value); } public override void Write(char[] buffer) { Writer.Write(buffer); } public override void Write(char[] buffer, int index, int count) { Writer.Write(buffer, index, count); } public override void Write(string value) { Writer.Write(value); } public override System.Text.Encoding Encoding { get { return Writer.Encoding; } } } private sealed class ConsoleOutput : ConsoleWriter { private static readonly ConsoleOutput _instance = new ConsoleOutput(); private static TextWriter _default, _expected; private static TextWriter _global; private static int _referenceCount = 0; [ThreadStatic] private static TextWriter _writer; public static TextWriter Capture(TextWriter output) { if (output == null) return null; lock (typeof (Console)) { if (1 == Interlocked.Increment(ref _referenceCount)) { _default = Console.Out; Console.SetOut(_instance); _expected = Console.Out; } else if (!ReferenceEquals(_expected, Console.Out)) { Console.SetOut(_instance); _expected = Console.Out; } Interlocked.Exchange(ref _global, output); return Interlocked.Exchange(ref _writer, output); } } public static void Restore(TextWriter replaced, TextWriter original) { if (replaced == null) return; lock (typeof (Console)) { Interlocked.CompareExchange(ref _writer, original, replaced); Interlocked.CompareExchange(ref _global, original, replaced); if (0 == Interlocked.Decrement(ref _referenceCount)) { Console.SetOut(_default); _default = null; } else if (!ReferenceEquals(_expected, Console.Out)) { Console.SetOut(_instance); _expected = Console.Out; } } } protected override TextWriter Writer { get { return _writer ?? _global ?? _default; } } } private sealed class ConsoleError : ConsoleWriter { private static readonly ConsoleError _instance = new ConsoleError(); private static TextWriter _default, _expected; private static TextWriter _global; private static int _referenceCount = 0; private ConsoleError() { } [ThreadStatic] private static TextWriter _writer; public static TextWriter Capture(TextWriter output) { if (output == null) return null; lock (typeof(Console)) { if (1 == Interlocked.Increment(ref _referenceCount)) { _default = Console.Error; Console.SetError(_instance); _expected = Console.Error; } else if (!ReferenceEquals(_expected, Console.Error)) { Console.SetError(_instance); _expected = Console.Error; } Interlocked.Exchange(ref _global, output); return Interlocked.Exchange(ref _writer, output); } } public static void Restore(TextWriter replaced, TextWriter original) { if (replaced == null) return; lock (typeof(Console)) { Interlocked.CompareExchange(ref _writer, original, replaced); Interlocked.CompareExchange(ref _global, original, replaced); if (0 == Interlocked.Decrement(ref _referenceCount)) { Console.SetError(_default); _default = null; } else if (!ReferenceEquals(_expected, Console.Error)) { Console.SetError(_instance); _expected = Console.Error; } } } protected override TextWriter Writer { get { return _writer ?? _global ?? _default; } } } private sealed class ConsoleInput : TextReader { private static readonly ConsoleInput _instance = new ConsoleInput(); private static TextReader _default, _expected; private static TextReader _global; private static int _referenceCount = 0; private ConsoleInput() { } [ThreadStatic] private static TextReader _reader; public static TextReader Capture(TextReader output) { if (output == null) return null; lock (typeof(Console)) { if (1 == Interlocked.Increment(ref _referenceCount)) { _default = Console.In; Console.SetIn(_instance); _expected = Console.In; } else if (!ReferenceEquals(_expected, Console.In)) { Console.SetIn(_instance); _expected = Console.In; } Interlocked.Exchange(ref _global, output); return Interlocked.Exchange(ref _reader, output); } } public static void Restore(TextReader replaced, TextReader original) { if (replaced == null) return; lock (typeof(Console)) { Interlocked.CompareExchange(ref _reader, original, replaced); Interlocked.CompareExchange(ref _global, original, replaced); if (0 == Interlocked.Decrement(ref _referenceCount)) { Console.SetIn(_default); _default = null; } else if (!ReferenceEquals(_expected, Console.In)) { Console.SetIn(_instance); _expected = Console.In; } } } private TextReader Reader { get { return _reader ?? _global ?? _default; } } public override void Close() { Reader.Close(); } protected override void Dispose(bool disposing) { } public override int Peek() { return Reader.Peek(); } public override int Read() { return Reader.Read(); } public override int Read(char[] buffer, int index, int count) { return Reader.Read(buffer, index, count); } public override string ReadToEnd() { return Reader.ReadToEnd(); } public override int ReadBlock(char[] buffer, int index, int count) { return Reader.ReadBlock(buffer, index, count); } public override string ReadLine() { return Reader.ReadLine(); } } #endregion } }