#region Copyright 2008-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.Text.RegularExpressions; using System.Text; namespace CSharpTest.Net.Utils { /// /// This is a private class as the means of sharing is to simply include the source file not /// reference a library. /// [System.Diagnostics.DebuggerNonUserCode] partial class ArgumentList : System.Collections.ObjectModel.KeyedCollection { #region Static Configuration Options static StringComparer _defaultCompare = StringComparer.OrdinalIgnoreCase; static char[] _prefix = new char[] { '/', '-' }; static char[] _delim = new char[] { ':', '=' }; static readonly string[] EmptyList = new string[0]; /// /// Controls the default string comparer used for this class /// public static StringComparer DefaultComparison { get { return _defaultCompare; } set { if (value == null) throw new ArgumentNullException(); _defaultCompare = value; } } /// /// Controls the allowable prefix characters that will preceed named arguments /// public static char[] PrefixChars { get { return (char[])_prefix.Clone(); } set { if (value == null) throw new ArgumentNullException(); if (value.Length == 0) throw new ArgumentOutOfRangeException(); _prefix = (char[])value.Clone(); } } /// /// Controls the allowable delimeter characters seperate argument names from values /// public static char[] NameDelimeters { get { return (char[])_delim.Clone(); } set { if (value == null) throw new ArgumentNullException(); if (value.Length == 0) throw new ArgumentOutOfRangeException(); _delim = (char[])value.Clone(); } } #endregion Static Configuration Options readonly List _unnamed; /// /// Initializes a new instance of the ArgumentList class using the argument list provided /// public ArgumentList(params string[] arguments) : this(DefaultComparison, arguments) { } /// /// Initializes a new instance of the ArgumentList class using the argument list provided /// and using the string comparer provided, by default this is case-insensitive /// public ArgumentList(StringComparer comparer, params string[] arguments) : base(comparer, 0) { _unnamed = new List(); this.AddRange(arguments); } /// /// Returns a list of arguments that did not start with a character in the PrefixChars /// static collection. These arguments can be modified by the methods on the returned /// collection, or you set this property to a new collection (a copy is made). /// public IList Unnamed { get { return _unnamed; } set { _unnamed.Clear(); if (value != null) _unnamed.AddRange(value); } } /// /// Parses the strings provided for switch names and optionally values, by default in one /// of the following forms: "/name=value", "/name:value", "-name=value", "-name:value" /// public void AddRange(params string[] arguments) { if (arguments == null) throw new ArgumentNullException(); foreach (string arg in arguments) { string name, value; if (TryParseNameValue(arg, out name, out value)) Add(name, value); else _unnamed.Add(CleanArgument(arg)); } } /// /// Adds a name/value pair to the collection of arguments, if value is null the name is /// added with no values. /// public void Add(string name, string value) { if (name == null) throw new ArgumentNullException(); Item item; if (!TryGetValue(name, out item)) base.Add(item = new Item(name)); if (value != null) item.Add(value); } /// /// A string collection of all keys in the arguments /// public string[] Keys { get { if (Dictionary == null) return new string[0]; List list = new List(Dictionary.Keys); list.Sort(); return list.ToArray(); } } /// /// Returns true if the value was found by that name and set the output value /// public bool TryGetValue(string name, out Item value) { if (name == null) throw new ArgumentNullException(); if (Dictionary != null) return Dictionary.TryGetValue(name, out value); value = null; return false; } /// /// Returns true if the value was found by that name and set the output value /// public bool TryGetValue(string name, out string value) { if (name == null) throw new ArgumentNullException(); Item test; if (Dictionary != null && Dictionary.TryGetValue(name, out test)) { value = test.Value; return true; } value = null; return false; } /// /// Returns an Item of name even if it does not exist /// public Item SafeGet(string name) { Item result; if (TryGetValue(name, out result)) return result; return new Item(name, null); } #region Protected / Private operations... static string CleanArgument(string argument) { if (argument == null) throw new ArgumentNullException(); if (argument.Length >= 2 && argument[0] == '"' && argument[argument.Length - 1] == '"') argument = argument.Substring(1, argument.Length - 2).Replace("\"\"", "\""); return argument; } /// /// Attempts to parse a name value pair from '/name=value' format /// public static bool TryParseNameValue(string argument, out string name, out string value) { argument = CleanArgument(argument);//strip quotes name = value = null; if (String.IsNullOrEmpty(argument) || 0 != argument.IndexOfAny(_prefix, 0, 1)) return false; name = argument.Substring(1); if (String.IsNullOrEmpty(name)) return false; int endName = name.IndexOfAny(_delim, 1); if (endName > 0) { value = name.Substring(endName + 1); name = name.Substring(0, endName); } return true; } /// /// Searches the arguments until it finds a switch or value by the name in find and /// if found it will: /// A) Remove the item from the arguments /// B) Set the out parameter value to any value found, or null if just '/name' /// C) Returns true that it was found and removed. /// public static bool Remove(ref string[] arguments, string find, out string value) { value = null; for (int i = 0; i < arguments.Length; i++) { string name, setting; if (TryParseNameValue(arguments[i], out name, out setting) && _defaultCompare.Equals(name, find)) { List args = new List(arguments); args.RemoveAt(i); arguments = args.ToArray(); value = setting; return true; } } return false; } /// /// Abract override for extracting key /// protected override string GetKeyForItem(ArgumentList.Item item) { return item.Name; } #endregion #region Item class used for collection /// /// This is a single named argument within an argument list collection, this /// can be implicitly assigned to a string, or a string[] array /// [System.Diagnostics.DebuggerNonUserCode] public class Item : System.Collections.ObjectModel.Collection { private readonly string _name; private readonly List _values; /// /// Constructs an item for the name and values provided. /// public Item(string name, params string[] items) : this(new List(), name, items) { } private Item(List impl, string name, string[] items) : base(impl) { if (name == null) throw new ArgumentNullException(); _name = name; _values = impl; if (items != null) _values.AddRange(items); } /// /// Returns the name of this item /// public string Name { get { return _name; } } /// /// Returns the first value of this named item or null if one doesn't exist /// public string Value { get { return _values.Count > 0 ? _values[0] : null; } set { _values.Clear(); if (value != null) _values.Add(value); } } /// /// Returns the collection of items in this named slot /// public string[] Values { get { return _values.ToArray(); } set { _values.Clear(); if (value != null) _values.AddRange(value); } } /// /// Same as the .Values property, returns the collection of items in this named slot /// /// public string[] ToArray() { return _values.ToArray(); } /// /// Add one or more values to this named item /// public void AddRange(IEnumerable items) { _values.AddRange(items); } /// /// Converts this item to key-value pair to rem to a dictionary /// public static implicit operator KeyValuePair(Item item) { if (item == null) throw new ArgumentNullException(); return new KeyValuePair(item.Name, item.Values); } /// /// Converts this item to a string by getting the first value or null if none /// public static implicit operator string(Item item) { return item == null ? null : item.Value; } /// /// Converts this item to array of strings /// public static implicit operator string[](Item item) { return item == null ? null : item.Values; } } #endregion Item class used for collection private class ArgReader { const char CharEmpty = (char)0; char[] _chars; int _pos; public ArgReader(string data) { _chars = data.ToCharArray(); _pos = 0; } public bool MoveNext() { _pos++; return _pos < _chars.Length; } public char Current { get { return (_pos < _chars.Length) ? _chars[_pos] : CharEmpty; } } public bool IsWhiteSpace { get { return Char.IsWhiteSpace(Current); } } public bool IsQuote { get { return (Current == '"'); } } public bool IsEOF { get { return _pos >= _chars.Length; } } } /// Parses the individual arguments from the given input string. public static string[] Parse(string rawtext) { List list = new List(); if (rawtext == null) throw new ArgumentNullException("rawtext"); ArgReader characters = new ArgReader(rawtext.Trim()); while (!characters.IsEOF) { if (characters.IsWhiteSpace) { characters.MoveNext(); continue; } StringBuilder sb = new StringBuilder(); if (characters.IsQuote) {//quoted string while (characters.MoveNext()) { if (characters.IsQuote) { if (!characters.MoveNext() || characters.IsWhiteSpace) break; } sb.Append(characters.Current); } } else { sb.Append(characters.Current); while (characters.MoveNext()) { if (characters.IsWhiteSpace) break; sb.Append(characters.Current); } } list.Add(sb.ToString()); } return list.ToArray(); } /// The inverse of Parse, joins the arguments together and properly escapes output [Obsolete("Consider migrating to EscapeArguments as it correctly escapes some situations that Join does not.")] public static string Join(params string[] arguments) { if (arguments == null) throw new ArgumentNullException("arguments"); char[] escaped = " \t\"&()[]{}^=;!'+,`~".ToCharArray(); StringBuilder sb = new StringBuilder(); foreach (string argument in arguments) { string arg = argument; if( arg.IndexOfAny(escaped) >= 0 ) sb.AppendFormat("\"{0}\"", arg.Replace("\"", "\"\"")); else sb.Append(arg); sb.Append(' '); } return sb.ToString(0, Math.Max(0, sb.Length-1)); } /// The 'more' correct escape/join for arguments public static string EscapeArguments(params string[] args) { StringBuilder arguments = new StringBuilder(); Regex invalidChar = new Regex("[\x00\x0a\x0d]");// these can not be escaped Regex needsQuotes = new Regex(@"\s|""");// contains whitespace or two quote characters Regex escapeQuote = new Regex(@"(\\*)(""|$)");// one or more '\' followed with a quote or end of string for (int carg = 0; args != null && carg < args.Length; carg++) { if (args[carg] == null) { throw new ArgumentNullException("args[" + carg + "]"); } if (invalidChar.IsMatch(args[carg])) { throw new ArgumentOutOfRangeException("args[" + carg + "]"); } if (args[carg] == String.Empty) { arguments.Append("\"\""); } else if (!needsQuotes.IsMatch(args[carg])) { arguments.Append(args[carg]); } else { arguments.Append('"'); arguments.Append(escapeQuote.Replace(args[carg], delegate(Match m) { return m.Groups[1].Value + m.Groups[1].Value + (m.Groups[2].Value == "\"" ? "\\\"" : ""); } )); arguments.Append('"'); } if (carg + 1 < args.Length) arguments.Append(' '); } return arguments.ToString(); } } }