5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne
\r
7 // Permission is hereby granted, free of charge, to any person obtaining a copy
\r
8 // of this software and associated documentation files (the "Software"), to deal
\r
9 // in the Software without restriction, including without limitation the rights
\r
10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\r
11 // copies of the Software, and to permit persons to whom the Software is
\r
12 // furnished to do so, subject to the following conditions:
\r
14 // The above copyright notice and this permission notice shall be included in
\r
15 // all copies or substantial portions of the Software.
\r
17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\r
18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\r
19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\r
20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\r
21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\r
22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
\r
30 using System.Reflection;
\r
31 using System.Collections.Generic;
\r
34 using DbMetal.Utility;
\r
39 /// Parameters base class.
\r
40 /// Allows to specify direct switches or place switches in a file (specified with @fileName).
\r
41 /// If a file specifies several line, the parameters will allow batch processing, one per line.
\r
42 /// Parameters specified before the @ file are inherited by each @ file line
\r
44 public abstract class AbstractParameters
\r
47 /// Describes a switch (/sprocs)
\r
49 public class OptionAttribute : Attribute
\r
52 /// Allows to specify a group. All options in the same group are displayed together
\r
54 public int Group { get; set; }
\r
59 public string Text { get; set; }
\r
62 /// Value name, used for help
\r
64 public string ValueName { get; set; }
\r
66 public OptionAttribute(string text)
\r
73 /// Describes an input file
\r
75 public class FileAttribute : Attribute
\r
78 /// Tells if the file is required
\r
79 /// TODO: add mandatory support in parameters check
\r
81 public bool Mandatory { get; set; }
\r
83 /// The name written in help
\r
85 public string Name { get; set; }
\r
89 public string Text { get; set; }
\r
91 public FileAttribute(string name, string text)
\r
98 public class AlternateAttribute : Attribute
\r
100 public string Name { get; set; }
\r
102 public AlternateAttribute(string name)
\r
108 public readonly IList<string> Extra = new List<string>();
\r
109 private TextWriter log;
\r
110 public TextWriter Log
\r
112 get { return log ?? Console.Out; }
\r
113 set { log = value; }
\r
116 private static bool IsParameter(string arg, string switchPrefix, out string parameterName, out string parameterValue)
\r
119 if (arg.StartsWith(switchPrefix))
\r
121 isParameter = true;
\r
122 string nameValue = arg.Substring(switchPrefix.Length);
\r
123 int separator = nameValue.IndexOfAny(new[] { ':', '=' });
\r
124 if (separator >= 0)
\r
126 parameterName = nameValue.Substring(0, separator);
\r
127 parameterValue = nameValue.Substring(separator + 1).Trim('\"');
\r
129 else if (nameValue.EndsWith("+"))
\r
131 parameterName = nameValue.Substring(0, nameValue.Length - 1);
\r
132 parameterValue = "+";
\r
134 else if (nameValue.EndsWith("-"))
\r
136 parameterName = nameValue.Substring(0, nameValue.Length - 1);
\r
137 parameterValue = "-";
\r
139 else if (nameValue.StartsWith("no-"))
\r
141 parameterName = nameValue.Substring(3);
\r
142 parameterValue = "-";
\r
146 parameterName = nameValue;
\r
147 parameterValue = null;
\r
152 isParameter = false;
\r
153 parameterName = null;
\r
154 parameterValue = null;
\r
156 return isParameter;
\r
159 protected static bool IsParameter(string arg, out string parameterName, out string parameterValue)
\r
161 return IsParameter(arg, "--", out parameterName, out parameterValue)
\r
162 || IsParameter(arg, "-", out parameterName, out parameterValue)
\r
163 || IsParameter(arg, "/", out parameterName, out parameterValue);
\r
166 protected static object GetValue(string value, Type targetType)
\r
169 if (typeof(bool).IsAssignableFrom(targetType))
\r
171 if (value == null || value == "+")
\r
173 else if (value == "-")
\r
174 typedValue = false;
\r
176 typedValue = Convert.ToBoolean(value);
\r
180 typedValue = Convert.ChangeType(value, targetType);
\r
185 protected virtual MemberInfo FindParameter(string name, Type type)
\r
187 // the easy way: find propery or field name
\r
188 var flags = BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public;
\r
189 var memberInfos = type.GetMember(name, flags);
\r
190 if (memberInfos.Length > 0)
\r
191 return memberInfos[0];
\r
192 // the hard way: look for alternate names
\r
193 memberInfos = type.GetMembers();
\r
194 foreach (var memberInfo in memberInfos)
\r
196 var alternates = (AlternateAttribute[])memberInfo.GetCustomAttributes(typeof(AlternateAttribute), true);
\r
197 if (Array.Exists(alternates, a => string.Compare(a.Name, name) == 0))
\r
203 protected virtual MemberInfo FindParameter(string name)
\r
205 return FindParameter(name, GetType());
\r
209 /// Assigns a parameter by reflection
\r
211 /// <param name="name">parameter name (case insensitive)</param>
\r
212 /// <param name="value">parameter value</param>
\r
213 protected void SetParameter(string name, string value)
\r
215 // cleanup and evaluate
\r
216 name = name.Trim();
\r
218 value = value.EvaluateEnvironment();
\r
220 var memberInfo = FindParameter(name);
\r
221 if (memberInfo == null)
\r
222 throw new ArgumentException(string.Format("Parameter {0} does not exist", name));
\r
223 memberInfo.SetMemberValue(this, GetValue(value, memberInfo.GetMemberType()));
\r
227 /// Loads arguments from a given list
\r
229 /// <param name="args"></param>
\r
230 public void Load(IList<string> args)
\r
232 foreach (string arg in args)
\r
235 if (IsParameter(arg, out key, out value))
\r
236 SetParameter(key, value);
\r
242 protected AbstractParameters()
\r
246 protected AbstractParameters(IList<string> args)
\r
252 /// Internal method allowing to extract arguments and specify quotes characters
\r
254 /// <param name="commandLine"></param>
\r
255 /// <param name="quotes"></param>
\r
256 /// <returns></returns>
\r
257 public IList<string> ExtractArguments(string commandLine, char[] quotes)
\r
259 var arg = new StringBuilder();
\r
260 var args = new List<string>();
\r
261 const char zero = '\0';
\r
263 foreach (char c in commandLine)
\r
267 if (quotes.Contains(c))
\r
269 else if (char.IsSeparator(c) && quote == zero)
\r
271 if (arg.Length > 0)
\r
273 args.Add(arg.ToString());
\r
274 arg = new StringBuilder();
\r
288 if (arg.Length > 0)
\r
289 args.Add(arg.ToString());
\r
293 private static readonly char[] Quotes = new[] { '\'', '\"' };
\r
295 /// Extracts arguments from a full line, in a .NET compatible way
\r
296 /// (includes strange quotes trimming)
\r
298 /// <param name="commandLine">The command line</param>
\r
299 /// <returns>Arguments list</returns>
\r
300 public IList<string> ExtractArguments(string commandLine)
\r
302 return ExtractArguments(commandLine, Quotes);
\r
306 /// Converts a list separated by a comma to a string array
\r
308 /// <param name="list"></param>
\r
309 /// <returns></returns>
\r
310 public string[] GetArray(string list)
\r
312 if (string.IsNullOrEmpty(list))
\r
313 return new string[0];
\r
314 return (from entityInterface in list.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
\r
315 select entityInterface.Trim()).ToArray();
\r
319 /// Processes different "lines" of parameters:
\r
320 /// 1. the original input parameter must be starting with @
\r
321 /// 2. all other parameters are kept as a common part
\r
323 /// <typeparam name="P"></typeparam>
\r
324 /// <param name="args"></param>
\r
325 /// <returns></returns>
\r
326 protected IList<P> GetParameterBatch<P>(IList<string> args)
\r
327 where P : AbstractParameters, new()
\r
329 return GetParameterBatch<P>(args, ".");
\r
332 public IList<P> GetParameterBatch<P>(IList<string> args, string argsFileDirectory)
\r
333 where P : AbstractParameters, new()
\r
335 var parameters = new List<P>();
\r
336 var commonArgs = new List<string>();
\r
337 var argsFiles = new List<string>();
\r
338 foreach (var arg in args)
\r
340 if (arg.StartsWith("@"))
\r
341 argsFiles.Add(arg.Substring(1));
\r
343 commonArgs.Add(arg);
\r
345 // if we specify files, we must recurse
\r
346 if (argsFiles.Count > 0)
\r
348 foreach (var argsFile in argsFiles)
\r
350 parameters.AddRange(GetParameterBatchFile<P>(commonArgs, Path.Combine(argsFileDirectory, argsFile)));
\r
353 // if we don't, just use the args
\r
354 else if (commonArgs.Count > 0)
\r
356 var p = new P { Log = Log };
\r
357 p.Load(commonArgs);
\r
363 private IList<P> GetParameterBatchFile<P>(IEnumerable<string> baseArgs, string argsList)
\r
364 where P : AbstractParameters, new()
\r
366 var parameters = new List<P>();
\r
367 string argsFileDirectory = Path.GetDirectoryName(argsList);
\r
368 using (var textReader = File.OpenText(argsList))
\r
370 while (!textReader.EndOfStream)
\r
372 string line = textReader.ReadLine();
\r
373 if (line.StartsWith("#"))
\r
375 var args = ExtractArguments(line);
\r
376 var allArgs = new List<string>(baseArgs);
\r
377 allArgs.AddRange(args);
\r
378 parameters.AddRange(GetParameterBatch<P>(allArgs, argsFileDirectory));
\r
385 /// Outputs a formatted string to the console.
\r
386 /// We're not using the ILogger here, since we want console output.
\r
388 /// <param name="format"></param>
\r
389 /// <param name="args"></param>
\r
390 public void Write(string format, params object[] args)
\r
392 Output.WriteLine(Log, OutputLevel.Information, format, args);
\r
396 /// Outputs an empty line
\r
398 public void WriteLine()
\r
400 Output.WriteLine(Log, OutputLevel.Information, string.Empty);
\r
403 // TODO: remove this
\r
404 protected static int TextWidth
\r
406 get { return Console.BufferWidth; }
\r
410 /// Returns the application (assembly) name (without extension)
\r
412 protected static string ApplicationName
\r
416 return Assembly.GetEntryAssembly().GetName().Name;
\r
421 /// Returns the application (assembly) version
\r
423 protected static Version ApplicationVersion
\r
427 return Assembly.GetEntryAssembly().GetName().Version;
\r
431 private bool headerWritten;
\r
433 /// Writes the application header
\r
435 public void WriteHeader()
\r
437 if (!headerWritten)
\r
439 WriteHeaderContents();
\r
441 headerWritten = true;
\r
445 protected abstract void WriteHeaderContents();
\r
448 /// Writes a small summary
\r
450 public abstract void WriteSummary();
\r
453 /// Writes examples
\r
455 public virtual void WriteExamples()
\r
460 /// The "syntax" is a bried containing the application name, "[options]" and eventually files.
\r
461 /// For example: "DbMetal [options] [<input file>]
\r
463 public virtual void WriteSyntax()
\r
465 var syntax = new StringBuilder();
\r
466 syntax.AppendFormat("{0} [options]", ApplicationName);
\r
467 foreach (var file in GetFiles())
\r
469 if (file.Description.Mandatory)
\r
470 syntax.AppendFormat(" {0}", GetFileText(file));
\r
472 syntax.AppendFormat(" [{0}]", GetFileText(file));
\r
474 Write(syntax.ToString());
\r
478 /// Describes an option
\r
480 protected class Option
\r
483 /// The member name (property or field)
\r
485 public string Name { get; set; }
\r
487 /// The attribute used to define the member as an option
\r
489 public OptionAttribute Description { get; set; }
\r
493 /// Describes an input file
\r
495 protected class FileName
\r
498 /// The member name (property or field)
\r
500 public string Name { get; set; }
\r
502 /// The attribute used to define the member as an input file
\r
504 public FileAttribute Description { get; set; }
\r
508 /// Internal class. I wrote it because I was thinking that the .NET framework already had such a class.
\r
509 /// At second thought, I may have made a confusion with STL
\r
510 /// (interesting, isn't it?)
\r
512 /// <typeparam name="A"></typeparam>
\r
513 /// <typeparam name="B"></typeparam>
\r
514 protected class Pair<A, B>
\r
516 public A First { get; set; }
\r
517 public B Second { get; set; }
\r
521 /// Enumerates all members (fields or properties) that have been marked with the specified attribute
\r
523 /// <typeparam name="T">The attribute type to search for</typeparam>
\r
524 /// <returns>A list of pairs with name and attribute</returns>
\r
525 protected IEnumerable<Pair<string, T>> EnumerateOptions<T>()
\r
526 where T : Attribute
\r
528 Type t = GetType();
\r
529 foreach (var propertyInfo in t.GetProperties())
\r
531 var descriptions = (T[])propertyInfo.GetCustomAttributes(typeof(T), true);
\r
532 if (descriptions.Length == 1)
\r
533 yield return new Pair<string, T> { First = propertyInfo.Name, Second = descriptions[0] };
\r
535 foreach (var fieldInfo in t.GetFields())
\r
537 var descriptions = (T[])fieldInfo.GetCustomAttributes(typeof(T), true);
\r
538 if (descriptions.Length == 1)
\r
539 yield return new Pair<string, T> { First = fieldInfo.Name, Second = descriptions[0] };
\r
543 protected IEnumerable<Option> EnumerateOptions()
\r
545 foreach (var pair in EnumerateOptions<OptionAttribute>())
\r
546 yield return new Option { Name = pair.First, Description = pair.Second };
\r
549 protected IEnumerable<FileName> GetFiles()
\r
551 foreach (var pair in from p in EnumerateOptions<FileAttribute>() orderby p.Second.Mandatory select p)
\r
552 yield return new FileName { Name = pair.First, Description = pair.Second };
\r
556 /// Returns options, grouped by group (the group number is the dictionary key)
\r
558 /// <returns></returns>
\r
559 protected IDictionary<int, IList<Option>> GetOptions()
\r
561 var options = new Dictionary<int, IList<Option>>();
\r
562 foreach (var option in EnumerateOptions())
\r
564 if (!options.ContainsKey(option.Description.Group))
\r
565 options[option.Description.Group] = new List<Option>();
\r
566 options[option.Description.Group].Add(option);
\r
572 /// Return a literal value based on an option
\r
574 /// <param name="option"></param>
\r
575 /// <returns></returns>
\r
576 protected virtual string GetOptionText(Option option)
\r
578 var optionName = option.Name[0].ToString().ToLower() + option.Name.Substring(1);
\r
579 if (string.IsNullOrEmpty(option.Description.ValueName))
\r
581 return string.Format("{0}:<{1}>",
\r
583 option.Description.ValueName);
\r
587 /// Returns a literal value base on an input file
\r
589 /// <param name="fileName"></param>
\r
590 /// <returns></returns>
\r
591 protected virtual string GetFileText(FileName fileName)
\r
593 return string.Format("<{0}>", fileName.Description.Name);
\r
597 /// Computes the maximum options and files length, to align all descriptions
\r
599 /// <param name="options"></param>
\r
600 /// <param name="files"></param>
\r
601 /// <returns></returns>
\r
602 private int GetMaximumLength(IDictionary<int, IList<Option>> options, IEnumerable<FileName> files)
\r
605 foreach (var optionsList in options.Values)
\r
607 foreach (var option in optionsList)
\r
609 var optionName = GetOptionText(option);
\r
610 int length = optionName.Length;
\r
611 if (length > maxLength)
\r
612 maxLength = length;
\r
615 foreach (var file in files)
\r
617 var fileName = GetFileText(file);
\r
618 int length = fileName.Length;
\r
619 if (length > maxLength)
\r
620 maxLength = length;
\r
625 protected static string[] SplitText(string text, int width)
\r
627 var lines = new List<string>(new[] { "" });
\r
628 var words = text.Split(' ');
\r
629 foreach (var word in words)
\r
631 var line = lines.Last();
\r
632 if (line.Length == 0)
\r
633 lines[lines.Count - 1] = word;
\r
634 else if (line.Length + word.Length + 1 < width)
\r
635 lines[lines.Count - 1] = line + " " + word;
\r
639 return lines.ToArray();
\r
642 protected void WriteOption(string firstLine, string text)
\r
644 int width = TextWidth - firstLine.Length - 2;
\r
645 var lines = SplitText(text, width);
\r
646 var padding = string.Empty.PadRight(firstLine.Length);
\r
647 for (int i = 0; i < lines.Length; i++)
\r
649 Write("{0} {1}", i == 0 ? firstLine : padding, lines[i]);
\r
654 /// Displays all available options and files
\r
656 protected void WriteOptions()
\r
658 var options = GetOptions();
\r
659 var files = GetFiles();
\r
660 int maxLength = GetMaximumLength(options, files);
\r
662 foreach (var group in from k in options.Keys orderby k select k)
\r
664 var optionsList = options[group];
\r
665 foreach (var option in from o in optionsList orderby o.Name select o)
\r
667 WriteOption(string.Format(" /{0}", GetOptionText(option).PadRight(maxLength)), option.Description.Text);
\r
671 foreach (var file in files)
\r
673 WriteOption(string.Format(" {0}", GetFileText(file).PadRight(maxLength + 1)), file.Description.Text);
\r
678 /// Displays application help
\r
680 public void WriteHelp()
\r
682 WriteHeader(); // includes a WriteLine()
\r