2008-01-18 Jb Evain <jbevain@novell.com>
[mono.git] / mcs / class / Mono.GetOptions / Mono.GetOptions / OptionList.cs
1 //
2 // OptionList.cs
3 //
4 // Author: Rafael Teixeira (rafaelteixeirabr@hotmail.com)
5 //
6 // (C) 2002 Rafael Teixeira
7 //
8
9 //
10 // Permission is hereby granted, free of charge, to any person obtaining
11 // a copy of this software and associated documentation files (the
12 // "Software"), to deal in the Software without restriction, including
13 // without limitation the rights to use, copy, modify, merge, publish,
14 // distribute, sublicense, and/or sell copies of the Software, and to
15 // permit persons to whom the Software is furnished to do so, subject to
16 // the following conditions:
17 // 
18 // The above copyright notice and this permission notice shall be
19 // included in all copies or substantial portions of the Software.
20 // 
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 //
29
30 using System;
31 using System.Collections;
32 using System.IO;
33 using System.Reflection;
34 using System.Text;
35
36 namespace Mono.GetOptions
37 {
38
39         /// <summary>
40         /// Option Parsing
41         /// </summary>
42         public class OptionList
43         {
44         
45                 private Options optionBundle = null;
46                 private OptionsParsingMode parsingMode;
47                 private bool breakSingleDashManyLettersIntoManyOptions;
48                 private bool endOptionProcessingWithDoubleDash;
49                 public ErrorReporter ReportError;
50                 
51                 private string appExeName;
52                 private string appVersion;
53
54                 private string appTitle = "Add a [assembly: AssemblyTitle(\"Here goes the application name\")] to your assembly";
55                 private string appCopyright = "Add a [assembly: AssemblyCopyright(\"(c)200n Here goes the copyright holder name\")] to your assembly";
56                 private string appDescription = "Add a [assembly: AssemblyDescription(\"Here goes the short description\")] to your assembly";
57                 private string appAboutDetails = "Add a [assembly: Mono.About(\"Here goes the short about details\")] to your assembly";
58                 private string appUsageComplement = "Add a [assembly: Mono.UsageComplement(\"Here goes the usage clause complement\")] to your assembly";
59                 private string appAdditionalInfo = null;
60                 private string appReportBugsTo = null;
61                 private string[] appAuthors;
62  
63                 private ArrayList list = new ArrayList();
64                 private ArrayList arguments = new ArrayList();
65                 private ArrayList argumentsTail = new ArrayList();
66                 private MethodInfo argumentProcessor = null;
67                 
68                 private bool HasSecondLevelHelp = false;
69
70                 internal bool MaybeAnOption(string arg)
71                 {
72                         return  ((parsingMode & OptionsParsingMode.Windows) > 0 && arg[0] == '/') || 
73                                         ((parsingMode & OptionsParsingMode.Linux)   > 0 && arg[0] == '-');
74                 }
75
76                 public string Usage
77                 {
78                         get {
79                                 return "Usage: " + appExeName + " [options] " + appUsageComplement;
80                         }
81                 }
82
83                 public string AboutDetails
84                 {
85                         get {
86                                 return appAboutDetails;
87                         }
88                 }
89
90                 #region Assembly Attributes
91
92                 Assembly entry;
93                 
94                 private object[] GetAssemblyAttributes(Type type)
95                 {
96                         return entry.GetCustomAttributes(type, false);
97                 }
98                         
99                 private string[] GetAssemblyAttributeStrings(Type type)
100                 {
101                         object[] result = GetAssemblyAttributes(type);
102                         
103                         if ((result == null) || (result.Length == 0))
104                                 return new string[0];
105
106                         int i = 0;
107                         string[] var = new string[result.Length];
108
109                         foreach(object o in result)
110                                 var[i++] = o.ToString(); 
111
112                         return var;
113                 }
114
115                 private void GetAssemblyAttributeValue(Type type, string propertyName, ref string var)
116                 {
117                         object[] result = GetAssemblyAttributes(type);
118                         
119                         if ((result != null) && (result.Length > 0))
120                                 var = (string)type.InvokeMember(propertyName, BindingFlags.Public | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Instance, null, result[0], new object [] {}); ;
121                 }
122
123                 private void GetAssemblyAttributeValue(Type type, ref string var)
124                 {
125                         object[] result = GetAssemblyAttributes(type);
126                         
127                         if ((result != null) && (result.Length > 0))
128                                 var = result[0].ToString();
129                 }
130
131                 private void ExtractEntryAssemblyInfo(Type optionsType)
132                 {
133                         entry = optionsType.Assembly;
134                         if (entry == this.GetType().Assembly)   {               
135                                 entry = Assembly.GetEntryAssembly();
136                         }
137
138                         appExeName = entry.GetName().Name;
139                         appVersion = entry.GetName().Version.ToString();
140                         GetAssemblyAttributeValue(typeof(AssemblyTitleAttribute), "Title", ref appTitle);
141                         GetAssemblyAttributeValue(typeof(AssemblyCopyrightAttribute), "Copyright", ref appCopyright);
142                         GetAssemblyAttributeValue(typeof(AssemblyDescriptionAttribute), "Description", ref appDescription);
143                         GetAssemblyAttributeValue(typeof(Mono.AboutAttribute), ref appAboutDetails);
144                         GetAssemblyAttributeValue(typeof(Mono.UsageComplementAttribute), ref appUsageComplement);
145                         GetAssemblyAttributeValue(typeof(Mono.AdditionalInfoAttribute), ref appAdditionalInfo);
146                         GetAssemblyAttributeValue(typeof(Mono.ReportBugsToAttribute), ref appReportBugsTo);
147                         appAuthors = GetAssemblyAttributeStrings(typeof(AuthorAttribute));
148                         if (appAuthors.Length == 0) {
149                                 appAuthors = new String[1];
150                                 appAuthors[0] = "Add one or more [assembly: Mono.Author(\"Here goes the author name\")] to your assembly";
151                         }               
152                 }
153
154                 #endregion
155
156                 #region Constructors
157
158                 private void AddArgumentProcessor(MemberInfo memberInfo)
159                 {
160                         if (argumentProcessor != null)
161                                 throw new NotSupportedException("More than one argument processor method found");
162
163                         if ((memberInfo.MemberType == MemberTypes.Method && memberInfo is MethodInfo)) {
164                                 if (((MethodInfo)memberInfo).ReturnType.FullName != typeof(void).FullName)
165                                         throw new NotSupportedException("Argument processor method must return 'void'");
166
167                                 ParameterInfo[] parameters = ((MethodInfo)memberInfo).GetParameters();
168                                 if ((parameters == null) || (parameters.Length != 1) || (parameters[0].ParameterType.FullName != typeof(string).FullName))
169                                         throw new NotSupportedException("Argument processor method must have a string parameter");
170                                 
171                                 argumentProcessor = (MethodInfo)memberInfo; 
172                         }
173                         else
174                                 throw new NotSupportedException("Argument processor marked member isn't a method");
175                 }
176
177                 public OptionList(Options optionBundle)
178                 {
179                         if (optionBundle == null)
180                                 throw new ArgumentNullException("optionBundle");
181
182                         Type optionsType = optionBundle.GetType();
183                         this.optionBundle = optionBundle; 
184                         this.parsingMode = optionBundle.ParsingMode ;
185                         this.breakSingleDashManyLettersIntoManyOptions = optionBundle.BreakSingleDashManyLettersIntoManyOptions;
186                         this.endOptionProcessingWithDoubleDash = optionBundle.EndOptionProcessingWithDoubleDash;
187                         this.ReportError = optionBundle.ReportError;
188                         
189                         ExtractEntryAssemblyInfo(optionsType);
190
191                         foreach(MemberInfo mi in optionsType.GetMembers()) {
192                                 object[] attribs = mi.GetCustomAttributes(typeof(KillOptionAttribute), true);
193                                 if (attribs == null || attribs.Length == 0) {
194                                         attribs = mi.GetCustomAttributes(typeof(OptionAttribute), true);
195                                         if (attribs != null && attribs.Length > 0) {
196                                                 OptionDetails option = new OptionDetails(mi, (OptionAttribute)attribs[0], optionBundle);
197                                                 list.Add(option);
198                                                 HasSecondLevelHelp = HasSecondLevelHelp || option.SecondLevelHelp;
199                                         } else if (mi.DeclaringType == mi.ReflectedType) { // not inherited
200                                                 attribs = mi.GetCustomAttributes(typeof(ArgumentProcessorAttribute), true); 
201                                                 if (attribs != null && attribs.Length > 0)
202                                                         AddArgumentProcessor(mi);
203                                         }
204                                 }
205                         }
206                         
207                         if (argumentProcessor == null) // try to find an inherited one
208                                 foreach(MemberInfo mi in optionsType.GetMembers()) 
209                                         if (mi.DeclaringType != mi.ReflectedType) { // inherited
210                                                 object[] attribs = mi.GetCustomAttributes(typeof(ArgumentProcessorAttribute), true);
211                                                 if (attribs != null && attribs.Length > 0)
212                                                         AddArgumentProcessor(mi);
213                                         }
214                 }
215
216                 #endregion
217
218                 #region Prebuilt Options
219
220                 private bool bannerAlreadyShown = false;
221                 
222                 internal string AdditionalBannerInfo;
223                 
224                 public void ShowBanner()
225                 {
226                         if (!bannerAlreadyShown) {
227                                 Console.WriteLine(appTitle + "  " + appVersion + " - " + appCopyright); 
228                                 if (AdditionalBannerInfo != null)
229                                         Console.WriteLine(AdditionalBannerInfo);
230                         }
231                         bannerAlreadyShown = true;
232                 }
233                 
234                 private void ShowTitleLines()
235                 {
236                         ShowBanner();
237                         Console.WriteLine(appDescription); 
238                         Console.WriteLine();
239                 }
240
241                 private void ShowAbout()
242                 {
243                         ShowTitleLines();
244                         Console.WriteLine(appAboutDetails); 
245                         Console.Write("Authors: ");
246                         Console.WriteLine(string.Join(", ", appAuthors));
247                 }
248
249                 private void ShowHelp(bool showSecondLevelHelp)
250                 {
251                         ShowTitleLines();
252                         Console.WriteLine(Usage);
253                         Console.WriteLine("Options:");
254                         ArrayList lines = new ArrayList(list.Count);
255                         int tabSize = 0;
256                         foreach (OptionDetails option in list)
257                                 if (option.SecondLevelHelp == showSecondLevelHelp) {
258                                         string[] optionLines = option.ToString().Split('\n');
259                                         foreach(string line in optionLines) {
260                                                 int pos = line.IndexOf('\t');
261                                                 if (pos > tabSize)
262                                                         tabSize = pos;
263                                                 lines.Add(line);
264                                         }
265                                 }
266                         tabSize += 2;
267                         foreach (string line in lines) {
268                                 string[] parts = line.Split('\t');
269                                 Console.Write(parts[0].PadRight(tabSize));
270                                 Console.WriteLine(parts[1]);
271                                 if (parts.Length > 2) {
272                                         string spacer = new string(' ', tabSize);
273                                         for(int i = 2; i < parts.Length; i++) {
274                                                 Console.Write(spacer);
275                                                 Console.WriteLine(parts[i]);
276                                         }
277                                 }
278                         }
279                         if (appAdditionalInfo != null)
280                                 Console.WriteLine("\n{0}", appAdditionalInfo);
281                         if (appReportBugsTo != null)
282                                 Console.WriteLine("\nPlease report bugs {0} <{1}>", (appReportBugsTo.IndexOf('@')>0)?"to":"at" , appReportBugsTo);
283                                 
284                 }
285
286                 private void ShowUsage()
287                 {
288                         Console.WriteLine(Usage);
289                         Console.Write("Short Options: ");
290                         foreach (OptionDetails option in list)
291                                 Console.Write(option.ShortForm.Trim());
292                         Console.WriteLine();
293                         
294                 }
295
296                 internal WhatToDoNext DoUsage()
297                 {
298                         ShowUsage();
299                         return WhatToDoNext.AbandonProgram;
300                 }
301
302                 internal WhatToDoNext DoAbout()
303                 {
304                         ShowAbout();
305                         return WhatToDoNext.AbandonProgram;
306                 }
307
308                 internal WhatToDoNext DoHelp()
309                 {
310                         ShowHelp(false);
311                         return WhatToDoNext.AbandonProgram;
312                 }
313
314                 internal WhatToDoNext DoHelp2()
315                 {
316                         ShowHelp(true);
317                         return WhatToDoNext.AbandonProgram;
318                 }
319                 
320                 #endregion
321
322                 #region Response File Expansion
323                 
324                 private void processResponseFileLine(string line, ArrayList result, StringBuilder sb)
325                 {
326                         int t = line.Length;
327                         for (int i = 0; i < t; i++) {
328                                 char c = line [i];
329                                 if (c == '"' || c == '\'') {
330                                         char end = c;
331                                         for (i++; i < t; i++) {
332                                                 c = line [i];   
333                                                 if (c == end)
334                                                         break;
335                                                 sb.Append(c);
336                                         }
337                                 } else if (c == ' ') {
338                                         if (sb.Length > 0) {
339                                                 result.Add(sb.ToString());
340                                                 sb.Length = 0;
341                                         }
342                                 } else {
343                                         sb.Append(c);
344                                 }
345                         }
346                         if (sb.Length > 0) {
347                                 result.Add(sb.ToString());
348                                 sb.Length = 0;
349                         }
350                 }
351                 
352                 private void processResponseFile(string filename, ArrayList result)
353                 {
354                         StringBuilder sb = new StringBuilder();
355                         string line;
356                         try {
357                                 using (StreamReader responseFile = new StreamReader(filename)) {
358                                         while ((line = responseFile.ReadLine()) != null)
359                                                 processResponseFileLine(line, result, sb);
360                                         responseFile.Close ();  
361                                 } 
362                         } catch (FileNotFoundException) {
363                                 ReportError(2011, "Unable to find response file '" + filename + "'");
364                         } catch (Exception exception) {
365                                 ReportError(2011, "Unable to open response file '" + filename + "'. " + exception.Message);
366                         }
367                 }
368
369                 private ArrayList ExpandResponseFiles(string[] args)
370                 {
371                         ArrayList result = new ArrayList();
372                         foreach(string arg in args)
373                                 if (arg.StartsWith("@")) 
374                                         processResponseFile(arg.Substring(1), result);
375                                 else
376                                         result.Add(arg);
377                         return result;
378                 }
379                 
380                 #endregion
381
382                 #region Arguments Processing
383
384
385                 private static int IndexOfAny(string where, params char[] what)
386                 {
387                         return where.IndexOfAny(what);
388                 }
389                 
390                 private string[] NormalizeArgs(string[] args)
391                 {
392                         bool ParsingOptions = true;
393                         ArrayList result = new ArrayList();
394                         
395                         foreach(string arg in ExpandResponseFiles(args)) {
396                                 if (arg.Length > 0) {
397                                         if (ParsingOptions) {
398                                                 if (endOptionProcessingWithDoubleDash && (arg == "--")) {
399                                                         ParsingOptions = false;
400                                                         continue;
401                                                 }
402
403                                                 if ((parsingMode & OptionsParsingMode.Linux) > 0 && 
404                                                          arg[0] == '-' && arg.Length > 1 && arg[1] != '-' &&
405                                                          breakSingleDashManyLettersIntoManyOptions) {
406                                                         foreach(char c in arg.Substring(1)) // many single-letter options
407                                                                 result.Add("-" + c); // expand into individualized options
408                                                         continue;
409                                                 }
410
411                                                 if (MaybeAnOption(arg)) {
412                                                         int pos = IndexOfAny(arg, ':', '=');
413
414                                                         if(pos < 0)
415                                                                 result.Add(arg);
416                                                         else {
417                                                                 result.Add(arg.Substring(0, pos));
418                                                                 result.Add(arg.Substring(pos+1));
419                                                         }
420                                                         continue;
421                                                 }
422                                         } else {
423                                                 argumentsTail.Add(arg);
424                                                 continue;
425                                         }
426
427                                         // if nothing else matches then it get here
428                                         result.Add(arg);
429                                 }
430                         }
431
432                         return (string[])result.ToArray(typeof(string));
433                 }
434
435                 public string[] ProcessArgs(string[] args)
436                 {
437                         string arg;
438                         string nextArg;
439                         bool OptionWasProcessed;
440
441                         list.Sort();
442                         
443                         OptionDetails.LinkAlternatesInsideList(list);
444
445                         args = NormalizeArgs(args);
446
447                         try {
448                                 int argc = args.Length;
449                                 for (int i = 0; i < argc; i++) {
450                                         arg =  args[i];
451                                         if (i+1 < argc)
452                                                 nextArg = args[i+1];
453                                         else
454                                                 nextArg = null;
455
456                                         OptionWasProcessed = false;
457
458                                         if (arg.Length > 1 && (arg.StartsWith("-") || arg.StartsWith("/"))) {
459                                                 foreach(OptionDetails option in list) {
460                                                         OptionProcessingResult result = option.ProcessArgument(arg, nextArg);
461                                                         if (result != OptionProcessingResult.NotThisOption) {
462                                                                 OptionWasProcessed = true;
463                                                                 if (result == OptionProcessingResult.OptionConsumedParameter)
464                                                                         i++;
465                                                                 break;
466                                                         }
467                                                 }
468                                         }
469
470                                         if (!OptionWasProcessed)
471                                                 ProcessNonOption(arg);
472                                 }
473
474                                 foreach(OptionDetails option in list)
475                                         option.TransferValues(); 
476
477                                 foreach(string argument in argumentsTail)
478                                         ProcessNonOption(argument);
479
480                                 return (string[])arguments.ToArray(typeof(string));
481                                 
482                         } catch (Exception ex) {
483                                 System.Console.WriteLine(ex.ToString());
484                                 System.Environment.Exit(1);
485                         }
486
487                         return null;
488                 }
489                 
490                 private void ProcessNonOption(string argument)
491                 {
492                         if (optionBundle.VerboseParsingOfOptions)
493                                         Console.WriteLine("argument [" + argument + "]");                                                       
494                         if (argumentProcessor == null)
495                                 arguments.Add(argument);
496                         else
497                                 argumentProcessor.Invoke(optionBundle, new object[] { argument });                                              
498                 }
499                 
500                 #endregion
501
502         }
503 }