2005-06-05 Peter Bartok <pbartok@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                         {
80                                 return "Usage: " + appExeName + " [options] " + appUsageComplement;
81                         }
82                 }
83
84                 public string AboutDetails
85                 {
86                         get
87                         {
88                                 return appAboutDetails;
89                         }
90                 }
91
92                 #region Assembly Attributes
93
94                 Assembly entry;
95                 
96                 private object[] GetAssemblyAttributes(Type type)
97                 {
98                         return entry.GetCustomAttributes(type, false);
99                 }
100                         
101                 private string[] GetAssemblyAttributeStrings(Type type)
102                 {
103                         object[] result = GetAssemblyAttributes(type);
104                         
105                         if ((result == null) || (result.Length == 0))
106                                 return new string[0];
107
108                         int i = 0;
109                         string[] var = new string[result.Length];
110
111                         foreach(object o in result)
112                                 var[i++] = o.ToString(); 
113
114                         return var;
115                 }
116
117                 private void GetAssemblyAttributeValue(Type type, string propertyName, ref string var)
118                 {
119                         object[] result = GetAssemblyAttributes(type);
120                         
121                         if ((result != null) && (result.Length > 0))
122                                 var = (string)type.InvokeMember(propertyName, BindingFlags.Public | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Instance, null, result[0], new object [] {}); ;
123                 }
124
125                 private void GetAssemblyAttributeValue(Type type, ref string var)
126                 {
127                         object[] result = GetAssemblyAttributes(type);
128                         
129                         if ((result != null) && (result.Length > 0))
130                                 var = result[0].ToString();
131                 }
132
133                 private void ExtractEntryAssemblyInfo(Type optionsType)
134                 {
135                         entry = optionsType.Assembly;
136                         if (entry == this.GetType().Assembly)   {               
137                                 entry = Assembly.GetEntryAssembly();
138                         }
139
140                         appExeName = entry.GetName().Name;
141                         appVersion = entry.GetName().Version.ToString();
142                         GetAssemblyAttributeValue(typeof(AssemblyTitleAttribute), "Title", ref appTitle);
143                         GetAssemblyAttributeValue(typeof(AssemblyCopyrightAttribute), "Copyright", ref appCopyright);
144                         GetAssemblyAttributeValue(typeof(AssemblyDescriptionAttribute), "Description", ref appDescription);
145                         GetAssemblyAttributeValue(typeof(Mono.AboutAttribute), ref appAboutDetails);
146                         GetAssemblyAttributeValue(typeof(Mono.UsageComplementAttribute), ref appUsageComplement);
147                         GetAssemblyAttributeValue(typeof(Mono.AdditionalInfoAttribute), ref appAdditionalInfo);
148                         GetAssemblyAttributeValue(typeof(Mono.ReportBugsToAttribute), ref appReportBugsTo);
149                         appAuthors = GetAssemblyAttributeStrings(typeof(AuthorAttribute));
150                         if (appAuthors.Length == 0) {
151                                 appAuthors = new String[1];
152                                 appAuthors[0] = "Add one or more [assembly: Mono.Author(\"Here goes the author name\")] to your assembly";
153                         }               
154                 }
155
156                 #endregion
157
158                 #region Constructors
159
160                 private void AddArgumentProcessor(MemberInfo memberInfo)
161                 {
162                         if (argumentProcessor != null)
163                                 throw new NotSupportedException("More than one argument processor method found");
164
165                         if ((memberInfo.MemberType == MemberTypes.Method && memberInfo is MethodInfo))
166                         {
167                                 if (((MethodInfo)memberInfo).ReturnType.FullName != typeof(void).FullName)
168                                         throw new NotSupportedException("Argument processor method must return 'void'");
169
170                                 ParameterInfo[] parameters = ((MethodInfo)memberInfo).GetParameters();
171                                 if ((parameters == null) || (parameters.Length != 1) || (parameters[0].ParameterType.FullName != typeof(string).FullName))
172                                         throw new NotSupportedException("Argument processor method must have a string parameter");
173                                 
174                                 argumentProcessor = (MethodInfo)memberInfo; 
175                         }
176                         else
177                                 throw new NotSupportedException("Argument processor marked member isn't a method");
178                 }
179
180                 public OptionList(Options optionBundle)
181                 {
182                         if (optionBundle == null)
183                                 throw new ArgumentNullException("optionBundle");
184
185                         Type optionsType = optionBundle.GetType();
186                         this.optionBundle = optionBundle; 
187                         this.parsingMode = optionBundle.ParsingMode ;
188                         this.breakSingleDashManyLettersIntoManyOptions = optionBundle.BreakSingleDashManyLettersIntoManyOptions;
189                         this.endOptionProcessingWithDoubleDash = optionBundle.EndOptionProcessingWithDoubleDash;
190                         this.ReportError = optionBundle.ReportError;
191                         
192                         ExtractEntryAssemblyInfo(optionsType);
193
194                         foreach(MemberInfo mi in optionsType.GetMembers()) {
195                                 object[] attribs = mi.GetCustomAttributes(typeof(KillOptionAttribute), true);
196                                 if (attribs == null || attribs.Length == 0) {
197                                         attribs = mi.GetCustomAttributes(typeof(OptionAttribute), true);
198                                         if (attribs != null && attribs.Length > 0) {
199                                                 OptionDetails option = new OptionDetails(mi, (OptionAttribute)attribs[0], optionBundle);
200                                                 list.Add(option);
201                                                 HasSecondLevelHelp = HasSecondLevelHelp || option.SecondLevelHelp;
202                                         } else if (mi.DeclaringType == mi.ReflectedType) { // not inherited
203                                                 attribs = mi.GetCustomAttributes(typeof(ArgumentProcessorAttribute), true); 
204                                                 if (attribs != null && attribs.Length > 0)
205                                                         AddArgumentProcessor(mi);
206                                         }
207                                 }
208                         }
209                         
210                         if (argumentProcessor == null) // try to find an inherited one
211                                 foreach(MemberInfo mi in optionsType.GetMembers()) 
212                                         if (mi.DeclaringType != mi.ReflectedType) { // inherited
213                                                 object[] attribs = mi.GetCustomAttributes(typeof(ArgumentProcessorAttribute), true);
214                                                 if (attribs != null && attribs.Length > 0)
215                                                         AddArgumentProcessor(mi);
216                                         }
217                 }
218
219                 #endregion
220
221                 #region Prebuilt Options
222
223                 private bool bannerAlreadyShown = false;
224                 
225                 internal string AdditionalBannerInfo;
226                 
227                 public void ShowBanner()
228                 {
229                         if (!bannerAlreadyShown) {
230                                 Console.WriteLine(appTitle + "  " + appVersion + " - " + appCopyright); 
231                                 if (AdditionalBannerInfo != null)
232                                         Console.WriteLine(AdditionalBannerInfo);
233                         }
234                         bannerAlreadyShown = true;
235                 }
236                 
237                 private void ShowTitleLines()
238                 {
239                         ShowBanner();
240                         Console.WriteLine(appDescription); 
241                         Console.WriteLine();
242                 }
243
244                 private void ShowAbout()
245                 {
246                         ShowTitleLines();
247                         Console.WriteLine(appAboutDetails); 
248                         StringBuilder sb = new StringBuilder("Authors: ");
249                         bool first = true;
250                         foreach(string s in appAuthors)
251                         {
252                                 if (first)
253                                         first = false;
254                                 else
255                                         sb.Append(", ");
256                                 sb.Append(s);
257                         }
258                         Console.WriteLine(sb.ToString());
259                 }
260
261                 private void ShowHelp(bool showSecondLevelHelp)
262                 {
263                         ShowTitleLines();
264                         Console.WriteLine(Usage);
265                         Console.WriteLine("Options:");
266                         ArrayList lines = new ArrayList(list.Count);
267                         int tabSize = 0;
268                         foreach (OptionDetails option in list)
269                                 if (option.SecondLevelHelp == showSecondLevelHelp) {
270                                         string[] optionLines = option.ToString().Split('\n');
271                                         foreach(string line in optionLines) {
272                                                 int pos = line.IndexOf('\t');
273                                                 if (pos > tabSize)
274                                                         tabSize = pos;
275                                                 lines.Add(line);
276                                         }
277                                 }
278                         tabSize += 2;
279                         foreach (string line in lines) {
280                                 string[] parts = line.Split('\t');
281                                 Console.Write(parts[0].PadRight(tabSize));
282                                 Console.WriteLine(parts[1]);
283                                 if (parts.Length > 2) {
284                                         string spacer = new string(' ', tabSize);
285                                         for(int i = 2; i < parts.Length; i++) {
286                                                 Console.Write(spacer);
287                                                 Console.WriteLine(parts[i]);
288                                         }
289                                 }
290                         }
291                         if (appAdditionalInfo != null)
292                                 Console.WriteLine("\n{0}", appAdditionalInfo);
293                         if (appReportBugsTo != null)
294                                 Console.WriteLine("\nPlease report bugs {0} <{1}>", (appReportBugsTo.IndexOf('@')>0)?"to":"at" , appReportBugsTo);
295                                 
296                 }
297
298                 private void ShowUsage()
299                 {
300                         Console.WriteLine(Usage);
301                         Console.Write("Short Options: ");
302                         foreach (OptionDetails option in list)
303                                 Console.Write(option.ShortForm.Trim());
304                         Console.WriteLine();
305                         
306                 }
307
308                 internal WhatToDoNext DoUsage()
309                 {
310                         ShowUsage();
311                         return WhatToDoNext.AbandonProgram;
312                 }
313
314                 internal WhatToDoNext DoAbout()
315                 {
316                         ShowAbout();
317                         return WhatToDoNext.AbandonProgram;
318                 }
319
320                 internal WhatToDoNext DoHelp()
321                 {
322                         ShowHelp(false);
323                         return WhatToDoNext.AbandonProgram;
324                 }
325
326                 internal WhatToDoNext DoHelp2()
327                 {
328                         ShowHelp(true);
329                         return WhatToDoNext.AbandonProgram;
330                 }
331                 
332                 #endregion
333
334                 #region Arguments Processing
335
336                 public string[] ExpandResponseFiles(string[] args)
337                 {
338                         ArrayList result = new ArrayList();
339
340                         foreach(string arg in args)
341                         {
342                                 if (arg.StartsWith("@"))
343                                 {
344                                         try 
345                                         {
346                                                 using (StreamReader tr = new StreamReader(arg.Substring(1))) {
347                                                         string line;
348                                                         StringBuilder sb = new StringBuilder ();
349
350                                                         while ((line = tr.ReadLine()) != null) {
351                                                                 int t = line.Length;
352
353                                                                 for (int i = 0; i < t; i++) {
354                                                                         char c = line [i];
355
356                                                                         if (c == '"' || c == '\'') {
357                                                                                 char end = c;
358
359                                                                                 for (i++; i < t; i++) {
360                                                                                         c = line [i];   
361
362                                                                                         if (c == end)
363                                                                                                 break;
364                                                                                         sb.Append(c);
365                                                                                 }
366                                                                                 } else if (c == ' ') {
367                                                                                 if (sb.Length > 0) {
368                                                                                         result.Add (sb.ToString());
369                                                                                         sb.Length = 0;
370                                                                                 }
371                                                                         } else {
372                                                                                 sb.Append (c);
373                                                                         }
374                                                                 }
375                                                                 if (sb.Length > 0) {
376                                                                         result.Add (sb.ToString());
377                                                                         sb.Length = 0;
378                                                                 }
379                                                         }
380                                                         tr.Close ();    
381                                                 } 
382                                         } catch (FileNotFoundException) {
383                                                 ReportError(2011, "Unable to open response file '" + arg.Substring(1) + "'");
384                                                 continue;
385                                         } catch (Exception exception) {
386                                                 ReportError(2011, "Unable to open response file '" + arg.Substring(1) + "'. " + exception.Message);
387                                                 continue;
388                                         }
389                                 }
390                                 else
391                                         result.Add(arg);
392                         }
393
394                         return (string[])result.ToArray(typeof(string));
395                 }
396
397                 private static int IndexOfAny(string where, params char[] what)
398                 {
399                         return where.IndexOfAny(what);
400                 }
401                 
402                 public string[] NormalizeArgs(string[] args)
403                 {
404                         bool ParsingOptions = true;
405                         ArrayList result = new ArrayList();
406
407                         foreach(string arg in ExpandResponseFiles(args)) {
408                                 if (arg.Length > 0) {
409                                         if (ParsingOptions) {
410                                                 if (endOptionProcessingWithDoubleDash && (arg == "--")) {
411                                                         ParsingOptions = false;
412                                                         continue;
413                                                 }
414
415                                                 if ((parsingMode & OptionsParsingMode.Linux) > 0 && 
416                                                          arg[0] == '-' && arg.Length > 1 && arg[1] != '-' &&
417                                                          breakSingleDashManyLettersIntoManyOptions) {
418                                                         foreach(char c in arg.Substring(1)) // many single-letter options
419                                                                 result.Add("-" + c); // expand into individualized options
420                                                         continue;
421                                                 }
422
423                                                 if (MaybeAnOption(arg)) {
424                                                         int pos = IndexOfAny(arg, ':', '=');
425
426                                                         if(pos < 0)
427                                                                 result.Add(arg);
428                                                         else {
429                                                                 result.Add(arg.Substring(0, pos));
430                                                                 result.Add(arg.Substring(pos+1));
431                                                         }
432                                                         continue;
433                                                 }
434                                         } else {
435                                                 argumentsTail.Add(arg);
436                                                 continue;
437                                         }
438
439                                         // if nothing else matches then it get here
440                                         result.Add(arg);
441                                 }
442                         }
443
444                         return (string[])result.ToArray(typeof(string));
445                 }
446
447                 public string[] ProcessArgs(string[] args)
448                 {
449                         string arg;
450                         string nextArg;
451                         bool OptionWasProcessed;
452
453                         list.Sort();
454
455                         args = NormalizeArgs(args);
456
457                         try {
458                                 int argc = args.Length;
459                                 for (int i = 0; i < argc; i++) {
460                                         arg =  args[i];
461                                         if (i+1 < argc)
462                                                 nextArg = args[i+1];
463                                         else
464                                                 nextArg = null;
465
466                                         OptionWasProcessed = false;
467
468                                         if (arg.Length > 1 && (arg.StartsWith("-") || arg.StartsWith("/"))) {
469                                                 foreach(OptionDetails option in list) {
470                                                         OptionProcessingResult result = option.ProcessArgument(arg, nextArg);
471                                                         if (result != OptionProcessingResult.NotThisOption) {
472                                                                 OptionWasProcessed = true;
473                                                                 if (result == OptionProcessingResult.OptionConsumedParameter)
474                                                                         i++;
475                                                                 break;
476                                                         }
477                                                 }
478                                         }
479
480                                         if (!OptionWasProcessed)
481                                                 ProcessNonOption(arg);
482                                 }
483
484                                 foreach(OptionDetails option in list)
485                                         option.TransferValues(); 
486
487                                 foreach(string argument in argumentsTail)
488                                         ProcessNonOption(argument);
489
490                                 return (string[])arguments.ToArray(typeof(string));
491                                 
492                         } catch (Exception ex) {
493                                 System.Console.WriteLine(ex.ToString());
494                                 System.Environment.Exit(1);
495                         }
496
497                         return null;
498                 }
499                 
500                 private void ProcessNonOption(string argument)
501                 {
502                         if (optionBundle.VerboseParsingOfOptions)
503                                         Console.WriteLine("argument [" + argument + "]");                                                       
504                         if (argumentProcessor == null)
505                                 arguments.Add(argument);
506                         else
507                                 argumentProcessor.Invoke(optionBundle, new object[] { argument });                                              
508                 }
509                 
510                 #endregion
511
512         }
513 }