1 #region MIT license 2 // 3 // MIT license 4 // 5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to deal 9 // in the Software without restriction, including without limitation the rights 10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 // copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in 15 // all copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 // THE SOFTWARE. 24 // 25 #endregion 26 27 using System; 28 using System.IO; 29 using System.Linq; 30 using System.Reflection; 31 using System.Collections.Generic; 32 using System.Text; 33 using DbLinq.Util; 34 using DbMetal.Utility; 35 36 namespace DbMetal 37 { 38 /// <summary> 39 /// Parameters base class. 40 /// Allows to specify direct switches or place switches in a file (specified with @fileName). 41 /// If a file specifies several line, the parameters will allow batch processing, one per line. 42 /// Parameters specified before the @ file are inherited by each @ file line 43 /// </summary> 44 public abstract class AbstractParameters 45 { 46 /// <summary> 47 /// Describes a switch (/sprocs) 48 /// </summary> 49 public class OptionAttribute : Attribute 50 { 51 /// <summary> 52 /// Allows to specify a group. All options in the same group are displayed together 53 /// </summary> 54 public int Group { get; set; } 55 56 /// <summary> 57 /// Description 58 /// </summary> 59 public string Text { get; set; } 60 61 /// <summary> 62 /// Value name, used for help 63 /// </summary> 64 public string ValueName { get; set; } 65 OptionAttribute(string text)66 public OptionAttribute(string text) 67 { 68 Text = text; 69 } 70 } 71 72 /// <summary> 73 /// Describes an input file 74 /// </summary> 75 public class FileAttribute : Attribute 76 { 77 /// <summary> 78 /// Tells if the file is required 79 /// TODO: add mandatory support in parameters check 80 /// </summary> 81 public bool Mandatory { get; set; } 82 /// <summary> 83 /// The name written in help 84 /// </summary> 85 public string Name { get; set; } 86 /// <summary> 87 /// Descriptions 88 /// </summary> 89 public string Text { get; set; } 90 FileAttribute(string name, string text)91 public FileAttribute(string name, string text) 92 { 93 Name = name; 94 Text = text; 95 } 96 } 97 98 public class AlternateAttribute : Attribute 99 { 100 public string Name { get; set; } 101 AlternateAttribute(string name)102 public AlternateAttribute(string name) 103 { 104 Name = name; 105 } 106 } 107 108 public readonly IList<string> Extra = new List<string>(); 109 private TextWriter log; 110 public TextWriter Log 111 { 112 get { return log ?? Console.Out; } 113 set { log = value; } 114 } 115 IsParameter(string arg, string switchPrefix, out string parameterName, out string parameterValue)116 private static bool IsParameter(string arg, string switchPrefix, out string parameterName, out string parameterValue) 117 { 118 bool isParameter; 119 if (arg.StartsWith(switchPrefix)) 120 { 121 isParameter = true; 122 string nameValue = arg.Substring(switchPrefix.Length); 123 int separator = nameValue.IndexOfAny(new[] { ':', '=' }); 124 if (separator >= 0) 125 { 126 parameterName = nameValue.Substring(0, separator); 127 parameterValue = nameValue.Substring(separator + 1).Trim('\"'); 128 } 129 else if (nameValue.EndsWith("+")) 130 { 131 parameterName = nameValue.Substring(0, nameValue.Length - 1); 132 parameterValue = "+"; 133 } 134 else if (nameValue.EndsWith("-")) 135 { 136 parameterName = nameValue.Substring(0, nameValue.Length - 1); 137 parameterValue = "-"; 138 } 139 else if (nameValue.StartsWith("no-")) 140 { 141 parameterName = nameValue.Substring(3); 142 parameterValue = "-"; 143 } 144 else 145 { 146 parameterName = nameValue; 147 parameterValue = null; 148 } 149 } 150 else 151 { 152 isParameter = false; 153 parameterName = null; 154 parameterValue = null; 155 } 156 return isParameter; 157 } 158 IsParameter(string arg, out string parameterName, out string parameterValue)159 protected static bool IsParameter(string arg, out string parameterName, out string parameterValue) 160 { 161 return IsParameter(arg, "--", out parameterName, out parameterValue) 162 || IsParameter(arg, "-", out parameterName, out parameterValue) 163 || IsParameter(arg, "/", out parameterName, out parameterValue); 164 } 165 GetValue(string value, Type targetType)166 protected static object GetValue(string value, Type targetType) 167 { 168 object typedValue; 169 if (typeof(bool).IsAssignableFrom(targetType)) 170 { 171 if (value == null || value == "+") 172 typedValue = true; 173 else if (value == "-") 174 typedValue = false; 175 else 176 typedValue = Convert.ToBoolean(value); 177 } 178 else 179 { 180 typedValue = Convert.ChangeType(value, targetType); 181 } 182 return typedValue; 183 } 184 FindParameter(string name, Type type)185 protected virtual MemberInfo FindParameter(string name, Type type) 186 { 187 // the easy way: find propery or field name 188 var flags = BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public; 189 var memberInfos = type.GetMember(name, flags); 190 if (memberInfos.Length > 0) 191 return memberInfos[0]; 192 // the hard way: look for alternate names 193 memberInfos = type.GetMembers(); 194 foreach (var memberInfo in memberInfos) 195 { 196 var alternates = (AlternateAttribute[])memberInfo.GetCustomAttributes(typeof(AlternateAttribute), true); 197 if (Array.Exists(alternates, a => string.Compare(a.Name, name) == 0)) 198 return memberInfo; 199 } 200 return null; 201 } 202 FindParameter(string name)203 protected virtual MemberInfo FindParameter(string name) 204 { 205 return FindParameter(name, GetType()); 206 } 207 208 /// <summary> 209 /// Assigns a parameter by reflection 210 /// </summary> 211 /// <param name="name">parameter name (case insensitive)</param> 212 /// <param name="value">parameter value</param> SetParameter(string name, string value)213 protected void SetParameter(string name, string value) 214 { 215 // cleanup and evaluate 216 name = name.Trim(); 217 // evaluate 218 value = value.EvaluateEnvironment(); 219 220 var memberInfo = FindParameter(name); 221 if (memberInfo == null) 222 throw new ArgumentException(string.Format("Parameter {0} does not exist", name)); 223 memberInfo.SetMemberValue(this, GetValue(value, memberInfo.GetMemberType())); 224 } 225 226 /// <summary> 227 /// Loads arguments from a given list 228 /// </summary> 229 /// <param name="args"></param> Load(IList<string> args)230 public void Load(IList<string> args) 231 { 232 foreach (string arg in args) 233 { 234 string key, value; 235 if (IsParameter(arg, out key, out value)) 236 SetParameter(key, value); 237 else 238 Extra.Add(arg); 239 } 240 } 241 AbstractParameters()242 protected AbstractParameters() 243 { 244 } 245 AbstractParameters(IList<string> args)246 protected AbstractParameters(IList<string> args) 247 { 248 Load(args); 249 } 250 251 /// <summary> 252 /// Internal method allowing to extract arguments and specify quotes characters 253 /// </summary> 254 /// <param name="commandLine"></param> 255 /// <param name="quotes"></param> 256 /// <returns></returns> ExtractArguments(string commandLine, char[] quotes)257 public IList<string> ExtractArguments(string commandLine, char[] quotes) 258 { 259 var arg = new StringBuilder(); 260 var args = new List<string>(); 261 const char zero = '\0'; 262 char quote = zero; 263 foreach (char c in commandLine) 264 { 265 if (quote == zero) 266 { 267 if (quotes.Contains(c)) 268 quote = c; 269 else if (char.IsSeparator(c) && quote == zero) 270 { 271 if (arg.Length > 0) 272 { 273 args.Add(arg.ToString()); 274 arg = new StringBuilder(); 275 } 276 } 277 else 278 arg.Append(c); 279 } 280 else 281 { 282 if (c == quote) 283 quote = zero; 284 else 285 arg.Append(c); 286 } 287 } 288 if (arg.Length > 0) 289 args.Add(arg.ToString()); 290 return args; 291 } 292 293 private static readonly char[] Quotes = new[] { '\'', '\"' }; 294 /// <summary> 295 /// Extracts arguments from a full line, in a .NET compatible way 296 /// (includes strange quotes trimming) 297 /// </summary> 298 /// <param name="commandLine">The command line</param> 299 /// <returns>Arguments list</returns> ExtractArguments(string commandLine)300 public IList<string> ExtractArguments(string commandLine) 301 { 302 return ExtractArguments(commandLine, Quotes); 303 } 304 305 /// <summary> 306 /// Converts a list separated by a comma to a string array 307 /// </summary> 308 /// <param name="list"></param> 309 /// <returns></returns> GetArray(string list)310 public string[] GetArray(string list) 311 { 312 if (string.IsNullOrEmpty(list)) 313 return new string[0]; 314 return (from entityInterface in list.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) 315 select entityInterface.Trim()).ToArray(); 316 } 317 318 /// <summary> 319 /// Processes different "lines" of parameters: 320 /// 1. the original input parameter must be starting with @ 321 /// 2. all other parameters are kept as a common part 322 /// </summary> 323 /// <typeparam name="P"></typeparam> 324 /// <param name="args"></param> 325 /// <returns></returns> 326 protected IList<P> GetParameterBatch<P>(IList<string> args) 327 where P : AbstractParameters, new() 328 { 329 return GetParameterBatch<P>(args, "."); 330 } 331 332 public IList<P> GetParameterBatch<P>(IList<string> args, string argsFileDirectory) 333 where P : AbstractParameters, new() 334 { 335 var parameters = new List<P>(); 336 var commonArgs = new List<string>(); 337 var argsFiles = new List<string>(); 338 foreach (var arg in args) 339 { 340 if (arg.StartsWith("@")) 341 argsFiles.Add(arg.Substring(1)); 342 else 343 commonArgs.Add(arg); 344 } 345 // if we specify files, we must recurse 346 if (argsFiles.Count > 0) 347 { 348 foreach (var argsFile in argsFiles) 349 { 350 parameters.AddRange(GetParameterBatchFile<P>(commonArgs, Path.Combine(argsFileDirectory, argsFile))); 351 } 352 } 353 // if we don't, just use the args 354 else if (commonArgs.Count > 0) 355 { 356 var p = new P { Log = Log }; 357 p.Load(commonArgs); 358 parameters.Add(p); 359 } 360 return parameters; 361 } 362 363 private IList<P> GetParameterBatchFile<P>(IEnumerable<string> baseArgs, string argsList) 364 where P : AbstractParameters, new() 365 { 366 var parameters = new List<P>(); 367 string argsFileDirectory = Path.GetDirectoryName(argsList); 368 using (var textReader = File.OpenText(argsList)) 369 { 370 while (!textReader.EndOfStream) 371 { 372 string line = textReader.ReadLine(); 373 if (line.StartsWith("#")) 374 continue; 375 var args = ExtractArguments(line); 376 var allArgs = new List<string>(baseArgs); 377 allArgs.AddRange(args); 378 parameters.AddRange(GetParameterBatch<P>(allArgs, argsFileDirectory)); 379 } 380 } 381 return parameters; 382 } 383 384 /// <summary> 385 /// Outputs a formatted string to the console. 386 /// We're not using the ILogger here, since we want console output. 387 /// </summary> 388 /// <param name="format"></param> 389 /// <param name="args"></param> Write(string format, params object[] args)390 public void Write(string format, params object[] args) 391 { 392 Output.WriteLine(Log, OutputLevel.Information, format, args); 393 } 394 395 /// <summary> 396 /// Outputs an empty line 397 /// </summary> WriteLine()398 public void WriteLine() 399 { 400 Output.WriteLine(Log, OutputLevel.Information, string.Empty); 401 } 402 403 // TODO: remove this 404 protected static int TextWidth 405 { 406 get { return Console.BufferWidth; } 407 } 408 409 /// <summary> 410 /// Returns the application (assembly) name (without extension) 411 /// </summary> 412 protected static string ApplicationName 413 { 414 get 415 { 416 return Assembly.GetEntryAssembly().GetName().Name; 417 } 418 } 419 420 /// <summary> 421 /// Returns the application (assembly) version 422 /// </summary> 423 protected static Version ApplicationVersion 424 { 425 get 426 { 427 // Assembly.GetEntryAssembly() is null when loading from the 428 // non-default AppDomain. 429 var a = Assembly.GetEntryAssembly(); 430 return a != null ? a.GetName().Version : new Version(); 431 } 432 } 433 434 private bool headerWritten; 435 /// <summary> 436 /// Writes the application header 437 /// </summary> WriteHeader()438 public void WriteHeader() 439 { 440 if (!headerWritten) 441 { 442 WriteHeaderContents(); 443 WriteLine(); 444 headerWritten = true; 445 } 446 } 447 WriteHeaderContents()448 protected abstract void WriteHeaderContents(); 449 450 /// <summary> 451 /// Writes a small summary 452 /// </summary> WriteSummary()453 public abstract void WriteSummary(); 454 455 /// <summary> 456 /// Writes examples 457 /// </summary> WriteExamples()458 public virtual void WriteExamples() 459 { 460 } 461 462 /// <summary> 463 /// The "syntax" is a bried containing the application name, "[options]" and eventually files. 464 /// For example: "DbMetal [options] [<input file>] 465 /// </summary> WriteSyntax()466 public virtual void WriteSyntax() 467 { 468 var syntax = new StringBuilder(); 469 syntax.AppendFormat("{0} [options]", ApplicationName); 470 foreach (var file in GetFiles()) 471 { 472 if (file.Description.Mandatory) 473 syntax.AppendFormat(" {0}", GetFileText(file)); 474 else 475 syntax.AppendFormat(" [{0}]", GetFileText(file)); 476 } 477 Write(syntax.ToString()); 478 } 479 480 /// <summary> 481 /// Describes an option 482 /// </summary> 483 protected class Option 484 { 485 /// <summary> 486 /// The member name (property or field) 487 /// </summary> 488 public string Name { get; set; } 489 /// <summary> 490 /// The attribute used to define the member as an option 491 /// </summary> 492 public OptionAttribute Description { get; set; } 493 } 494 495 /// <summary> 496 /// Describes an input file 497 /// </summary> 498 protected class FileName 499 { 500 /// <summary> 501 /// The member name (property or field) 502 /// </summary> 503 public string Name { get; set; } 504 /// <summary> 505 /// The attribute used to define the member as an input file 506 /// </summary> 507 public FileAttribute Description { get; set; } 508 } 509 510 /// <summary> 511 /// Internal class. I wrote it because I was thinking that the .NET framework already had such a class. 512 /// At second thought, I may have made a confusion with STL 513 /// (interesting, isn't it?) 514 /// </summary> 515 /// <typeparam name="A"></typeparam> 516 /// <typeparam name="B"></typeparam> 517 protected class Pair<A, B> 518 { 519 public A First { get; set; } 520 public B Second { get; set; } 521 } 522 523 /// <summary> 524 /// Enumerates all members (fields or properties) that have been marked with the specified attribute 525 /// </summary> 526 /// <typeparam name="T">The attribute type to search for</typeparam> 527 /// <returns>A list of pairs with name and attribute</returns> 528 protected IEnumerable<Pair<string, T>> EnumerateOptions<T>() 529 where T : Attribute 530 { 531 Type t = GetType(); 532 foreach (var propertyInfo in t.GetProperties()) 533 { 534 var descriptions = (T[])propertyInfo.GetCustomAttributes(typeof(T), true); 535 if (descriptions.Length == 1) 536 yield return new Pair<string, T> { First = propertyInfo.Name, Second = descriptions[0] }; 537 } 538 foreach (var fieldInfo in t.GetFields()) 539 { 540 var descriptions = (T[])fieldInfo.GetCustomAttributes(typeof(T), true); 541 if (descriptions.Length == 1) 542 yield return new Pair<string, T> { First = fieldInfo.Name, Second = descriptions[0] }; 543 } 544 } 545 EnumerateOptions()546 protected IEnumerable<Option> EnumerateOptions() 547 { 548 foreach (var pair in EnumerateOptions<OptionAttribute>()) 549 yield return new Option { Name = pair.First, Description = pair.Second }; 550 } 551 GetFiles()552 protected IEnumerable<FileName> GetFiles() 553 { 554 foreach (var pair in from p in EnumerateOptions<FileAttribute>() orderby p.Second.Mandatory select p) 555 yield return new FileName { Name = pair.First, Description = pair.Second }; 556 } 557 558 /// <summary> 559 /// Returns options, grouped by group (the group number is the dictionary key) 560 /// </summary> 561 /// <returns></returns> GetOptions()562 protected IDictionary<int, IList<Option>> GetOptions() 563 { 564 var options = new Dictionary<int, IList<Option>>(); 565 foreach (var option in EnumerateOptions()) 566 { 567 if (!options.ContainsKey(option.Description.Group)) 568 options[option.Description.Group] = new List<Option>(); 569 options[option.Description.Group].Add(option); 570 } 571 return options; 572 } 573 574 /// <summary> 575 /// Return a literal value based on an option 576 /// </summary> 577 /// <param name="option"></param> 578 /// <returns></returns> GetOptionText(Option option)579 protected virtual string GetOptionText(Option option) 580 { 581 var optionName = option.Name[0].ToString().ToLower() + option.Name.Substring(1); 582 if (string.IsNullOrEmpty(option.Description.ValueName)) 583 return optionName; 584 return string.Format("{0}:<{1}>", 585 optionName, 586 option.Description.ValueName); 587 } 588 589 /// <summary> 590 /// Returns a literal value base on an input file 591 /// </summary> 592 /// <param name="fileName"></param> 593 /// <returns></returns> GetFileText(FileName fileName)594 protected virtual string GetFileText(FileName fileName) 595 { 596 return string.Format("<{0}>", fileName.Description.Name); 597 } 598 599 /// <summary> 600 /// Computes the maximum options and files length, to align all descriptions 601 /// </summary> 602 /// <param name="options"></param> 603 /// <param name="files"></param> 604 /// <returns></returns> GetMaximumLength(IDictionary<int, IList<Option>> options, IEnumerable<FileName> files)605 private int GetMaximumLength(IDictionary<int, IList<Option>> options, IEnumerable<FileName> files) 606 { 607 int maxLength = 0; 608 foreach (var optionsList in options.Values) 609 { 610 foreach (var option in optionsList) 611 { 612 var optionName = GetOptionText(option); 613 int length = optionName.Length; 614 if (length > maxLength) 615 maxLength = length; 616 } 617 } 618 foreach (var file in files) 619 { 620 var fileName = GetFileText(file); 621 int length = fileName.Length; 622 if (length > maxLength) 623 maxLength = length; 624 } 625 return maxLength; 626 } 627 SplitText(string text, int width)628 protected static string[] SplitText(string text, int width) 629 { 630 var lines = new List<string>(new[] { "" }); 631 var words = text.Split(' '); 632 foreach (var word in words) 633 { 634 var line = lines.Last(); 635 if (line.Length == 0) 636 lines[lines.Count - 1] = word; 637 else if (line.Length + word.Length + 1 < width) 638 lines[lines.Count - 1] = line + " " + word; 639 else 640 lines.Add(word); 641 } 642 return lines.ToArray(); 643 } 644 WriteOption(string firstLine, string text)645 protected void WriteOption(string firstLine, string text) 646 { 647 int width = TextWidth - firstLine.Length - 2; 648 var lines = SplitText(text, width); 649 var padding = string.Empty.PadRight(firstLine.Length); 650 for (int i = 0; i < lines.Length; i++) 651 { 652 Write("{0} {1}", i == 0 ? firstLine : padding, lines[i]); 653 } 654 } 655 656 /// <summary> 657 /// Displays all available options and files 658 /// </summary> WriteOptions()659 protected void WriteOptions() 660 { 661 var options = GetOptions(); 662 var files = GetFiles(); 663 int maxLength = GetMaximumLength(options, files); 664 Write("Options:"); 665 foreach (var group in from k in options.Keys orderby k select k) 666 { 667 var optionsList = options[group]; 668 foreach (var option in from o in optionsList orderby o.Name select o) 669 { 670 WriteOption(string.Format(" /{0}", GetOptionText(option).PadRight(maxLength)), option.Description.Text); 671 } 672 WriteLine(); 673 } 674 foreach (var file in files) 675 { 676 WriteOption(string.Format(" {0}", GetFileText(file).PadRight(maxLength + 1)), file.Description.Text); 677 } 678 } 679 680 /// <summary> 681 /// Displays application help 682 /// </summary> WriteHelp()683 public void WriteHelp() 684 { 685 WriteHeader(); // includes a WriteLine() 686 WriteSyntax(); 687 WriteLine(); 688 WriteSummary(); 689 WriteLine(); 690 WriteOptions(); 691 WriteLine(); 692 WriteExamples(); 693 } 694 } 695 } 696